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 & BuildOption.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 & BuildOption.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 & BuildOption.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 & BuildOption.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 		import std.path : buildNormalizedPath, dirSeparator, stripDrive;
357 		return stripDrive(buildNormalizedPath(getcwd(), path~objSuffix))[1..$].replace(dirSeparator, ".");
358 	}
359 
360 	/// Compile a single source file (srcFile), and write the object to objName.
361 	static string compileUnit(string srcFile, string objName, BuildSettings bs, GeneratorSettings gs) {
362 		Path tempobj = Path(bs.targetPath)~objName;
363 		string objPath = tempobj.toNativeString();
364 		bs.libs = null;
365 		bs.lflags = null;
366 		bs.sourceFiles = [ srcFile ];
367 		bs.targetType = TargetType.object;
368 		gs.compiler.prepareBuildSettings(bs, BuildSetting.commandLine);
369 		gs.compiler.setTarget(bs, gs.platform, objPath);
370 		gs.compiler.invoke(bs, gs.platform, gs.compileCallback);
371 		return objPath;
372 	}
373 
374 	void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings)
375 	{
376 		auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly);
377 		auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library;
378 
379 		Path target_file;
380 		scope (failure) {
381 			logInfo("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType);
382 			auto tpath = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform);
383 			if (generate_binary && existsFile(tpath))
384 				removeFile(tpath);
385 		}
386 		if (settings.buildMode == BuildMode.singleFile && generate_binary) {
387 			import std.parallelism, std.range : walkLength;
388 
389 			auto lbuildsettings = buildsettings;
390 			auto srcs = buildsettings.sourceFiles.filter!(f => !isLinkerFile(f));
391 			auto objs = new string[](srcs.walkLength);
392 			logInfo("Compiling using %s...", settings.platform.compilerBinary);
393 
394 			void compileSource(size_t i, string src) {
395 				logInfo("Compiling %s...", src);
396 				objs[i] = compileUnit(src, pathToObjName(src), buildsettings, settings);
397 			}
398 
399 			if (settings.parallelBuild) {
400 				foreach (i, src; srcs.parallel(1)) compileSource(i, src);
401 			} else {
402 				foreach (i, src; srcs.array) compileSource(i, src);
403 			}
404 
405 			logInfo("Linking...");
406 			lbuildsettings.sourceFiles = is_static_library ? [] : lbuildsettings.sourceFiles.filter!(f=> f.isLinkerFile()).array;
407 			settings.compiler.setTarget(lbuildsettings, settings.platform);
408 			settings.compiler.prepareBuildSettings(lbuildsettings, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles);
409 			settings.compiler.invokeLinker(lbuildsettings, settings.platform, objs, settings.linkCallback);
410 
411 		/*
412 			NOTE: for DMD experimental separate compile/link is used, but this is not yet implemented
413 			      on the other compilers. Later this should be integrated somehow in the build process
414 			      (either in the dub.json, or using a command line flag)
415 		*/
416 		} else if (settings.buildMode == BuildMode.allAtOnce || settings.platform.compilerBinary != "dmd" || !generate_binary || is_static_library) {
417 			// setup for command line
418 			if (generate_binary) settings.compiler.setTarget(buildsettings, settings.platform);
419 			settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine);
420 
421 			// don't include symbols of dependencies (will be included by the top level target)
422 			if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !f.isLinkerFile()).array;
423 
424 			// invoke the compiler
425 			logInfo("Running %s...", settings.platform.compilerBinary);
426 			settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback);
427 		} else {
428 			// determine path for the temporary object file
429 			string tempobjname = buildsettings.targetName ~ objSuffix;
430 			Path tempobj = Path(buildsettings.targetPath) ~ tempobjname;
431 
432 			// setup linker command line
433 			auto lbuildsettings = buildsettings;
434 			lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(f)).array;
435 			settings.compiler.setTarget(lbuildsettings, settings.platform);
436 			settings.compiler.prepareBuildSettings(lbuildsettings, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles);
437 
438 			// setup compiler command line
439 			buildsettings.libs = null;
440 			buildsettings.lflags = null;
441 			buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString());
442 			buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(f)).array;
443 			settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine);
444 
445 			logInfo("Compiling using %s...", settings.platform.compilerBinary);
446 			settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback);
447 
448 			logInfo("Linking...");
449 			settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()], settings.linkCallback);
450 		}
451 	}
452 
453 	void runTarget(Path exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings)
454 	{
455 		if (buildsettings.targetType == TargetType.executable) {
456 			auto cwd = Path(getcwd());
457 			auto runcwd = cwd;
458 			if (buildsettings.workingDirectory.length) {
459 				runcwd = Path(buildsettings.workingDirectory);
460 				if (!runcwd.absolute) runcwd = cwd ~ runcwd;
461 				logDiagnostic("Switching to %s", runcwd.toNativeString());
462 				chdir(runcwd.toNativeString());
463 			}
464 			scope(exit) chdir(cwd.toNativeString());
465 			if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path;
466 			auto exe_path_string = exe_file_path.relativeTo(runcwd).toNativeString();
467 			version (Posix) {
468 				if (!exe_path_string.startsWith(".") && !exe_path_string.startsWith("/"))
469 					exe_path_string = "./" ~ exe_path_string;
470 			}
471 			version (Windows) {
472 				if (!exe_path_string.startsWith(".") && (exe_path_string.length < 2 || exe_path_string[1] != ':'))
473 					exe_path_string = ".\\" ~ exe_path_string;
474 			}
475 			logInfo("Running %s %s", exe_path_string, run_args.join(" "));
476 			if (settings.runCallback) {
477 				auto res = execute(exe_path_string ~ run_args);
478 				settings.runCallback(res.status, res.output);
479 			} else {
480 				auto prg_pid = spawnProcess(exe_path_string ~ run_args);
481 				auto result = prg_pid.wait();
482 				enforce(result == 0, "Program exited with code "~to!string(result));
483 			}
484 		} else
485 			enforce(false, "Target is a library. Skipping execution.");
486 	}
487 
488 	void cleanupTemporaries()
489 	{
490 		foreach_reverse (f; m_temporaryFiles) {
491 			try {
492 				if (f.endsWithSlash) rmdir(f.toNativeString());
493 				else remove(f.toNativeString());
494 			} catch (Exception e) {
495 				logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg);
496 				logDiagnostic("Full error: %s", e.toString().sanitize);
497 			}
498 		}
499 		m_temporaryFiles = null;
500 	}
501 }
502 
503 private Path getMainSourceFile(in Package prj)
504 {
505 	foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"])
506 		if (existsFile(prj.path ~ f))
507 			return prj.path ~ f;
508 	return prj.path ~ "source/app.d";
509 }
510 
511 unittest {
512 	version (Windows) {
513 		assert(isLinkerFile("test.obj"));
514 		assert(isLinkerFile("test.lib"));
515 		assert(isLinkerFile("test.res"));
516 		assert(!isLinkerFile("test.o"));
517 		assert(!isLinkerFile("test.d"));
518 	} else {
519 		assert(isLinkerFile("test.o"));
520 		assert(isLinkerFile("test.a"));
521 		assert(isLinkerFile("test.so"));
522 		assert(isLinkerFile("test.dylib"));
523 		assert(!isLinkerFile("test.obj"));
524 		assert(!isLinkerFile("test.d"));
525 	}
526 }