397 lines
12 KiB
JavaScript
397 lines
12 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { dirname, join } from "node:path";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const packageJsonPath = join(__dirname, "..", "package.json");
|
|
const buildDir = join(__dirname, "..", "build");
|
|
const releaseDir = join(buildDir, "release");
|
|
const runtimeConfigPath = join(buildDir, "electron-builder.runtime.json");
|
|
const localElectronDistPath = join(__dirname, "..", "node_modules", "electron", "dist");
|
|
const customMacSignPath = join(__dirname, "electron-builder-mac-sign.mjs");
|
|
|
|
const extraArgs = process.argv.slice(2);
|
|
const env = { ...process.env };
|
|
const explicitElectronDistPath = env.AGENTCOMPANY_ELECTRON_DIST?.trim() || "";
|
|
const noDeprecationFlag = "--no-deprecation";
|
|
const requestedUpdateUrl = env.AGENTCOMPANY_UPDATE_URL?.trim() ?? "";
|
|
const strictRelease = env.AGENTCOMPANY_STRICT_RELEASE === "true";
|
|
const hasMacSigningMaterial = Boolean(
|
|
env.CSC_LINK?.trim() ||
|
|
env.CSC_NAME?.trim() ||
|
|
env.APPLE_API_KEY?.trim(),
|
|
);
|
|
const isDirBuild = extraArgs.includes("--dir");
|
|
const r2AccountId = env.AGENTCOMPANY_R2_ACCOUNT_ID?.trim() ?? env.R2_ACCOUNT_ID?.trim() ?? "";
|
|
const r2Bucket = env.AGENTCOMPANY_R2_BUCKET?.trim() ?? env.R2_BUCKET?.trim() ?? "";
|
|
const r2PublicUrl = env.AGENTCOMPANY_R2_PUBLIC_URL?.trim() ?? env.R2_PUBLIC_BASE_URL?.trim() ?? "";
|
|
const r2Path = env.AGENTCOMPANY_R2_PATH?.trim() ?? env.AGENTCOMPANY_RELEASE_PREFIX?.trim() ?? "desktop";
|
|
const r2Region = env.AGENTCOMPANY_R2_REGION?.trim() || "auto";
|
|
const r2Endpoint = env.AGENTCOMPANY_R2_ENDPOINT?.trim() || (r2AccountId ? `https://${r2AccountId}.r2.cloudflarestorage.com` : "");
|
|
const filteredUnsignedMacWarningSnippets = [
|
|
"skipped macOS code signing reason=identity explicitly is set to null",
|
|
"arm64 requires signing, but identity is set to null and signing is being skipped",
|
|
];
|
|
|
|
function validateBuildVersion(currentVersion) {
|
|
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(currentVersion);
|
|
if (!match) {
|
|
throw new Error(`[build-desktop] Unsupported package version format: ${currentVersion}`);
|
|
}
|
|
return currentVersion;
|
|
}
|
|
|
|
function normalizeBaseUrl(url) {
|
|
return url.replace(/\/+$/, "");
|
|
}
|
|
|
|
function normalizePublishPath(targetPath) {
|
|
return targetPath.replace(/^\/+|\/+$/g, "");
|
|
}
|
|
|
|
function resolveUpdateUrl() {
|
|
if (requestedUpdateUrl && !requestedUpdateUrl.includes("example.invalid")) {
|
|
return normalizeBaseUrl(requestedUpdateUrl);
|
|
}
|
|
if (!r2PublicUrl) {
|
|
return "";
|
|
}
|
|
const baseUrl = normalizeBaseUrl(r2PublicUrl);
|
|
const publishPath = normalizePublishPath(r2Path);
|
|
return publishPath ? `${baseUrl}/${publishPath}` : baseUrl;
|
|
}
|
|
|
|
const resolvedUpdateUrl = resolveUpdateUrl();
|
|
const hasR2PublishConfig = Boolean(
|
|
r2Bucket &&
|
|
r2Endpoint &&
|
|
resolvedUpdateUrl &&
|
|
(env.AGENTCOMPANY_R2_ACCESS_KEY_ID?.trim() || env.R2_ACCESS_KEY_ID?.trim() || env.AWS_ACCESS_KEY_ID?.trim()) &&
|
|
(env.AGENTCOMPANY_R2_SECRET_ACCESS_KEY?.trim() || env.R2_SECRET_ACCESS_KEY?.trim() || env.AWS_SECRET_ACCESS_KEY?.trim()),
|
|
);
|
|
|
|
if (!env.AWS_ACCESS_KEY_ID && (env.AGENTCOMPANY_R2_ACCESS_KEY_ID?.trim() || env.R2_ACCESS_KEY_ID?.trim())) {
|
|
env.AWS_ACCESS_KEY_ID = env.AGENTCOMPANY_R2_ACCESS_KEY_ID?.trim() || env.R2_ACCESS_KEY_ID?.trim();
|
|
}
|
|
if (!env.AWS_SECRET_ACCESS_KEY && (env.AGENTCOMPANY_R2_SECRET_ACCESS_KEY?.trim() || env.R2_SECRET_ACCESS_KEY?.trim())) {
|
|
env.AWS_SECRET_ACCESS_KEY = env.AGENTCOMPANY_R2_SECRET_ACCESS_KEY?.trim() || env.R2_SECRET_ACCESS_KEY?.trim();
|
|
}
|
|
if (!env.AWS_REGION) {
|
|
env.AWS_REGION = r2Region;
|
|
}
|
|
if (!env.AWS_DEFAULT_REGION) {
|
|
env.AWS_DEFAULT_REGION = env.AWS_REGION;
|
|
}
|
|
if (!env.AWS_EC2_METADATA_DISABLED) {
|
|
env.AWS_EC2_METADATA_DISABLED = "true";
|
|
}
|
|
if (!env.AGENTCOMPANY_UPDATE_URL && resolvedUpdateUrl) {
|
|
env.AGENTCOMPANY_UPDATE_URL = resolvedUpdateUrl;
|
|
}
|
|
|
|
function resolveCurrentPlatformArg() {
|
|
switch (process.platform) {
|
|
case "darwin":
|
|
return "--mac";
|
|
case "win32":
|
|
return "--win";
|
|
default:
|
|
return "--linux";
|
|
}
|
|
}
|
|
|
|
function resolveCurrentArchArg() {
|
|
switch (process.arch) {
|
|
case "arm64":
|
|
return "--arm64";
|
|
case "x64":
|
|
return "--x64";
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function hasExplicitArchArg(args) {
|
|
return args.some((arg) =>
|
|
arg === "--arm64" ||
|
|
arg === "--x64" ||
|
|
arg === "--ia32" ||
|
|
arg === "--universal" ||
|
|
arg.startsWith("--arm64=") ||
|
|
arg.startsWith("--x64=") ||
|
|
arg.startsWith("--ia32=") ||
|
|
arg.startsWith("--universal="),
|
|
);
|
|
}
|
|
|
|
function resolveCurrentMacArch() {
|
|
if (process.platform !== "darwin" || env.AGENTCOMPANY_MAC_ALL_ARCHES === "true" || hasExplicitArchArg(extraArgs)) {
|
|
return null;
|
|
}
|
|
return process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : null;
|
|
}
|
|
|
|
async function resolveBuildVersion() {
|
|
const raw = await readFile(packageJsonPath, "utf8");
|
|
const pkg = JSON.parse(raw);
|
|
return validateBuildVersion(pkg.version);
|
|
}
|
|
|
|
async function writeRuntimeConfig(buildVersion) {
|
|
const forcedMacArch = resolveCurrentMacArch();
|
|
const isUnsignedLocalMacBuild = (
|
|
process.platform === "darwin" &&
|
|
!hasMacSigningMaterial &&
|
|
env.AGENTCOMPANY_ENABLE_LOCAL_DMG !== "true"
|
|
);
|
|
const runtimeConfig = {
|
|
extends: join(__dirname, "..", "electron-builder.yml"),
|
|
directories: {
|
|
output: releaseDir,
|
|
},
|
|
extraMetadata: {
|
|
version: buildVersion,
|
|
},
|
|
};
|
|
if (explicitElectronDistPath) {
|
|
runtimeConfig.electronDist = explicitElectronDistPath;
|
|
} else if (isDirBuild) {
|
|
runtimeConfig.electronDist = localElectronDistPath;
|
|
}
|
|
if (hasR2PublishConfig) {
|
|
runtimeConfig.publish = [
|
|
{
|
|
provider: "s3",
|
|
bucket: r2Bucket,
|
|
endpoint: r2Endpoint,
|
|
region: r2Region,
|
|
path: normalizePublishPath(r2Path) || undefined,
|
|
acl: null,
|
|
publishAutoUpdate: true,
|
|
},
|
|
];
|
|
} else if (resolvedUpdateUrl) {
|
|
runtimeConfig.publish = [
|
|
{
|
|
provider: "generic",
|
|
url: resolvedUpdateUrl,
|
|
},
|
|
];
|
|
}
|
|
if (isUnsignedLocalMacBuild) {
|
|
if (strictRelease) {
|
|
throw new Error("[build-desktop] Strict release mode requires macOS signing/notarization material.");
|
|
}
|
|
runtimeConfig.mac = {
|
|
hardenedRuntime: false,
|
|
identity: null,
|
|
};
|
|
if (!isDirBuild) {
|
|
runtimeConfig.mac.target = forcedMacArch
|
|
? [{ target: "zip", arch: [forcedMacArch] }]
|
|
: ["zip"];
|
|
}
|
|
} else if (process.platform === "darwin" && !hasMacSigningMaterial) {
|
|
runtimeConfig.mac = {
|
|
hardenedRuntime: false,
|
|
identity: null,
|
|
};
|
|
} else if (process.platform === "darwin") {
|
|
runtimeConfig.mac = {
|
|
hardenedRuntime: true,
|
|
};
|
|
if (hasMacSigningMaterial) {
|
|
runtimeConfig.mac.sign = customMacSignPath;
|
|
}
|
|
}
|
|
if (process.platform === "darwin" && !isDirBuild && forcedMacArch && !isUnsignedLocalMacBuild) {
|
|
runtimeConfig.mac ??= {};
|
|
runtimeConfig.mac.target = [
|
|
{ target: "dmg", arch: [forcedMacArch] },
|
|
{ target: "zip", arch: [forcedMacArch] },
|
|
];
|
|
}
|
|
await writeFile(runtimeConfigPath, `${JSON.stringify(runtimeConfig, null, 2)}\n`, "utf8");
|
|
}
|
|
|
|
if (env.CSC_IDENTITY_AUTO_DISCOVERY === undefined) {
|
|
env.CSC_IDENTITY_AUTO_DISCOVERY = hasMacSigningMaterial ? "true" : "false";
|
|
}
|
|
|
|
const existingNodeOptions = env.NODE_OPTIONS?.trim();
|
|
if (!existingNodeOptions) {
|
|
env.NODE_OPTIONS = noDeprecationFlag;
|
|
} else if (!existingNodeOptions.includes(noDeprecationFlag)) {
|
|
env.NODE_OPTIONS = `${existingNodeOptions} ${noDeprecationFlag}`;
|
|
}
|
|
|
|
const allowedPublishModes = new Set(["always", "never", "onTag", "onTagOrDraft"]);
|
|
const explicitPublishArg = extraArgs.some((arg) => arg === "--publish" || arg.startsWith("--publish="));
|
|
const explicitPlatformArg = extraArgs.some((arg) =>
|
|
arg === "--mac" ||
|
|
arg === "--win" ||
|
|
arg === "--linux" ||
|
|
arg === "-m" ||
|
|
arg === "-w" ||
|
|
arg === "-l" ||
|
|
arg.startsWith("--mac=") ||
|
|
arg.startsWith("--win=") ||
|
|
arg.startsWith("--linux="),
|
|
);
|
|
const explicitArchArg = hasExplicitArchArg(extraArgs);
|
|
const shouldForceLocalMacZip = (
|
|
process.platform === "darwin" &&
|
|
!hasMacSigningMaterial &&
|
|
env.AGENTCOMPANY_ENABLE_LOCAL_DMG !== "true" &&
|
|
!explicitPlatformArg &&
|
|
!isDirBuild
|
|
);
|
|
const requestedPublishMode = env.AGENTCOMPANY_ELECTRON_PUBLISH?.trim();
|
|
let publishMode = "never";
|
|
|
|
if (requestedPublishMode) {
|
|
if (!allowedPublishModes.has(requestedPublishMode)) {
|
|
console.warn(
|
|
`[build-desktop] Ignoring invalid AGENTCOMPANY_ELECTRON_PUBLISH="${requestedPublishMode}". ` +
|
|
"Falling back to --publish never.",
|
|
);
|
|
} else {
|
|
publishMode = requestedPublishMode;
|
|
}
|
|
}
|
|
|
|
const builderArgs = ["exec", "electron-builder", "--config", runtimeConfigPath];
|
|
if (!explicitPublishArg) {
|
|
builderArgs.push("--publish", publishMode);
|
|
}
|
|
if (!explicitPlatformArg) {
|
|
builderArgs.push(resolveCurrentPlatformArg());
|
|
}
|
|
if (!explicitArchArg) {
|
|
const currentArchArg = resolveCurrentArchArg();
|
|
if (currentArchArg) {
|
|
builderArgs.push(currentArchArg);
|
|
}
|
|
}
|
|
if (shouldForceLocalMacZip) {
|
|
builderArgs.push("zip");
|
|
}
|
|
builderArgs.push(...extraArgs);
|
|
|
|
if (strictRelease && !resolvedUpdateUrl) {
|
|
throw new Error("[build-desktop] Strict release mode requires AGENTCOMPANY_UPDATE_URL or an R2 public URL to be configured.");
|
|
}
|
|
if (publishMode !== "never" && !hasR2PublishConfig && !resolvedUpdateUrl) {
|
|
throw new Error(
|
|
"[build-desktop] Publishing requires either AGENTCOMPANY_UPDATE_URL (generic feed only) " +
|
|
"or full Cloudflare R2 publish configuration.",
|
|
);
|
|
}
|
|
|
|
const transientBuildFailurePatterns = [
|
|
/chmod .*Electron Framework.*no such file or directory/i,
|
|
];
|
|
|
|
function shouldRetryElectronBuilder(output) {
|
|
return transientBuildFailurePatterns.some((pattern) => pattern.test(output));
|
|
}
|
|
|
|
function delay(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
function shouldSuppressBuildLine(line) {
|
|
return filteredUnsignedMacWarningSnippets.some((snippet) => line.includes(snippet));
|
|
}
|
|
|
|
function forwardChildOutput(stream, writer, onLine) {
|
|
if (!stream) return;
|
|
let pending = "";
|
|
stream.on("data", (chunk) => {
|
|
pending += chunk.toString();
|
|
const lines = pending.split(/\r?\n/);
|
|
pending = lines.pop() ?? "";
|
|
for (const line of lines) {
|
|
onLine(line);
|
|
if (!shouldSuppressBuildLine(line)) {
|
|
writer.write(`${line}\n`);
|
|
}
|
|
}
|
|
});
|
|
stream.on("end", () => {
|
|
if (!pending) return;
|
|
onLine(pending);
|
|
if (!shouldSuppressBuildLine(pending)) {
|
|
writer.write(`${pending}\n`);
|
|
}
|
|
});
|
|
}
|
|
|
|
function runElectronBuilder() {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn("pnpm", builderArgs, {
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
env,
|
|
shell: process.platform === "win32",
|
|
});
|
|
let output = "";
|
|
const appendOutputLine = (line) => {
|
|
output += `${line}\n`;
|
|
};
|
|
forwardChildOutput(child.stdout, process.stdout, appendOutputLine);
|
|
forwardChildOutput(child.stderr, process.stderr, appendOutputLine);
|
|
|
|
child.on("error", reject);
|
|
child.on("close", (code, signal) => {
|
|
resolve({ code: code ?? 1, signal, output });
|
|
});
|
|
});
|
|
}
|
|
|
|
for (let attempt = 0; attempt < 5; attempt++) {
|
|
try {
|
|
await rm(buildDir, { recursive: true, force: true });
|
|
break;
|
|
} catch (rmErr) {
|
|
const retryable = rmErr.code === "EBUSY" || rmErr.code === "ENOTEMPTY" || rmErr.code === "EPERM";
|
|
if (attempt < 4 && retryable) {
|
|
const delay = (attempt + 1) * 3000;
|
|
console.log(
|
|
`[build-desktop] Build dir cleanup blocked (${rmErr.code}), retrying in ${delay / 1000}s... ` +
|
|
`(attempt ${attempt + 1}/5)`,
|
|
);
|
|
await new Promise((r) => setTimeout(r, delay));
|
|
} else {
|
|
throw rmErr;
|
|
}
|
|
}
|
|
}
|
|
await mkdir(buildDir, { recursive: true });
|
|
const buildVersion = env.AGENTCOMPANY_BUILD_VERSION?.trim() || await resolveBuildVersion();
|
|
await writeRuntimeConfig(buildVersion);
|
|
env.AGENTCOMPANY_BUILD_VERSION = buildVersion;
|
|
env.npm_package_version = buildVersion;
|
|
console.log(`[build-desktop] Cleaned ${buildDir} and staged build version ${buildVersion}.`);
|
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
try {
|
|
const result = await runElectronBuilder();
|
|
if (result.signal) {
|
|
process.kill(process.pid, result.signal);
|
|
break;
|
|
}
|
|
if (result.code === 0) {
|
|
process.exit(0);
|
|
}
|
|
if (attempt === 0 && shouldRetryElectronBuilder(result.output)) {
|
|
console.warn("[build-desktop] electron-builder hit a transient macOS framework extraction error; retrying once.");
|
|
await delay(1000);
|
|
continue;
|
|
}
|
|
process.exit(result.code);
|
|
} catch (error) {
|
|
console.error(error);
|
|
process.exit(1);
|
|
}
|
|
}
|