ldc/runtime/ldc-build-runtime.d.in
Martin Kinkelin c63b52ab6b CMake: Tweak library versioning symlink scheme
E.g., for v2.100.0:

libphobos2-ldc-shared.so ->
libphobos2-ldc-shared.so.100 ->
libphobos2-ldc-shared.so.100.0
2022-05-06 20:11:58 +02:00

361 lines
12 KiB
D

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=<path/to/ldc2%s>'. 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 <buildDir>/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 '<buildDir>/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);
}
}