Files
nextcraft/apps/cli/scripts/build-binary.mjs
T
CIAgent 1a88ab3199
Release / release (push) Successful in 3m45s
CI / python (push) Successful in 1m52s
CI / node (push) Successful in 1m0s
merge: milestone/v0.8-ship-anywhere -> main (v0.8 complete — Ship Anywhere)
The v0.8 milestone: installable on any Debian-based server via docker
(Gitea registry image, honest sandbox degrade), deb (amd64+arm64,
advisory postinst), and arch-aware binary — with release consistency
enforced by pre-commit hooks + Gitea Actions (3-runner fleet on CT 113,
machine-made releases with the full 8-asset matrix + registry image), the
CLI carrying the full suite (guide, channel-aware doctor), and the admin
runbook (deploy/README.md, evidence-backed recipes).

---ci---
phase: 6
milestone: v0.8
status: complete
requirements:
  covered: [REQ-8-001, REQ-8-002, REQ-8-003, REQ-8-004, REQ-8-005]
  partial: []
---/ci---
2026-09-29 16:04:40 +00:00

117 lines
4.6 KiB
JavaScript

#!/usr/bin/env node
// SEA binary build (v0.4; v0.8 P3 gains --arch per R-25/G-201).
//
// Mechanism: esbuild bundle (CJS, node18 target, arch-AGNOSTIC) →
// node --experimental-sea-config blob (arch-agnostic file surgery) →
// postject injection into a copy of a node BINARY (the ONLY arch-specific
// step — v0.4 copied process.execPath, x64-only; v0.8 --arch arm64
// downloads the official arm64 node tarball instead, QEMU-free per D-069).
//
// Usage: build-binary.mjs <tag> [--arch x64|arm64]
// Output: dist/nextcraft-linux-<arch> + .sha256
import { execFileSync } from "node:child_process";
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const pkgDir = dirname(fileURLToPath(import.meta.url)) + "/..";
const dist = join(pkgDir, "dist");
const bundle = join(dist, "bundle.cjs");
const blob = join(dist, "sea-prep.blob");
const config = join(dist, "sea-config.json");
// ---- args ------------------------------------------------------------------
const rawArgs = process.argv.slice(2);
const archIdx = rawArgs.indexOf("--arch");
const arch = archIdx !== -1 ? rawArgs[archIdx + 1] : "x64";
const version = rawArgs.find((a, i) => i !== archIdx && a !== "--arch") ?? "0.0.0-dev";
if (arch !== "x64" && arch !== "arm64") {
console.error(`unknown arch: ${arch} (expected x64|arm64)`);
process.exit(1);
}
if (version !== "0.0.0-dev" && !/^v?\d/.test(version)) {
console.error(`refusing to stamp implausible version: ${version}`);
process.exit(1);
}
const out = join(dist, `nextcraft-linux-${arch}`);
const checksum = out + ".sha256";
// ---- bundle (arch-agnostic) -------------------------------------------------
// Per-arch build wipes ONLY this arch's artifacts — a matrix build
// (x64 then arm64) must not nuke the sibling arch's outputs (rc1 release
// finding: rmSync(dist) deleted the x64 binary before deb packaging).
for (const f of [bundle, blob, config, out, checksum]) {
rmSync(f, { force: true });
}
rmSync(join(dist, "node-arm64-tarball"), { recursive: true, force: true });
mkdirSync(dist, { recursive: true });
execFileSync(
join(pkgDir, "node_modules/.bin/esbuild"),
[
join(pkgDir, "src/index.ts"),
"--bundle",
"--platform=node",
"--format=cjs",
"--target=node18",
`--define:NEXTCRAFT_VERSION_STAMP=${JSON.stringify(version)}`,
"--outfile=" + bundle,
],
{ stdio: "inherit" },
);
// ---- SEA blob (arch-agnostic file surgery) -----------------------------------
const seaConfig = {
main: bundle,
output: blob,
disableExperimentalSEAWarning: true,
};
writeFileSync(config, JSON.stringify(seaConfig));
execFileSync(process.execPath, ["--experimental-sea-config", config], { stdio: "inherit" });
// ---- node binary base (the ONLY arch-specific step — R-25) ------------------
// x64: the running node binary (the v0.4 mechanism, byte-identical path).
// arm64: the official arm64 node tarball (probe-verified dist URL) —
// QEMU-free: esbuild + postject are file surgery; nothing arm64 EXECUTES
// on the build host.
let nodeBin;
if (arch === "x64") {
nodeBin = process.execPath;
} else {
const nodeVersion = process.versions.node;
const tarUrl = `https://nodejs.org/dist/v${nodeVersion}/node-v${nodeVersion}-linux-arm64.tar.xz`;
const cacheDir = join(dist, "node-arm64-tarball");
mkdirSync(cacheDir, { recursive: true });
const tarPath = join(cacheDir, `node-v${nodeVersion}-linux-arm64.tar.xz`);
if (!existsSync(tarPath)) {
execFileSync("curl", ["-fsSL", "--max-time", "120", "-o", tarPath, tarUrl], { stdio: "inherit" });
}
execFileSync("tar", ["-xJf", tarPath, "-C", cacheDir], { stdio: "inherit" });
nodeBin = join(cacheDir, `node-v${nodeVersion}-linux-arm64`, "bin", "node");
if (!existsSync(nodeBin)) {
console.error(`arm64 node binary missing after tarball extract: ${nodeBin}`);
process.exit(1);
}
}
copyFileSync(nodeBin, out);
execFileSync(
"npx",
["--yes", "postject", out, "NODE_SEA_BLOB", blob, "--sentinel-fuse", "NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2"],
{ stdio: "inherit" },
);
execFileSync("chmod", ["+x", out]);
const size = statSync(out).size;
const hash = createHash("sha256").update(readFileSync(out)).digest("hex");
writeFileSync(checksum, `${hash} nextcraft-linux-${arch}\n`);
console.log(`built ${out} (${(size / 1024 / 1024).toFixed(1)} MB) stamped ${version}`);
console.log(`checksum ${checksum}: ${hash}`);
if (!existsSync(out) || !existsSync(checksum)) {
console.error("expected artifacts missing");
process.exit(1);
}