/** Generator for direct compiler builds. Copyright: © 2013-2013 rejectedsoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ module dub.generators.build; import dub.compilers.compiler; import dub.generators.generator; import dub.internal.utils; import dub.internal.vibecompat.core.file; import dub.internal.vibecompat.core.log; import dub.internal.vibecompat.inet.path; import dub.package_; import dub.packagemanager; import dub.project; import std.algorithm; import std.array; import std.conv; import std.exception; import std.file; import std.process; import std.string; class BuildGenerator : ProjectGenerator { private { Project m_project; PackageManager m_packageMan; Path[] m_temporaryFiles; } bool useRDMD = false; this(Project app, PackageManager mgr) { m_project = app; m_packageMan = mgr; } void generateProject(GeneratorSettings settings) { scope(exit) cleanupTemporaries(); auto cwd = Path(getcwd()); if (!settings.config.length) settings.config = m_project.getDefaultConfiguration(settings.platform); auto buildsettings = settings.buildSettings; m_project.addBuildSettings(buildsettings, settings.platform, settings.config, null, settings.buildType == "ddox"); m_project.addBuildTypeSettings(buildsettings, settings.platform, settings.buildType); // make all paths relative to shrink the command line string makeRelative(string path) { auto p = Path(path); if (p.absolute) p = p.relativeTo(cwd); return p.toNativeString(); } foreach (ref f; buildsettings.sourceFiles) f = makeRelative(f); foreach (ref p; buildsettings.importPaths) p = makeRelative(p); foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p); // perform the actual build if (this.useRDMD) performRDMDBuild(settings, buildsettings); else if (settings.direct) performDirectBuild(settings, buildsettings); else performCachedBuild(settings, buildsettings); // run post-build commands if (buildsettings.postBuildCommands.length) { logInfo("Running post-build commands..."); runBuildCommands(buildsettings.postBuildCommands, buildsettings); } // run the generated executable if (!(buildsettings.options & BuildOptions.syntaxOnly) && settings.run) { auto exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); runTarget(exe_file_path, buildsettings, settings.runArgs); } } void performCachedBuild(GeneratorSettings settings, BuildSettings buildsettings) { auto cwd = Path(getcwd()); auto build_id = computeBuildID(settings); auto target_path = m_project.mainPackage.path ~ format(".dub/build/%s/", build_id); if (isUpToDate(target_path, buildsettings, settings.platform)) { logInfo("Target is up to date. Skipping build."); copyTargetFile(target_path, buildsettings, settings.platform); return; } if (!isWritableDir(target_path, true)) { logInfo("Build directory %s is not writable. Falling back to direct build in the system's temp folder.", target_path.relativeTo(cwd).toNativeString()); performDirectBuild(settings, buildsettings); return; } // determine basic build properties auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); // run pre-/post-generate commands and copy "copyFiles" prepareGeneration(buildsettings); finalizeGeneration(buildsettings, generate_binary); logInfo("Building configuration \""~settings.config~"\", build type "~settings.buildType); if( buildsettings.preBuildCommands.length ){ logInfo("Running pre-build commands..."); runBuildCommands(buildsettings.preBuildCommands, buildsettings); } // override target path auto cbuildsettings = buildsettings; cbuildsettings.targetPath = target_path.relativeTo(cwd).toNativeString(); buildWithCompiler(settings, cbuildsettings); copyTargetFile(target_path, buildsettings, settings.platform); } void performRDMDBuild(GeneratorSettings settings, ref BuildSettings buildsettings) { //Added check for existance of [AppNameInPackagejson].d //If exists, use that as the starting file. auto mainsrc = getMainSourceFile(m_project); // do not pass all source files to RDMD, only the main source file buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(s => !s.endsWith(".d"))().array(); settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); auto generate_binary = !buildsettings.dflags.canFind("-o-"); // Create start script, which will be used by the calling bash/cmd script. // build "rdmd --force %DFLAGS% -I%~dp0..\source -Jviews -Isource @deps.txt %LIBS% source\app.d" ~ application arguments // or with "/" instead of "\" Path exe_file_path; bool tmp_target = false; if (generate_binary) { if (settings.run && !isWritableDir(Path(buildsettings.targetPath), true)) { import std.random; auto rnd = to!string(uniform(uint.min, uint.max)) ~ "-"; auto tmpdir = getTempDir()~".rdmd/source/"; buildsettings.targetPath = tmpdir.toNativeString(); buildsettings.targetName = rnd ~ buildsettings.targetName; m_temporaryFiles ~= tmpdir; tmp_target = true; } exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); settings.compiler.setTarget(buildsettings, settings.platform); } logDiagnostic("Application output name is '%s'", getTargetFileName(buildsettings, settings.platform)); string[] flags = ["--build-only", "--compiler="~settings.platform.compilerBinary]; flags ~= buildsettings.dflags; flags ~= (mainsrc).toNativeString(); prepareGeneration(buildsettings); finalizeGeneration(buildsettings, generate_binary); if (buildsettings.preBuildCommands.length){ logInfo("Running pre-build commands..."); runCommands(buildsettings.preBuildCommands); } logInfo("Building configuration "~settings.config~", build type "~settings.buildType); logInfo("Running rdmd..."); logDiagnostic("rdmd %s", join(flags, " ")); auto rdmd_pid = spawnProcess("rdmd" ~ flags); auto result = rdmd_pid.wait(); enforce(result == 0, "Build command failed with exit code "~to!string(result)); if (tmp_target) { m_temporaryFiles ~= exe_file_path; foreach (f; buildsettings.copyFiles) m_temporaryFiles ~= Path(buildsettings.targetPath).parentPath ~ Path(f).head; } } void performDirectBuild(GeneratorSettings settings, ref BuildSettings buildsettings) { auto cwd = Path(getcwd()); auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; // make file paths relative to shrink the command line foreach (ref f; buildsettings.sourceFiles) { auto fp = Path(f); if( fp.absolute ) fp = fp.relativeTo(cwd); f = fp.toNativeString(); } logInfo("Building configuration \""~settings.config~"\", build type "~settings.buildType); prepareGeneration(buildsettings); // determine the absolute target path if (!Path(buildsettings.targetPath).absolute) buildsettings.targetPath = (m_project.mainPackage.path ~ Path(buildsettings.targetPath)).toNativeString(); // make all target/import paths relative string makeRelative(string path) { auto p = Path(path); if (p.absolute) p = p.relativeTo(cwd); return p.toNativeString(); } buildsettings.targetPath = makeRelative(buildsettings.targetPath); foreach (ref p; buildsettings.importPaths) p = makeRelative(p); foreach (ref p; buildsettings.stringImportPaths) p = makeRelative(p); Path exe_file_path; bool is_temp_target = false; if (generate_binary) { if (settings.run && !isWritableDir(Path(buildsettings.targetPath), true)) { import std.random; auto rnd = to!string(uniform(uint.min, uint.max)); auto tmppath = getTempDir()~("dub/"~rnd~"/"); buildsettings.targetPath = tmppath.toNativeString(); m_temporaryFiles ~= tmppath; is_temp_target = true; } exe_file_path = Path(buildsettings.targetPath) ~ getTargetFileName(buildsettings, settings.platform); settings.compiler.setTarget(buildsettings, settings.platform); } finalizeGeneration(buildsettings, generate_binary); if( buildsettings.preBuildCommands.length ){ logInfo("Running pre-build commands..."); runBuildCommands(buildsettings.preBuildCommands, buildsettings); } buildWithCompiler(settings, buildsettings); if (is_temp_target) { m_temporaryFiles ~= exe_file_path; foreach (f; buildsettings.copyFiles) m_temporaryFiles ~= Path(buildsettings.targetPath).parentPath ~ Path(f).head; } } private string computeBuildID(GeneratorSettings settings) { import std.digest.digest; import std.digest.sha; SHA1 hash; hash.start(); // ... auto hashstr = hash.finish().toHexString().idup; return format("%s-%s-%s-%s-%s", settings.config, settings.buildType, settings.platform.architecture.join("."), settings.platform.compilerBinary, hashstr); } private void copyTargetFile(Path build_path, BuildSettings buildsettings, BuildPlatform platform) { auto filename = getTargetFileName(buildsettings, platform); auto src = build_path ~ filename; copyFile(src, Path(buildsettings.targetPath) ~ filename, true); } private bool isUpToDate(Path target_path, BuildSettings buildsettings, BuildPlatform platform) { import std.datetime; auto targetfile = target_path ~ getTargetFileName(buildsettings, platform); if (!existsFile(targetfile)) return false; auto targettime = getFileInfo(targetfile).timeModified; auto allfiles = appender!(string[]); allfiles ~= buildsettings.sourceFiles; allfiles ~= buildsettings.importFiles; allfiles ~= buildsettings.stringImportFiles; foreach (p; m_project.getTopologicalPackageList()) allfiles ~= p.packageInfoFile.toNativeString(); foreach (file; allfiles.data) { auto ftime = getFileInfo(file).timeModified; if (ftime > Clock.currTime) logWarn("File '%s' was modified in the future. Please re-save."); if (ftime > targettime) return false; } return true; } void buildWithCompiler(GeneratorSettings settings, BuildSettings buildsettings) { auto generate_binary = !(buildsettings.options & BuildOptions.syntaxOnly); auto is_static_library = buildsettings.targetType == TargetType.staticLibrary || buildsettings.targetType == TargetType.library; /* NOTE: for DMD experimental separate compile/link is used, but this is not yet implemented on the other compilers. Later this should be integrated somehow in the build process (either in the package.json, or using a command line flag) */ if (settings.platform.compilerBinary != "dmd" || !generate_binary || is_static_library) { // setup for command line settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); // invoke the compiler logInfo("Running %s...", settings.platform.compilerBinary); settings.compiler.invoke(buildsettings, settings.platform); } else { // determine path for the temporary object file string tempobjname = buildsettings.targetName; version(Windows) tempobjname ~= ".obj"; else tempobjname ~= ".o"; Path tempobj = Path(buildsettings.targetPath) ~ tempobjname; if (buildsettings.targetType == TargetType.dynamicLibrary) buildsettings.addDFlags("-shared", "-fPIC"); // setup linker command line auto lbuildsettings = buildsettings; lbuildsettings.sourceFiles = lbuildsettings.sourceFiles.filter!(f => f.endsWith(".lib"))().array(); settings.compiler.prepareBuildSettings(lbuildsettings, BuildSetting.commandLineSeparate|BuildSetting.sourceFiles); // setup compiler command line buildsettings.libs = null; buildsettings.lflags = null; buildsettings.addDFlags("-c", "-of"~tempobj.toNativeString()); buildsettings.sourceFiles = buildsettings.sourceFiles.filter!(f => !f.endsWith(".lib"))().array(); settings.compiler.prepareBuildSettings(buildsettings, BuildSetting.commandLine); logInfo("Compiling..."); settings.compiler.invoke(buildsettings, settings.platform); logInfo("Linking..."); settings.compiler.invokeLinker(lbuildsettings, settings.platform, [tempobj.toNativeString()]); } } void runTarget(Path exe_file_path, BuildSettings buildsettings, string[] run_args) { if (buildsettings.targetType == TargetType.executable) { auto cwd = Path(getcwd()); auto runcwd = cwd; if (buildsettings.workingDirectory.length) { runcwd = cwd ~ buildsettings.workingDirectory; logDiagnostic("Switching to %s", runcwd.toNativeString()); chdir(runcwd.toNativeString()); } scope(exit) chdir(cwd.toNativeString()); if (!exe_file_path.absolute) exe_file_path = cwd ~ exe_file_path; exe_file_path = exe_file_path.relativeTo(runcwd); logInfo("Running %s %s", exe_file_path.toNativeString(), run_args.join(" ")); auto prg_pid = spawnProcess(exe_file_path.toNativeString() ~ run_args); auto result = prg_pid.wait(); enforce(result == 0, "Program exited with code "~to!string(result)); } else logInfo("Target is a library. Skipping execution."); } void cleanupTemporaries() { foreach_reverse (f; m_temporaryFiles) { try { if (f.endsWithSlash) rmdir(f.toNativeString()); else remove(f.toNativeString()); } catch (Exception e) { logWarn("Failed to remove temporary file '%s': %s", f.toNativeString(), e.msg); logDiagnostic("Full error: %s", e.toString().sanitize); } } m_temporaryFiles = null; } } private Path getMainSourceFile(in Project prj) { foreach( f; ["source/app.d", "src/app.d", "source/"~prj.name~".d", "src/"~prj.name~".d"]) if( exists(f) ) return Path(f); return Path("source/app.d"); }