module ldcBuildRuntime; import core.stdc.stdlib : exit; import std.algorithm; import std.array; import std.file; import std.path; import std.stdio; struct Config { string ldcExecutable; string buildDir; bool resetBuildDir; bool resetOnly; string ldcSourceDir; bool ninja; bool buildTestrunners; string[] targetSystem; string[] dFlags; string[] cFlags; string[] linkerFlags; uint numBuildJobs; string[string] cmakeVars; } version (Windows) enum exeSuffix = ".exe"; else enum exeSuffix = ""; string defaultLdcExecutable; Config config; int main(string[] args) { enum exeName = "ldc2" ~ exeSuffix; defaultLdcExecutable = buildPath(thisExePath.dirName, exeName); parseCommandLine(args); findLdcExecutable(); prepareBuildDir(); if (config.resetOnly) { writefln(".: Build directory successfully reset (%s)", config.buildDir); return 0; } prepareLdcSource(); runCMake(); build(); writefln(".: Runtime libraries built successfully into: %s", config.buildDir); return 0; } void findLdcExecutable() { if (config.ldcExecutable !is null) { if (!config.ldcExecutable.exists) { writefln(".: Error: LDC executable not found: %s", config.ldcExecutable); exit(1); } config.ldcExecutable = config.ldcExecutable.absolutePath; return; } if (defaultLdcExecutable.exists) { config.ldcExecutable = defaultLdcExecutable; return; } writefln(".: Please specify LDC executable via '--ldc='. Aborting.", exeSuffix); exit(1); } void prepareBuildDir() { if (config.buildDir is null) config.buildDir = "ldc-build-runtime.tmp"; if (config.buildDir.exists) { if (!config.resetBuildDir) { writefln(".: Warning: build directory already exists: %s", config.buildDir); } else { writefln(".: Resetting build directory: %s", config.buildDir); auto items = dirEntries(config.buildDir, SpanMode.shallow, false).array; const ldcSrc = buildPath(config.buildDir, "ldc-src"); foreach (i; items) { if (i.isFile) { remove(i.name); } else if (i.isDir && i.name != ldcSrc) { rmdirRecurse(i.name); } } } } else { writefln(".: Creating build directory: %s", config.buildDir); mkdirRecurse(config.buildDir); } config.buildDir = config.buildDir.absolutePath; } void prepareLdcSource() { if (config.ldcSourceDir !is null) { if (!config.ldcSourceDir.exists) { writefln(".: Error: LDC source directory not found: %s", config.ldcSourceDir); exit(1); } config.ldcSourceDir = config.ldcSourceDir.absolutePath; return; } const ldcSrc = "ldc-src"; config.ldcSourceDir = buildPath(config.buildDir, ldcSrc); if (buildPath(config.ldcSourceDir, "runtime").exists) return; // Download & extract LDC source archive if /ldc-src/runtime doesn't exist yet. const wd = WorkingDirScope(config.buildDir); auto ldcVersion = "@LDC_VERSION@"; void removeVersionSuffix(string beginning) { const suffixIndex = ldcVersion.countUntil(beginning); if (suffixIndex > 0) ldcVersion = ldcVersion[0 .. suffixIndex]; } removeVersionSuffix("git-"); removeVersionSuffix("-dirty"); import std.format : format; const localArchiveFile = "ldc-src.zip"; if (!localArchiveFile.exists) { const url = "https://github.com/ldc-developers/ldc/releases/download/v%1$s/ldc-%1$s-src.zip".format(ldcVersion); writefln(".: Downloading LDC source archive: %s", url); import std.net.curl : download; download(url, localArchiveFile); if (getSize(localArchiveFile) < 1_048_576) { writefln(".: Error: downloaded file is corrupt; has LDC v%s been released?", ldcVersion); writefln(" You can work around this by manually downloading a src package and moving it to: %s", buildPath(config.buildDir, localArchiveFile)); localArchiveFile.remove; exit(1); } } extractZipArchive(localArchiveFile, "."); rename("ldc-%1$s-src".format(ldcVersion), ldcSrc); } void runCMake() { const wd = WorkingDirScope(config.buildDir); string[] args = [ "cmake", "-DLDC_EXE_FULL=" ~ config.ldcExecutable, "-DD_VERSION=@D_VERSION@", "-DDMDFE_MINOR_VERSION=@DMDFE_MINOR_VERSION@", "-DDMDFE_PATCH_VERSION=@DMDFE_PATCH_VERSION@", ]; if (config.targetSystem.length) args ~= "-DTARGET_SYSTEM=" ~ config.targetSystem.join(";"); if (config.dFlags.length) args ~= "-DD_EXTRA_FLAGS=" ~ config.dFlags.join(";"); if (config.cFlags.length) args ~= "-DRT_CFLAGS=" ~ config.cFlags.join(" "); if (config.linkerFlags.length) args ~= "-DLD_FLAGS=" ~ config.linkerFlags.join(" "); foreach (pair; config.cmakeVars.byPair) args ~= "-D" ~ pair[0] ~ '=' ~ pair[1]; if (config.ninja) args ~= [ "-G", "Ninja" ]; args ~= buildPath(config.ldcSourceDir, "runtime"); exec(args); } void build() { const wd = WorkingDirScope(config.buildDir); string[] args = [ config.ninja ? "ninja" : "make" ]; if (config.numBuildJobs != 0) { import std.conv : to; args ~= "-j" ~ config.numBuildJobs.to!string; } args ~= "all"; if (config.buildTestrunners) args ~= "all-test-runners"; exec(args); } /*** helpers ***/ struct WorkingDirScope { string originalPath; this(string path) { originalPath = getcwd(); chdir(path); } ~this() { chdir(originalPath); } } void exec(string[] command) { import std.process; static string quoteIfNeeded(string arg) { const r = arg.findAmong(" ;"); return !r.length ? arg : "'" ~ arg ~ "'"; } string flattened = command.map!quoteIfNeeded.join(" "); writefln(".: Invoking: %s", flattened); stdout.flush(); auto pid = spawnProcess(command); const exitStatus = wait(pid); if (exitStatus != 0) { writeln(".: Error: command failed with status ", exitStatus); exit(1); } } void extractZipArchive(string archivePath, string destination) { import std.zip; auto archive = new ZipArchive(std.file.read(archivePath)); foreach (name, am; archive.directory) { const destPath = buildNormalizedPath(destination, name); const isDir = name.endsWith("/"); const destDir = isDir ? destPath : destPath.dirName; mkdirRecurse(destDir); if (!isDir) std.file.write(destPath, archive.expand(am)); } } void parseCommandLine(string[] args) { import std.getopt : arraySep, getopt, defaultGetoptPrinter; try { arraySep = ";"; auto helpInformation = getopt( args, "ldc", "Path to LDC executable (default: '" ~ defaultLdcExecutable ~ "')", &config.ldcExecutable, "buildDir", "Path to build directory (default: './ldc-build-runtime.tmp')", &config.buildDir, "reset", "If build directory exists, start with removing everything but the ldc-src subdirectory", &config.resetBuildDir, "resetOnly", "Like --reset, but only resets the build directory. No other actions are taken.", &config.resetOnly, "ldcSrcDir", "Path to LDC source directory (if not specified: downloads & extracts source archive into '/ldc-src')", &config.ldcSourceDir, "ninja", "Use Ninja as CMake build system", &config.ninja, "testrunners", "Build the testrunner executables too", &config.buildTestrunners, "targetSystem","Target OS/platform definitions (separated by ';'), e.g., 'Windows;MSVC' or 'Android;Linux;UNIX'. Defaults to the host OS/platform", &config.targetSystem, "dFlags", "Extra LDC flags for the D modules (separated by ';')", &config.dFlags, "cFlags", "Extra C/ASM compiler flags for the handful of C/ASM files (separated by ';')", &config.cFlags, "linkerFlags", "Extra C linker flags for shared libraries and testrunner executables (separated by ';')", &config.linkerFlags, "j", "Number of parallel build jobs", &config.numBuildJobs ); // getopt() has removed all consumed args from `args` foreach (arg; args[1 .. $]) { const r = arg.findSplit("="); if (r[1].length == 0) { helpInformation.helpWanted = true; break; } config.cmakeVars[r[0]] = r[2]; } if (helpInformation.helpWanted) { defaultGetoptPrinter( "Builds the LDC runtime libraries.\n\n" ~ "Programs required to be found in your PATH:\n" ~ " * CMake\n" ~ " * either Make or Ninja (recommended, enable with '--ninja')\n" ~ " * C toolchain (compiler and linker)\n\n" ~ "All arguments are optional.\n" ~ "CMake variables (see runtime/CMakeLists.txt in LDC source) can be specified via arguments like 'VAR=value'.\n", helpInformation.options ); exit(1); } if (config.resetOnly) config.resetBuildDir = true; } catch (Exception e) { writefln("Error processing command line arguments: %s", e.msg); writeln("Use '--help' for help."); exit(1); } }