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.generators.generator; 12 import dub.internal.utils; 13 import dub.internal.vibecompat.core.file; 14 import dub.internal.vibecompat.core.log; 15 import dub.internal.vibecompat.inet.path; 16 import dub.package_; 17 import dub.packagemanager; 18 import dub.project; 19 20 import std.algorithm; 21 import std.array; 22 import std.conv; 23 import std.exception; 24 import std.file; 25 import std.process; 26 import std..string; 27 import std.encoding : sanitize; 28 29 version(Windows) enum objSuffix = ".obj"; 30 else enum objSuffix = ".o"; 31 32 class BuildGenerator : ProjectGenerator { 33 private { 34 PackageManager m_packageMan; 35 Path[] m_temporaryFiles; 36 } 37 38 this(Project project) 39 { 40 super(project); 41 m_packageMan = project.packageManager; 42 } 43 44 override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) 45 { 46 scope (exit) cleanupTemporaries(); 47 48 bool[string] visited; 49 void buildTargetRec(string target) 50 { 51 if (target in visited) return; 52 visited[target] = true; 53 54 auto ti = targets[target]; 55 56 foreach (dep; ti.dependencies) 57 buildTargetRec(dep); 58 59 Path[] additional_dep_files; 60 auto bs = ti.buildSettings.dup; 61 foreach (ldep; ti.linkDependencies) { 62 auto dbs = targets[ldep].buildSettings; 63 if (bs.targetType != TargetType.staticLibrary) { 64 bs.addSourceFiles((Path(dbs.targetPath) ~ getTargetFileName(dbs, settings.platform)).toNativeString()); 65 } else { 66 additional_dep_files ~= Path(dbs.targetPath) ~ getTargetFileName(dbs, settings.platform); 67 } 68 } 69 buildTarget(settings, bs, ti.pack, ti.config, ti.packages, additional_dep_files); 70 } 71 72 // build all targets 73 auto root_ti = targets[m_project.rootPackage.name]; 74 if (settings.rdmd || root_ti.buildSettings.targetType == TargetType.staticLibrary) { 75 // RDMD always builds everything at once and static libraries don't need their 76 // dependencies to be built 77 buildTarget(settings, root_ti.buildSettings.dup, m_project.rootPackage, root_ti.config, root_ti.packages, null); 78 } else buildTargetRec(m_project.rootPackage.name); 79 } 80 81 override void performPostGenerateActions(GeneratorSettings settings, in TargetInfo[string] targets) 82 { 83 // run the generated executable 84 auto buildsettings = targets[m_project.rootPackage.name].buildSettings; 85 if (settings.run && !(buildsettings.options & BuildOption.syntaxOnly)) { 86 auto exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); 87 runTarget(exe_file_path, buildsettings, settings.runArgs, settings); 88 } 89 } 90 91 private void buildTarget(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, in Package[] packages, in Path[] additional_dep_files) 92 { 93 auto cwd = Path(getcwd()); 94 bool generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); 95 96 auto build_id = computeBuildID(config, buildsettings, settings); 97 98 // make all paths relative to shrink the command line 99 string makeRelative(string path) { auto p = Path(path); if (p.absolute) p = p.relativeTo(cwd); return p.toNativeString(); } 100 foreach (ref f; buildsettings.sourceFiles) f = makeRelative(f); 101 foreach (ref p; buildsettings.importPaths) p = makeRelative(p); 102 foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p); 103 104 // perform the actual build 105 bool cached = false; 106 if (settings.rdmd) performRDMDBuild(settings, buildsettings, pack, config); 107 else if (settings.direct || !generate_binary) performDirectBuild(settings, buildsettings, pack, config); 108 else cached = performCachedBuild(settings, buildsettings, pack, config, build_id, packages, additional_dep_files); 109 110 // run post-build commands 111 if (!cached && buildsettings.postBuildCommands.length) { 112 logInfo("Running post-build commands..."); 113 runBuildCommands(buildsettings.postBuildCommands, buildsettings); 114 } 115 } 116 117 bool performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, string build_id, in Package[] packages, in Path[] additional_dep_files) 118 { 119 auto cwd = Path(getcwd()); 120 auto target_path = pack.path ~ format(".dub/build/%s/", build_id); 121 122 if (!settings.force && isUpToDate(target_path, buildsettings, settings.platform, pack, packages, additional_dep_files)) { 123 logInfo("Target %s %s is up to date. Use --force to rebuild.", pack.name, pack.vers); 124 logDiagnostic("Using existing build in %s.", target_path.toNativeString()); 125 copyTargetFile(target_path, buildsettings, settings.platform); 126 return true; 127 } 128 129 if (settings.tempBuild || !isWritableDir(target_path, true)) { 130 if (!settings.tempBuild) 131 logInfo("Build directory %s is not writable. Falling back to direct build in the system's temp folder.", target_path.relativeTo(cwd).toNativeString()); 132 performDirectBuild(settings, buildsettings, pack, config); 133 return false; 134 } 135 136 // determine basic build properties 137 auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); 138 139 logInfo("Building %s %s configuration \"%s\", build type %s.", pack.name, pack.vers, config, settings.buildType); 140 141 if( buildsettings.preBuildCommands.length ){ 142 logInfo("Running pre-build commands..."); 143 runBuildCommands(buildsettings.preBuildCommands, buildsettings); 144 } 145 146 // override target path 147 auto cbuildsettings = buildsettings; 148 cbuildsettings.targetPath = target_path.relativeTo(cwd).toNativeString(); 149 buildWithCompiler(settings, cbuildsettings); 150 151 copyTargetFile(target_path, buildsettings, settings.platform); 152 153 return false; 154 } 155 156 void performRDMDBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config) 157 { 158 auto cwd = Path(getcwd()); 159 //Added check for existance of [AppNameInPackagejson].d 160 //If exists, use that as the starting file. 161 Path mainsrc; 162 if (buildsettings.mainSourceFile.length) { 163 mainsrc = Path(buildsettings.mainSourceFile); 164 if (!mainsrc.absolute) mainsrc = pack.path ~ mainsrc; 165 } else { 166 mainsrc = getMainSourceFile(pack); 167 logWarn(`Package has no "mainSourceFile" defined. Using best guess: %s`, mainsrc.relativeTo(pack.path).toNativeString()); 168 } 169 170 // do not pass all source files to RDMD, only the main source file 171 buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(s => !s.endsWith(".d"))().array(); 172 settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); 173 174 auto generate_binary = !buildsettings.dflags.canFind("-o-"); 175 176 // Create start script, which will be used by the calling bash/cmd script. 177 // build "rdmd --force %DFLAGS% -I%~dp0..\source -Jviews -Isource @deps.txt %LIBS% source\app.d" ~ application arguments 178 // or with "/" instead of "\" 179 Path exe_file_path; 180 bool tmp_target = false; 181 if (generate_binary) { 182 if (settings.tempBuild || (settings.run && !isWritableDir(Path(buildsettings.targetPath), true))) { 183 import std.random; 184 auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-"; 185 auto tmpdir = getTempDir()~".rdmd/source/"; 186 buildsettings.targetPath = tmpdir.toNativeString(); 187 buildsettings.targetName = rnd ~ buildsettings.targetName; 188 m_temporaryFiles ~= tmpdir; 189 tmp_target = true; 190 } 191 exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); 192 settings.compiler.setTarget(buildsettings, settings.platform); 193 } 194 195 logDiagnostic("Application output name is '%s'", getTargetFileName(buildsettings, settings.platform)); 196 197 string[] flags = ["--build-only", "--compiler="~settings.platform.compilerBinary]; 198 if (settings.force) flags ~= "--force"; 199 flags ~= buildsettings.dflags; 200 flags ~= mainsrc.relativeTo(cwd).toNativeString(); 201 202 if (buildsettings.preBuildCommands.length){ 203 logInfo("Running pre-build commands..."); 204 runCommands(buildsettings.preBuildCommands); 205 } 206 207 logInfo("Building configuration "~config~", build type "~settings.buildType); 208 209 logInfo("Running rdmd..."); 210 logDiagnostic("rdmd %s", join(flags, " ")); 211 auto rdmd_pid = spawnProcess("rdmd" ~ flags); 212 auto result = rdmd_pid.wait(); 213 enforce(result == 0, "Build command failed with exit code "~to!string(result)); 214 215 if (tmp_target) { 216 m_temporaryFiles ~= exe_file_path; 217 foreach (f; buildsettings.copyFiles) 218 m_temporaryFiles ~= Path(buildsettings.targetPath).parentPath ~ Path(f).head; 219 } 220 } 221 222 void performDirectBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config) 223 { 224 auto cwd = Path(getcwd()); 225 226 auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); 227 auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; 228 229 // make file paths relative to shrink the command line 230 foreach (ref f; buildsettings.sourceFiles) { 231 auto fp = Path(f); 232 if( fp.absolute ) fp = fp.relativeTo(cwd); 233 f = fp.toNativeString(); 234 } 235 236 logInfo("Building configuration \""~config~"\", build type "~settings.buildType); 237 238 // make all target/import paths relative 239 string makeRelative(string path) { auto p = Path(path); if (p.absolute) p = p.relativeTo(cwd); return p.toNativeString(); } 240 buildsettings.targetPath = makeRelative(buildsettings.targetPath); 241 foreach (ref p; buildsettings.importPaths) p = makeRelative(p); 242 foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p); 243 244 Path exe_file_path; 245 bool is_temp_target = false; 246 if (generate_binary) { 247 if (settings.tempBuild || (settings.run && !isWritableDir(Path(buildsettings.targetPath), true))) { 248 import std.random; 249 auto rnd = to!string(uniform(uint.min, uint.max)); 250 auto tmppath = getTempDir()~("dub/"~rnd~"/"); 251 buildsettings.targetPath = tmppath.toNativeString(); 252 m_temporaryFiles ~= tmppath; 253 is_temp_target = true; 254 } 255 exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); 256 } 257 258 if( buildsettings.preBuildCommands.length ){ 259 logInfo("Running pre-build commands..."); 260 runBuildCommands(buildsettings.preBuildCommands, buildsettings); 261 } 262 263 buildWithCompiler(settings, buildsettings); 264 265 if (is_temp_target) { 266 m_temporaryFiles ~= exe_file_path; 267 foreach (f; buildsettings.copyFiles) 268 m_temporaryFiles ~= Path(buildsettings.targetPath).parentPath ~ Path(f).head; 269 } 270 } 271 272 private string computeBuildID(string config, in BuildSettings buildsettings, GeneratorSettings settings) 273 { 274 import std.digest.digest; 275 import std.digest.md; 276 import std.bitmanip; 277 278 MD5 hash; 279 hash.start(); 280 void addHash(in string[] strings...) { foreach (s; strings) { hash.put(cast(ubyte[])s); hash.put(0); } hash.put(0); } 281 void addHashI(int value) { hash.put(nativeToLittleEndian(value)); } 282 addHash(buildsettings.versions); 283 addHash(buildsettings.debugVersions); 284 //addHash(buildsettings.versionLevel); 285 //addHash(buildsettings.debugLevel); 286 addHash(buildsettings.dflags); 287 addHash(buildsettings.lflags); 288 addHash((cast(uint)buildsettings.options).to!string); 289 addHash(buildsettings.stringImportPaths); 290 addHash(settings.platform.architecture); 291 addHash(settings.platform.compilerBinary); 292 addHash(settings.platform.compiler); 293 addHashI(settings.platform.frontendVersion); 294 auto hashstr = hash.finish().toHexString().idup; 295 296 return format("%s-%s-%s-%s-%s_%s-%s", config, settings.buildType, 297 settings.platform.platform.join("."), 298 settings.platform.architecture.join("."), 299 settings.platform.compiler, settings.platform.frontendVersion, hashstr); 300 } 301 302 private void copyTargetFile(Path build_path, BuildSettings buildsettings, BuildPlatform platform) 303 { 304 auto filename = getTargetFileName(buildsettings, platform); 305 auto src = build_path ~ filename; 306 logDiagnostic("Copying target from %s to %s", src.toNativeString(), buildsettings.targetPath); 307 if (!existsFile(Path(buildsettings.targetPath))) 308 mkdirRecurse(buildsettings.targetPath); 309 hardLinkFile(src, Path(buildsettings.targetPath) ~ filename, true); 310 } 311 312 private bool isUpToDate(Path target_path, BuildSettings buildsettings, BuildPlatform platform, in Package main_pack, in Package[] packages, in Path[] additional_dep_files) 313 { 314 import std.datetime; 315 316 auto targetfile = target_path ~ getTargetFileName(buildsettings, platform); 317 if (!existsFile(targetfile)) { 318 logDiagnostic("Target '%s' doesn't exist, need rebuild.", targetfile.toNativeString()); 319 return false; 320 } 321 auto targettime = getFileInfo(targetfile).timeModified; 322 323 auto allfiles = appender!(string[]); 324 allfiles ~= buildsettings.sourceFiles; 325 allfiles ~= buildsettings.importFiles; 326 allfiles ~= buildsettings.stringImportFiles; 327 // TODO: add library files 328 foreach (p; packages) 329 allfiles ~= (p.packageInfoFilename != Path.init ? p : p.basePackage).packageInfoFilename.toNativeString(); 330 foreach (f; additional_dep_files) allfiles ~= f.toNativeString(); 331 if (main_pack is m_project.rootPackage) 332 allfiles ~= (main_pack.path ~ SelectedVersions.defaultFile).toNativeString(); 333 334 foreach (file; allfiles.data) { 335 if (!existsFile(file)) { 336 logDiagnostic("File %s doesn't exists, triggering rebuild.", file); 337 return false; 338 } 339 auto ftime = getFileInfo(file).timeModified; 340 if (ftime > Clock.currTime) 341 logWarn("File '%s' was modified in the future. Please re-save.", file); 342 if (ftime > targettime) { 343 logDiagnostic("File '%s' modified, need rebuild.", file); 344 return false; 345 } 346 } 347 return true; 348 } 349 350 /// Output an unique name to represent the source file. 351 /// Calls with path that resolve to the same file on the filesystem will return the same, 352 /// unless they include different symbolic links (which are not resolved). 353 354 static string pathToObjName(string path) 355 { 356 import std.path : buildNormalizedPath, dirSeparator, stripDrive; 357 return stripDrive(buildNormalizedPath(getcwd(), path~objSuffix))[1..$].replace(dirSeparator, "."); 358 } 359 360 /// Compile a single source file (srcFile), and write the object to objName. 361 static string compileUnit(string srcFile, string objName, BuildSettings bs, GeneratorSettings gs) { 362 Path tempobj = Path(bs.targetPath)~objName; 363 string objPath = tempobj.toNativeString(); 364 bs.libs = null; 365 bs.lflags = null; 366 bs.sourceFiles = [ srcFile ]; 367 bs.targetType = TargetType.object; 368 gs.compiler.prepareBuildSettings(bs, BuildSetting.commandLine); 369 gs.compiler.setTarget(bs, gs.platform, objPath); 370 gs.compiler.invoke(bs, gs.platform, gs.compileCallback); 371 return objPath; 372 } 373 374 void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) 375 { 376 auto generate_binary = !(buildsettings.options & BuildOption.syntaxOnly); 377 auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; 378 379 Path target_file; 380 scope (failure) { 381 logInfo("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType); 382 auto tpath = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); 383 if (generate_binary && existsFile(tpath)) 384 removeFile(tpath); 385 } 386 if (settings.buildMode == BuildMode.singleFile && generate_binary) { 387 import std.parallelism, std.range : walkLength; 388 389 auto lbuildsettings = buildsettings; 390 auto srcs = buildsettings.sourceFiles.filter!(f => !isLinkerFile(f)); 391 auto objs = new string[](srcs.walkLength); 392 logInfo("Compiling using %s...", settings.platform.compilerBinary); 393 394 void compileSource(size_t i, string src) { 395 logInfo("Compiling %s...", src); 396 objs[i] = compileUnit(src, pathToObjName(src), buildsettings, settings); 397 } 398 399 if (settings.parallelBuild) { 400 foreach (i, src; srcs.parallel(1)) compileSource(i, src); 401 } else { 402 foreach (i, src; srcs.array) compileSource(i, src); 403 } 404 405 logInfo("Linking..."); 406 lbuildsettings.sourceFiles = is_static_library ? [] : lbuildsettings.sourceFiles.filter!(f=> f.isLinkerFile()).array; 407 settings.compiler.setTarget(lbuildsettings, settings.platform); 408 settings.compiler.prepareBuildSettings(lbuildsettings, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); 409 settings.compiler.invokeLinker(lbuildsettings, settings.platform, objs, settings.linkCallback); 410 411 /* 412 NOTE: for DMD experimental separate compile/link is used, but this is not yet implemented 413 on the other compilers. Later this should be integrated somehow in the build process 414 (either in the dub.json, or using a command line flag) 415 */ 416 } else if (settings.buildMode == BuildMode.allAtOnce || settings.platform.compilerBinary != "dmd" || !generate_binary || is_static_library) { 417 // setup for command line 418 if (generate_binary) settings.compiler.setTarget(buildsettings, settings.platform); 419 settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); 420 421 // don't include symbols of dependencies (will be included by the top level target) 422 if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !f.isLinkerFile()).array; 423 424 // invoke the compiler 425 logInfo("Running %s...", settings.platform.compilerBinary); 426 settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback); 427 } else { 428 // determine path for the temporary object file 429 string tempobjname = buildsettings.targetName ~ objSuffix; 430 Path tempobj = Path(buildsettings.targetPath) ~ tempobjname; 431 432 // setup linker command line 433 auto lbuildsettings = buildsettings; 434 lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(f)).array; 435 settings.compiler.setTarget(lbuildsettings, settings.platform); 436 settings.compiler.prepareBuildSettings(lbuildsettings, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); 437 438 // setup compiler command line 439 buildsettings.libs = null; 440 buildsettings.lflags = null; 441 buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString()); 442 buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(f)).array; 443 settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); 444 445 logInfo("Compiling using %s...", settings.platform.compilerBinary); 446 settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback); 447 448 logInfo("Linking..."); 449 settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()], settings.linkCallback); 450 } 451 } 452 453 void runTarget(Path exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings) 454 { 455 if (buildsettings.targetType == TargetType.executable) { 456 auto cwd = Path(getcwd()); 457 auto runcwd = cwd; 458 if (buildsettings.workingDirectory.length) { 459 runcwd = Path(buildsettings.workingDirectory); 460 if (!runcwd.absolute) runcwd = cwd ~ runcwd; 461 logDiagnostic("Switching to %s", runcwd.toNativeString()); 462 chdir(runcwd.toNativeString()); 463 } 464 scope(exit) chdir(cwd.toNativeString()); 465 if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path; 466 auto exe_path_string = exe_file_path.relativeTo(runcwd).toNativeString(); 467 version (Posix) { 468 if (!exe_path_string.startsWith(".") && !exe_path_string.startsWith("/")) 469 exe_path_string = "./" ~ exe_path_string; 470 } 471 version (Windows) { 472 if (!exe_path_string.startsWith(".") && (exe_path_string.length < 2 || exe_path_string[1] != ':')) 473 exe_path_string = ".\\" ~ exe_path_string; 474 } 475 logInfo("Running %s %s", exe_path_string, run_args.join(" ")); 476 if (settings.runCallback) { 477 auto res = execute(exe_path_string ~ run_args); 478 settings.runCallback(res.status, res.output); 479 } else { 480 auto prg_pid = spawnProcess(exe_path_string ~ run_args); 481 auto result = prg_pid.wait(); 482 enforce(result == 0, "Program exited with code "~to!string(result)); 483 } 484 } else 485 enforce(false, "Target is a library. Skipping execution."); 486 } 487 488 void cleanupTemporaries() 489 { 490 foreach_reverse (f; m_temporaryFiles) { 491 try { 492 if (f.endsWithSlash) rmdir(f.toNativeString()); 493 else remove(f.toNativeString()); 494 } catch (Exception e) { 495 logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg); 496 logDiagnostic("Full error: %s", e.toString().sanitize); 497 } 498 } 499 m_temporaryFiles = null; 500 } 501 } 502 503 private Path getMainSourceFile(in Package prj) 504 { 505 foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"]) 506 if (existsFile(prj.path ~ f)) 507 return prj.path ~ f; 508 return prj.path ~ "source/app.d"; 509 } 510 511 unittest { 512 version (Windows) { 513 assert(isLinkerFile("test.obj")); 514 assert(isLinkerFile("test.lib")); 515 assert(isLinkerFile("test.res")); 516 assert(!isLinkerFile("test.o")); 517 assert(!isLinkerFile("test.d")); 518 } else { 519 assert(isLinkerFile("test.o")); 520 assert(isLinkerFile("test.a")); 521 assert(isLinkerFile("test.so")); 522 assert(isLinkerFile("test.dylib")); 523 assert(!isLinkerFile("test.obj")); 524 assert(!isLinkerFile("test.d")); 525 } 526 }