Files
AgentCompany/scripts/release-local-winlinux.mjs
T

224 lines
7.4 KiB
JavaScript

import { spawn } from "node:child_process";
import { readFile, writeFile, mkdir, rm, readdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { prepareR2ReleaseHost, purgeReleasePrefix, uploadReleaseFiles } from "./release-local/r2.mjs";
import { loadEnvFile } from "./release/lib/dotenv.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, "..");
const packageJsonPath = join(rootDir, "package.json");
const buildReleaseDir = join(rootDir, "build", "release");
const localReleaseDir = join(rootDir, "local-release");
const versionPattern = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
function parseArgs(argv) {
const options = { version: null, skipBoss: false, only: null };
for (const arg of argv) {
if (arg.startsWith("--version=")) {
options.version = arg.slice("--version=".length).trim();
continue;
}
if (arg === "--skip-boss") {
options.skipBoss = true;
continue;
}
if (arg === "--win-only") {
options.only = "win";
continue;
}
if (arg === "--linux-only") {
options.only = "linux";
continue;
}
if (arg === "--help") {
console.log(`Usage: node scripts/release-local-winlinux.mjs [options]
Options:
--version=x.y.z Override version (defaults to current package.json).
--win-only Build and upload Windows only.
--linux-only Build and upload Linux only.
--skip-boss Skip Boss Windows EXE build.
--help Show this help.
Environment:
On Windows host: Windows builds run natively via electron-builder.
On any host: Linux builds run through Docker + electronuserland/builder image.
`);
process.exit(0);
}
throw new Error(`Unknown argument: ${arg}`);
}
return options;
}
async function readPackageJson() {
return JSON.parse(await readFile(packageJsonPath, "utf8"));
}
async function run(command, args, env = process.env) {
await new Promise((resolve, reject) => {
const child = spawn(command, args, {
cwd: rootDir,
env,
stdio: "inherit",
shell: process.platform === "win32",
});
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code ?? 1}`));
});
});
}
function hasDockerInstalled() {
return new Promise((resolve) => {
const child = spawn("docker", ["--version"], { stdio: "ignore" });
child.on("error", () => resolve(false));
child.on("close", (code) => resolve(code === 0));
});
}
async function buildPublicWinNative() {
const env = {
...process.env,
AGENTCOMPANY_ELECTRON_PUBLISH: "never",
CSC_LINK: process.env.WIN_CSC_LINK ?? "",
CSC_KEY_PASSWORD: process.env.WIN_CSC_KEY_PASSWORD ?? "",
};
await run("pnpm", ["exec", "electron-vite", "build"], env);
await run("node", ["scripts/build-desktop.mjs", "--win", "nsis", "--x64"], env);
await run("node", ["scripts/build-desktop.mjs", "--win", "nsis", "--arm64"], env);
}
async function buildPublicLinuxDocker() {
if (!(await hasDockerInstalled())) {
throw new Error("Docker is required for Linux builds. Install Docker Desktop and retry.");
}
// Reuse the existing Docker-based Linux build path from release-local.mjs by
// shelling into it with --platforms=linux --no-upload. That keeps one source
// of truth for the container mounts.
await run("node", [
"scripts/release-local.mjs",
"--platforms=linux",
"--no-upload",
"--skip-verify",
]);
}
async function stage(platform) {
const destination = join(localReleaseDir, platform);
await rm(destination, { recursive: true, force: true });
await mkdir(localReleaseDir, { recursive: true });
await run(process.platform === "win32" ? "xcopy" : "cp", process.platform === "win32"
? [buildReleaseDir, destination, "/E", "/I", "/Y"]
: ["-R", buildReleaseDir, destination]);
console.log(`[release-local-winlinux] staged ${platform} artifacts in ${destination}`);
return destination;
}
async function buildBossWin(version) {
if (process.platform !== "win32") {
throw new Error("Boss Windows EXE build must run on Windows (electron-builder --win native).");
}
await run("node", ["boss-build/build-boss-edition.mjs", `--platform=win`, `--version=${version}`]);
}
async function main() {
const options = parseArgs(process.argv.slice(2));
await loadEnvFile(join(rootDir, ".env.release.local"));
await loadEnvFile(join(rootDir, ".env.release"));
process.env.AGENTCOMPANY_ELECTRON_PUBLISH = "never";
const pkgVersion = (await readPackageJson()).version;
const nextVersion = options.version ?? pkgVersion;
if (!versionPattern.test(nextVersion)) throw new Error(`Invalid version: ${nextVersion}`);
if (nextVersion !== pkgVersion) {
console.log(`[release-local-winlinux] version override: ${pkgVersion} -> ${nextVersion}`);
}
await run("pnpm", ["audit:deps"]);
await rm(localReleaseDir, { recursive: true, force: true });
await rm(buildReleaseDir, { recursive: true, force: true });
const r2 = await prepareR2ReleaseHost({ rootDir });
console.log(`[release-local-winlinux] update URL: ${process.env.AGENTCOMPANY_UPDATE_URL}`);
console.log(`[release-local-winlinux] version: ${nextVersion}`);
// Purge only Win+Linux artifacts — Mac files (owned by release-local-mac) stay put.
const keepClasses = options.only === "win"
? ["mac", "linux", "other"]
: options.only === "linux"
? ["mac", "win", "other"]
: ["mac", "other"];
await purgeReleasePrefix({
bucket: r2.bucket,
releasePrefix: r2.releasePrefix,
keep: keepClasses,
});
if (options.only !== "linux") {
if (process.platform !== "win32") {
throw new Error(
"release:local-winlinux Windows builds must run on a Windows host. " +
"Use --linux-only on non-Windows machines, or run the full command on the Windows box.",
);
}
await buildPublicWinNative();
await stage("win");
await uploadReleaseFiles({
rootDir,
releaseDir: join(localReleaseDir, "win"),
bucket: r2.bucket,
releasePrefix: r2.releasePrefix,
});
}
if (options.only !== "win") {
await buildPublicLinuxDocker();
// release-local.mjs already staged into local-release/linux when it ran.
// Upload from there so we do not wipe the staged tree.
await uploadReleaseFiles({
rootDir,
releaseDir: join(localReleaseDir, "linux"),
bucket: r2.bucket,
releasePrefix: r2.releasePrefix,
});
}
if (!options.skipBoss && options.only !== "linux" && process.platform === "win32") {
await buildBossWin(nextVersion);
}
await mkdir(localReleaseDir, { recursive: true });
await writeFile(
join(localReleaseDir, "manifest.json"),
JSON.stringify({
generatedAt: new Date().toISOString(),
command: "release:local-winlinux",
version: nextVersion,
only: options.only,
updateUrl: process.env.AGENTCOMPANY_UPDATE_URL,
}, null, 2),
"utf8",
);
const bossFiles = options.skipBoss || process.platform !== "win32"
? []
: (await readdir(join(rootDir, "boss-build", "build", "out")).catch(() => []))
.filter((name) => name.endsWith(".exe"));
console.log(JSON.stringify({
version: nextVersion,
only: options.only,
updateUrl: process.env.AGENTCOMPANY_UPDATE_URL,
bossArtifacts: bossFiles,
}, null, 2));
}
await main();