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