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(); generateTestRunnerXcodeProjects(); 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, "-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); } /** * Generates Xcode projects for running the unit test for druntime and Phobos * on an iOS device. * * This works by coping the `runtime/TestRunnerTemplate` directory to the build * directory. Inside the `runtime/TestRunnerTemplate` directory is an Xcode * project located. This project acts as a template. The `project.pbxproj` file * inside the Xcode project contains variables, denoted by `{{ var }}`, which * are replaced with the actual values when the template is rendered. * * The Xcode project has been created using Xcode 11.3.1 (11C504) and then the * `project.pbxproj` file has been manually edited to replace the original * values with variables. */ void generateTestRunnerXcodeProjects() { import std.uni : icmp; const struct ProjectContext { string druntimeArchive; string phobosArchive; string deploymentTarget; string objectPath; string libraryPath; } static string renderTemplate(string temp, ProjectContext context) { import std.conv : text; string result = temp; foreach (i, _; typeof(ProjectContext.tupleof)) { enum name = __traits(identifier, ProjectContext.tupleof[i]); result = result.replace("{{ " ~ name ~ " }}", context.tupleof[i].text); } return result; } static void copyRecurse(string source, string destination) { mkdirRecurse(destination); foreach (e; dirEntries(source, SpanMode.breadth)) { const newPath = destination.buildPath(e[source.length + 1.. $]); if (e.isDir) mkdirRecurse(newPath); else copy(e.name, newPath); } } const systemName = config.cmakeVars.get("CMAKE_SYSTEM_NAME", null); if (systemName.icmp("iOS") != 0) return; const templatePath = config.ldcSourceDir.buildPath("runtime", "TestRunnerTemplate"); const targetPath = config.buildDir.buildPath("TestRunner").absolutePath; copyRecurse(templatePath, targetPath); ProjectContext context = { druntimeArchive: "libdruntime-ldc-unittest-debug.a", phobosArchive: "libphobos2-ldc-unittest-debug.a", deploymentTarget: config.cmakeVars["CMAKE_OSX_DEPLOYMENT_TARGET"], objectPath: config.buildDir.buildPath("objects-unittest-debug"), libraryPath: config.buildDir.buildPath("lib") }; const pbxprojPath = targetPath.buildPath("TestRunner.xcodeproj", "project.pbxproj"); const projectContent = readText(pbxprojPath); const result = renderTemplate(projectContent, context); std.file.write(pbxprojPath, result); } /*** 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); } }