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