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