#!/usr/bin/env node import { spawn as nodeSpawn } from "node:child_process"; import { chmodSync, cpSync, existsSync, mkdirSync, readFileSync } from "node:fs"; import { basename, join, relative, resolve } from "node:path"; import { formatAnnotationToHtml, getSecret, isCI, isWindows, parseAnnotations, printEnvironment, reportAnnotationToBuildKite, startGroup, } from "./utils.mjs"; // https://cmake.org/cmake/help/latest/manual/cmake.1.html#generate-a-project-buildsystem const generateFlags = [ ["-S", "string", "path to source directory"], ["-B", "string", "path to build directory"], ["-D", "string", "define a build option (e.g. -DCMAKE_BUILD_TYPE=Release)"], ["-G", "string", "build generator (e.g. -GNinja)"], ["-W", "string", "enable warnings (e.g. -Wno-dev)"], ["--fresh", "boolean", "force a fresh build"], ["--log-level", "string", "set the log level"], ["--debug-output", "boolean", "print debug output"], ["--toolchain", "string", "the toolchain to use"], ]; // https://cmake.org/cmake/help/latest/manual/cmake.1.html#generate-a-project-buildsystem const buildFlags = [ ["--config", "string", "build configuration (e.g. --config Release)"], ["--target", "string", "build target"], ["-t", "string", "same as --target"], ["--parallel", "number", "number of parallel jobs"], ["-j", "number", "same as --parallel"], ["--verbose", "boolean", "enable verbose output"], ["-v", "boolean", "same as --verbose"], ]; async function build(args) { const startTime = Date.now(); if (process.platform === "win32" && !process.env["VSINSTALLDIR"]) { const shellPath = join(import.meta.dirname, "vs-shell.ps1"); const scriptPath = import.meta.filename; return spawn("pwsh", ["-NoProfile", "-NoLogo", "-File", shellPath, process.argv0, scriptPath, ...args]); } if (isCI) { printEnvironment(); } const env = { ...process.env, FORCE_COLOR: "1", CLICOLOR_FORCE: "1", }; const generateOptions = parseOptions(args, generateFlags); const buildOptions = parseOptions(args, buildFlags); const buildPath = resolve(generateOptions["-B"] || buildOptions["--build"] || "build"); generateOptions["-B"] = buildPath; buildOptions["--build"] = buildPath; if (!generateOptions["-S"]) { generateOptions["-S"] = process.cwd(); } const cacheRead = isCacheReadEnabled(); const cacheWrite = isCacheWriteEnabled(); if (cacheRead || cacheWrite) { const cachePath = getCachePath(); if (cacheRead && !existsSync(cachePath)) { const mainCachePath = getCachePath(getDefaultBranch()); if (existsSync(mainCachePath)) { mkdirSync(cachePath, { recursive: true }); try { cpSync(mainCachePath, cachePath, { recursive: true, force: true }); } catch (error) { const { code } = error; switch (code) { case "EPERM": case "EACCES": try { chmodSync(mainCachePath, 0o777); cpSync(mainCachePath, cachePath, { recursive: true, force: true }); } catch (error) { console.warn("Failed to copy cache with permissions fix", error); } break; default: console.warn("Failed to copy cache", error); } } } } generateOptions["-DCACHE_PATH"] = cmakePath(cachePath); generateOptions["--fresh"] = undefined; if (cacheRead && cacheWrite) { generateOptions["-DCACHE_STRATEGY"] = "read-write"; } else if (cacheRead) { generateOptions["-DCACHE_STRATEGY"] = "read-only"; } else if (cacheWrite) { generateOptions["-DCACHE_STRATEGY"] = "write-only"; } } const toolchain = generateOptions["--toolchain"]; if (toolchain) { const toolchainPath = resolve(import.meta.dirname, "..", "cmake", "toolchains", `${toolchain}.cmake`); generateOptions["--toolchain"] = toolchainPath; } const generateArgs = Object.entries(generateOptions).flatMap(([flag, value]) => flag.startsWith("-D") ? [`${flag}=${value}`] : [flag, value], ); await startGroup("CMake Configure", () => spawn("cmake", generateArgs, { env })); const envPath = resolve(buildPath, ".env"); if (existsSync(envPath)) { const envFile = readFileSync(envPath, "utf8"); for (const line of envFile.split(/\r\n|\n|\r/)) { const [key, value] = line.split("="); env[key] = value; } } const buildArgs = Object.entries(buildOptions) .sort(([a], [b]) => (a === "--build" ? -1 : a.localeCompare(b))) .flatMap(([flag, value]) => [flag, value]); await startGroup("CMake Build", () => spawn("cmake", buildArgs, { env })); printDuration("total", Date.now() - startTime); } function cmakePath(path) { return path.replace(/\\/g, "/"); } /** @param {string} str */ const toAlphaNumeric = str => str.replace(/[^a-z0-9]/gi, "-"); function getCachePath(branch) { const { BUILDKITE_BUILD_PATH: buildPath, BUILDKITE_REPO: repository, BUILDKITE_PULL_REQUEST_REPO: fork, BUILDKITE_BRANCH, BUILDKITE_STEP_KEY, } = process.env; // NOTE: settings that could be long should be truncated to avoid hitting max // path length limit on windows (4096) const repositoryKey = toAlphaNumeric( // remove domain name, only leaving 'org/repo' (fork || repository).replace(/^https?:\/\/github\.com\/?/, ""), ); const branchName = toAlphaNumeric(branch || BUILDKITE_BRANCH); const branchKey = branchName.startsWith("gh-readonly-queue-") ? branchName.slice(18, branchName.indexOf("-pr-")) : branchName.slice(0, 32); const stepKey = toAlphaNumeric(BUILDKITE_STEP_KEY); return resolve(buildPath, "..", "cache", repositoryKey, branchKey, stepKey); } function isCacheReadEnabled() { return ( isBuildkite() && process.env.BUILDKITE_CLEAN_CHECKOUT !== "true" && process.env.BUILDKITE_BRANCH !== getDefaultBranch() ); } function isCacheWriteEnabled() { return isBuildkite(); } function isBuildkite() { return process.env.BUILDKITE === "true"; } function getDefaultBranch() { return process.env.BUILDKITE_PIPELINE_DEFAULT_BRANCH || "main"; } function parseOptions(args, flags = []) { const options = {}; for (let i = 0; i < args.length; i++) { const arg = args[i]; for (const [flag, type] of flags) { if (arg === flag) { if (type === "boolean") { options[arg] = undefined; } else { options[arg] = args[++i]; } } else if (arg.startsWith(flag)) { const delim = arg.indexOf("="); if (delim === -1) { options[flag] = arg.slice(flag.length); } else { options[arg.slice(0, delim)] = arg.slice(delim + 1); } } } } return options; } async function spawn(command, args, options, label) { const effectiveArgs = args.filter(Boolean); const description = [command, ...effectiveArgs].map(arg => (arg.includes(" ") ? JSON.stringify(arg) : arg)).join(" "); let env = options?.env; console.log("$", description); label ??= basename(command); const pipe = process.env.CI === "true"; if (isBuildkite()) { if (process.env.BUN_LINK_ONLY && isWindows) { env ||= options?.env || { ...process.env }; // Pass signing secrets directly to the build process // The PowerShell signing script will handle certificate decoding env.SM_CLIENT_CERT_PASSWORD = getSecret("SM_CLIENT_CERT_PASSWORD", { redact: true, required: true, }); env.SM_CLIENT_CERT_FILE = getSecret("SM_CLIENT_CERT_FILE", { redact: true, required: true, }); env.SM_API_KEY = getSecret("SM_API_KEY", { redact: true, required: true, }); env.SM_KEYPAIR_ALIAS = getSecret("SM_KEYPAIR_ALIAS", { redact: true, required: true, }); env.SM_HOST = getSecret("SM_HOST", { redact: true, required: true, }); } } const subprocess = nodeSpawn(command, effectiveArgs, { stdio: pipe ? "pipe" : "inherit", ...options, env, }); let killedManually = false; function onKill() { clearOnKill(); if (!subprocess.killed) { killedManually = true; subprocess.kill?.(); } } function clearOnKill() { process.off("beforeExit", onKill); process.off("SIGINT", onKill); process.off("SIGTERM", onKill); } // Kill the entire process tree so everything gets cleaned up. On Windows, job // control groups make this haappen automatically so we don't need to do this // on Windows. if (process.platform !== "win32") { process.once("beforeExit", onKill); process.once("SIGINT", onKill); process.once("SIGTERM", onKill); } let timestamp; subprocess.on("spawn", () => { timestamp = Date.now(); }); let stdoutBuffer = ""; let done; if (pipe) { const stdout = new Promise(resolve => { subprocess.stdout.on("end", resolve); subprocess.stdout.on("data", data => { stdoutBuffer += data.toString(); process.stdout.write(data); }); }); const stderr = new Promise(resolve => { subprocess.stderr.on("end", resolve); subprocess.stderr.on("data", data => { stdoutBuffer += data.toString(); process.stderr.write(data); }); }); done = Promise.all([stdout, stderr]); } const { error, exitCode, signalCode } = await new Promise(resolve => { subprocess.on("error", error => { clearOnKill(); resolve({ error }); }); subprocess.on("exit", (exitCode, signalCode) => { clearOnKill(); resolve({ exitCode, signalCode }); }); }); if (done) { await done; } printDuration(label, Date.now() - timestamp); if (exitCode === 0) { return; } if (isBuildkite()) { let annotated; try { const { annotations } = parseAnnotations(stdoutBuffer); for (const annotation of annotations) { const content = formatAnnotationToHtml(annotation); reportAnnotationToBuildKite({ priority: 10, label: annotation.title || annotation.filename, content, }); annotated = true; } } catch (error) { console.error(`Failed to parse annotations:`, error); } if (!annotated) { const content = formatAnnotationToHtml({ filename: relative(process.cwd(), import.meta.filename), title: "build failed", content: stdoutBuffer, source: "build", level: "error", }); reportAnnotationToBuildKite({ priority: 10, label: "build failed", content, }); } } if (signalCode) { if (!killedManually) { console.error(`Command killed: ${signalCode}`); } } else { console.error(`Command exited: code ${exitCode}`); } process.exit(exitCode ?? 1); } function printDuration(label, duration) { if (duration > 60000) { console.log(`${label} took ${(duration / 60000).toFixed(2)} minutes`); } else { console.log(`${label} took ${(duration / 1000).toFixed(2)} seconds`); } } build(process.argv.slice(2));