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