1 /**
2 	Generator for direct compiler builds.
3 
4 	Copyright: © 2013-2013 rejectedsoftware e.K.
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Sönke Ludwig
7 */
8 module dub.generators.build;
9 
10 import dub.compilers.compiler;
11 import dub.compilers.utils;
12 import dub.generators.generator;
13 import dub.internal.utils;
14 import dub.internal.vibecompat.core.file;
15 import dub.internal.vibecompat.inet.path;
16 import dub.internal.logging;
17 import dub.package_;
18 import dub.packagemanager;
19 import dub.project;
20 
21 import std.algorithm;
22 import std.array;
23 import std.conv;
24 import std.exception;
25 import std.file;
26 import std.process;
27 import std.string;
28 import std.encoding : sanitize;
29 
30 string getObjSuffix(const scope ref BuildPlatform platform)
31 {
32     return platform.isWindows() ? ".obj" : ".o";
33 }
34 
35 string computeBuildName(string config, in GeneratorSettings settings, const string[][] hashing...)
36 {
37 	import std.digest.sha : SHA256, toHexString;
38 
39 	SHA256 hash;
40 	hash.start();
41 	void addHash(in string[] strings...) { foreach (s; strings) { hash.put(cast(ubyte[])s); hash.put(0); } hash.put(0); }
42 	foreach(strings; hashing)
43 		addHash(strings);
44 	const hashstr = hash.finish().toHexString();
45 
46     return format("%s-%s-%s-%s-%s_v%s-%s", config, settings.buildType,
47 			settings.platform.platform.join("."),
48 			settings.platform.architecture.join("."),
49 			settings.platform.compiler, settings.platform.compilerVersion, hashstr);
50 }
51 
52 class BuildGenerator : ProjectGenerator {
53 	private {
54 		PackageManager m_packageMan;
55 		NativePath[] m_temporaryFiles;
56 	}
57 
58 	this(Project project)
59 	{
60 		super(project);
61 		m_packageMan = project.packageManager;
62 	}
63 
64 	override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets)
65 	{
66 		import std.path : setExtension;
67 		scope (exit) cleanupTemporaries();
68 
69 		void checkPkgRequirements(const(Package) pkg)
70 		{
71 			const tr = pkg.recipe.toolchainRequirements;
72 			tr.checkPlatform(settings.platform, pkg.name);
73 		}
74 
75 		checkPkgRequirements(m_project.rootPackage);
76 		foreach (pkg; m_project.dependencies)
77 			checkPkgRequirements(pkg);
78 
79 		auto root_ti = targets[m_project.rootPackage.name];
80 		const rootTT = root_ti.buildSettings.targetType;
81 
82 		enforce(!(settings.rdmd && rootTT == TargetType.none),
83 				"Building package with target type \"none\" with rdmd is not supported yet.");
84 
85 		logInfo("Starting", Color.light_green,
86 		    "Performing \"%s\" build using %s for %-(%s, %).",
87 			settings.buildType.color(Color.magenta), settings.platform.compilerBinary,
88 			settings.platform.architecture);
89 
90 		bool any_cached = false;
91 
92 		NativePath[string] target_paths;
93 
94 		NativePath[] dynamicLibDepsFilesToCopy; // to the root package output dir
95 		const copyDynamicLibDepsLinkerFiles = rootTT == TargetType.dynamicLibrary || rootTT == TargetType.none;
96 		const copyDynamicLibDepsRuntimeFiles = copyDynamicLibDepsLinkerFiles || rootTT == TargetType.executable;
97 
98 		bool[string] visited;
99 		void buildTargetRec(string target)
100 		{
101 			if (target in visited) return;
102 			visited[target] = true;
103 
104 			auto ti = targets[target];
105 
106 			foreach (dep; ti.dependencies)
107 				buildTargetRec(dep);
108 
109 			NativePath[] additional_dep_files;
110 			auto bs = ti.buildSettings.dup;
111 			const tt = bs.targetType;
112 			foreach (ldep; ti.linkDependencies) {
113 				const ldepPath = target_paths[ldep].toNativeString();
114 				const doLink = tt != TargetType.staticLibrary && !(bs.options & BuildOption.syntaxOnly);
115 
116 				if (doLink && isLinkerFile(settings.platform, ldepPath))
117 					bs.addSourceFiles(ldepPath);
118 				else
119 					additional_dep_files ~= target_paths[ldep];
120 
121 				if (targets[ldep].buildSettings.targetType == TargetType.dynamicLibrary) {
122 					// copy the .{dll,so,dylib}
123 					if (copyDynamicLibDepsRuntimeFiles)
124 						dynamicLibDepsFilesToCopy ~= NativePath(ldepPath);
125 
126 					if (settings.platform.isWindows()) {
127 						// copy the accompanying .pdb if found
128 						if (copyDynamicLibDepsRuntimeFiles) {
129 							const pdb = ldepPath.setExtension(".pdb");
130 							if (existsFile(pdb))
131 								dynamicLibDepsFilesToCopy ~= NativePath(pdb);
132 						}
133 
134 						const importLib = ldepPath.setExtension(".lib");
135 						if (existsFile(importLib)) {
136 							// link dependee against the import lib
137 							if (doLink)
138 								bs.addSourceFiles(importLib);
139 							// and copy
140 							if (copyDynamicLibDepsLinkerFiles)
141 								dynamicLibDepsFilesToCopy ~= NativePath(importLib);
142 						}
143 
144 						// copy the .exp file if found
145 						const exp = ldepPath.setExtension(".exp");
146 						if (copyDynamicLibDepsLinkerFiles && existsFile(exp))
147 							dynamicLibDepsFilesToCopy ~= NativePath(exp);
148 					}
149 				}
150 			}
151 			NativePath tpath;
152 			if (tt != TargetType.none) {
153 				if (buildTarget(settings, bs, ti.pack, ti.config, ti.packages, additional_dep_files, tpath))
154 					any_cached = true;
155 			}
156 			target_paths[target] = tpath;
157 		}
158 
159 		// build all targets
160 		if (settings.rdmd || rootTT == TargetType.staticLibrary) {
161 			// RDMD always builds everything at once and static libraries don't need their
162 			// dependencies to be built
163 			NativePath tpath;
164 			buildTarget(settings, root_ti.buildSettings.dup, m_project.rootPackage, root_ti.config, root_ti.packages, null, tpath);
165 			return;
166 		}
167 
168 		buildTargetRec(m_project.rootPackage.name);
169 
170 		if (dynamicLibDepsFilesToCopy.length) {
171 			const rootTargetPath = NativePath(root_ti.buildSettings.targetPath);
172 
173 			ensureDirectory(rootTargetPath);
174 			foreach (src; dynamicLibDepsFilesToCopy) {
175 				logDiagnostic("Copying target from %s to %s",
176 					src.toNativeString(), rootTargetPath.toNativeString());
177 				hardLinkFile(src, rootTargetPath ~ src.head, true);
178 			}
179 		}
180 
181 		if (any_cached) {
182 			logInfo("Finished", Color.green,
183 				"To force a rebuild of up-to-date targets, run again with --force"
184 			);
185 		}
186 	}
187 
188 	override void performPostGenerateActions(GeneratorSettings settings, in TargetInfo[string] targets)
189 	{
190 		// run the generated executable
191 		auto buildsettings = targets[m_project.rootPackage.name].buildSettings.dup;
192 		if (settings.run && !(buildsettings.options & BuildOption.syntaxOnly)) {
193 			NativePath exe_file_path;
194 			if (m_tempTargetExecutablePath.empty)
195 				exe_file_path = getTargetPath(buildsettings, settings);
196 			else
197 				exe_file_path = m_tempTargetExecutablePath ~ settings.compiler.getTargetFileName(buildsettings, settings.platform);
198 			runTarget(exe_file_path, buildsettings, settings.runArgs, settings);
199 		}
200 	}
201 
202 	private bool buildTarget(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, in Package[] packages, in NativePath[] additional_dep_files, out NativePath target_path)
203 	{
204 		import std.path : absolutePath;
205 
206 		auto cwd = NativePath(getcwd());
207 		bool generate_binary = !(buildsettings.options & BuildOption.syntaxOnly);
208 
209 		auto build_id = buildsettings.computeBuildID(config, settings);
210 
211 		// make all paths relative to shrink the command line
212 		string makeRelative(string path) { return shrinkPath(NativePath(path), cwd); }
213 		foreach (ref f; buildsettings.sourceFiles) f = makeRelative(f);
214 		foreach (ref p; buildsettings.importPaths) p = makeRelative(p);
215 		foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p);
216 
217 		// perform the actual build
218 		bool cached = false;
219 		if (settings.rdmd) performRDMDBuild(settings, buildsettings, pack, config, target_path);
220 		else if (settings.direct || !generate_binary) performDirectBuild(settings, buildsettings, pack, config, target_path);
221 		else cached = performCachedBuild(settings, buildsettings, pack, config, build_id, packages, additional_dep_files, target_path);
222 
223 		// HACK: cleanup dummy doc files, we shouldn't specialize on buildType
224 		// here and the compiler shouldn't need dummy doc output.
225 		if (settings.buildType == "ddox") {
226 			if ("__dummy.html".exists)
227 				removeFile("__dummy.html");
228 			if ("__dummy_docs".exists)
229 				rmdirRecurse("__dummy_docs");
230 		}
231 
232 		// run post-build commands
233 		if (!cached && buildsettings.postBuildCommands.length) {
234 			logInfo("Post-build", Color.light_green, "Running commands");
235 			runBuildCommands(CommandType.postBuild, buildsettings.postBuildCommands, pack, m_project, settings, buildsettings,
236 							 [["DUB_BUILD_PATH" : target_path is NativePath.init ? "" : target_path.parentPath.toNativeString.absolutePath]]);
237 		}
238 
239 		return cached;
240 	}
241 
242 	private bool performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config,
243 		string build_id, in Package[] packages, in NativePath[] additional_dep_files, out NativePath target_binary_path)
244 	{
245 		auto cwd = NativePath(getcwd());
246 
247 		NativePath target_path;
248 		if (settings.tempBuild) {
249 			string packageName = pack.basePackage is null ? pack.name : pack.basePackage.name;
250 			m_tempTargetExecutablePath = target_path = getTempDir() ~ format(".dub/build/%s-%s/%s/", packageName, pack.version_, build_id);
251 		}
252 		else
253 			target_path = packageCache(settings.cache, pack) ~ "build/" ~ build_id;
254 
255 		if (!settings.force && isUpToDate(target_path, buildsettings, settings, pack, packages, additional_dep_files)) {
256 			logInfo("Up-to-date", Color.green, "%s %s: target for configuration [%s] is up to date.",
257 				pack.name.color(Mode.bold), pack.version_, config.color(Color.blue));
258 			logDiagnostic("Using existing build in %s.", target_path.toNativeString());
259 			target_binary_path = target_path ~ settings.compiler.getTargetFileName(buildsettings, settings.platform);
260 			if (!settings.tempBuild)
261 				copyTargetFile(target_path, buildsettings, settings);
262 			return true;
263 		}
264 
265 		if (!isWritableDir(target_path, true)) {
266 			if (!settings.tempBuild)
267 				logInfo("Build directory %s is not writable. Falling back to direct build in the system's temp folder.", target_path.relativeTo(cwd).toNativeString());
268 			performDirectBuild(settings, buildsettings, pack, config, target_path);
269 			return false;
270 		}
271 
272 		logInfo("Building", Color.light_green, "%s %s: building configuration [%s]", pack.name.color(Mode.bold), pack.version_, config.color(Color.blue));
273 
274 		if( buildsettings.preBuildCommands.length ){
275 			logInfo("Pre-build", Color.light_green, "Running commands");
276 			runBuildCommands(CommandType.preBuild, buildsettings.preBuildCommands, pack, m_project, settings, buildsettings);
277 		}
278 
279 		// override target path
280 		auto cbuildsettings = buildsettings;
281 		cbuildsettings.targetPath = shrinkPath(target_path, cwd);
282 		buildWithCompiler(settings, cbuildsettings);
283 		target_binary_path = getTargetPath(cbuildsettings, settings);
284 
285 		if (!settings.tempBuild)
286 			copyTargetFile(target_path, buildsettings, settings);
287 
288 		return false;
289 	}
290 
291 	private void performRDMDBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config, out NativePath target_path)
292 	{
293 		auto cwd = NativePath(getcwd());
294 		//Added check for existence of [AppNameInPackagejson].d
295 		//If exists, use that as the starting file.
296 		NativePath mainsrc;
297 		if (buildsettings.mainSourceFile.length) {
298 			mainsrc = NativePath(buildsettings.mainSourceFile);
299 			if (!mainsrc.absolute) mainsrc = pack.path ~ mainsrc;
300 		} else {
301 			mainsrc = getMainSourceFile(pack);
302 			logWarn(`Package has no "mainSourceFile" defined. Using best guess: %s`, mainsrc.relativeTo(pack.path).toNativeString());
303 		}
304 
305 		// do not pass all source files to RDMD, only the main source file
306 		buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(s => !s.endsWith(".d"))().array();
307 		settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine);
308 
309 		auto generate_binary = !buildsettings.dflags.canFind("-o-");
310 
311 		// Create start script, which will be used by the calling bash/cmd script.
312 		// build "rdmd --force %DFLAGS% -I%~dp0..\source -Jviews -Isource @deps.txt %LIBS% source\app.d" ~ application arguments
313 		// or with "/" instead of "\"
314 		bool tmp_target = false;
315 		if (generate_binary) {
316 			if (settings.tempBuild || (settings.run && !isWritableDir(NativePath(buildsettings.targetPath), true))) {
317 				import std.random;
318 				auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-";
319 				auto tmpdir = getTempDir()~".rdmd/source/";
320 				buildsettings.targetPath = tmpdir.toNativeString();
321 				buildsettings.targetName = rnd ~ buildsettings.targetName;
322 				m_temporaryFiles ~= tmpdir;
323 				tmp_target = true;
324 			}
325 			target_path = getTargetPath(buildsettings, settings);
326 			settings.compiler.setTarget(buildsettings, settings.platform);
327 		}
328 
329 		logDiagnostic("Application output name is '%s'", settings.compiler.getTargetFileName(buildsettings, settings.platform));
330 
331 		string[] flags = ["--build-only", "--compiler="~settings.platform.compilerBinary];
332 		if (settings.force) flags ~= "--force";
333 		flags ~= buildsettings.dflags;
334 		flags ~= mainsrc.relativeTo(cwd).toNativeString();
335 
336 		if (buildsettings.preBuildCommands.length){
337 			logInfo("Pre-build", Color.light_green, "Running commands");
338 			runCommands(buildsettings.preBuildCommands);
339 		}
340 
341 		logInfo("Building", Color.light_green, "%s %s [%s]", pack.name.color(Mode.bold), pack.version_, config.color(Color.blue));
342 
343 		logInfo("Running rdmd...");
344 		logDiagnostic("rdmd %s", join(flags, " "));
345 		auto rdmd_pid = spawnProcess("rdmd" ~ flags);
346 		auto result = rdmd_pid.wait();
347 		enforce(result == 0, "Build command failed with exit code "~to!string(result));
348 
349 		if (tmp_target) {
350 			m_temporaryFiles ~= target_path;
351 			foreach (f; buildsettings.copyFiles)
352 				m_temporaryFiles ~= NativePath(buildsettings.targetPath).parentPath ~ NativePath(f).head;
353 		}
354 	}
355 
356 	private void performDirectBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config, out NativePath target_path)
357 	{
358 		auto cwd = NativePath(getcwd());
359 		auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly);
360 
361 		// make file paths relative to shrink the command line
362 		foreach (ref f; buildsettings.sourceFiles) {
363 			auto fp = NativePath(f);
364 			if( fp.absolute ) fp = fp.relativeTo(cwd);
365 			f = fp.toNativeString();
366 		}
367 
368 		logInfo("Building", Color.light_green, "%s %s [%s]", pack.name.color(Mode.bold), pack.version_, config.color(Color.blue));
369 
370 		// make all target/import paths relative
371 		string makeRelative(string path) {
372 			auto p = NativePath(path);
373 			// storing in a separate temprary to work around #601
374 			auto prel = p.absolute ? p.relativeTo(cwd) : p;
375 			return prel.toNativeString();
376 		}
377 		buildsettings.targetPath = makeRelative(buildsettings.targetPath);
378 		foreach (ref p; buildsettings.importPaths) p = makeRelative(p);
379 		foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p);
380 
381 		bool is_temp_target = false;
382 		if (generate_binary) {
383 			if (settings.tempBuild || (settings.run && !isWritableDir(NativePath(buildsettings.targetPath), true))) {
384 				import std.random;
385 				auto rnd = to!string(uniform(uint.min, uint.max));
386 				auto tmppath = getTempDir()~("dub/"~rnd~"/");
387 				buildsettings.targetPath = tmppath.toNativeString();
388 				m_temporaryFiles ~= tmppath;
389 				is_temp_target = true;
390 			}
391 			target_path = getTargetPath(buildsettings, settings);
392 		}
393 
394 		if( buildsettings.preBuildCommands.length ){
395 			logInfo("Pre-build", Color.light_green, "Running commands");
396 			runBuildCommands(CommandType.preBuild, buildsettings.preBuildCommands, pack, m_project, settings, buildsettings);
397 		}
398 
399 		buildWithCompiler(settings, buildsettings);
400 
401 		if (is_temp_target) {
402 			m_temporaryFiles ~= target_path;
403 			foreach (f; buildsettings.copyFiles)
404 				m_temporaryFiles ~= NativePath(buildsettings.targetPath).parentPath ~ NativePath(f).head;
405 		}
406 	}
407 
408 	private void copyTargetFile(in NativePath build_path, in BuildSettings buildsettings, in GeneratorSettings settings)
409 	{
410 		ensureDirectory(NativePath(buildsettings.targetPath));
411 
412 		string[] filenames = [
413 			settings.compiler.getTargetFileName(buildsettings, settings.platform)
414 		];
415 
416 		// Windows: add .pdb (for executables and DLLs) and/or import .lib & .exp (for DLLs) if found
417 		if (settings.platform.isWindows()) {
418 			void addIfFound(string extension) {
419 				import std.path : setExtension;
420 				const candidate = filenames[0].setExtension(extension);
421 				if (existsFile(build_path ~ candidate))
422 					filenames ~= candidate;
423 			}
424 
425 			const tt = buildsettings.targetType;
426 			if (tt == TargetType.executable || tt == TargetType.dynamicLibrary)
427 				addIfFound(".pdb");
428 
429 			if (tt == TargetType.dynamicLibrary) {
430 				addIfFound(".lib");
431 				addIfFound(".exp");
432 			}
433 		}
434 
435 		foreach (filename; filenames)
436 		{
437 			auto src = build_path ~ filename;
438 			logDiagnostic("Copying target from %s to %s", src.toNativeString(), buildsettings.targetPath);
439 			hardLinkFile(src, NativePath(buildsettings.targetPath) ~ filename, true);
440 		}
441 	}
442 
443 	private bool isUpToDate(NativePath target_path, BuildSettings buildsettings, GeneratorSettings settings, in Package main_pack, in Package[] packages, in NativePath[] additional_dep_files)
444 	{
445 		import std.datetime;
446 
447 		auto targetfile = target_path ~ settings.compiler.getTargetFileName(buildsettings, settings.platform);
448 		if (!existsFile(targetfile)) {
449 			logDiagnostic("Target '%s' doesn't exist, need rebuild.", targetfile.toNativeString());
450 			return false;
451 		}
452 		auto targettime = getFileInfo(targetfile).timeModified;
453 
454 		auto allfiles = appender!(string[]);
455 		allfiles ~= buildsettings.sourceFiles;
456 		allfiles ~= buildsettings.importFiles;
457 		allfiles ~= buildsettings.stringImportFiles;
458 		allfiles ~= buildsettings.extraDependencyFiles;
459 		// TODO: add library files
460 		foreach (p; packages)
461 			allfiles ~= (p.recipePath != NativePath.init ? p : p.basePackage).recipePath.toNativeString();
462 		foreach (f; additional_dep_files) allfiles ~= f.toNativeString();
463 		bool checkSelectedVersions = !settings.single;
464 		if (checkSelectedVersions && main_pack is m_project.rootPackage && m_project.rootPackage.getAllDependencies().length > 0)
465 			allfiles ~= (main_pack.path ~ SelectedVersions.defaultFile).toNativeString();
466 
467 		foreach (file; allfiles.data) {
468 			if (!existsFile(file)) {
469 				logDiagnostic("File %s doesn't exist, triggering rebuild.", file);
470 				return false;
471 			}
472 			auto ftime = getFileInfo(file).timeModified;
473 			if (ftime > Clock.currTime)
474 				logWarn("File '%s' was modified in the future. Please re-save.", file);
475 			if (ftime > targettime) {
476 				logDiagnostic("File '%s' modified, need rebuild.", file);
477 				return false;
478 			}
479 		}
480 		return true;
481 	}
482 
483 	/// Output an unique name to represent the source file.
484 	/// Calls with path that resolve to the same file on the filesystem will return the same,
485 	/// unless they include different symbolic links (which are not resolved).
486 
487 	static string pathToObjName(const scope ref BuildPlatform platform, string path)
488 	{
489 		import std.digest.crc : crc32Of;
490 		import std.path : buildNormalizedPath, dirSeparator, relativePath, stripDrive;
491 		if (path.endsWith(".d")) path = path[0 .. $-2];
492 		auto ret = buildNormalizedPath(getcwd(), path).replace(dirSeparator, ".");
493 		auto idx = ret.lastIndexOf('.');
494 		const objSuffix = getObjSuffix(platform);
495 		return idx < 0 ? ret ~ objSuffix : format("%s_%(%02x%)%s", ret[idx+1 .. $], crc32Of(ret[0 .. idx]), objSuffix);
496 	}
497 
498 	/// Compile a single source file (srcFile), and write the object to objName.
499 	static string compileUnit(string srcFile, string objName, BuildSettings bs, GeneratorSettings gs) {
500 		NativePath tempobj = NativePath(bs.targetPath)~objName;
501 		string objPath = tempobj.toNativeString();
502 		bs.libs = null;
503 		bs.lflags = null;
504 		bs.sourceFiles = [ srcFile ];
505 		bs.targetType = TargetType.object;
506 		gs.compiler.prepareBuildSettings(bs, gs.platform, BuildSetting.commandLine);
507 		gs.compiler.setTarget(bs, gs.platform, objPath);
508 		gs.compiler.invoke(bs, gs.platform, gs.compileCallback);
509 		return objPath;
510 	}
511 
512 	private void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings)
513 	{
514 		auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly);
515 		auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library;
516 
517 		scope (failure) {
518 			logDiagnostic("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType);
519 			auto tpath = getTargetPath(buildsettings, settings);
520 			if (generate_binary && existsFile(tpath))
521 				removeFile(tpath);
522 		}
523 		if (settings.buildMode == BuildMode.singleFile && generate_binary) {
524 			import std.parallelism, std.range : walkLength;
525 
526 			auto lbuildsettings = buildsettings;
527 			auto srcs = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f));
528 			auto objs = new string[](srcs.walkLength);
529 
530 			void compileSource(size_t i, string src) {
531 				logInfo("Compiling", Color.light_green, "%s", src);
532 				const objPath = pathToObjName(settings.platform, src);
533 				objs[i] = compileUnit(src, objPath, buildsettings, settings);
534 			}
535 
536 			if (settings.parallelBuild) {
537 				foreach (i, src; srcs.parallel(1)) compileSource(i, src);
538 			} else {
539 				foreach (i, src; srcs.array) compileSource(i, src);
540 			}
541 
542 			logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold));
543 			lbuildsettings.sourceFiles = is_static_library ? [] : lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array;
544 			settings.compiler.setTarget(lbuildsettings, settings.platform);
545 			settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles);
546 			settings.compiler.invokeLinker(lbuildsettings, settings.platform, objs, settings.linkCallback);
547 
548 		// NOTE: separate compile/link is not yet enabled for GDC.
549 		} else if (generate_binary && (settings.buildMode == BuildMode.allAtOnce || settings.compiler.name == "gdc" || is_static_library)) {
550 			// don't include symbols of dependencies (will be included by the top level target)
551 			if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array;
552 
553 			// setup for command line
554 			settings.compiler.setTarget(buildsettings, settings.platform);
555 			settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine);
556 
557 			// invoke the compiler
558 			settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback);
559 		} else {
560 			// determine path for the temporary object file
561 			string tempobjname = buildsettings.targetName ~ getObjSuffix(settings.platform);
562 			NativePath tempobj = NativePath(buildsettings.targetPath) ~ tempobjname;
563 
564 			// setup linker command line
565 			auto lbuildsettings = buildsettings;
566 			lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array;
567 			if (generate_binary) settings.compiler.setTarget(lbuildsettings, settings.platform);
568 			settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles);
569 
570 			// setup compiler command line
571 			buildsettings.libs = null;
572 			buildsettings.lflags = null;
573 			if (generate_binary) buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString());
574 			buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array;
575 
576 			settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine);
577 
578 			settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback);
579 
580 			if (generate_binary) {
581 				if (settings.tempBuild) {
582 					logInfo("Linking", Color.light_green, "%s => %s", buildsettings.targetName.color(Mode.bold), buildsettings.getTargetPath(settings));
583 				} else {
584 					logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold));
585 				}
586 				settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()], settings.linkCallback);
587 			}
588 		}
589 	}
590 
591 	private void runTarget(NativePath exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings)
592 	{
593 		if (buildsettings.targetType == TargetType.executable) {
594 			auto cwd = NativePath(getcwd());
595 			auto runcwd = cwd;
596 			if (buildsettings.workingDirectory.length) {
597 				runcwd = NativePath(buildsettings.workingDirectory);
598 				if (!runcwd.absolute) runcwd = cwd ~ runcwd;
599 			}
600 			if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path;
601 			runPreRunCommands(m_project.rootPackage, m_project, settings, buildsettings);
602 			logInfo("Running", Color.green, "%s %s", exe_file_path.relativeTo(runcwd), run_args.join(" "));
603 			string[string] env;
604 			foreach (aa; [buildsettings.environments, buildsettings.runEnvironments])
605 				foreach (k, v; aa)
606 					env[k] = v;
607 			if (settings.runCallback) {
608 				auto res = execute([ exe_file_path.toNativeString() ] ~ run_args,
609 						   env, Config.none, size_t.max, runcwd.toNativeString());
610 				settings.runCallback(res.status, res.output);
611 				settings.targetExitStatus = res.status;
612 				runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings);
613 			} else {
614 				auto prg_pid = spawnProcess([ exe_file_path.toNativeString() ] ~ run_args,
615 								env, Config.none, runcwd.toNativeString());
616 				auto result = prg_pid.wait();
617 				settings.targetExitStatus = result;
618 				runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings);
619 				enforce(result == 0, "Program exited with code "~to!string(result));
620 			}
621 		} else
622 			enforce(false, "Target is a library. Skipping execution.");
623 	}
624 
625 	private void runPreRunCommands(in Package pack, in Project proj, in GeneratorSettings settings,
626 		in BuildSettings buildsettings)
627 	{
628 		if (buildsettings.preRunCommands.length) {
629 			logInfo("Pre-run", Color.light_green, "Running commands...");
630 			runBuildCommands(CommandType.preRun, buildsettings.preRunCommands, pack, proj, settings, buildsettings);
631 		}
632 	}
633 
634 	private void runPostRunCommands(in Package pack, in Project proj, in GeneratorSettings settings,
635 		in BuildSettings buildsettings)
636 	{
637 		if (buildsettings.postRunCommands.length) {
638 			logInfo("Post-run", Color.light_green, "Running commands...");
639 			runBuildCommands(CommandType.postRun, buildsettings.postRunCommands, pack, proj, settings, buildsettings);
640 		}
641 	}
642 
643 	private void cleanupTemporaries()
644 	{
645 		foreach_reverse (f; m_temporaryFiles) {
646 			try {
647 				if (f.endsWithSlash) rmdir(f.toNativeString());
648 				else remove(f.toNativeString());
649 			} catch (Exception e) {
650 				logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg);
651 				logDiagnostic("Full error: %s", e.toString().sanitize);
652 			}
653 		}
654 		m_temporaryFiles = null;
655 	}
656 }
657 
658 /**
659  * Provides a unique (per build) identifier
660  *
661  * When building a package, it is important to have a unique but stable
662  * identifier to differentiate builds and allow their caching.
663  * This function provides such an identifier.
664  * Example:
665  * ```
666  * application-debug-linux.posix-x86_64-dmd_v2.100.2-D80285212AEC1FF9855F18AD52C68B9EEB5C7690609C224575F920096FB1965B
667  * ```
668  */
669 private string computeBuildID(in BuildSettings buildsettings, string config, GeneratorSettings settings)
670 {
671 	const(string[])[] hashing = [
672 		buildsettings.versions,
673 		buildsettings.debugVersions,
674 		buildsettings.dflags,
675 		buildsettings.lflags,
676 		buildsettings.stringImportPaths,
677 		buildsettings.importPaths,
678 		settings.platform.architecture,
679 		[
680 			(cast(uint)(buildsettings.options & ~BuildOption.color)).to!string, // exclude color option from id
681 			settings.platform.compilerBinary,
682 			settings.platform.compiler,
683 			settings.platform.compilerVersion,
684 		],
685 	];
686 
687 	return computeBuildName(config, settings, hashing);
688 }
689 
690 private NativePath getMainSourceFile(in Package prj)
691 {
692 	foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"])
693 		if (existsFile(prj.path ~ f))
694 			return prj.path ~ f;
695 	return prj.path ~ "source/app.d";
696 }
697 
698 private NativePath getTargetPath(const scope ref BuildSettings bs, const scope ref GeneratorSettings settings)
699 {
700 	return NativePath(bs.targetPath) ~ settings.compiler.getTargetFileName(bs, settings.platform);
701 }
702 
703 private string shrinkPath(NativePath path, NativePath base)
704 {
705 	auto orig = path.toNativeString();
706 	if (!path.absolute) return orig;
707 	version (Windows)
708 	{
709 		// avoid relative paths starting with `..\`: https://github.com/dlang/dub/issues/2143
710 		if (!path.startsWith(base)) return orig;
711 	}
712 	auto rel = path.relativeTo(base).toNativeString();
713 	return rel.length < orig.length ? rel : orig;
714 }
715 
716 unittest {
717 	assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo")) == NativePath("bar/baz").toNativeString());
718 	version (Windows)
719 		assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("/foo/bar/baz").toNativeString());
720 	else
721 		assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("../bar/baz").toNativeString());
722 	assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/")) == NativePath("/foo/bar/baz").toNativeString());
723 	assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/baz")) == NativePath("/foo/bar/baz").toNativeString());
724 }
725 
726 unittest { // issue #1235 - pass no library files to compiler command line when building a static lib
727 	import dub.internal.vibecompat.data.json : parseJsonString;
728 	import dub.compilers.gdc : GDCCompiler;
729 	import dub.platform : determinePlatform;
730 
731 	version (Windows) auto libfile = "bar.lib";
732 	else auto libfile = "bar.a";
733 
734 	auto desc = parseJsonString(`{"name": "test", "targetType": "library", "sourceFiles": ["foo.d", "`~libfile~`"]}`);
735 	auto pack = new Package(desc, NativePath("/tmp/fooproject"));
736 	auto pman = new PackageManager(pack.path, NativePath("/tmp/foo/"), NativePath("/tmp/foo/"), false);
737 	auto prj = new Project(pman, pack);
738 
739 	final static class TestCompiler : GDCCompiler {
740 		override void invoke(in BuildSettings settings, in BuildPlatform platform, void delegate(int, string) output_callback) {
741 			assert(!settings.dflags[].any!(f => f.canFind("bar")));
742 		}
743 		override void invokeLinker(in BuildSettings settings, in BuildPlatform platform, string[] objects, void delegate(int, string) output_callback) {
744 			assert(false);
745 		}
746 	}
747 
748 	GeneratorSettings settings;
749 	settings.platform = BuildPlatform(determinePlatform(), ["x86"], "gdc", "test", 2075);
750 	settings.compiler = new TestCompiler;
751 	settings.config = "library";
752 	settings.buildType = "debug";
753 	settings.tempBuild = true;
754 
755 	auto gen = new BuildGenerator(prj);
756 	gen.generate(settings);
757 }