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 28 29 class BuildGenerator : ProjectGenerator { 30 private { 31 Project m_project; 32 PackageManager m_packageMan; 33 Path[] m_temporaryFiles; 34 } 35 36 this(Project app, PackageManager mgr) 37 { 38 super(app); 39 m_project = app; 40 m_packageMan = mgr; 41 } 42 43 override void generateTargets(GeneratorSettings settings, in TargetInfo[string] targets) 44 { 45 scope (exit) cleanupTemporaries(); 46 47 bool[string] visited; 48 void buildTargetRec(string target) 49 { 50 if (target in visited) return; 51 visited[target] = true; 52 53 auto ti = targets[target]; 54 55 foreach (dep; ti.dependencies) 56 buildTargetRec(dep); 57 58 auto bs = ti.buildSettings.dup; 59 if (bs.targetType != TargetType.staticLibrary) 60 foreach (ldep; ti.linkDependencies) { 61 auto dbs = targets[ldep].buildSettings; 62 bs.addSourceFiles((Path(dbs.targetPath) ~ getTargetFileName(dbs, settings.platform)).toNativeString()); 63 } 64 buildTarget(settings, bs, ti.pack, ti.config); 65 } 66 67 // build all targets 68 buildTargetRec(m_project.mainPackage.name); 69 70 // run the generated executable 71 auto buildsettings = targets[m_project.mainPackage.name].buildSettings; 72 if (settings.run && !(buildsettings.options & BuildOptions.syntaxOnly)) { 73 auto exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); 74 runTarget(exe_file_path, buildsettings, settings.runArgs); 75 } 76 } 77 78 private void buildTarget(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config) 79 { 80 auto cwd = Path(getcwd()); 81 bool generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); 82 83 auto build_id = computeBuildID(config, buildsettings, settings); 84 85 // make all paths relative to shrink the command line 86 string makeRelative(string path) { auto p = Path(path); if (p.absolute) p = p.relativeTo(cwd); return p.toNativeString(); } 87 foreach (ref f; buildsettings.sourceFiles) f = makeRelative(f); 88 foreach (ref p; buildsettings.importPaths) p = makeRelative(p); 89 foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p); 90 91 // perform the actual build 92 if (settings.rdmd) performRDMDBuild(settings, buildsettings, pack, config); 93 else if (settings.direct || !generate_binary) performDirectBuild(settings, buildsettings, pack, config); 94 else performCachedBuild(settings, buildsettings, pack, config, build_id); 95 96 // run post-build commands 97 if (buildsettings.postBuildCommands.length) { 98 logInfo("Running post-build commands..."); 99 runBuildCommands(buildsettings.postBuildCommands, buildsettings); 100 } 101 } 102 103 void performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings, in Package pack, string config, string build_id) 104 { 105 auto cwd = Path(getcwd()); 106 auto target_path = pack.path ~ format(".dub/build/%s/", build_id); 107 108 if (!settings.force && isUpToDate(target_path, buildsettings, settings.platform)) { 109 logInfo("Target is up to date. Using existing build in %s. Use --force to force a rebuild.", target_path.toNativeString()); 110 copyTargetFile(target_path, buildsettings, settings.platform); 111 return; 112 } 113 114 if (!isWritableDir(target_path, true)) { 115 logInfo("Build directory %s is not writable. Falling back to direct build in the system's temp folder.", target_path.relativeTo(cwd).toNativeString()); 116 performDirectBuild(settings, buildsettings, pack, config); 117 return; 118 } 119 120 // determine basic build properties 121 auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); 122 123 // run pre-/post-generate commands and copy "copyFiles" 124 prepareGeneration(buildsettings); 125 finalizeGeneration(buildsettings, generate_binary); 126 127 logInfo("Building %s configuration \"%s\", build type %s.", pack.name, config, settings.buildType); 128 129 if( buildsettings.preBuildCommands.length ){ 130 logInfo("Running pre-build commands..."); 131 runBuildCommands(buildsettings.preBuildCommands, buildsettings); 132 } 133 134 // override target path 135 auto cbuildsettings = buildsettings; 136 cbuildsettings.targetPath = target_path.relativeTo(cwd).toNativeString(); 137 buildWithCompiler(settings, cbuildsettings); 138 139 copyTargetFile(target_path, buildsettings, settings.platform); 140 } 141 142 void performRDMDBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config) 143 { 144 auto cwd = Path(getcwd()); 145 //Added check for existance of [AppNameInPackagejson].d 146 //If exists, use that as the starting file. 147 auto mainsrc = buildsettings.mainSourceFile.length ? pack.path ~ buildsettings.mainSourceFile : getMainSourceFile(pack); 148 149 // do not pass all source files to RDMD, only the main source file 150 buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(s => !s.endsWith(".d"))().array(); 151 settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); 152 153 auto generate_binary = !buildsettings.dflags.canFind("-o-"); 154 155 // Create start script, which will be used by the calling bash/cmd script. 156 // build "rdmd --force %DFLAGS% -I%~dp0..\source -Jviews -Isource @deps.txt %LIBS% source\app.d" ~ application arguments 157 // or with "/" instead of "\" 158 Path exe_file_path; 159 bool tmp_target = false; 160 if (generate_binary) { 161 if (settings.run && !isWritableDir(Path(buildsettings.targetPath), true)) { 162 import std.random; 163 auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-"; 164 auto tmpdir = getTempDir()~".rdmd/source/"; 165 buildsettings.targetPath = tmpdir.toNativeString(); 166 buildsettings.targetName = rnd ~ buildsettings.targetName; 167 m_temporaryFiles ~= tmpdir; 168 tmp_target = true; 169 } 170 exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); 171 settings.compiler.setTarget(buildsettings, settings.platform); 172 } 173 174 logDiagnostic("Application output name is '%s'", getTargetFileName(buildsettings, settings.platform)); 175 176 string[] flags = ["--build-only", "--compiler="~settings.platform.compilerBinary]; 177 if (settings.force) flags ~= "--force"; 178 flags ~= buildsettings.dflags; 179 flags ~= mainsrc.relativeTo(cwd).toNativeString(); 180 181 prepareGeneration(buildsettings); 182 finalizeGeneration(buildsettings, generate_binary); 183 184 if (buildsettings.preBuildCommands.length){ 185 logInfo("Running pre-build commands..."); 186 runCommands(buildsettings.preBuildCommands); 187 } 188 189 logInfo("Building configuration "~config~", build type "~settings.buildType); 190 191 logInfo("Running rdmd..."); 192 logDiagnostic("rdmd %s", join(flags, " ")); 193 auto rdmd_pid = spawnProcess("rdmd" ~ flags); 194 auto result = rdmd_pid.wait(); 195 enforce(result == 0, "Build command failed with exit code "~to!string(result)); 196 197 if (tmp_target) { 198 m_temporaryFiles ~= exe_file_path; 199 foreach (f; buildsettings.copyFiles) 200 m_temporaryFiles ~= Path(buildsettings.targetPath).parentPath ~ Path(f).head; 201 } 202 } 203 204 void performDirectBuild(GeneratorSettings settings, ref BuildSettings buildsettings, in Package pack, string config) 205 { 206 auto cwd = Path(getcwd()); 207 208 auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); 209 auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; 210 211 // make file paths relative to shrink the command line 212 foreach (ref f; buildsettings.sourceFiles) { 213 auto fp = Path(f); 214 if( fp.absolute ) fp = fp.relativeTo(cwd); 215 f = fp.toNativeString(); 216 } 217 218 logInfo("Building configuration \""~config~"\", build type "~settings.buildType); 219 220 prepareGeneration(buildsettings); 221 222 // make all target/import paths relative 223 string makeRelative(string path) { auto p = Path(path); if (p.absolute) p = p.relativeTo(cwd); return p.toNativeString(); } 224 buildsettings.targetPath = makeRelative(buildsettings.targetPath); 225 foreach (ref p; buildsettings.importPaths) p = makeRelative(p); 226 foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p); 227 228 Path exe_file_path; 229 bool is_temp_target = false; 230 if (generate_binary) { 231 if (settings.run && !isWritableDir(Path(buildsettings.targetPath), true)) { 232 import std.random; 233 auto rnd = to!string(uniform(uint.min, uint.max)); 234 auto tmppath = getTempDir()~("dub/"~rnd~"/"); 235 buildsettings.targetPath = tmppath.toNativeString(); 236 m_temporaryFiles ~= tmppath; 237 is_temp_target = true; 238 } 239 exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); 240 } 241 242 finalizeGeneration(buildsettings, generate_binary); 243 244 if( buildsettings.preBuildCommands.length ){ 245 logInfo("Running pre-build commands..."); 246 runBuildCommands(buildsettings.preBuildCommands, buildsettings); 247 } 248 249 buildWithCompiler(settings, buildsettings); 250 251 if (is_temp_target) { 252 m_temporaryFiles ~= exe_file_path; 253 foreach (f; buildsettings.copyFiles) 254 m_temporaryFiles ~= Path(buildsettings.targetPath).parentPath ~ Path(f).head; 255 } 256 } 257 258 private string computeBuildID(string config, in BuildSettings buildsettings, GeneratorSettings settings) 259 { 260 import std.digest.digest; 261 import std.digest.md; 262 MD5 hash; 263 hash.start(); 264 void addHash(in string[] strings...) { foreach (s; strings) { hash.put(cast(ubyte[])s); hash.put(0); } hash.put(0); } 265 addHash(buildsettings.versions); 266 addHash(buildsettings.debugVersions); 267 //addHash(buildsettings.versionLevel); 268 //addHash(buildsettings.debugLevel); 269 addHash(buildsettings.dflags); 270 addHash(buildsettings.lflags); 271 addHash((cast(uint)buildsettings.options).to!string); 272 addHash(buildsettings.stringImportPaths); 273 addHash(settings.platform.architecture); 274 addHash(settings.platform.compiler); 275 //addHash(settings.platform.frontendVersion); 276 auto hashstr = hash.finish().toHexString().idup; 277 278 return format("%s-%s-%s-%s-%s", config, settings.buildType, 279 settings.platform.architecture.join("."), 280 settings.platform.compilerBinary, hashstr); 281 } 282 283 private void copyTargetFile(Path build_path, BuildSettings buildsettings, BuildPlatform platform) 284 { 285 auto filename = getTargetFileName(buildsettings, platform); 286 auto src = build_path ~ filename; 287 logDiagnostic("Copying target from %s to %s", src.toNativeString(), buildsettings.targetPath); 288 copyFile(src, Path(buildsettings.targetPath) ~ filename, true); 289 } 290 291 private bool isUpToDate(Path target_path, BuildSettings buildsettings, BuildPlatform platform) 292 { 293 import std.datetime; 294 295 auto targetfile = target_path ~ getTargetFileName(buildsettings, platform); 296 if (!existsFile(targetfile)) { 297 logDiagnostic("Target '%s' doesn't exist, need rebuild.", targetfile.toNativeString()); 298 return false; 299 } 300 auto targettime = getFileInfo(targetfile).timeModified; 301 302 auto allfiles = appender!(string[]); 303 allfiles ~= buildsettings.sourceFiles; 304 allfiles ~= buildsettings.importFiles; 305 allfiles ~= buildsettings.stringImportFiles; 306 // TODO: add library files 307 /*foreach (p; m_project.getTopologicalPackageList()) 308 allfiles ~= p.packageInfoFile.toNativeString();*/ 309 310 foreach (file; allfiles.data) { 311 auto ftime = getFileInfo(file).timeModified; 312 if (ftime > Clock.currTime) 313 logWarn("File '%s' was modified in the future. Please re-save.", file); 314 if (ftime > targettime) { 315 logDiagnostic("File '%s' modified, need rebuild.", file); 316 return false; 317 } 318 } 319 return true; 320 } 321 322 void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) 323 { 324 auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); 325 auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; 326 327 Path target_file; 328 scope (failure) { 329 logInfo("FAIL %s %s %s" , buildsettings.targetPath, buildsettings.targetName, buildsettings.targetType); 330 auto tpath = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); 331 if (generate_binary && existsFile(tpath)) 332 removeFile(tpath); 333 } 334 335 /* 336 NOTE: for DMD experimental separate compile/link is used, but this is not yet implemented 337 on the other compilers. Later this should be integrated somehow in the build process 338 (either in the package.json, or using a command line flag) 339 */ 340 if (settings.platform.compilerBinary != "dmd" || !generate_binary || is_static_library) { 341 // setup for command line 342 if (generate_binary) settings.compiler.setTarget(buildsettings, settings.platform); 343 settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); 344 345 // don't include symbols of dependencies (will be included by the top level target) 346 if (is_static_library) buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !f.isLinkerFile()).array; 347 348 // invoke the compiler 349 logInfo("Running %s...", settings.platform.compilerBinary); 350 settings.compiler.invoke(buildsettings, settings.platform); 351 } else { 352 // determine path for the temporary object file 353 string tempobjname = buildsettings.targetName; 354 version(Windows) tempobjname ~= ".obj"; 355 else tempobjname ~= ".o"; 356 Path tempobj = Path(buildsettings.targetPath) ~ tempobjname; 357 358 // setup linker command line 359 auto lbuildsettings = buildsettings; 360 lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => isLinkerFile(f)).array; 361 settings.compiler.setTarget(lbuildsettings, settings.platform); 362 settings.compiler.prepareBuildSettings(lbuildsettings, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); 363 364 // setup compiler command line 365 buildsettings.libs = null; 366 buildsettings.lflags = null; 367 buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString()); 368 buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !isLinkerFile(f)).array; 369 settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); 370 371 logInfo("Compiling..."); 372 settings.compiler.invoke(buildsettings, settings.platform); 373 374 logInfo("Linking..."); 375 settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()]); 376 } 377 } 378 379 void runTarget(Path exe_file_path, in BuildSettings buildsettings, string[] run_args) 380 { 381 if (buildsettings.targetType == TargetType.executable) { 382 auto cwd = Path(getcwd()); 383 auto runcwd = cwd; 384 if (buildsettings.workingDirectory.length) { 385 runcwd = Path(buildsettings.workingDirectory); 386 if (!runcwd.absolute) runcwd = cwd ~ runcwd; 387 logDiagnostic("Switching to %s", runcwd.toNativeString()); 388 chdir(runcwd.toNativeString()); 389 } 390 scope(exit) chdir(cwd.toNativeString()); 391 if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path; 392 auto exe_path_string = exe_file_path.relativeTo(runcwd).toNativeString(); 393 version (Posix) { // spawnProcess on Posix systems requires an explicit path to the executable 394 if (!exe_path_string.startsWith(".") && !exe_path_string.startsWith("/")) 395 exe_path_string = "./" ~ exe_path_string; 396 } 397 logInfo("Running %s %s", exe_path_string, run_args.join(" ")); 398 auto prg_pid = spawnProcess(exe_path_string ~ run_args); 399 auto result = prg_pid.wait(); 400 enforce(result == 0, "Program exited with code "~to!string(result)); 401 } else logInfo("Target is a library. Skipping execution."); 402 } 403 404 void cleanupTemporaries() 405 { 406 foreach_reverse (f; m_temporaryFiles) { 407 try { 408 if (f.endsWithSlash) rmdir(f.toNativeString()); 409 else remove(f.toNativeString()); 410 } catch (Exception e) { 411 logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg); 412 logDiagnostic("Full error: %s", e.toString().sanitize); 413 } 414 } 415 m_temporaryFiles = null; 416 } 417 } 418 419 private Path getMainSourceFile(in Package prj) 420 { 421 foreach (f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"]) 422 if (existsFile(prj.path ~ f)) 423 return prj.path ~ f; 424 return prj.path ~ "source/app.d"; 425 } 426 427 unittest { 428 version (Windows) { 429 assert(isLinkerFile("test.obj")); 430 assert(isLinkerFile("test.lib")); 431 assert(isLinkerFile("test.res")); 432 assert(!isLinkerFile("test.o")); 433 assert(!isLinkerFile("test.d")); 434 } else { 435 assert(isLinkerFile("test.o")); 436 assert(isLinkerFile("test.a")); 437 assert(isLinkerFile("test.so")); 438 assert(isLinkerFile("test.dylib")); 439 assert(!isLinkerFile("test.obj")); 440 assert(!isLinkerFile("test.d")); 441 } 442 }