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 				copyFile(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 			copyFile(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 			if (p.recipePath != NativePath.init)
530 				allfiles ~= p.recipePath.toNativeString();
531 			else if (p.basePackage.recipePath != NativePath.init)
532 				allfiles ~= p.basePackage.recipePath.toNativeString();
533 		}
534 		foreach (f; additional_dep_files) allfiles ~= f.toNativeString();
535 		bool checkSelectedVersions = !settings.single;
536 		if (checkSelectedVersions && main_pack is m_project.rootPackage && m_project.rootPackage.getAllDependencies().length > 0)
537 			allfiles ~= (main_pack.path ~ SelectedVersions.defaultFile).toNativeString();
538 
539 		foreach (file; allfiles.data) {
540 			if (!existsFile(file)) {
541 				logDiagnostic("File %s doesn't exist, triggering rebuild.", file);
542 				return false;
543 			}
544 			auto ftime = getFileInfo(file).timeModified;
545 			if (ftime > Clock.currTime)
546 				logWarn("File '%s' was modified in the future. Please re-save.", file);
547 			if (ftime > targettime) {
548 				logDiagnostic("File '%s' modified, need rebuild.", file);
549 				return false;
550 			}
551 		}
552 		return true;
553 	}
554 
555 	/// Output an unique name to represent the source file.
556 	/// Calls with path that resolve to the same file on the filesystem will return the same,
557 	/// unless they include different symbolic links (which are not resolved).
558 	deprecated("Use the overload taking in the current working directory")
559 	static string pathToObjName(const scope ref BuildPlatform platform, string path)
560 	{
561 		return pathToObjName(platform, path, getWorkingDirectory);
562 	}
563 
564 	/// ditto
565 	static string pathToObjName(const scope ref BuildPlatform platform, string path, NativePath cwd)
566 	{
567 		import std.digest.crc : crc32Of;
568 		import std.path : buildNormalizedPath, dirSeparator, relativePath, stripDrive;
569 		if (path.endsWith(".d")) path = path[0 .. $-2];
570 		auto ret = buildNormalizedPath(cwd.toNativeString(), path).replace(dirSeparator, ".");
571 		auto idx = ret.lastIndexOf('.');
572 		const objSuffix = getObjSuffix(platform);
573 		return idx < 0 ? ret ~ objSuffix : format("%s_%(%02x%)%s", ret[idx+1 .. $], crc32Of(ret[0 .. idx]), objSuffix);
574 	}
575 
576 	/// Compile a single source file (srcFile), and write the object to objName.
577 	static string compileUnit(string srcFile, string objName, BuildSettings bs, GeneratorSettings gs) {
578 		NativePath tempobj = NativePath(bs.targetPath)~objName;
579 		string objPath = tempobj.toNativeString();
580 		bs.libs = null;
581 		bs.frameworks = null;
582 		bs.lflags = null;
583 		bs.sourceFiles = [ srcFile ];
584 		bs.targetType = TargetType.object;
585 		gs.compiler.prepareBuildSettings(bs, gs.platform, BuildSetting.commandLine);
586 		gs.compiler.setTarget(bs, gs.platform, objPath);
587 		gs.compiler.invoke(bs, gs.platform, gs.compileCallback, gs.toolWorkingDirectory);
588 		return objPath;
589 	}
590 
591 	private void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings)
592 	{
593 		auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly);
594 		auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library;
595 
596 		scope (failure) {
597 			logDiagnostic("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType);
598 			auto tpath = getTargetPath(buildsettings, settings);
599 			if (generate_binary && existsFile(tpath))
600 				removeFile(tpath);
601 		}
602 		if (settings.buildMode == BuildMode.singleFile && generate_binary) {
603 			import std.parallelism, std.range : walkLength;
604 			import std.algorithm : filter, startsWith;
605 
606 			auto lbuildsettings = buildsettings;
607 			auto srcs = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f));
608 			auto objs = new string[](srcs.walkLength);
609 
610 			// Resolve pkg-config flags early to get C preprocessor flags for compilation.
611 			// We do this on lbuildsettings first, then copy -P flags to buildsettings.
612 			lbuildsettings.sourceFiles = is_static_library ? [] : lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array;
613 			settings.compiler.setTarget(lbuildsettings, settings.platform);
614 			settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles);
615 
616 			// Copy C preprocessor flags (-P...) from linker settings to compiler settings.
617 			foreach (flag; lbuildsettings.dflags.filter!(f => f.startsWith("-P")))
618 				buildsettings.addDFlags(flag);
619 
620 			void compileSource(size_t i, string src) {
621 				logInfo("Compiling", Color.light_green, "%s", src);
622 				const objPath = pathToObjName(settings.platform, src, settings.toolWorkingDirectory);
623 				objs[i] = compileUnit(src, objPath, buildsettings, settings);
624 			}
625 
626 			if (settings.parallelBuild) {
627 				foreach (i, src; srcs.parallel(1)) compileSource(i, src);
628 			} else {
629 				foreach (i, src; srcs.array) compileSource(i, src);
630 			}
631 
632 			logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold));
633 			settings.compiler.invokeLinker(lbuildsettings, settings.platform, objs, settings.linkCallback, settings.toolWorkingDirectory);
634 
635 		// NOTE: separate compile/link is not yet enabled for GDC.
636 		} else if (generate_binary && (settings.buildMode == BuildMode.allAtOnce || settings.compiler.name == "gdc" || is_static_library)) {
637 			// don't include symbols of dependencies (will be included by the top level target)
638 			if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array;
639 
640 			// setup for command line
641 			settings.compiler.setTarget(buildsettings, settings.platform);
642 			settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine);
643 
644 			// invoke the compiler
645 			settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback, settings.toolWorkingDirectory);
646 		} else {
647 			// determine path for the temporary object file
648 			string tempobjname = buildsettings.targetName ~ getObjSuffix(settings.platform);
649 			NativePath tempobj = NativePath(buildsettings.targetPath) ~ tempobjname;
650 
651 			// setup linker command line
652 			auto lbuildsettings = buildsettings;
653 			lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array;
654 			if (generate_binary) settings.compiler.setTarget(lbuildsettings, settings.platform);
655 			settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles);
656 
657 			// Copy C preprocessor flags (-P...) from linker settings to compiler settings.
658 			// These come from pkg-config --cflags and are needed for ImportC compilation.
659 			// We must copy them before clearing libs, since resolveLibs extracts them.
660 			import std.algorithm : filter, startsWith;
661 			foreach (flag; lbuildsettings.dflags.filter!(f => f.startsWith("-P")))
662 				buildsettings.addDFlags(flag);
663 
664 			// setup compiler command line
665 			buildsettings.libs = null;
666 			buildsettings.frameworks = null;
667 			buildsettings.lflags = null;
668 			if (generate_binary) buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString());
669 			buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array;
670 
671 			settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine);
672 
673 			settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback, settings.toolWorkingDirectory);
674 
675 			if (generate_binary) {
676 				if (settings.tempBuild) {
677 					logInfo("Linking", Color.light_green, "%s => %s", buildsettings.targetName.color(Mode.bold), buildsettings.getTargetPath(settings));
678 				} else {
679 					logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold));
680 				}
681 				settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()], settings.linkCallback, settings.toolWorkingDirectory);
682 			}
683 		}
684 	}
685 
686 	private void runTarget(NativePath exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings)
687 	{
688 		if (buildsettings.targetType == TargetType.executable) {
689 			auto cwd = settings.toolWorkingDirectory;
690 			auto runcwd = cwd;
691 			if (buildsettings.workingDirectory.length) {
692 				runcwd = NativePath(buildsettings.workingDirectory);
693 				if (!runcwd.absolute) runcwd = cwd ~ runcwd;
694 			}
695 			ensureDirectory(runcwd);
696 			if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path;
697 			runPreRunCommands(m_project.rootPackage, m_project, settings, buildsettings);
698 			logInfo("Running", Color.green, "%s %s", exe_file_path.relativeTo(runcwd), run_args.join(" "));
699 			string[string] env;
700 			foreach (aa; [buildsettings.environments, buildsettings.runEnvironments])
701 				foreach (k, v; aa)
702 					env[k] = v;
703 			if (settings.runCallback) {
704 				auto res = execute([ exe_file_path.toNativeString() ] ~ run_args,
705 						   env, Config.none, size_t.max, runcwd.toNativeString());
706 				settings.runCallback(res.status, res.output);
707 				settings.targetExitStatus = res.status;
708 				runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings);
709 			} else {
710 				auto prg_pid = spawnProcess([ exe_file_path.toNativeString() ] ~ run_args,
711 								env, Config.none, runcwd.toNativeString());
712 				auto result = prg_pid.wait();
713 				settings.targetExitStatus = result;
714 				runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings);
715 				enforce(result == 0, "Program exited with code "~to!string(result));
716 			}
717 		} else
718 			enforce(false, "Target is a library. Skipping execution.");
719 	}
720 
721 	private void runPreRunCommands(in Package pack, in Project proj, in GeneratorSettings settings,
722 		in BuildSettings buildsettings)
723 	{
724 		if (buildsettings.preRunCommands.length) {
725 			logInfo("Pre-run", Color.light_green, "Running commands...");
726 			runBuildCommands(CommandType.preRun, buildsettings.preRunCommands, pack, proj, settings, buildsettings);
727 		}
728 	}
729 
730 	private void runPostRunCommands(in Package pack, in Project proj, in GeneratorSettings settings,
731 		in BuildSettings buildsettings)
732 	{
733 		if (buildsettings.postRunCommands.length) {
734 			logInfo("Post-run", Color.light_green, "Running commands...");
735 			runBuildCommands(CommandType.postRun, buildsettings.postRunCommands, pack, proj, settings, buildsettings);
736 		}
737 	}
738 
739 	private void cleanupTemporaries()
740 	{
741 		foreach_reverse (f; m_temporaryFiles) {
742 			try {
743 				if (f.endsWithSlash) rmdir(f.toNativeString());
744 				else remove(f.toNativeString());
745 			} catch (Exception e) {
746 				logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg);
747 				logDiagnostic("Full error: %s", e.toString().sanitize);
748 			}
749 		}
750 		m_temporaryFiles = null;
751 	}
752 }
753 
754 private NativePath getMainSourceFile(in Package prj)
755 {
756 	foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"])
757 		if (existsFile(prj.path ~ f))
758 			return prj.path ~ f;
759 	return prj.path ~ "source/app.d";
760 }
761 
762 private NativePath getTargetPath(const scope ref BuildSettings bs, const scope ref GeneratorSettings settings)
763 {
764 	return NativePath(bs.targetPath) ~ settings.compiler.getTargetFileName(bs, settings.platform);
765 }
766 
767 private string shrinkPath(NativePath path, NativePath base)
768 {
769 	auto orig = path.toNativeString();
770 	if (!path.absolute) return orig;
771 	version (Windows)
772 	{
773 		// avoid relative paths starting with `..\`: https://github.com/dlang/dub/issues/2143
774 		if (!path.startsWith(base)) return orig;
775 	}
776 	auto rel = path.relativeTo(base).toNativeString();
777 	return rel.length < orig.length ? rel : orig;
778 }
779 
780 unittest {
781 	assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo")) == NativePath("bar/baz").toNativeString());
782 	version (Windows)
783 		assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("/foo/bar/baz").toNativeString());
784 	else
785 		assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("../bar/baz").toNativeString());
786 	assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/")) == NativePath("/foo/bar/baz").toNativeString());
787 	assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/baz")) == NativePath("/foo/bar/baz").toNativeString());
788 }
789 
790 unittest { // issue #1235 - pass no library files to compiler command line when building a static lib
791 	import dub.recipe.io : parsePackageRecipe;
792 	import dub.compilers.gdc : GDCCompiler;
793 	import dub.platform : determinePlatform;
794 
795 	version (Windows) auto libfile = "bar.lib";
796 	else auto libfile = "bar.a";
797 
798 	auto recipe = parsePackageRecipe(
799         `{"name":"test", "targetType":"library", "sourceFiles":["foo.d", "`~libfile~`"]}`,
800         `/tmp/fooproject/dub.json`);
801 	auto pack = new Package(recipe, NativePath("/tmp/fooproject"));
802 	auto pman = new PackageManager(pack.path, NativePath("/tmp/foo/"), NativePath("/tmp/foo/"), false);
803 	auto prj = new Project(pman, pack);
804 
805 	final static class TestCompiler : GDCCompiler {
806 		override void invoke(in BuildSettings settings, in BuildPlatform platform, void delegate(int, string) output_callback, NativePath cwd) {
807 			assert(!settings.dflags[].any!(f => f.canFind("bar")));
808 		}
809 		override void invokeLinker(in BuildSettings settings, in BuildPlatform platform, string[] objects, void delegate(int, string) output_callback, NativePath cwd) {
810 			assert(false);
811 		}
812 	}
813 
814 	GeneratorSettings settings;
815 	settings.platform = BuildPlatform(determinePlatform(), ["x86"], "gdc", "test", 2075);
816 	settings.compiler = new TestCompiler;
817 	settings.config = "library";
818 	settings.buildType = "debug";
819 	settings.tempBuild = true;
820 
821 	auto gen = new BuildGenerator(prj);
822 	gen.generate(settings);
823 }