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.inet.path; 16 import dub.internal.logging; 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 string getObjSuffix(const scope ref BuildPlatform platform) 31 { 32 return platform.isWindows() ? ".obj" : ".o"; 33 } 34 35 string computeBuildName(string config, in GeneratorSettings settings, const string[][] hashing...) 36 { 37 import std.digest.sha : SHA256; 38 import std.base64 : Base64URL; 39 40 SHA256 hash; 41 hash.start(); 42 void addHash(in string[] strings...) { foreach (s; strings) { hash.put(cast(ubyte[])s); hash.put(0); } hash.put(0); } 43 foreach(strings; hashing) 44 addHash(strings); 45 addHash(settings.platform.platform); 46 addHash(settings.platform.architecture); 47 addHash(settings.platform.compiler); 48 addHash(settings.platform.compilerVersion); 49 if(settings.recipeName != "") 50 addHash(settings.recipeName); 51 const hashstr = Base64URL.encode(hash.finish()[0 .. $ / 2]).stripRight("="); 52 53 if(settings.recipeName != "") 54 { 55 import std.path:stripExtension, baseName; 56 string recipeName = settings.recipeName.baseName.stripExtension; 57 return format("%s-%s-%s-%s", config, settings.buildType, recipeName, hashstr); 58 } 59 return format("%s-%s-%s", config, settings.buildType, hashstr); 60 } 61 62 class BuildGenerator : ProjectGenerator { 63 private { 64 NativePath[] m_temporaryFiles; 65 } 66 67 this(Project project) 68 { 69 super(project); 70 } 71 72 override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) 73 { 74 import std.path : setExtension; 75 scope (exit) cleanupTemporaries(); 76 77 void checkPkgRequirements(const(Package) pkg) 78 { 79 const tr = pkg.recipe.toolchainRequirements; 80 tr.checkPlatform(settings.platform, pkg.name); 81 } 82 83 checkPkgRequirements(m_project.rootPackage); 84 foreach (pkg; m_project.dependencies) 85 checkPkgRequirements(pkg); 86 87 auto root_ti = targets[m_project.rootPackage.name]; 88 const rootTT = root_ti.buildSettings.targetType; 89 90 enforce(!(settings.rdmd && rootTT == TargetType.none), 91 "Building package with target type \"none\" with rdmd is not supported yet."); 92 93 logInfo("Starting", Color.light_green, 94 "Performing \"%s\" build using %s for %-(%s, %).", 95 settings.buildType.color(Color.magenta), settings.platform.compilerBinary, 96 settings.platform.architecture); 97 98 if (settings.rdmd || (rootTT == TargetType.staticLibrary && !settings.buildDeep)) { 99 // Only build the main target. 100 // RDMD always builds everything at once and static libraries don't need their 101 // dependencies to be built, unless --deep flag is specified 102 NativePath tpath; 103 buildTarget(settings, root_ti.buildSettings.dup, m_project.rootPackage, root_ti.config, root_ti.packages, null, tpath); 104 return; 105 } 106 107 // Recursive build starts here 108 109 bool any_cached = false; 110 111 NativePath[string] target_paths; 112 113 NativePath[] dynamicLibDepsFilesToCopy; // to the root package output dir 114 const copyDynamicLibDepsLinkerFiles = rootTT == TargetType.dynamicLibrary || rootTT == TargetType.none; 115 const copyDynamicLibDepsRuntimeFiles = copyDynamicLibDepsLinkerFiles || rootTT == TargetType.executable; 116 117 bool[string] visited; 118 void buildTargetRec(string target) 119 { 120 if (target in visited) return; 121 visited[target] = true; 122 123 auto ti = targets[target]; 124 125 foreach (dep; ti.dependencies) 126 buildTargetRec(dep); 127 128 NativePath[] additional_dep_files; 129 auto bs = ti.buildSettings.dup; 130 const tt = bs.targetType; 131 foreach (ldep; ti.linkDependencies) { 132 const ldepPath = target_paths[ldep].toNativeString(); 133 const doLink = tt != TargetType.staticLibrary && !(bs.options & BuildOption.syntaxOnly); 134 135 if (doLink && isLinkerFile(settings.platform, ldepPath)) 136 bs.addSourceFiles(ldepPath); 137 else 138 additional_dep_files ~= target_paths[ldep]; 139 140 if (targets[ldep].buildSettings.targetType == TargetType.dynamicLibrary) { 141 // copy the .{dll,so,dylib} 142 if (copyDynamicLibDepsRuntimeFiles) 143 dynamicLibDepsFilesToCopy ~= NativePath(ldepPath); 144 145 if (settings.platform.isWindows()) { 146 // copy the accompanying .pdb if found 147 if (copyDynamicLibDepsRuntimeFiles) { 148 const pdb = ldepPath.setExtension(".pdb"); 149 if (existsFile(pdb)) 150 dynamicLibDepsFilesToCopy ~= NativePath(pdb); 151 } 152 153 const importLib = ldepPath.setExtension(".lib"); 154 if (existsFile(importLib)) { 155 // link dependee against the import lib 156 if (doLink) 157 bs.addSourceFiles(importLib); 158 // and copy 159 if (copyDynamicLibDepsLinkerFiles) 160 dynamicLibDepsFilesToCopy ~= NativePath(importLib); 161 } 162 163 // copy the .exp file if found 164 const exp = ldepPath.setExtension(".exp"); 165 if (copyDynamicLibDepsLinkerFiles && existsFile(exp)) 166 dynamicLibDepsFilesToCopy ~= NativePath(exp); 167 } 168 } 169 } 170 NativePath tpath; 171 if (tt != TargetType.none) { 172 if (buildTarget(settings, bs, ti.pack, ti.config, ti.packages, additional_dep_files, tpath)) 173 any_cached = true; 174 } 175 target_paths[target] = tpath; 176 } 177 178 buildTargetRec(m_project.rootPackage.name); 179 180 if (dynamicLibDepsFilesToCopy.length) { 181 const rootTargetPath = NativePath(root_ti.buildSettings.targetPath); 182 183 ensureDirectory(rootTargetPath); 184 foreach (src; dynamicLibDepsFilesToCopy) { 185 logDiagnostic("Copying target from %s to %s", 186 src.toNativeString(), rootTargetPath.toNativeString()); 187 hardLinkFile(src, rootTargetPath ~ src.head, true); 188 } 189 } 190 191 if (any_cached) { 192 logInfo("Finished", Color.green, 193 "To force a rebuild of up-to-date targets, run again with --force" 194 ); 195 } 196 } 197 198 override void performPostGenerateActions(GeneratorSettings settings, in TargetInfo[string] targets) 199 { 200 // run the generated executable 201 auto buildsettings = targets[m_project.rootPackage.name].buildSettings.dup; 202 if (settings.run && !(buildsettings.options & BuildOption.syntaxOnly)) { 203 NativePath exe_file_path; 204 if (m_tempTargetExecutablePath.empty) 205 exe_file_path = getTargetPath(buildsettings, settings); 206 else 207 exe_file_path = m_tempTargetExecutablePath ~ settings.compiler.getTargetFileName(buildsettings, settings.platform); 208 runTarget(exe_file_path, buildsettings, settings.runArgs, settings); 209 } 210 } 211 212 private bool buildTarget(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, in Package[] packages, in NativePath[] additional_dep_files, out NativePath target_path) 213 { 214 import std.path : absolutePath; 215 216 auto cwd = settings.toolWorkingDirectory; 217 bool generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); 218 219 auto build_id = buildsettings.computeBuildID(pack.path, config, settings); 220 221 // make all paths relative to shrink the command line 222 string makeRelative(string path) { return shrinkPath(NativePath(path), cwd); } 223 foreach (ref f; buildsettings.sourceFiles) f = makeRelative(f); 224 foreach (ref p; buildsettings.importPaths) p = makeRelative(p); 225 foreach (ref p; buildsettings.cImportPaths) p = makeRelative(p); 226 foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p); 227 228 // perform the actual build 229 bool cached = false; 230 if (settings.rdmd) performRDMDBuild(settings, buildsettings, pack, config, target_path); 231 else if (!generate_binary) performDirectBuild(settings, buildsettings, pack, config, target_path); 232 else cached = performCachedBuild(settings, buildsettings, pack, config, build_id, packages, additional_dep_files, target_path); 233 234 // HACK: cleanup dummy doc files, we shouldn't specialize on buildType 235 // here and the compiler shouldn't need dummy doc output. 236 if (settings.buildType == "ddox") { 237 if ("__dummy.html".exists) 238 removeFile("__dummy.html"); 239 if ("__dummy_docs".exists) 240 rmdirRecurse("__dummy_docs"); 241 } 242 243 // run post-build commands 244 if (!cached && buildsettings.postBuildCommands.length) { 245 logInfo("Post-build", Color.light_green, "Running commands"); 246 runBuildCommands(CommandType.postBuild, buildsettings.postBuildCommands, pack, m_project, settings, buildsettings, 247 [["DUB_BUILD_PATH" : target_path is NativePath.init 248 ? "" 249 : target_path.parentPath.toNativeString.absolutePath(settings.toolWorkingDirectory.toNativeString)]]); 250 } 251 252 return cached; 253 } 254 255 private bool performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, 256 string build_id, in Package[] packages, in NativePath[] additional_dep_files, out NativePath target_binary_path) 257 { 258 NativePath target_path; 259 if (settings.tempBuild) { 260 string packageName = pack.basePackage is null ? pack.name : pack.basePackage.name; 261 m_tempTargetExecutablePath = target_path = getTempDir() ~ format(".dub/build/%s-%s/%s/", packageName, pack.version_, build_id); 262 } 263 else 264 target_path = targetCacheDir(settings.cache, pack, build_id); 265 266 if (!settings.force && isUpToDate(target_path, buildsettings, settings, pack, packages, additional_dep_files)) { 267 logInfo("Up-to-date", Color.green, "%s %s: target for configuration [%s] is up to date.", 268 pack.name.color(Mode.bold), pack.version_, config.color(Color.blue)); 269 logDiagnostic("Using existing build in %s.", target_path.toNativeString()); 270 target_binary_path = target_path ~ settings.compiler.getTargetFileName(buildsettings, settings.platform); 271 if (!settings.tempBuild) 272 copyTargetFile(target_path, buildsettings, settings); 273 return true; 274 } 275 276 if (!isWritableDir(target_path, true)) { 277 if (!settings.tempBuild) 278 logInfo("Build directory %s is not writable. Falling back to direct build in the system's temp folder.", target_path); 279 performDirectBuild(settings, buildsettings, pack, config, target_path); 280 return false; 281 } 282 283 logInfo("Building", Color.light_green, "%s %s: building configuration [%s]", pack.name.color(Mode.bold), pack.version_, config.color(Color.blue)); 284 285 if( buildsettings.preBuildCommands.length ){ 286 logInfo("Pre-build", Color.light_green, "Running commands"); 287 runBuildCommands(CommandType.preBuild, buildsettings.preBuildCommands, pack, m_project, settings, buildsettings); 288 } 289 290 // override target path 291 auto cbuildsettings = buildsettings; 292 cbuildsettings.targetPath = target_path.toNativeString(); 293 buildWithCompiler(settings, cbuildsettings); 294 target_binary_path = getTargetPath(cbuildsettings, settings); 295 296 if (!settings.tempBuild) { 297 copyTargetFile(target_path, buildsettings, settings); 298 updateCacheDatabase(settings, cbuildsettings, pack, config, build_id, target_binary_path.toNativeString()); 299 } 300 301 return false; 302 } 303 304 private void updateCacheDatabase(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, 305 string build_id, string target_binary_path) 306 { 307 import dub.internal.vibecompat.data.json; 308 import core.time : seconds; 309 310 // Generate a `db.json` in the package version cache directory. 311 // This is read by 3rd party software (e.g. Meson) in order to find 312 // relevant build artifacts in Dub's cache. 313 314 enum jsonFileName = "db.json"; 315 enum lockFileName = "db.lock"; 316 317 const pkgCacheDir = packageCache(settings.cache, pack); 318 auto lock = lockFile((pkgCacheDir ~ lockFileName).toNativeString(), 3.seconds); 319 320 const dbPath = pkgCacheDir ~ jsonFileName; 321 const dbPathStr = dbPath.toNativeString(); 322 Json db; 323 if (exists(dbPathStr)) { 324 const text = readText(dbPath); 325 db = parseJsonString(text, dbPathStr); 326 enforce(db.type == Json.Type.array, "Expected a JSON array in " ~ dbPathStr); 327 } 328 else { 329 db = Json.emptyArray; 330 } 331 332 foreach_reverse (entry; db) { 333 if (entry["buildId"].get!string == build_id) { 334 // duplicate 335 return; 336 } 337 } 338 339 Json entry = Json.emptyObject; 340 341 entry["architecture"] = serializeToJson(settings.platform.architecture); 342 entry["buildId"] = build_id; 343 entry["buildType"] = settings.buildType; 344 entry["compiler"] = settings.platform.compiler; 345 entry["compilerBinary"] = settings.platform.compilerBinary; 346 entry["compilerVersion"] = settings.platform.compilerVersion; 347 entry["configuration"] = config; 348 entry["package"] = pack.name; 349 entry["platform"] = serializeToJson(settings.platform.platform); 350 entry["targetBinaryPath"] = target_binary_path; 351 entry["version"] = pack.version_.toString(); 352 353 db ~= entry; 354 355 writeFile(dbPath, representation(db.toPrettyString())); 356 } 357 358 private void performRDMDBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config, out NativePath target_path) 359 { 360 auto cwd = settings.toolWorkingDirectory; 361 //Added check for existence of [AppNameInPackagejson].d 362 //If exists, use that as the starting file. 363 NativePath mainsrc; 364 if (buildsettings.mainSourceFile.length) { 365 mainsrc = NativePath(buildsettings.mainSourceFile); 366 if (!mainsrc.absolute) mainsrc = pack.path ~ mainsrc; 367 } else { 368 mainsrc = getMainSourceFile(pack); 369 logWarn(`Package has no "mainSourceFile" defined. Using best guess: %s`, mainsrc.relativeTo(pack.path).toNativeString()); 370 } 371 372 // do not pass all source files to RDMD, only the main source file 373 buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(s => !s.endsWith(".d"))().array(); 374 settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine); 375 376 auto generate_binary = !buildsettings.dflags.canFind("-o-"); 377 378 // Create start script, which will be used by the calling bash/cmd script. 379 // build "rdmd --force %DFLAGS% -I%~dp0..\source -Jviews -Isource @deps.txt %LIBS% source\app.d" ~ application arguments 380 // or with "/" instead of "\" 381 bool tmp_target = false; 382 if (generate_binary) { 383 if (settings.tempBuild || (settings.run && !isWritableDir(NativePath(buildsettings.targetPath), true))) { 384 import std.random; 385 auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-"; 386 auto tmpdir = getTempDir()~".rdmd/source/"; 387 buildsettings.targetPath = tmpdir.toNativeString(); 388 buildsettings.targetName = rnd ~ buildsettings.targetName; 389 m_temporaryFiles ~= tmpdir; 390 tmp_target = true; 391 } 392 target_path = getTargetPath(buildsettings, settings); 393 settings.compiler.setTarget(buildsettings, settings.platform); 394 } 395 396 logDiagnostic("Application output name is '%s'", settings.compiler.getTargetFileName(buildsettings, settings.platform)); 397 398 string[] flags = ["--build-only", "--compiler="~settings.platform.compilerBinary]; 399 if (settings.force) flags ~= "--force"; 400 flags ~= buildsettings.dflags; 401 flags ~= mainsrc.relativeTo(cwd).toNativeString(); 402 403 if (buildsettings.preBuildCommands.length){ 404 logInfo("Pre-build", Color.light_green, "Running commands"); 405 runCommands(buildsettings.preBuildCommands, null, cwd.toNativeString()); 406 } 407 408 logInfo("Building", Color.light_green, "%s %s [%s]", pack.name.color(Mode.bold), pack.version_, config.color(Color.blue)); 409 410 logInfo("Running rdmd..."); 411 logDiagnostic("rdmd %s", join(flags, " ")); 412 auto rdmd_pid = spawnProcess("rdmd" ~ flags, null, Config.none, cwd.toNativeString()); 413 auto result = rdmd_pid.wait(); 414 enforce(result == 0, "Build command failed with exit code "~to!string(result)); 415 416 if (tmp_target) { 417 m_temporaryFiles ~= target_path; 418 foreach (f; buildsettings.copyFiles) 419 m_temporaryFiles ~= NativePath(buildsettings.targetPath).parentPath ~ NativePath(f).head; 420 } 421 } 422 423 private void performDirectBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config, out NativePath target_path) 424 { 425 auto cwd = settings.toolWorkingDirectory; 426 auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); 427 428 // make file paths relative to shrink the command line 429 foreach (ref f; buildsettings.sourceFiles) { 430 auto fp = NativePath(f); 431 if( fp.absolute ) fp = fp.relativeTo(cwd); 432 f = fp.toNativeString(); 433 } 434 435 logInfo("Building", Color.light_green, "%s %s [%s]", pack.name.color(Mode.bold), pack.version_, config.color(Color.blue)); 436 437 // make all target/import paths relative 438 string makeRelative(string path) { 439 auto p = NativePath(path); 440 // storing in a separate temprary to work around #601 441 auto prel = p.absolute ? p.relativeTo(cwd) : p; 442 return prel.toNativeString(); 443 } 444 buildsettings.targetPath = makeRelative(buildsettings.targetPath); 445 foreach (ref p; buildsettings.importPaths) p = makeRelative(p); 446 foreach (ref p; buildsettings.cImportPaths) p = makeRelative(p); 447 foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p); 448 449 bool is_temp_target = false; 450 if (generate_binary) { 451 if (settings.tempBuild || (settings.run && !isWritableDir(NativePath(buildsettings.targetPath), true))) { 452 import std.random; 453 auto rnd = to!string(uniform(uint.min, uint.max)); 454 auto tmppath = getTempDir()~("dub/"~rnd~"/"); 455 buildsettings.targetPath = tmppath.toNativeString(); 456 m_temporaryFiles ~= tmppath; 457 is_temp_target = true; 458 } 459 target_path = getTargetPath(buildsettings, settings); 460 } 461 462 if( buildsettings.preBuildCommands.length ){ 463 logInfo("Pre-build", Color.light_green, "Running commands"); 464 runBuildCommands(CommandType.preBuild, buildsettings.preBuildCommands, pack, m_project, settings, buildsettings); 465 } 466 467 buildWithCompiler(settings, buildsettings); 468 469 if (is_temp_target) { 470 m_temporaryFiles ~= target_path; 471 foreach (f; buildsettings.copyFiles) 472 m_temporaryFiles ~= NativePath(buildsettings.targetPath).parentPath ~ NativePath(f).head; 473 } 474 } 475 476 private void copyTargetFile(in NativePath build_path, in BuildSettings buildsettings, in GeneratorSettings settings) 477 { 478 ensureDirectory(NativePath(buildsettings.targetPath)); 479 480 string[] filenames = [ 481 settings.compiler.getTargetFileName(buildsettings, settings.platform) 482 ]; 483 484 // Windows: add .pdb (for executables and DLLs) and/or import .lib & .exp (for DLLs) if found 485 if (settings.platform.isWindows()) { 486 void addIfFound(string extension) { 487 import std.path : setExtension; 488 const candidate = filenames[0].setExtension(extension); 489 if (existsFile(build_path ~ candidate)) 490 filenames ~= candidate; 491 } 492 493 const tt = buildsettings.targetType; 494 if (tt == TargetType.executable || tt == TargetType.dynamicLibrary) 495 addIfFound(".pdb"); 496 497 if (tt == TargetType.dynamicLibrary) { 498 addIfFound(".lib"); 499 addIfFound(".exp"); 500 } 501 } 502 503 foreach (filename; filenames) 504 { 505 auto src = build_path ~ filename; 506 logDiagnostic("Copying target from %s to %s", src.toNativeString(), buildsettings.targetPath); 507 hardLinkFile(src, NativePath(buildsettings.targetPath) ~ filename, true); 508 } 509 } 510 511 private bool isUpToDate(NativePath target_path, BuildSettings buildsettings, GeneratorSettings settings, in Package main_pack, in Package[] packages, in NativePath[] additional_dep_files) 512 { 513 import std.datetime; 514 515 auto targetfile = target_path ~ settings.compiler.getTargetFileName(buildsettings, settings.platform); 516 if (!existsFile(targetfile)) { 517 logDiagnostic("Target '%s' doesn't exist, need rebuild.", targetfile.toNativeString()); 518 return false; 519 } 520 auto targettime = getFileInfo(targetfile).timeModified; 521 522 auto allfiles = appender!(string[]); 523 allfiles ~= buildsettings.sourceFiles; 524 allfiles ~= buildsettings.importFiles; 525 allfiles ~= buildsettings.stringImportFiles; 526 allfiles ~= buildsettings.extraDependencyFiles; 527 // TODO: add library files 528 foreach (p; packages) 529 allfiles ~= (p.recipePath != NativePath.init ? p : p.basePackage).recipePath.toNativeString(); 530 foreach (f; additional_dep_files) allfiles ~= f.toNativeString(); 531 bool checkSelectedVersions = !settings.single; 532 if (checkSelectedVersions && main_pack is m_project.rootPackage && m_project.rootPackage.getAllDependencies().length > 0) 533 allfiles ~= (main_pack.path ~ SelectedVersions.defaultFile).toNativeString(); 534 535 foreach (file; allfiles.data) { 536 if (!existsFile(file)) { 537 logDiagnostic("File %s doesn't exist, triggering rebuild.", file); 538 return false; 539 } 540 auto ftime = getFileInfo(file).timeModified; 541 if (ftime > Clock.currTime) 542 logWarn("File '%s' was modified in the future. Please re-save.", file); 543 if (ftime > targettime) { 544 logDiagnostic("File '%s' modified, need rebuild.", file); 545 return false; 546 } 547 } 548 return true; 549 } 550 551 /// Output an unique name to represent the source file. 552 /// Calls with path that resolve to the same file on the filesystem will return the same, 553 /// unless they include different symbolic links (which are not resolved). 554 deprecated("Use the overload taking in the current working directory") 555 static string pathToObjName(const scope ref BuildPlatform platform, string path) 556 { 557 return pathToObjName(platform, path, getWorkingDirectory); 558 } 559 560 /// ditto 561 static string pathToObjName(const scope ref BuildPlatform platform, string path, NativePath cwd) 562 { 563 import std.digest.crc : crc32Of; 564 import std.path : buildNormalizedPath, dirSeparator, relativePath, stripDrive; 565 if (path.endsWith(".d")) path = path[0 .. $-2]; 566 auto ret = buildNormalizedPath(cwd.toNativeString(), path).replace(dirSeparator, "."); 567 auto idx = ret.lastIndexOf('.'); 568 const objSuffix = getObjSuffix(platform); 569 return idx < 0 ? ret ~ objSuffix : format("%s_%(%02x%)%s", ret[idx+1 .. $], crc32Of(ret[0 .. idx]), objSuffix); 570 } 571 572 /// Compile a single source file (srcFile), and write the object to objName. 573 static string compileUnit(string srcFile, string objName, BuildSettings bs, GeneratorSettings gs) { 574 NativePath tempobj = NativePath(bs.targetPath)~objName; 575 string objPath = tempobj.toNativeString(); 576 bs.libs = null; 577 bs.lflags = null; 578 bs.sourceFiles = [ srcFile ]; 579 bs.targetType = TargetType.object; 580 gs.compiler.prepareBuildSettings(bs, gs.platform, BuildSetting.commandLine); 581 gs.compiler.setTarget(bs, gs.platform, objPath); 582 gs.compiler.invoke(bs, gs.platform, gs.compileCallback, gs.toolWorkingDirectory); 583 return objPath; 584 } 585 586 private void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) 587 { 588 auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); 589 auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; 590 591 scope (failure) { 592 logDiagnostic("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType); 593 auto tpath = getTargetPath(buildsettings, settings); 594 if (generate_binary && existsFile(tpath)) 595 removeFile(tpath); 596 } 597 if (settings.buildMode == BuildMode.singleFile && generate_binary) { 598 import std.parallelism, std.range : walkLength; 599 600 auto lbuildsettings = buildsettings; 601 auto srcs = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)); 602 auto objs = new string[](srcs.walkLength); 603 604 void compileSource(size_t i, string src) { 605 logInfo("Compiling", Color.light_green, "%s", src); 606 const objPath = pathToObjName(settings.platform, src, settings.toolWorkingDirectory); 607 objs[i] = compileUnit(src, objPath, buildsettings, settings); 608 } 609 610 if (settings.parallelBuild) { 611 foreach (i, src; srcs.parallel(1)) compileSource(i, src); 612 } else { 613 foreach (i, src; srcs.array) compileSource(i, src); 614 } 615 616 logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold)); 617 lbuildsettings.sourceFiles = is_static_library ? [] : lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array; 618 settings.compiler.setTarget(lbuildsettings, settings.platform); 619 settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); 620 settings.compiler.invokeLinker(lbuildsettings, settings.platform, objs, settings.linkCallback, settings.toolWorkingDirectory); 621 622 // NOTE: separate compile/link is not yet enabled for GDC. 623 } else if (generate_binary && (settings.buildMode == BuildMode.allAtOnce || settings.compiler.name == "gdc" || is_static_library)) { 624 // don't include symbols of dependencies (will be included by the top level target) 625 if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array; 626 627 // setup for command line 628 settings.compiler.setTarget(buildsettings, settings.platform); 629 settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine); 630 631 // invoke the compiler 632 settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback, settings.toolWorkingDirectory); 633 } else { 634 // determine path for the temporary object file 635 string tempobjname = buildsettings.targetName ~ getObjSuffix(settings.platform); 636 NativePath tempobj = NativePath(buildsettings.targetPath) ~ tempobjname; 637 638 // setup linker command line 639 auto lbuildsettings = buildsettings; 640 lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array; 641 if (generate_binary) settings.compiler.setTarget(lbuildsettings, settings.platform); 642 settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); 643 644 // setup compiler command line 645 buildsettings.libs = null; 646 buildsettings.lflags = null; 647 if (generate_binary) buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString()); 648 buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array; 649 650 settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine); 651 652 settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback, settings.toolWorkingDirectory); 653 654 if (generate_binary) { 655 if (settings.tempBuild) { 656 logInfo("Linking", Color.light_green, "%s => %s", buildsettings.targetName.color(Mode.bold), buildsettings.getTargetPath(settings)); 657 } else { 658 logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold)); 659 } 660 settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()], settings.linkCallback, settings.toolWorkingDirectory); 661 } 662 } 663 } 664 665 private void runTarget(NativePath exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings) 666 { 667 if (buildsettings.targetType == TargetType.executable) { 668 auto cwd = settings.toolWorkingDirectory; 669 auto runcwd = cwd; 670 if (buildsettings.workingDirectory.length) { 671 runcwd = NativePath(buildsettings.workingDirectory); 672 if (!runcwd.absolute) runcwd = cwd ~ runcwd; 673 } 674 if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path; 675 runPreRunCommands(m_project.rootPackage, m_project, settings, buildsettings); 676 logInfo("Running", Color.green, "%s %s", exe_file_path.relativeTo(runcwd), run_args.join(" ")); 677 string[string] env; 678 foreach (aa; [buildsettings.environments, buildsettings.runEnvironments]) 679 foreach (k, v; aa) 680 env[k] = v; 681 if (settings.runCallback) { 682 auto res = execute([ exe_file_path.toNativeString() ] ~ run_args, 683 env, Config.none, size_t.max, runcwd.toNativeString()); 684 settings.runCallback(res.status, res.output); 685 settings.targetExitStatus = res.status; 686 runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings); 687 } else { 688 auto prg_pid = spawnProcess([ exe_file_path.toNativeString() ] ~ run_args, 689 env, Config.none, runcwd.toNativeString()); 690 auto result = prg_pid.wait(); 691 settings.targetExitStatus = result; 692 runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings); 693 enforce(result == 0, "Program exited with code "~to!string(result)); 694 } 695 } else 696 enforce(false, "Target is a library. Skipping execution."); 697 } 698 699 private void runPreRunCommands(in Package pack, in Project proj, in GeneratorSettings settings, 700 in BuildSettings buildsettings) 701 { 702 if (buildsettings.preRunCommands.length) { 703 logInfo("Pre-run", Color.light_green, "Running commands..."); 704 runBuildCommands(CommandType.preRun, buildsettings.preRunCommands, pack, proj, settings, buildsettings); 705 } 706 } 707 708 private void runPostRunCommands(in Package pack, in Project proj, in GeneratorSettings settings, 709 in BuildSettings buildsettings) 710 { 711 if (buildsettings.postRunCommands.length) { 712 logInfo("Post-run", Color.light_green, "Running commands..."); 713 runBuildCommands(CommandType.postRun, buildsettings.postRunCommands, pack, proj, settings, buildsettings); 714 } 715 } 716 717 private void cleanupTemporaries() 718 { 719 foreach_reverse (f; m_temporaryFiles) { 720 try { 721 if (f.endsWithSlash) rmdir(f.toNativeString()); 722 else remove(f.toNativeString()); 723 } catch (Exception e) { 724 logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg); 725 logDiagnostic("Full error: %s", e.toString().sanitize); 726 } 727 } 728 m_temporaryFiles = null; 729 } 730 } 731 732 private NativePath getMainSourceFile(in Package prj) 733 { 734 foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"]) 735 if (existsFile(prj.path ~ f)) 736 return prj.path ~ f; 737 return prj.path ~ "source/app.d"; 738 } 739 740 private NativePath getTargetPath(const scope ref BuildSettings bs, const scope ref GeneratorSettings settings) 741 { 742 return NativePath(bs.targetPath) ~ settings.compiler.getTargetFileName(bs, settings.platform); 743 } 744 745 private string shrinkPath(NativePath path, NativePath base) 746 { 747 auto orig = path.toNativeString(); 748 if (!path.absolute) return orig; 749 version (Windows) 750 { 751 // avoid relative paths starting with `..\`: https://github.com/dlang/dub/issues/2143 752 if (!path.startsWith(base)) return orig; 753 } 754 auto rel = path.relativeTo(base).toNativeString(); 755 return rel.length < orig.length ? rel : orig; 756 } 757 758 unittest { 759 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo")) == NativePath("bar/baz").toNativeString()); 760 version (Windows) 761 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("/foo/bar/baz").toNativeString()); 762 else 763 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("../bar/baz").toNativeString()); 764 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/")) == NativePath("/foo/bar/baz").toNativeString()); 765 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/baz")) == NativePath("/foo/bar/baz").toNativeString()); 766 } 767 768 unittest { // issue #1235 - pass no library files to compiler command line when building a static lib 769 import dub.internal.vibecompat.data.json : parseJsonString; 770 import dub.compilers.gdc : GDCCompiler; 771 import dub.platform : determinePlatform; 772 773 version (Windows) auto libfile = "bar.lib"; 774 else auto libfile = "bar.a"; 775 776 auto desc = parseJsonString(`{"name": "test", "targetType": "library", "sourceFiles": ["foo.d", "`~libfile~`"]}`); 777 auto pack = new Package(desc, NativePath("/tmp/fooproject")); 778 auto pman = new PackageManager(pack.path, NativePath("/tmp/foo/"), NativePath("/tmp/foo/"), false); 779 auto prj = new Project(pman, pack); 780 781 final static class TestCompiler : GDCCompiler { 782 override void invoke(in BuildSettings settings, in BuildPlatform platform, void delegate(int, string) output_callback, NativePath cwd) { 783 assert(!settings.dflags[].any!(f => f.canFind("bar"))); 784 } 785 override void invokeLinker(in BuildSettings settings, in BuildPlatform platform, string[] objects, void delegate(int, string) output_callback, NativePath cwd) { 786 assert(false); 787 } 788 } 789 790 GeneratorSettings settings; 791 settings.platform = BuildPlatform(determinePlatform(), ["x86"], "gdc", "test", 2075); 792 settings.compiler = new TestCompiler; 793 settings.config = "library"; 794 settings.buildType = "debug"; 795 settings.tempBuild = true; 796 797 auto gen = new BuildGenerator(prj); 798 gen.generate(settings); 799 }