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 = settings.toolWorkingDirectory;
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
237 								? ""
238 								: target_path.parentPath.toNativeString.absolutePath(settings.toolWorkingDirectory.toNativeString)]]);
239 		}
240 
241 		return cached;
242 	}
243 
244 	private bool performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config,
245 		string build_id, in Package[] packages, in NativePath[] additional_dep_files, out NativePath target_binary_path)
246 	{
247 		auto cwd = settings.toolWorkingDirectory;
248 
249 		NativePath target_path;
250 		if (settings.tempBuild) {
251 			string packageName = pack.basePackage is null ? pack.name : pack.basePackage.name;
252 			m_tempTargetExecutablePath = target_path = getTempDir() ~ format(".dub/build/%s-%s/%s/", packageName, pack.version_, build_id);
253 		}
254 		else
255 			target_path = packageCache(settings.cache, pack) ~ "build/" ~ build_id;
256 
257 		if (!settings.force && isUpToDate(target_path, buildsettings, settings, pack, packages, additional_dep_files)) {
258 			logInfo("Up-to-date", Color.green, "%s %s: target for configuration [%s] is up to date.",
259 				pack.name.color(Mode.bold), pack.version_, config.color(Color.blue));
260 			logDiagnostic("Using existing build in %s.", target_path.toNativeString());
261 			target_binary_path = target_path ~ settings.compiler.getTargetFileName(buildsettings, settings.platform);
262 			if (!settings.tempBuild)
263 				copyTargetFile(target_path, buildsettings, settings);
264 			return true;
265 		}
266 
267 		if (!isWritableDir(target_path, true)) {
268 			if (!settings.tempBuild)
269 				logInfo("Build directory %s is not writable. Falling back to direct build in the system's temp folder.", target_path.relativeTo(cwd).toNativeString());
270 			performDirectBuild(settings, buildsettings, pack, config, target_path);
271 			return false;
272 		}
273 
274 		logInfo("Building", Color.light_green, "%s %s: building configuration [%s]", pack.name.color(Mode.bold), pack.version_, config.color(Color.blue));
275 
276 		if( buildsettings.preBuildCommands.length ){
277 			logInfo("Pre-build", Color.light_green, "Running commands");
278 			runBuildCommands(CommandType.preBuild, buildsettings.preBuildCommands, pack, m_project, settings, buildsettings);
279 		}
280 
281 		// override target path
282 		auto cbuildsettings = buildsettings;
283 		cbuildsettings.targetPath = shrinkPath(target_path, cwd);
284 		buildWithCompiler(settings, cbuildsettings);
285 		target_binary_path = getTargetPath(cbuildsettings, settings);
286 
287 		if (!settings.tempBuild)
288 			copyTargetFile(target_path, buildsettings, settings);
289 
290 		return false;
291 	}
292 
293 	private void performRDMDBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config, out NativePath target_path)
294 	{
295 		auto cwd = settings.toolWorkingDirectory;
296 		//Added check for existence of [AppNameInPackagejson].d
297 		//If exists, use that as the starting file.
298 		NativePath mainsrc;
299 		if (buildsettings.mainSourceFile.length) {
300 			mainsrc = NativePath(buildsettings.mainSourceFile);
301 			if (!mainsrc.absolute) mainsrc = pack.path ~ mainsrc;
302 		} else {
303 			mainsrc = getMainSourceFile(pack);
304 			logWarn(`Package has no "mainSourceFile" defined. Using best guess: %s`, mainsrc.relativeTo(pack.path).toNativeString());
305 		}
306 
307 		// do not pass all source files to RDMD, only the main source file
308 		buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(s => !s.endsWith(".d"))().array();
309 		settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine);
310 
311 		auto generate_binary = !buildsettings.dflags.canFind("-o-");
312 
313 		// Create start script, which will be used by the calling bash/cmd script.
314 		// build "rdmd --force %DFLAGS% -I%~dp0..\source -Jviews -Isource @deps.txt %LIBS% source\app.d" ~ application arguments
315 		// or with "/" instead of "\"
316 		bool tmp_target = false;
317 		if (generate_binary) {
318 			if (settings.tempBuild || (settings.run && !isWritableDir(NativePath(buildsettings.targetPath), true))) {
319 				import std.random;
320 				auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-";
321 				auto tmpdir = getTempDir()~".rdmd/source/";
322 				buildsettings.targetPath = tmpdir.toNativeString();
323 				buildsettings.targetName = rnd ~ buildsettings.targetName;
324 				m_temporaryFiles ~= tmpdir;
325 				tmp_target = true;
326 			}
327 			target_path = getTargetPath(buildsettings, settings);
328 			settings.compiler.setTarget(buildsettings, settings.platform);
329 		}
330 
331 		logDiagnostic("Application output name is '%s'", settings.compiler.getTargetFileName(buildsettings, settings.platform));
332 
333 		string[] flags = ["--build-only", "--compiler="~settings.platform.compilerBinary];
334 		if (settings.force) flags ~= "--force";
335 		flags ~= buildsettings.dflags;
336 		flags ~= mainsrc.relativeTo(cwd).toNativeString();
337 
338 		if (buildsettings.preBuildCommands.length){
339 			logInfo("Pre-build", Color.light_green, "Running commands");
340 			runCommands(buildsettings.preBuildCommands, null, cwd.toNativeString());
341 		}
342 
343 		logInfo("Building", Color.light_green, "%s %s [%s]", pack.name.color(Mode.bold), pack.version_, config.color(Color.blue));
344 
345 		logInfo("Running rdmd...");
346 		logDiagnostic("rdmd %s", join(flags, " "));
347 		auto rdmd_pid = spawnProcess("rdmd" ~ flags, null, Config.none, cwd.toNativeString());
348 		auto result = rdmd_pid.wait();
349 		enforce(result == 0, "Build command failed with exit code "~to!string(result));
350 
351 		if (tmp_target) {
352 			m_temporaryFiles ~= target_path;
353 			foreach (f; buildsettings.copyFiles)
354 				m_temporaryFiles ~= NativePath(buildsettings.targetPath).parentPath ~ NativePath(f).head;
355 		}
356 	}
357 
358 	private void performDirectBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config, out NativePath target_path)
359 	{
360 		auto cwd = settings.toolWorkingDirectory;
361 		auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly);
362 
363 		// make file paths relative to shrink the command line
364 		foreach (ref f; buildsettings.sourceFiles) {
365 			auto fp = NativePath(f);
366 			if( fp.absolute ) fp = fp.relativeTo(cwd);
367 			f = fp.toNativeString();
368 		}
369 
370 		logInfo("Building", Color.light_green, "%s %s [%s]", pack.name.color(Mode.bold), pack.version_, config.color(Color.blue));
371 
372 		// make all target/import paths relative
373 		string makeRelative(string path) {
374 			auto p = NativePath(path);
375 			// storing in a separate temprary to work around #601
376 			auto prel = p.absolute ? p.relativeTo(cwd) : p;
377 			return prel.toNativeString();
378 		}
379 		buildsettings.targetPath = makeRelative(buildsettings.targetPath);
380 		foreach (ref p; buildsettings.importPaths) p = makeRelative(p);
381 		foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p);
382 
383 		bool is_temp_target = false;
384 		if (generate_binary) {
385 			if (settings.tempBuild || (settings.run && !isWritableDir(NativePath(buildsettings.targetPath), true))) {
386 				import std.random;
387 				auto rnd = to!string(uniform(uint.min, uint.max));
388 				auto tmppath = getTempDir()~("dub/"~rnd~"/");
389 				buildsettings.targetPath = tmppath.toNativeString();
390 				m_temporaryFiles ~= tmppath;
391 				is_temp_target = true;
392 			}
393 			target_path = getTargetPath(buildsettings, settings);
394 		}
395 
396 		if( buildsettings.preBuildCommands.length ){
397 			logInfo("Pre-build", Color.light_green, "Running commands");
398 			runBuildCommands(CommandType.preBuild, buildsettings.preBuildCommands, pack, m_project, settings, buildsettings);
399 		}
400 
401 		buildWithCompiler(settings, buildsettings);
402 
403 		if (is_temp_target) {
404 			m_temporaryFiles ~= target_path;
405 			foreach (f; buildsettings.copyFiles)
406 				m_temporaryFiles ~= NativePath(buildsettings.targetPath).parentPath ~ NativePath(f).head;
407 		}
408 	}
409 
410 	private void copyTargetFile(in NativePath build_path, in BuildSettings buildsettings, in GeneratorSettings settings)
411 	{
412 		ensureDirectory(NativePath(buildsettings.targetPath));
413 
414 		string[] filenames = [
415 			settings.compiler.getTargetFileName(buildsettings, settings.platform)
416 		];
417 
418 		// Windows: add .pdb (for executables and DLLs) and/or import .lib & .exp (for DLLs) if found
419 		if (settings.platform.isWindows()) {
420 			void addIfFound(string extension) {
421 				import std.path : setExtension;
422 				const candidate = filenames[0].setExtension(extension);
423 				if (existsFile(build_path ~ candidate))
424 					filenames ~= candidate;
425 			}
426 
427 			const tt = buildsettings.targetType;
428 			if (tt == TargetType.executable || tt == TargetType.dynamicLibrary)
429 				addIfFound(".pdb");
430 
431 			if (tt == TargetType.dynamicLibrary) {
432 				addIfFound(".lib");
433 				addIfFound(".exp");
434 			}
435 		}
436 
437 		foreach (filename; filenames)
438 		{
439 			auto src = build_path ~ filename;
440 			logDiagnostic("Copying target from %s to %s", src.toNativeString(), buildsettings.targetPath);
441 			hardLinkFile(src, NativePath(buildsettings.targetPath) ~ filename, true);
442 		}
443 	}
444 
445 	private bool isUpToDate(NativePath target_path, BuildSettings buildsettings, GeneratorSettings settings, in Package main_pack, in Package[] packages, in NativePath[] additional_dep_files)
446 	{
447 		import std.datetime;
448 
449 		auto targetfile = target_path ~ settings.compiler.getTargetFileName(buildsettings, settings.platform);
450 		if (!existsFile(targetfile)) {
451 			logDiagnostic("Target '%s' doesn't exist, need rebuild.", targetfile.toNativeString());
452 			return false;
453 		}
454 		auto targettime = getFileInfo(targetfile).timeModified;
455 
456 		auto allfiles = appender!(string[]);
457 		allfiles ~= buildsettings.sourceFiles;
458 		allfiles ~= buildsettings.importFiles;
459 		allfiles ~= buildsettings.stringImportFiles;
460 		allfiles ~= buildsettings.extraDependencyFiles;
461 		// TODO: add library files
462 		foreach (p; packages)
463 			allfiles ~= (p.recipePath != NativePath.init ? p : p.basePackage).recipePath.toNativeString();
464 		foreach (f; additional_dep_files) allfiles ~= f.toNativeString();
465 		bool checkSelectedVersions = !settings.single;
466 		if (checkSelectedVersions && main_pack is m_project.rootPackage && m_project.rootPackage.getAllDependencies().length > 0)
467 			allfiles ~= (main_pack.path ~ SelectedVersions.defaultFile).toNativeString();
468 
469 		foreach (file; allfiles.data) {
470 			if (!existsFile(file)) {
471 				logDiagnostic("File %s doesn't exist, triggering rebuild.", file);
472 				return false;
473 			}
474 			auto ftime = getFileInfo(file).timeModified;
475 			if (ftime > Clock.currTime)
476 				logWarn("File '%s' was modified in the future. Please re-save.", file);
477 			if (ftime > targettime) {
478 				logDiagnostic("File '%s' modified, need rebuild.", file);
479 				return false;
480 			}
481 		}
482 		return true;
483 	}
484 
485 	/// Output an unique name to represent the source file.
486 	/// Calls with path that resolve to the same file on the filesystem will return the same,
487 	/// unless they include different symbolic links (which are not resolved).
488 	deprecated("Use the overload taking in the current working directory")
489 	static string pathToObjName(const scope ref BuildPlatform platform, string path)
490 	{
491 		return pathToObjName(platform, path, getWorkingDirectory);
492 	}
493 
494 	/// ditto
495 	static string pathToObjName(const scope ref BuildPlatform platform, string path, NativePath cwd)
496 	{
497 		import std.digest.crc : crc32Of;
498 		import std.path : buildNormalizedPath, dirSeparator, relativePath, stripDrive;
499 		if (path.endsWith(".d")) path = path[0 .. $-2];
500 		auto ret = buildNormalizedPath(cwd.toNativeString(), path).replace(dirSeparator, ".");
501 		auto idx = ret.lastIndexOf('.');
502 		const objSuffix = getObjSuffix(platform);
503 		return idx < 0 ? ret ~ objSuffix : format("%s_%(%02x%)%s", ret[idx+1 .. $], crc32Of(ret[0 .. idx]), objSuffix);
504 	}
505 
506 	/// Compile a single source file (srcFile), and write the object to objName.
507 	static string compileUnit(string srcFile, string objName, BuildSettings bs, GeneratorSettings gs) {
508 		NativePath tempobj = NativePath(bs.targetPath)~objName;
509 		string objPath = tempobj.toNativeString();
510 		bs.libs = null;
511 		bs.lflags = null;
512 		bs.sourceFiles = [ srcFile ];
513 		bs.targetType = TargetType.object;
514 		gs.compiler.prepareBuildSettings(bs, gs.platform, BuildSetting.commandLine);
515 		gs.compiler.setTarget(bs, gs.platform, objPath);
516 		gs.compiler.invoke(bs, gs.platform, gs.compileCallback, gs.toolWorkingDirectory);
517 		return objPath;
518 	}
519 
520 	private void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings)
521 	{
522 		auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly);
523 		auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library;
524 
525 		scope (failure) {
526 			logDiagnostic("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType);
527 			auto tpath = getTargetPath(buildsettings, settings);
528 			if (generate_binary && existsFile(tpath))
529 				removeFile(tpath);
530 		}
531 		if (settings.buildMode == BuildMode.singleFile && generate_binary) {
532 			import std.parallelism, std.range : walkLength;
533 
534 			auto lbuildsettings = buildsettings;
535 			auto srcs = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f));
536 			auto objs = new string[](srcs.walkLength);
537 
538 			void compileSource(size_t i, string src) {
539 				logInfo("Compiling", Color.light_green, "%s", src);
540 				const objPath = pathToObjName(settings.platform, src, settings.toolWorkingDirectory);
541 				objs[i] = compileUnit(src, objPath, buildsettings, settings);
542 			}
543 
544 			if (settings.parallelBuild) {
545 				foreach (i, src; srcs.parallel(1)) compileSource(i, src);
546 			} else {
547 				foreach (i, src; srcs.array) compileSource(i, src);
548 			}
549 
550 			logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold));
551 			lbuildsettings.sourceFiles = is_static_library ? [] : lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array;
552 			settings.compiler.setTarget(lbuildsettings, settings.platform);
553 			settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles);
554 			settings.compiler.invokeLinker(lbuildsettings, settings.platform, objs, settings.linkCallback, settings.toolWorkingDirectory);
555 
556 		// NOTE: separate compile/link is not yet enabled for GDC.
557 		} else if (generate_binary && (settings.buildMode == BuildMode.allAtOnce || settings.compiler.name == "gdc" || is_static_library)) {
558 			// don't include symbols of dependencies (will be included by the top level target)
559 			if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array;
560 
561 			// setup for command line
562 			settings.compiler.setTarget(buildsettings, settings.platform);
563 			settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine);
564 
565 			// invoke the compiler
566 			settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback, settings.toolWorkingDirectory);
567 		} else {
568 			// determine path for the temporary object file
569 			string tempobjname = buildsettings.targetName ~ getObjSuffix(settings.platform);
570 			NativePath tempobj = NativePath(buildsettings.targetPath) ~ tempobjname;
571 
572 			// setup linker command line
573 			auto lbuildsettings = buildsettings;
574 			lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array;
575 			if (generate_binary) settings.compiler.setTarget(lbuildsettings, settings.platform);
576 			settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles);
577 
578 			// setup compiler command line
579 			buildsettings.libs = null;
580 			buildsettings.lflags = null;
581 			if (generate_binary) buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString());
582 			buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array;
583 
584 			settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine);
585 
586 			settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback, settings.toolWorkingDirectory);
587 
588 			if (generate_binary) {
589 				if (settings.tempBuild) {
590 					logInfo("Linking", Color.light_green, "%s => %s", buildsettings.targetName.color(Mode.bold), buildsettings.getTargetPath(settings));
591 				} else {
592 					logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold));
593 				}
594 				settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()], settings.linkCallback, settings.toolWorkingDirectory);
595 			}
596 		}
597 	}
598 
599 	private void runTarget(NativePath exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings)
600 	{
601 		if (buildsettings.targetType == TargetType.executable) {
602 			auto cwd = settings.toolWorkingDirectory;
603 			auto runcwd = cwd;
604 			if (buildsettings.workingDirectory.length) {
605 				runcwd = NativePath(buildsettings.workingDirectory);
606 				if (!runcwd.absolute) runcwd = cwd ~ runcwd;
607 			}
608 			if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path;
609 			runPreRunCommands(m_project.rootPackage, m_project, settings, buildsettings);
610 			logInfo("Running", Color.green, "%s %s", exe_file_path.relativeTo(runcwd), run_args.join(" "));
611 			string[string] env;
612 			foreach (aa; [buildsettings.environments, buildsettings.runEnvironments])
613 				foreach (k, v; aa)
614 					env[k] = v;
615 			if (settings.runCallback) {
616 				auto res = execute([ exe_file_path.toNativeString() ] ~ run_args,
617 						   env, Config.none, size_t.max, runcwd.toNativeString());
618 				settings.runCallback(res.status, res.output);
619 				settings.targetExitStatus = res.status;
620 				runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings);
621 			} else {
622 				auto prg_pid = spawnProcess([ exe_file_path.toNativeString() ] ~ run_args,
623 								env, Config.none, runcwd.toNativeString());
624 				auto result = prg_pid.wait();
625 				settings.targetExitStatus = result;
626 				runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings);
627 				enforce(result == 0, "Program exited with code "~to!string(result));
628 			}
629 		} else
630 			enforce(false, "Target is a library. Skipping execution.");
631 	}
632 
633 	private void runPreRunCommands(in Package pack, in Project proj, in GeneratorSettings settings,
634 		in BuildSettings buildsettings)
635 	{
636 		if (buildsettings.preRunCommands.length) {
637 			logInfo("Pre-run", Color.light_green, "Running commands...");
638 			runBuildCommands(CommandType.preRun, buildsettings.preRunCommands, pack, proj, settings, buildsettings);
639 		}
640 	}
641 
642 	private void runPostRunCommands(in Package pack, in Project proj, in GeneratorSettings settings,
643 		in BuildSettings buildsettings)
644 	{
645 		if (buildsettings.postRunCommands.length) {
646 			logInfo("Post-run", Color.light_green, "Running commands...");
647 			runBuildCommands(CommandType.postRun, buildsettings.postRunCommands, pack, proj, settings, buildsettings);
648 		}
649 	}
650 
651 	private void cleanupTemporaries()
652 	{
653 		foreach_reverse (f; m_temporaryFiles) {
654 			try {
655 				if (f.endsWithSlash) rmdir(f.toNativeString());
656 				else remove(f.toNativeString());
657 			} catch (Exception e) {
658 				logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg);
659 				logDiagnostic("Full error: %s", e.toString().sanitize);
660 			}
661 		}
662 		m_temporaryFiles = null;
663 	}
664 }
665 
666 /**
667  * Provides a unique (per build) identifier
668  *
669  * When building a package, it is important to have a unique but stable
670  * identifier to differentiate builds and allow their caching.
671  * This function provides such an identifier.
672  * Example:
673  * ```
674  * application-debug-linux.posix-x86_64-dmd_v2.100.2-D80285212AEC1FF9855F18AD52C68B9EEB5C7690609C224575F920096FB1965B
675  * ```
676  */
677 private string computeBuildID(in BuildSettings buildsettings, string config, GeneratorSettings settings)
678 {
679 	const(string[])[] hashing = [
680 		buildsettings.versions,
681 		buildsettings.debugVersions,
682 		buildsettings.dflags,
683 		buildsettings.lflags,
684 		buildsettings.stringImportPaths,
685 		buildsettings.importPaths,
686 		settings.platform.architecture,
687 		[
688 			(cast(uint)(buildsettings.options & ~BuildOption.color)).to!string, // exclude color option from id
689 			settings.platform.compilerBinary,
690 			settings.platform.compiler,
691 			settings.platform.compilerVersion,
692 		],
693 	];
694 
695 	return computeBuildName(config, settings, hashing);
696 }
697 
698 private NativePath getMainSourceFile(in Package prj)
699 {
700 	foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"])
701 		if (existsFile(prj.path ~ f))
702 			return prj.path ~ f;
703 	return prj.path ~ "source/app.d";
704 }
705 
706 private NativePath getTargetPath(const scope ref BuildSettings bs, const scope ref GeneratorSettings settings)
707 {
708 	return NativePath(bs.targetPath) ~ settings.compiler.getTargetFileName(bs, settings.platform);
709 }
710 
711 private string shrinkPath(NativePath path, NativePath base)
712 {
713 	auto orig = path.toNativeString();
714 	if (!path.absolute) return orig;
715 	version (Windows)
716 	{
717 		// avoid relative paths starting with `..\`: https://github.com/dlang/dub/issues/2143
718 		if (!path.startsWith(base)) return orig;
719 	}
720 	auto rel = path.relativeTo(base).toNativeString();
721 	return rel.length < orig.length ? rel : orig;
722 }
723 
724 unittest {
725 	assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo")) == NativePath("bar/baz").toNativeString());
726 	version (Windows)
727 		assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("/foo/bar/baz").toNativeString());
728 	else
729 		assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("../bar/baz").toNativeString());
730 	assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/")) == NativePath("/foo/bar/baz").toNativeString());
731 	assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/baz")) == NativePath("/foo/bar/baz").toNativeString());
732 }
733 
734 unittest { // issue #1235 - pass no library files to compiler command line when building a static lib
735 	import dub.internal.vibecompat.data.json : parseJsonString;
736 	import dub.compilers.gdc : GDCCompiler;
737 	import dub.platform : determinePlatform;
738 
739 	version (Windows) auto libfile = "bar.lib";
740 	else auto libfile = "bar.a";
741 
742 	auto desc = parseJsonString(`{"name": "test", "targetType": "library", "sourceFiles": ["foo.d", "`~libfile~`"]}`);
743 	auto pack = new Package(desc, NativePath("/tmp/fooproject"));
744 	auto pman = new PackageManager(pack.path, NativePath("/tmp/foo/"), NativePath("/tmp/foo/"), false);
745 	auto prj = new Project(pman, pack);
746 
747 	final static class TestCompiler : GDCCompiler {
748 		override void invoke(in BuildSettings settings, in BuildPlatform platform, void delegate(int, string) output_callback, NativePath cwd) {
749 			assert(!settings.dflags[].any!(f => f.canFind("bar")));
750 		}
751 		override void invokeLinker(in BuildSettings settings, in BuildPlatform platform, string[] objects, void delegate(int, string) output_callback, NativePath cwd) {
752 			assert(false);
753 		}
754 	}
755 
756 	GeneratorSettings settings;
757 	settings.platform = BuildPlatform(determinePlatform(), ["x86"], "gdc", "test", 2075);
758 	settings.compiler = new TestCompiler;
759 	settings.config = "library";
760 	settings.buildType = "debug";
761 	settings.tempBuild = true;
762 
763 	auto gen = new BuildGenerator(prj);
764 	gen.generate(settings);
765 }