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