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 & BuildOptions.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 & BuildOptions.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 & BuildOptions.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 & BuildOptions.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 return std.path.stripDrive(std.path.buildNormalizedPath(getcwd(), path~objSuffix))[1..$].replace(std.path.dirSeparator, "."); 357 } 358 359 /// Compile a single source file (srcFile), and write the object to objName. 360 static string compileUnit(string srcFile, string objName, BuildSettings bs, GeneratorSettings gs) { 361 Path tempobj = Path(bs.targetPath)~objName; 362 string objPath = tempobj.toNativeString(); 363 bs.libs = null; 364 bs.lflags = null; 365 bs.sourceFiles = [ srcFile ]; 366 bs.targetType = TargetType.object; 367 gs.compiler.prepareBuildSettings(bs, BuildSetting.commandLine); 368 gs.compiler.setTarget(bs, gs.platform, objPath); 369 gs.compiler.invoke(bs, gs.platform, gs.compileCallback); 370 return objPath; 371 } 372 373 void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) 374 { 375 auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); 376 auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; 377 378 Path target_file; 379 scope (failure) { 380 logInfo("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType); 381 auto tpath = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); 382 if (generate_binary && existsFile(tpath)) 383 removeFile(tpath); 384 } 385 if (settings.buildMode == BuildMode.singleFile && generate_binary) { 386 import std.parallelism, std.range : walkLength; 387 388 auto lbuildsettings = buildsettings; 389 auto srcs = buildsettings.sourceFiles.filter!(f => !isLinkerFile(f)); 390 auto objs = new string[](srcs.walkLength); 391 logInfo("Compiling using %s...", settings.platform.compilerBinary); 392 393 void compileSource(size_t i, string src) { 394 logInfo("Compiling %s...", src); 395 objs[i] = compileUnit(src, pathToObjName(src), buildsettings, settings); 396 } 397 398 if (settings.parallelBuild) { 399 foreach (i, src; srcs.parallel(1)) compileSource(i, src); 400 } else { 401 foreach (i, src; srcs.array) compileSource(i, src); 402 } 403 404 logInfo("Linking..."); 405 lbuildsettings.sourceFiles = is_static_library ? [] : lbuildsettings.sourceFiles.filter!(f=> f.isLinkerFile()).array; 406 settings.compiler.setTarget(lbuildsettings, settings.platform); 407 settings.compiler.prepareBuildSettings(lbuildsettings, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); 408 settings.compiler.invokeLinker(lbuildsettings, settings.platform, objs, settings.linkCallback); 409 410 /* 411 NOTE: for DMD experimental separate compile/link is used, but this is not yet implemented 412 on the other compilers. Later this should be integrated somehow in the build process 413 (either in the dub.json, or using a command line flag) 414 */ 415 } else if (settings.buildMode == BuildMode.allAtOnce || settings.platform.compilerBinary != "dmd" || !generate_binary || is_static_library) { 416 // setup for command line 417 if (generate_binary) settings.compiler.setTarget(buildsettings, settings.platform); 418 settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); 419 420 // don't include symbols of dependencies (will be included by the top level target) 421 if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !f.isLinkerFile()).array; 422 423 // invoke the compiler 424 logInfo("Running %s...", settings.platform.compilerBinary); 425 settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback); 426 } else { 427 // determine path for the temporary object file 428 string tempobjname = buildsettings.targetName ~ objSuffix; 429 Path tempobj = Path(buildsettings.targetPath) ~ tempobjname; 430 431 // setup linker command line 432 auto lbuildsettings = buildsettings; 433 lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(f)).array; 434 settings.compiler.setTarget(lbuildsettings, settings.platform); 435 settings.compiler.prepareBuildSettings(lbuildsettings, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); 436 437 // setup compiler command line 438 buildsettings.libs = null; 439 buildsettings.lflags = null; 440 buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString()); 441 buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(f)).array; 442 settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); 443 444 logInfo("Compiling using %s...", settings.platform.compilerBinary); 445 settings.compiler.invoke(buildsettings, settings.platform, settings.compileCallback); 446 447 logInfo("Linking..."); 448 settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()], settings.linkCallback); 449 } 450 } 451 452 void runTarget(Path exe_file_path, in BuildSettings buildsettings, string[] run_args, GeneratorSettings settings) 453 { 454 if (buildsettings.targetType == TargetType.executable) { 455 auto cwd = Path(getcwd()); 456 auto runcwd = cwd; 457 if (buildsettings.workingDirectory.length) { 458 runcwd = Path(buildsettings.workingDirectory); 459 if (!runcwd.absolute) runcwd = cwd ~ runcwd; 460 logDiagnostic("Switching to %s", runcwd.toNativeString()); 461 chdir(runcwd.toNativeString()); 462 } 463 scope(exit) chdir(cwd.toNativeString()); 464 if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path; 465 auto exe_path_string = exe_file_path.relativeTo(runcwd).toNativeString(); 466 version (Posix) { 467 if (!exe_path_string.startsWith(".") && !exe_path_string.startsWith("/")) 468 exe_path_string = "./" ~ exe_path_string; 469 } 470 version (Windows) { 471 if (!exe_path_string.startsWith(".") && (exe_path_string.length < 2 || exe_path_string[1] != ':')) 472 exe_path_string = ".\\" ~ exe_path_string; 473 } 474 logInfo("Running %s %s", exe_path_string, run_args.join(" ")); 475 if (settings.runCallback) { 476 auto res = execute(exe_path_string ~ run_args); 477 settings.runCallback(res.status, res.output); 478 } else { 479 auto prg_pid = spawnProcess(exe_path_string ~ run_args); 480 auto result = prg_pid.wait(); 481 enforce(result == 0, "Program exited with code "~to!string(result)); 482 } 483 } else logInfo("Target is a library. Skipping execution."); 484 } 485 486 void cleanupTemporaries() 487 { 488 foreach_reverse (f; m_temporaryFiles) { 489 try { 490 if (f.endsWithSlash) rmdir(f.toNativeString()); 491 else remove(f.toNativeString()); 492 } catch (Exception e) { 493 logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg); 494 logDiagnostic("Full error: %s", e.toString().sanitize); 495 } 496 } 497 m_temporaryFiles = null; 498 } 499 } 500 501 private Path getMainSourceFile(in Package prj) 502 { 503 foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"]) 504 if (existsFile(prj.path ~ f)) 505 return prj.path ~ f; 506 return prj.path ~ "source/app.d"; 507 } 508 509 unittest { 510 version (Windows) { 511 assert(isLinkerFile("test.obj")); 512 assert(isLinkerFile("test.lib")); 513 assert(isLinkerFile("test.res")); 514 assert(!isLinkerFile("test.o")); 515 assert(!isLinkerFile("test.d")); 516 } else { 517 assert(isLinkerFile("test.o")); 518 assert(isLinkerFile("test.a")); 519 assert(isLinkerFile("test.so")); 520 assert(isLinkerFile("test.dylib")); 521 assert(!isLinkerFile("test.obj")); 522 assert(!isLinkerFile("test.d")); 523 } 524 }