1a88ab3199
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---
117 lines
4.6 KiB
JavaScript
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);
|
|
}
|