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 copyFile(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 copyFile(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 if (p.recipePath != NativePath.init) 530 allfiles ~= p.recipePath.toNativeString(); 531 else if (p.basePackage.recipePath != NativePath.init) 532 allfiles ~= p.basePackage.recipePath.toNativeString(); 533 } 534 foreach (f; additional_dep_files) allfiles ~= f.toNativeString(); 535 bool checkSelectedVersions = !settings.single; 536 if (checkSelectedVersions && main_pack is m_project.rootPackage && m_project.rootPackage.getAllDependencies().length > 0) 537 allfiles ~= (main_pack.path ~ SelectedVersions.defaultFile).toNativeString(); 538 539 foreach (file; allfiles.data) { 540 if (!existsFile(file)) { 541 logDiagnostic("File %s doesn't exist, triggering rebuild.", file); 542 return false; 543 } 544 auto ftime = getFileInfo(file).timeModified; 545 if (ftime > Clock.currTime) 546 logWarn("File '%s' was modified in the future. Please re-save.", file); 547 if (ftime > targettime) { 548 logDiagnostic("File '%s' modified, need rebuild.", file); 549 return false; 550 } 551 } 552 return true; 553 } 554 555 /// Output an unique name to represent the source file. 556 /// Calls with path that resolve to the same file on the filesystem will return the same, 557 /// unless they include different symbolic links (which are not resolved). 558 deprecated("Use the overload taking in the current working directory") 559 static string pathToObjName(const scope ref BuildPlatform platform, string path) 560 { 561 return pathToObjName(platform, path, getWorkingDirectory); 562 } 563 564 /// ditto 565 static string pathToObjName(const scope ref BuildPlatform platform, string path, NativePath cwd) 566 { 567 import std.digest.crc : crc32Of; 568 import std.path : buildNormalizedPath, dirSeparator, relativePath, stripDrive; 569 if (path.endsWith(".d")) path = path[0 .. $-2]; 570 auto ret = buildNormalizedPath(cwd.toNativeString(), path).replace(dirSeparator, "."); 571 auto idx = ret.lastIndexOf('.'); 572 const objSuffix = getObjSuffix(platform); 573 return idx < 0 ? ret ~ objSuffix : format("%s_%(%02x%)%s", ret[idx+1 .. $], crc32Of(ret[0 .. idx]), objSuffix); 574 } 575 576 /// Compile a single source file (srcFile), and write the object to objName. 577 static string compileUnit(string srcFile, string objName, BuildSettings bs, GeneratorSettings gs) { 578 NativePath tempobj = NativePath(bs.targetPath)~objName; 579 string objPath = tempobj.toNativeString(); 580 bs.libs = null; 581 bs.frameworks = null; 582 bs.lflags = null; 583 bs.sourceFiles = [ srcFile ]; 584 bs.targetType = TargetType.object; 585 gs.compiler.prepareBuildSettings(bs, gs.platform, BuildSetting.commandLine); 586 gs.compiler.setTarget(bs, gs.platform, objPath); 587 gs.compiler.invoke(bs, gs.platform, gs.compileCallback, gs.toolWorkingDirectory); 588 return objPath; 589 } 590 591 private void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) 592 { 593 auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); 594 auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; 595 596 scope (failure) { 597 logDiagnostic("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType); 598 auto tpath = getTargetPath(buildsettings, settings); 599 if (generate_binary && existsFile(tpath)) 600 removeFile(tpath); 601 } 602 if (settings.buildMode == BuildMode.singleFile && generate_binary) { 603 import std.parallelism, std.range : walkLength; 604 import std.algorithm : filter, startsWith; 605 606 auto lbuildsettings = buildsettings; 607 auto srcs = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)); 608 auto objs = new string[](srcs.walkLength); 609 610 // Resolve pkg-config flags early to get C preprocessor flags for compilation. 611 // We do this on lbuildsettings first, then copy -P flags to buildsettings. 612 lbuildsettings.sourceFiles = is_static_library ? [] : lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array; 613 settings.compiler.setTarget(lbuildsettings, settings.platform); 614 settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); 615 616 // Copy C preprocessor flags (-P...) from linker settings to compiler settings. 617 foreach (flag; lbuildsettings.dflags.filter!(f => f.startsWith("-P"))) 618 buildsettings.addDFlags(flag); 619 620 void compileSource(size_t i, string src) { 621 logInfo("Compiling", Color.light_green, "%s", src); 622 const objPath = pathToObjName(settings.platform, src, settings.toolWorkingDirectory); 623 objs[i] = compileUnit(src, objPath, buildsettings, settings); 624 } 625 626 if (settings.parallelBuild) { 627 foreach (i, src; srcs.parallel(1)) compileSource(i, src); 628 } else { 629 foreach (i, src; srcs.array) compileSource(i, src); 630 } 631 632 logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold)); 633 settings.compiler.invokeLinker(lbuildsettings, settings.platform, objs, settings.linkCallback, settings.toolWorkingDirectory); 634 635 // NOTE: separate compile/link is not yet enabled for GDC. 636 } else if (generate_binary && (settings.buildMode == BuildMode.allAtOnce || settings.compiler.name == "gdc" || is_static_library)) { 637 // don't include symbols of dependencies (will be included by the top level target) 638 if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array; 639 640 // setup for command line 641 settings.compiler.setTarget(buildsettings, settings.platform); 642 settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine); 643 644 // invoke the compiler 645 settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback, settings.toolWorkingDirectory); 646 } else { 647 // determine path for the temporary object file 648 string tempobjname = buildsettings.targetName ~ getObjSuffix(settings.platform); 649 NativePath tempobj = NativePath(buildsettings.targetPath) ~ tempobjname; 650 651 // setup linker command line 652 auto lbuildsettings = buildsettings; 653 lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(settings.platform, f)).array; 654 if (generate_binary) settings.compiler.setTarget(lbuildsettings, settings.platform); 655 settings.compiler.prepareBuildSettings(lbuildsettings, settings.platform, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); 656 657 // Copy C preprocessor flags (-P...) from linker settings to compiler settings. 658 // These come from pkg-config --cflags and are needed for ImportC compilation. 659 // We must copy them before clearing libs, since resolveLibs extracts them. 660 import std.algorithm : filter, startsWith; 661 foreach (flag; lbuildsettings.dflags.filter!(f => f.startsWith("-P"))) 662 buildsettings.addDFlags(flag); 663 664 // setup compiler command line 665 buildsettings.libs = null; 666 buildsettings.frameworks = null; 667 buildsettings.lflags = null; 668 if (generate_binary) buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString()); 669 buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(settings.platform, f)).array; 670 671 settings.compiler.prepareBuildSettings(buildsettings, settings.platform, BuildSetting.commandLine); 672 673 settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback, settings.toolWorkingDirectory); 674 675 if (generate_binary) { 676 if (settings.tempBuild) { 677 logInfo("Linking", Color.light_green, "%s => %s", buildsettings.targetName.color(Mode.bold), buildsettings.getTargetPath(settings)); 678 } else { 679 logInfo("Linking", Color.light_green, "%s", buildsettings.targetName.color(Mode.bold)); 680 } 681 settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()], settings.linkCallback, settings.toolWorkingDirectory); 682 } 683 } 684 } 685 686 private void runTarget(NativePath exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings) 687 { 688 if (buildsettings.targetType == TargetType.executable) { 689 auto cwd = settings.toolWorkingDirectory; 690 auto runcwd = cwd; 691 if (buildsettings.workingDirectory.length) { 692 runcwd = NativePath(buildsettings.workingDirectory); 693 if (!runcwd.absolute) runcwd = cwd ~ runcwd; 694 } 695 ensureDirectory(runcwd); 696 if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path; 697 runPreRunCommands(m_project.rootPackage, m_project, settings, buildsettings); 698 logInfo("Running", Color.green, "%s %s", exe_file_path.relativeTo(runcwd), run_args.join(" ")); 699 string[string] env; 700 foreach (aa; [buildsettings.environments, buildsettings.runEnvironments]) 701 foreach (k, v; aa) 702 env[k] = v; 703 if (settings.runCallback) { 704 auto res = execute([ exe_file_path.toNativeString() ] ~ run_args, 705 env, Config.none, size_t.max, runcwd.toNativeString()); 706 settings.runCallback(res.status, res.output); 707 settings.targetExitStatus = res.status; 708 runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings); 709 } else { 710 auto prg_pid = spawnProcess([ exe_file_path.toNativeString() ] ~ run_args, 711 env, Config.none, runcwd.toNativeString()); 712 auto result = prg_pid.wait(); 713 settings.targetExitStatus = result; 714 runPostRunCommands(m_project.rootPackage, m_project, settings, buildsettings); 715 enforce(result == 0, "Program exited with code "~to!string(result)); 716 } 717 } else 718 enforce(false, "Target is a library. Skipping execution."); 719 } 720 721 private void runPreRunCommands(in Package pack, in Project proj, in GeneratorSettings settings, 722 in BuildSettings buildsettings) 723 { 724 if (buildsettings.preRunCommands.length) { 725 logInfo("Pre-run", Color.light_green, "Running commands..."); 726 runBuildCommands(CommandType.preRun, buildsettings.preRunCommands, pack, proj, settings, buildsettings); 727 } 728 } 729 730 private void runPostRunCommands(in Package pack, in Project proj, in GeneratorSettings settings, 731 in BuildSettings buildsettings) 732 { 733 if (buildsettings.postRunCommands.length) { 734 logInfo("Post-run", Color.light_green, "Running commands..."); 735 runBuildCommands(CommandType.postRun, buildsettings.postRunCommands, pack, proj, settings, buildsettings); 736 } 737 } 738 739 private void cleanupTemporaries() 740 { 741 foreach_reverse (f; m_temporaryFiles) { 742 try { 743 if (f.endsWithSlash) rmdir(f.toNativeString()); 744 else remove(f.toNativeString()); 745 } catch (Exception e) { 746 logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg); 747 logDiagnostic("Full error: %s", e.toString().sanitize); 748 } 749 } 750 m_temporaryFiles = null; 751 } 752 } 753 754 private NativePath getMainSourceFile(in Package prj) 755 { 756 foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"]) 757 if (existsFile(prj.path ~ f)) 758 return prj.path ~ f; 759 return prj.path ~ "source/app.d"; 760 } 761 762 private NativePath getTargetPath(const scope ref BuildSettings bs, const scope ref GeneratorSettings settings) 763 { 764 return NativePath(bs.targetPath) ~ settings.compiler.getTargetFileName(bs, settings.platform); 765 } 766 767 private string shrinkPath(NativePath path, NativePath base) 768 { 769 auto orig = path.toNativeString(); 770 if (!path.absolute) return orig; 771 version (Windows) 772 { 773 // avoid relative paths starting with `..\`: https://github.com/dlang/dub/issues/2143 774 if (!path.startsWith(base)) return orig; 775 } 776 auto rel = path.relativeTo(base).toNativeString(); 777 return rel.length < orig.length ? rel : orig; 778 } 779 780 unittest { 781 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo")) == NativePath("bar/baz").toNativeString()); 782 version (Windows) 783 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("/foo/bar/baz").toNativeString()); 784 else 785 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/foo/baz")) == NativePath("../bar/baz").toNativeString()); 786 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/")) == NativePath("/foo/bar/baz").toNativeString()); 787 assert(shrinkPath(NativePath("/foo/bar/baz"), NativePath("/bar/baz")) == NativePath("/foo/bar/baz").toNativeString()); 788 } 789 790 unittest { // issue #1235 - pass no library files to compiler command line when building a static lib 791 import dub.recipe.io : parsePackageRecipe; 792 import dub.compilers.gdc : GDCCompiler; 793 import dub.platform : determinePlatform; 794 795 version (Windows) auto libfile = "bar.lib"; 796 else auto libfile = "bar.a"; 797 798 auto recipe = parsePackageRecipe( 799 `{"name":"test", "targetType":"library", "sourceFiles":["foo.d", "`~libfile~`"]}`, 800 `/tmp/fooproject/dub.json`); 801 auto pack = new Package(recipe, NativePath("/tmp/fooproject")); 802 auto pman = new PackageManager(pack.path, NativePath("/tmp/foo/"), NativePath("/tmp/foo/"), false); 803 auto prj = new Project(pman, pack); 804 805 final static class TestCompiler : GDCCompiler { 806 override void invoke(in BuildSettings settings, in BuildPlatform platform, void delegate(int, string) output_callback, NativePath cwd) { 807 assert(!settings.dflags[].any!(f => f.canFind("bar"))); 808 } 809 override void invokeLinker(in BuildSettings settings, in BuildPlatform platform, string[] objects, void delegate(int, string) output_callback, NativePath cwd) { 810 assert(false); 811 } 812 } 813 814 GeneratorSettings settings; 815 settings.platform = BuildPlatform(determinePlatform(), ["x86"], "gdc", "test", 2075); 816 settings.compiler = new TestCompiler; 817 settings.config = "library"; 818 settings.buildType = "debug"; 819 settings.tempBuild = true; 820 821 auto gen = new BuildGenerator(prj); 822 gen.generate(settings); 823 }