mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
254 lines
8.5 KiB
JavaScript
254 lines
8.5 KiB
JavaScript
/**
|
|
* Build standalone `bl` binaries with Bun --compile, then pack each as a
|
|
* per-platform `.zip` via binary-zip.mjs (Release / OSS download asset).
|
|
*
|
|
* Used by lib/binary-release.mjs (and publish-stable / publish-channel orchestrators).
|
|
* Debug:
|
|
* node tools/release/lib/binary-build.mjs --mode stable --host
|
|
*
|
|
* Manifests:
|
|
* --mode stable → writes latest.json (fed into OSS manifest.json + latest.json)
|
|
* --mode channel → always writes sync-release.json (npm --channel is dist-tag only)
|
|
*/
|
|
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
import { join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { spawnSync } from "node:child_process";
|
|
import { parseArgs } from "node:util";
|
|
import { ROOT, readPackageJson, PACKAGES } from "./packages.mjs";
|
|
import {
|
|
normalizeModeChannel,
|
|
rollingManifestChannelId,
|
|
rollingManifestFileName,
|
|
} from "./binary-options.mjs";
|
|
import { ensureZip, zipOne } from "./binary-zip.mjs";
|
|
|
|
const BINARY_COMPILE = fileURLToPath(new URL("./binary-compile.mjs", import.meta.url));
|
|
const CLI_ENTRY = join(ROOT, "packages/cli/src/main.ts");
|
|
const DEFAULT_OUTDIR = join(ROOT, "dist-bin");
|
|
const USAGE =
|
|
"Usage: node tools/release/lib/binary-build.mjs [--mode stable|channel] [--channel <name>] [--host] [--target <bun-target>] [--outdir <dir>]\n";
|
|
|
|
/** Bun compile targets → asset (os, arch, exe). */
|
|
export const BINARY_TARGETS = [
|
|
{ bunTarget: "bun-darwin-arm64", os: "darwin", arch: "arm64", exe: false },
|
|
{ bunTarget: "bun-darwin-x64", os: "darwin", arch: "x64", exe: false },
|
|
{ bunTarget: "bun-linux-x64", os: "linux", arch: "x64", exe: false },
|
|
{ bunTarget: "bun-windows-x64", os: "windows", arch: "x64", exe: true },
|
|
];
|
|
|
|
/** Uncompressed binary basename inside the zip: `bl-<ver>-<os>-<arch>[.exe]`. */
|
|
export function binaryInnerName(version, { os, arch, exe }) {
|
|
return `bl-${version}-${os}-${arch}${exe ? ".exe" : ""}`;
|
|
}
|
|
|
|
/** Release asset basename: `bl-<ver>-<os>-<arch>.zip`. */
|
|
export function binaryAssetName(version, { os, arch }) {
|
|
return `bl-${version}-${os}-${arch}.zip`;
|
|
}
|
|
|
|
/** Full matrix zip basenames for a version (order matches BINARY_TARGETS). */
|
|
export function matrixAssetNames(version) {
|
|
return BINARY_TARGETS.map((target) => binaryAssetName(version, target));
|
|
}
|
|
|
|
function log(message = "") {
|
|
process.stdout.write(`${message}\n`);
|
|
}
|
|
|
|
function writeJson(path, value) {
|
|
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
}
|
|
|
|
function parseCliArgs(argv) {
|
|
const { values } = parseArgs({
|
|
args: argv,
|
|
options: {
|
|
outdir: { type: "string" },
|
|
target: { type: "string" },
|
|
host: { type: "boolean", default: false },
|
|
mode: { type: "string", default: "stable" },
|
|
channel: { type: "string" },
|
|
help: { type: "boolean", short: "h", default: false },
|
|
},
|
|
allowPositionals: false,
|
|
});
|
|
if (values.help) {
|
|
process.stdout.write(USAGE);
|
|
process.exit(0);
|
|
}
|
|
return normalizeBuildOptions({
|
|
outdir: values.outdir ? resolve(values.outdir) : DEFAULT_OUTDIR,
|
|
onlyTarget: values.target ?? null,
|
|
hostOnly: values.host,
|
|
mode: values.mode,
|
|
channel: values.channel ?? null,
|
|
});
|
|
}
|
|
|
|
function normalizeBuildOptions({
|
|
outdir,
|
|
onlyTarget = null,
|
|
hostOnly = false,
|
|
mode = "stable",
|
|
channel = null,
|
|
}) {
|
|
const modeChannel = normalizeModeChannel(mode, channel);
|
|
return {
|
|
outdir: outdir ?? DEFAULT_OUTDIR,
|
|
onlyTarget,
|
|
hostOnly: Boolean(hostOnly),
|
|
...modeChannel,
|
|
};
|
|
}
|
|
|
|
function hostBunTarget() {
|
|
const os = process.platform === "win32" ? "windows" : process.platform;
|
|
const match = BINARY_TARGETS.find((target) => target.os === os && target.arch === process.arch);
|
|
if (!match) {
|
|
throw new Error(`Unsupported host for --host build: ${process.platform}/${process.arch}`);
|
|
}
|
|
return match.bunTarget;
|
|
}
|
|
|
|
function resolveTargets({ hostOnly, onlyTarget }) {
|
|
if (hostOnly) {
|
|
const host = hostBunTarget();
|
|
return BINARY_TARGETS.filter((target) => target.bunTarget === host);
|
|
}
|
|
if (!onlyTarget) return BINARY_TARGETS;
|
|
|
|
const targets = BINARY_TARGETS.filter((target) => target.bunTarget === onlyTarget);
|
|
if (targets.length === 0) {
|
|
const known = BINARY_TARGETS.map((target) => target.bunTarget).join(", ");
|
|
throw new Error(`Unknown --target ${onlyTarget}. Known: ${known}`);
|
|
}
|
|
return targets;
|
|
}
|
|
|
|
function ensureBun() {
|
|
const result = spawnSync("bun", ["--version"], { encoding: "utf-8" });
|
|
if (result.status !== 0) {
|
|
throw new Error("bun not found on PATH. Install from https://bun.sh");
|
|
}
|
|
return result.stdout.trim();
|
|
}
|
|
|
|
function compileOne({ bunTarget, os, arch, exe }, version, outdir, entry) {
|
|
const innerName = binaryInnerName(version, { os, arch, exe });
|
|
const innerPath = join(outdir, innerName);
|
|
log(`compile ${bunTarget} → ${innerName}`);
|
|
|
|
const result = spawnSync(
|
|
"bun",
|
|
[BINARY_COMPILE, "--entry", entry, "--outfile", innerPath, "--target", bunTarget],
|
|
{ cwd: ROOT, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] },
|
|
);
|
|
if (result.status !== 0) {
|
|
process.stderr.write(result.stderr || result.stdout || "");
|
|
throw new Error(`Bun compile failed for ${bunTarget}`);
|
|
}
|
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
// Bun 1.2.19 writes windows-x64 .exe with mode 000 on Unix hosts (oven-sh/bun#21308).
|
|
chmodSync(innerPath, 0o755);
|
|
return { innerName, innerPath, os, arch, exe };
|
|
}
|
|
|
|
function writeChecksums(outdir, artifacts) {
|
|
const lines = artifacts.map((item) => `${item.sha256} ${item.fileName}`);
|
|
writeFileSync(join(outdir, "SHA256SUMS"), `${lines.join("\n")}\n`);
|
|
}
|
|
|
|
/** Write the rolling channel manifest (`latest.json` / `sync-release.json`) with per-platform zip + sha256. */
|
|
function writeChannelManifest(outdir, version, artifacts, mode) {
|
|
const channel = rollingManifestChannelId(mode);
|
|
const assets = Object.fromEntries(
|
|
artifacts.map((item) => [
|
|
`${item.os}-${item.arch}`,
|
|
{
|
|
file: item.fileName,
|
|
sha256: item.sha256,
|
|
inner: item.innerName,
|
|
},
|
|
]),
|
|
);
|
|
const manifest = {
|
|
name: "bailian-cli",
|
|
channel,
|
|
version,
|
|
releasedAt: new Date().toISOString(),
|
|
assets,
|
|
};
|
|
const name = rollingManifestFileName(mode);
|
|
writeJson(join(outdir, name), manifest);
|
|
return name;
|
|
}
|
|
|
|
function cliVersion() {
|
|
return readPackageJson(PACKAGES.find((pkg) => pkg.key === "cli")).version;
|
|
}
|
|
|
|
/** Run `--version` on the host platform's uncompressed binary, if present. */
|
|
function smokeTestHostBinary(compiled, outdir) {
|
|
const hostOs = process.platform === "win32" ? "windows" : process.platform;
|
|
const host = compiled.find((item) => item.os === hostOs && item.arch === process.arch);
|
|
if (!host) return;
|
|
const binary = join(outdir, host.innerName);
|
|
log(`smoke test ${host.innerName} --version`);
|
|
const result = spawnSync(binary, ["--version"], { encoding: "utf-8" });
|
|
if (result.status !== 0) {
|
|
process.stderr.write(result.stderr || result.stdout || "");
|
|
throw new Error(`smoke test failed: ${host.innerName} --version`);
|
|
}
|
|
}
|
|
|
|
/** Compile binaries into `outdir`, zip per platform, write checksums (+ channel manifest). */
|
|
export function buildBinaryArtifacts(rawOptions = {}) {
|
|
const options = normalizeBuildOptions(rawOptions);
|
|
const { outdir, mode, channel } = options;
|
|
const bunVersion = ensureBun();
|
|
ensureZip();
|
|
const version = cliVersion();
|
|
const targets = resolveTargets(options);
|
|
|
|
mkdirSync(outdir, { recursive: true });
|
|
log(`bun ${bunVersion}`);
|
|
log(`bailian-cli ${version}`);
|
|
log(`mode ${mode}${channel ? ` channel=${channel}` : ""}`);
|
|
log(`outdir ${outdir}`);
|
|
|
|
const compiled = targets.map((target) => compileOne(target, version, outdir, CLI_ENTRY));
|
|
smokeTestHostBinary(compiled, outdir);
|
|
const artifacts = compiled.map((item) =>
|
|
zipOne(item, { outdir, zipFileName: binaryAssetName(version, item), log }),
|
|
);
|
|
writeChecksums(outdir, artifacts);
|
|
|
|
const extras = ["SHA256SUMS"];
|
|
extras.push(writeChannelManifest(outdir, version, artifacts, mode));
|
|
|
|
log(`\nBuilt ${artifacts.length} zip(s):`);
|
|
for (const item of artifacts) {
|
|
log(` ${item.fileName} ${item.sha256.slice(0, 12)}… (inner ${item.innerName})`);
|
|
}
|
|
log(`Also wrote ${extras.join(", ")}`);
|
|
return {
|
|
version,
|
|
mode,
|
|
channel,
|
|
outdir,
|
|
artifacts,
|
|
manifests: extras.filter((name) => name.endsWith(".json")),
|
|
};
|
|
}
|
|
|
|
if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) {
|
|
try {
|
|
buildBinaryArtifacts(parseCliArgs(process.argv.slice(2)));
|
|
} catch (error) {
|
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
process.exit(1);
|
|
}
|
|
}
|