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, toHexString; 38 39 SHA256 hash; 40 hash.start(); 41 void addHash(in string[] strings...) { foreach (s; strings) { hash.put(cast(ubyte[])s); hash.put(0); } hash.put(0); } 42 foreach(strings; hashing) 43 addHash(strings); 44 const hashstr = hash.finish().toHexString(); 45 46 return format("%s-%s-%s-%s-%s_v%s-%s", config, settings.buildType, 47 settings.platform.platform.join("."), 48 settings.platform.architecture.join("."), 49 settings.platform.compiler, settings.platform.compilerVersion, hashstr); 50 } 51 52 class BuildGenerator : ProjectGenerator { 53 private { 54 PackageManager m_packageMan; 55 NativePath[] m_temporaryFiles; 56 } 57 58 this(Project project) 59 { 60 super(project); 61 m_packageMan = project.packageManager; 62 } 63 64 override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) 65 { 66 import std.path : setExtension; 67 scope (exit) cleanupTemporaries(); 68 69 void checkPkgRequirements(const(Package) pkg) 70 { 71 const tr = pkg.recipe.toolchainRequirements; 72 tr.checkPlatform(settings.platform, pkg.name); 73 } 74 75 checkPkgRequirements(m_project.rootPackage); 76 foreach (pkg; m_project.dependencies) 77 checkPkgRequirements(pkg); 78 79 auto root_ti = targets[m_project.rootPackage.name]; 80 const rootTT = root_ti.buildSettings.targetType; 81 82 enforce(!(settings.rdmd && rootTT == TargetType.none), 83 "Building package with target type \"none\" with rdmd is not supported yet."); 84 85 logInfo("Starting", Color.light_green, 86 "Performing \"%s\" build using %s for %-(%s, %).", 87 settings.buildType.color(Color.magenta), settings.platform.compilerBinary, 88 settings.platform.architecture); 89 90 bool any_cached = false; 91 92 NativePath[string] target_paths; 93 94 NativePath[] dynamicLibDepsFilesToCopy; // to the root package output dir 95 const copyDynamicLibDepsLinkerFiles = rootTT == TargetType.dynamicLibrary || rootTT == TargetType.none; 96 const copyDynamicLibDepsRuntimeFiles = copyDynamicLibDepsLinkerFiles || rootTT == TargetType.executable; 97 98 bool[string] visited; 99 void buildTargetRec(string target) 100 { 101 if (target in visited) return; 102 visited[target] = true; 103 104 auto ti = targets[target]; 105 106 foreach (dep; ti.dependencies) 107 buildTargetRec(dep); 108 109 NativePath[] additional_dep_files; 110 auto bs = ti.buildSettings.dup; 111 const tt = bs.targetType; 112 foreach (ldep; ti.linkDependencies) { 113 const ldepPath = target_paths[ldep].toNativeString(); 114 const doLink = tt != TargetType.staticLibrary && !(bs.options & BuildOption.syntaxOnly); 115 116 if (doLink && isLinkerFile(settings.platform, ldepPath)) 117 bs.addSourceFiles(ldepPath); 118 else 119 additional_dep_files ~= target_paths[ldep]; 120 121 if (targets[ldep].buildSettings.targetType == TargetType.dynamicLibrary) { 122 // copy the .{dll,so,dylib} 123 if (copyDynamicLibDepsRuntimeFiles) 124 dynamicLibDepsFilesToCopy ~= NativePath(ldepPath); 125 126 if (settings.platform.isWindows()) { 127 // copy the accompanying .pdb if found 128 if (copyDynamicLibDepsRuntimeFiles) { 129 const pdb = ldepPath.setExtension(".pdb"); 130 if (existsFile(pdb)) 131 dynamicLibDepsFilesToCopy ~= NativePath(pdb); 132 } 133 134 const importLib = ldepPath.setExtension(".lib"); 135 if (existsFile(importLib)) { 136 // link dependee against the import lib 137 if (doLink) 138 bs.addSourceFiles(importLib); 139 // and copy 140 if (copyDynamicLibDepsLinkerFiles) 141 dynamicLibDepsFilesToCopy ~= NativePath(importLib); 142 } 143 144 // copy the .exp file if found 145 const exp = ldepPath.setExtension(".exp"); 146 if (copyDynamicLibDepsLinkerFiles && existsFile(exp)) 147 dynamicLibDepsFilesToCopy ~= NativePath(exp); 148 } 149 } 150 } 151 NativePath tpath; 152 if (tt != TargetType.none) { 153 if (buildTarget(settings, bs, ti.pack, ti.config, ti.packages, additional_dep_files, tpath)) 154 any_cached = true; 155 } 156 target_paths[target] = tpath; 157 } 158 159 // build all targets 160 if (settings.rdmd || rootTT == TargetType.staticLibrary) { 161 // RDMD always builds everything at once and static libraries don't need their 162 // dependencies to be built 163 NativePath tpath; 164 buildTarget(settings, root_ti.buildSettings.dup, m_project.rootPackage, root_ti.config, root_ti.packages, null, tpath); 165 return; 166 } 167 168 buildTargetRec(m_project.rootPackage.name); 169 170 if (dynamicLibDepsFilesToCopy.length) { 171 const rootTargetPath = NativePath(root_ti.buildSettings.targetPath); 172 173 ensureDirectory(rootTargetPath); 174 foreach (src; dynamicLibDepsFilesToCopy) { 175 logDiagnostic("Copying target from %s to %s", 176 src.toNativeString(), rootTargetPath.toNativeString()); 177 hardLinkFile(src, rootTargetPath ~ src.head, true); 178 } 179 } 180 181 if (any_cached) { 182 logInfo("Finished", Color.green, 183 "To force a rebuild of up-to-date targets, run again with --force" 184 ); 185 } 186 } 187 188 override void performPostGenerateActions(GeneratorSettings settings, in TargetInfo[string] targets) 189 { 190 // run the generated executable 191 auto buildsettings = targets[m_project.rootPackage.name].buildSettings.dup; 192 if (settings.run && !(buildsettings.options & BuildOption.syntaxOnly)) { 193 NativePath exe_file_path; 194 if (m_tempTargetExecutablePath.empty) 195 exe_file_path = getTargetPath(buildsettings, settings); 196 else 197 exe_file_path = m_tempTargetExecutablePath ~ settings.compiler.getTargetFileName(buildsettings, settings.platform); 198 runTarget(exe_file_path, buildsettings, settings.runArgs, settings); 199 } 200 } 201 202 private bool buildTarget(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, in Package[] packages, in NativePath[] additional_dep_files, out NativePath target_path) 203 { 204 import std.path : absolutePath; 205 206 auto cwd = settings.toolWorkingDirectory; 207 bool generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); 208 209 auto build_id = buildsettings.computeBuildID(config, settings); 210 211 // make all paths relative to shrink the command line 212 string makeRelative(string path) { return shrinkPath(NativePath(path), cwd); } 213 foreach (ref f; buildsettings.sourceFiles) f = makeRelative(f); 214 foreach (ref p; buildsettings.importPaths) p = makeRelative(p); 215 foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p); 216 217 // perform the actual build 218 bool cached = false; 219 if (settings.rdmd) performRDMDBuild(settings, buildsettings, pack, config, target_path); 220 else if (settings.direct || !generate_binary) performDirectBuild(settings, buildsettings, pack, config, target_path); 221 else cached = performCachedBuild(settings, buildsettings, pack, config, build_id, packages, additional_dep_files, target_path); 222 223 // HACK: cleanup dummy doc files, we shouldn't specialize on buildType 224 // here and the compiler shouldn't need dummy doc output. 225 if (settings.buildType == "ddox") { 226 if ("__dummy.html".exists) 227 removeFile("__dummy.html"); 228 if ("__dummy_docs".exists) 229 rmdirRecurse("__dummy_docs"); 230 } 231 232 // run post-build commands 233 if (!cached && buildsettings.postBuildCommands.length) { 234 logInfo("Post-build", Color.light_green, "Running commands"); 235 runBuildCommands(CommandType.postBuild, buildsettings.postBuildCommands, pack, m_project, settings, buildsettings, 236 [["DUB_BUILD_PATH" : target_path is NativePath.init 237 ? "" 238 : target_path.parentPath.toNativeString.absolutePath(settings.toolWorkingDirectory.toNativeString)]]); 239 } 240 241 return cached; 242 } 243 244 private bool performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, 245 string build_id, in Package[] packages, in NativePath[] additional_dep_files, out NativePath target_binary_path) 246 { 247 auto cwd = settings.toolWorkingDirectory; 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.relativeTo(cwd).toNativeString()); 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 = shrinkPath(target_path, cwd); 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 }