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