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