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