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