refactor(build-desktop): split 397-line script into env/config/runner modules (P1-15)
This commit is contained in:
1 parent
b58e64b555
commit
527a730bec
4 files changed
+305
-1
No files matched your search
+1
-1
@@ -10,7 +10,7 @@ coverage/
|
||||
win-unpacked
|
||||
.blockmap
|
||||
.claude
|
||||
build
|
||||
/build
|
||||
.omc
|
||||
.omx
|
||||
.planning/*
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
function validateBuildVersion(currentVersion) {
|
||||
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(currentVersion);
|
||||
if (!match) {
|
||||
throw new Error(`[build-desktop] Unsupported package version format: ${currentVersion}`);
|
||||
}
|
||||
return currentVersion;
|
||||
}
|
||||
|
||||
export async function resolveBuildVersion(packageJsonPath, override) {
|
||||
if (override?.trim()) return validateBuildVersion(override.trim());
|
||||
const raw = await readFile(packageJsonPath, "utf8");
|
||||
const pkg = JSON.parse(raw);
|
||||
return validateBuildVersion(pkg.version);
|
||||
}
|
||||
|
||||
export 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(env, extraArgs) {
|
||||
if (process.platform !== "darwin" || env.AGENTCOMPANY_MAC_ALL_ARCHES === "true" || hasExplicitArchArg(extraArgs)) {
|
||||
return null;
|
||||
}
|
||||
return process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : null;
|
||||
}
|
||||
|
||||
export async function writeRuntimeBuilderConfig({
|
||||
rootDir,
|
||||
buildDir,
|
||||
runtimeConfigPath,
|
||||
buildVersion,
|
||||
extraArgs,
|
||||
prepared,
|
||||
customMacSignPath,
|
||||
localElectronDistPath,
|
||||
}) {
|
||||
const { env, resolvedUpdateUrl, hasR2PublishConfig, hasMacSigningMaterial, strictRelease, isDirBuild, explicitElectronDistPath, r2 } = prepared;
|
||||
const forcedMacArch = resolveCurrentMacArch(env, extraArgs);
|
||||
const isUnsignedLocalMacBuild = (
|
||||
process.platform === "darwin" &&
|
||||
!hasMacSigningMaterial &&
|
||||
env.AGENTCOMPANY_ENABLE_LOCAL_DMG !== "true"
|
||||
);
|
||||
|
||||
const releaseDir = join(buildDir, "release");
|
||||
const runtimeConfig = {
|
||||
extends: join(rootDir, "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: r2.bucket,
|
||||
endpoint: r2.endpoint,
|
||||
region: r2.region,
|
||||
path: r2.path || 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] },
|
||||
];
|
||||
}
|
||||
|
||||
if (strictRelease && !resolvedUpdateUrl) {
|
||||
throw new Error("[build-desktop] Strict release mode requires AGENTCOMPANY_UPDATE_URL or an R2 public URL to be configured.");
|
||||
}
|
||||
|
||||
await writeFile(runtimeConfigPath, `${JSON.stringify(runtimeConfig, null, 2)}\n`, "utf8");
|
||||
return { forcedMacArch, isUnsignedLocalMacBuild };
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
function normalizeBaseUrl(url) {
|
||||
return url.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function normalizePublishPath(targetPath) {
|
||||
return targetPath.replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
export function prepareBuildEnv(sourceEnv, extraArgs) {
|
||||
const env = { ...sourceEnv };
|
||||
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 resolvedUpdateUrl = (() => {
|
||||
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 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;
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
|
||||
return {
|
||||
env,
|
||||
resolvedUpdateUrl,
|
||||
hasR2PublishConfig,
|
||||
hasMacSigningMaterial,
|
||||
strictRelease,
|
||||
isDirBuild,
|
||||
explicitElectronDistPath: env.AGENTCOMPANY_ELECTRON_DIST?.trim() || "",
|
||||
r2: { bucket: r2Bucket, endpoint: r2Endpoint, region: r2Region, path: normalizePublishPath(r2Path) },
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePublishMode(env) {
|
||||
const allowedPublishModes = new Set(["always", "never", "onTag", "onTagOrDraft"]);
|
||||
const requested = env.AGENTCOMPANY_ELECTRON_PUBLISH?.trim();
|
||||
if (!requested) return "never";
|
||||
if (!allowedPublishModes.has(requested)) {
|
||||
console.warn(
|
||||
`[build-desktop] Ignoring invalid AGENTCOMPANY_ELECTRON_PUBLISH="${requested}". ` +
|
||||
"Falling back to --publish never.",
|
||||
);
|
||||
return "never";
|
||||
}
|
||||
return requested;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { hasExplicitArchArg } from "./emit-builder-config.mjs";
|
||||
|
||||
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 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 shouldSuppressBuildLine(line) {
|
||||
return filteredUnsignedMacWarningSnippets.some((snippet) => line.includes(snippet));
|
||||
}
|
||||
|
||||
function forwardChildOutput(stream, writer) {
|
||||
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) {
|
||||
if (!shouldSuppressBuildLine(line)) writer.write(`${line}\n`);
|
||||
}
|
||||
});
|
||||
stream.on("end", () => {
|
||||
if (!pending) return;
|
||||
if (!shouldSuppressBuildLine(pending)) writer.write(`${pending}\n`);
|
||||
});
|
||||
}
|
||||
|
||||
export function buildElectronBuilderArgs({ runtimeConfigPath, extraArgs, publishMode, prepared }) {
|
||||
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 { env, hasR2PublishConfig, resolvedUpdateUrl, hasMacSigningMaterial, isDirBuild } = prepared;
|
||||
const shouldForceLocalMacZip = (
|
||||
process.platform === "darwin" &&
|
||||
!hasMacSigningMaterial &&
|
||||
env.AGENTCOMPANY_ENABLE_LOCAL_DMG !== "true" &&
|
||||
!explicitPlatformArg &&
|
||||
!isDirBuild
|
||||
);
|
||||
|
||||
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 args = ["exec", "electron-builder", "--config", runtimeConfigPath];
|
||||
if (!explicitPublishArg) args.push("--publish", publishMode);
|
||||
if (!explicitPlatformArg) args.push(resolveCurrentPlatformArg());
|
||||
if (!explicitArchArg) {
|
||||
const currentArchArg = resolveCurrentArchArg();
|
||||
if (currentArchArg) args.push(currentArchArg);
|
||||
}
|
||||
if (shouldForceLocalMacZip) args.push("zip");
|
||||
args.push(...extraArgs);
|
||||
return args;
|
||||
}
|
||||
|
||||
export function runElectronBuilder(builderArgs, env) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn("pnpm", builderArgs, {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
env,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
forwardChildOutput(child.stdout, process.stdout);
|
||||
forwardChildOutput(child.stderr, process.stderr);
|
||||
child.on("error", reject);
|
||||
child.on("close", (code, signal) => resolve({ code: code ?? 1, signal }));
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user