This branch snapshots the in-progress desktop workspace so GitHub-hosted Windows and Linux runners can package the same source tree that already passes local verification on macOS. Constraint: Local Apple Silicon Docker/Wine packaging crashes before Windows artifacts are produced Rejected: Publish from the incomplete base repo state | remote typecheck failed because current source additions were missing Confidence: medium Scope-risk: broad Directive: Treat this branch as a build snapshot only; do not merge it without reviewing the unrelated workspace changes it contains Tested: local typecheck, lint, workspace typecheck, unit tests, integration tests, e2e smoke, macOS signed release upload Not-tested: Native Windows/Linux artifacts before this CI run
152 lines
3.8 KiB
JavaScript
152 lines
3.8 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { readdir } from "node:fs/promises";
|
|
import { extname } from "node:path";
|
|
|
|
function delay(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function runCommand(command, args, capture = false) {
|
|
return await new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
|
|
if (capture) {
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk.toString();
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk.toString();
|
|
});
|
|
}
|
|
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
if (code === 0) {
|
|
resolve({ stdout, stderr });
|
|
return;
|
|
}
|
|
reject(new Error(stderr || stdout || `${command} failed with exit code ${code ?? 1}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function runCodesign(args) {
|
|
await runCommand("/usr/bin/codesign", args, false);
|
|
}
|
|
|
|
function buildCodesignArgs(targetPath, identity, keychain, fileOptions = {}) {
|
|
const args = [
|
|
"--sign", identity,
|
|
"--force",
|
|
];
|
|
|
|
if (keychain) {
|
|
args.push("--keychain", keychain);
|
|
}
|
|
if (fileOptions.timestamp !== false) {
|
|
args.push("--timestamp");
|
|
}
|
|
if (fileOptions.hardenedRuntime) {
|
|
args.push("--options", "runtime");
|
|
}
|
|
if (fileOptions.entitlements) {
|
|
args.push("--entitlements", fileOptions.entitlements);
|
|
}
|
|
if (Array.isArray(fileOptions.additionalArguments) && fileOptions.additionalArguments.length > 0) {
|
|
args.push(...fileOptions.additionalArguments);
|
|
}
|
|
|
|
args.push(targetPath);
|
|
return args;
|
|
}
|
|
|
|
async function isMachBinary(filePath) {
|
|
const extension = extname(filePath);
|
|
if (extension === ".dylib" || extension === ".node") {
|
|
return true;
|
|
}
|
|
|
|
const { stdout } = await runCommand("/usr/bin/file", ["-b", filePath], true);
|
|
return stdout.includes("Mach-O");
|
|
}
|
|
|
|
async function collectSignTargets(rootPath) {
|
|
const bundleTargets = [];
|
|
const binaryTargets = [];
|
|
|
|
async function walk(currentPath) {
|
|
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const fullPath = `${currentPath}/${entry.name}`;
|
|
|
|
if (entry.isSymbolicLink()) {
|
|
continue;
|
|
}
|
|
|
|
if (entry.isDirectory()) {
|
|
if (
|
|
entry.name.endsWith(".app") ||
|
|
entry.name.endsWith(".framework") ||
|
|
entry.name.endsWith(".xpc") ||
|
|
entry.name.endsWith(".appex") ||
|
|
entry.name.endsWith(".plugin")
|
|
) {
|
|
bundleTargets.push(fullPath);
|
|
}
|
|
await walk(fullPath);
|
|
continue;
|
|
}
|
|
|
|
if (await isMachBinary(fullPath)) {
|
|
binaryTargets.push(fullPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
await walk(rootPath);
|
|
|
|
bundleTargets.sort((left, right) => right.length - left.length);
|
|
binaryTargets.sort((left, right) => right.length - left.length);
|
|
|
|
return { bundleTargets, binaryTargets };
|
|
}
|
|
|
|
async function signTarget(targetPath, context) {
|
|
const fileOptions = context.optionsForFile?.(targetPath) ?? {};
|
|
const args = buildCodesignArgs(targetPath, context.identity, context.keychain, fileOptions);
|
|
|
|
let lastError;
|
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
try {
|
|
await runCodesign(args);
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
if (attempt === 2) {
|
|
throw error;
|
|
}
|
|
await delay(5000);
|
|
}
|
|
}
|
|
|
|
throw lastError;
|
|
}
|
|
|
|
export default async function sign(context) {
|
|
const { bundleTargets, binaryTargets } = await collectSignTargets(context.app);
|
|
|
|
for (const target of binaryTargets) {
|
|
await signTarget(target, context);
|
|
}
|
|
|
|
for (const target of bundleTargets) {
|
|
await signTarget(target, context);
|
|
}
|
|
|
|
await signTarget(context.app, context);
|
|
}
|