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