Files
scriptc/scripts/build-llvm-dist.mjs
T
Chris Tate 4c3a1d9c98 Link the Linux release helpers against LLVM built with ThinLTO and a helper profile
The Linux helpers linked LLVM 22.1.8's release archive. The helper's code
generation is LLVM's code, so building that LLVM from its release sources
with ThinLTO and with a profile of the helper's own work makes code
generation 5-9% faster than the helper as released, with identical objects.

- native/llvm-codegen/llvm.profdata.gz is the profile, collected by
  scripts/refresh-llvm-profile.mjs: it builds LLVM with IR instrumentation
  and an instrumented helper, and runs that helper on tsc-ts, five Effect
  fixtures, the runtime benchmark workloads and every 50th corpus program
  (scripts/llvm-helper-inputs.mjs). It follows LLVM's sources, so it needs
  a refresh when LLVM is bumped.
- scripts/build-llvm-dist.mjs builds LLVM with that profile and ThinLTO,
  and packs it with a checksum. The llvm-dist workflow, run by hand, builds
  it for x64 and arm64, requires a helper linked against it to give the
  same objects as one linked against the release archive
  (scripts/check-llvm-dist.mjs), prints both helpers' times and the
  profile's mismatch count, and attaches the archive to the LLVM_DIST_TAG
  release of this repository.
- The release workflow's Linux jobs, and the wasm32-wasi job, which also
  uploads the x64 GNU helper, download that archive and build the helper
  with SCRIPTC_LLVM_THINLTO=1: clang, lld and ThinLTO, and the profile for
  the helper's own sources. Without the archive, or if that build fails,
  the helper links the release archive as before.
- The four Linux helper packages share their build in
  native/llvm-codegen/linux-helper.mjs. Without SCRIPTC_LLVM_THINLTO it
  builds the same executable as before.
2026-10-25 18:53:25 -05:00

97 lines
4.3 KiB
JavaScript

#!/usr/bin/env node
/* Builds the LLVM distribution the Linux release helpers link
* (native/llvm-codegen/llvm-dist.mjs): LLVM_VERSION from its release source
* tarball, with ThinLTO and native/llvm-codegen/llvm.profdata.gz, the
* helper's targets, and static libraries only. clang and lld of the same
* version must be on PATH (LLVM's release archive for the host has both).
* Writes the install tree and, with --archive, a .tar.xz of it and its
* .sha256. The llvm-dist workflow runs it for each Linux architecture; it
* takes about 8 minutes on 32 cores and about an hour on 4.
*
* With --instrumented it builds the IR-instrumented libraries that
* scripts/refresh-llvm-profile.mjs collects the profile with instead.
*
* node scripts/build-llvm-dist.mjs --source <llvm-project-X.src> --prefix <dir>
* [--archive <file.tar.xz>] [--work <dir>] [--jobs N] [--instrumented]
*/
import { spawnSync } from "node:child_process";
import { createHash } from "node:crypto";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { cpus, tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { parseArgs } from "node:util";
import { llvmCmakeArgs, writeLlvmProfile } from "../native/llvm-codegen/llvm-dist.mjs";
const { values } = parseArgs({
options: {
source: { type: "string" },
prefix: { type: "string" },
archive: { type: "string" },
work: { type: "string" },
jobs: { type: "string", default: String(cpus().length) },
instrumented: { type: "boolean", default: false },
help: { type: "boolean", default: false },
},
});
if (values.help || values.source === undefined || values.prefix === undefined) {
console.log(
"Usage: node scripts/build-llvm-dist.mjs --source <llvm-project source> --prefix <install dir> [--archive <file.tar.xz>] [--work <dir>] [--jobs N] [--instrumented]",
);
process.exit(values.help ? 0 : 2);
}
if (process.platform !== "linux") throw new Error("the LLVM distribution is built on Linux");
const source = resolve(values.source);
if (!existsSync(join(source, "llvm/CMakeLists.txt")))
throw new Error(`${source} is not an llvm-project source tree`);
const prefix = resolve(values.prefix);
const work = resolve(values.work ?? mkdtempSync(join(tmpdir(), "scriptc-llvm-dist-")));
const build = join(work, values.instrumented ? "llvm-instrumented" : "llvm");
mkdirSync(build, { recursive: true });
function run(command, args, log, env = process.env) {
console.log(`$ ${command} ${args.join(" ")}`);
const result = spawnSync(command, args, { encoding: "utf8", maxBuffer: 1 << 30, env });
if (log !== undefined) writeFileSync(log, result.stdout + result.stderr);
if (result.status !== 0) {
process.stderr.write((result.stdout + result.stderr).slice(-8000));
throw new Error(`${command} failed with status ${result.status}`);
}
return result.stdout + result.stderr;
}
const profile = values.instrumented ? undefined : writeLlvmProfile(work);
run("cmake", llvmCmakeArgs({ source, build, prefix, profile, instrumented: values.instrumented }));
const log = join(work, "build.log");
const output = run("ninja", ["-C", build, "-j", values.jobs, "install"], log);
if (profile !== undefined) {
// clang reports, per file, how many of its functions the profile no longer
// matches; on a host other than the one it was collected on, that count
// shows how much of the profile still applies.
let files = 0,
functions = 0,
mismatched = 0;
for (const match of output.matchAll(/of (\d+) functions?, (\d+) ha(?:ve|s) mismatched data/g)) {
files++;
functions += Number(match[1]);
mismatched += Number(match[2]);
}
console.log(
files === 0
? "profile: no file reports functions with mismatched profile data"
: `profile: ${mismatched} of ${functions} functions in ${files} files have mismatched profile data (build log ${log})`,
);
}
if (values.archive !== undefined) {
const archive = resolve(values.archive);
mkdirSync(dirname(archive), { recursive: true });
run("tar", ["-C", dirname(prefix), "-cJf", archive, basename(prefix)], undefined, {
...process.env,
XZ_OPT: "-T0",
});
const digest = createHash("sha256").update(readFileSync(archive)).digest("hex");
writeFileSync(`${archive}.sha256`, `${digest} ${basename(archive)}\n`);
console.log(`wrote ${archive} (${digest})`);
}