Files
openwork/apps/desktop/scripts/electron-after-pack.cjs
T

182 lines
6.6 KiB
JavaScript

const fs = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const asar = require("@electron/asar");
const computerUseHelperAppName = "OpenWork Computer Use.app";
const sidecarBases = [
"opencode",
];
function targetTriple(platformName, arch) {
if (platformName === "darwin") {
if (arch === "arm64") return "aarch64-apple-darwin";
if (arch === "x64") return "x86_64-apple-darwin";
}
if (platformName === "linux") {
if (arch === "arm64") return "aarch64-unknown-linux-gnu";
if (arch === "x64") return "x86_64-unknown-linux-gnu";
}
if (platformName === "win32") {
if (arch === "arm64") return "aarch64-pc-windows-msvc";
if (arch === "x64") return "x86_64-pc-windows-msvc";
}
return null;
}
function resolveSidecarsDir(context) {
if (context.electronPlatformName === "darwin") {
const entries = fs.existsSync(context.appOutDir) ? fs.readdirSync(context.appOutDir) : [];
const appName = entries.find((entry) => entry.endsWith(".app"));
return appName ? path.join(context.appOutDir, appName, "Contents", "Resources", "sidecars") : null;
}
return path.join(context.appOutDir, "resources", "sidecars");
}
function resolveMacAppPath(context) {
if (context.electronPlatformName !== "darwin") return null;
const appName = `${context.packager.appInfo.productFilename}.app`;
const direct = path.join(context.appOutDir, appName);
if (fs.existsSync(direct)) return direct;
const entries = fs.existsSync(context.appOutDir) ? fs.readdirSync(context.appOutDir) : [];
const fallback = entries.find((entry) => entry.endsWith(".app"));
return fallback ? path.join(context.appOutDir, fallback) : null;
}
function resolveAppAsarPath(context) {
if (context.electronPlatformName === "darwin") {
const appPath = resolveMacAppPath(context);
return appPath ? path.join(appPath, "Contents", "Resources", "app.asar") : null;
}
return path.join(context.appOutDir, "resources", "app.asar");
}
function normalizeAsarEntryPath(entry, separator) {
return entry.split(separator).join("/");
}
function verifyRuntimeDependencies(context) {
const appAsarPath = resolveAppAsarPath(context);
if (!appAsarPath || !fs.existsSync(appAsarPath)) {
throw new Error(`Missing packaged app.asar at ${appAsarPath || context.appOutDir}`);
}
const packagedFiles = new Set(
asar
.listPackage(appAsarPath, { isPack: false })
.map((entry) => normalizeAsarEntryPath(entry, path.sep)),
);
const stagedNodeModules = path.resolve(__dirname, "..", ".electron-runtime", "node_modules");
const packageJsonPaths = [];
function visitNodeModules(nodeModulesPath, archivePrefix) {
for (const entry of fs.readdirSync(nodeModulesPath, { withFileTypes: true })) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith("@")) {
visitNodeModules(path.join(nodeModulesPath, entry.name), `${archivePrefix}/${entry.name}`);
continue;
}
const packagePath = path.join(nodeModulesPath, entry.name);
if (!fs.existsSync(path.join(packagePath, "package.json"))) continue;
const packageJsonPath = `${archivePrefix}/${entry.name}/package.json`;
packageJsonPaths.push(packageJsonPath);
const nestedNodeModules = path.join(packagePath, "node_modules");
if (fs.existsSync(nestedNodeModules)) {
visitNodeModules(nestedNodeModules, `${archivePrefix}/${entry.name}/node_modules`);
}
}
}
if (!fs.existsSync(stagedNodeModules)) {
throw new Error(`Missing staged MCP runtime at ${stagedNodeModules}`);
}
visitNodeModules(stagedNodeModules, "/node_modules");
if (packageJsonPaths.length === 0) throw new Error("The staged MCP runtime is empty.");
for (const packageJsonPath of packageJsonPaths) {
if (!packagedFiles.has(packageJsonPath)) {
throw new Error(`Missing staged MCP runtime package from app.asar: ${packageJsonPath}`);
}
}
}
function signComputerUseHelper(context) {
const appPath = resolveMacAppPath(context);
if (!appPath) return;
const helperPath = path.join(appPath, "Contents", "Resources", "helpers", computerUseHelperAppName);
if (!fs.existsSync(helperPath)) {
throw new Error(`Missing Computer Use helper app at ${helperPath}`);
}
const identity = process.env.OPENWORK_COMPUTER_USE_CODESIGN_IDENTITY
|| process.env.CSC_NAME
|| process.env.APPLE_CODESIGN_IDENTITY
|| "-";
const args = ["--force", "--deep", "--options", "runtime", "--sign", identity];
if (identity !== "-") args.push("--timestamp");
args.push(helperPath);
const result = spawnSync("codesign", args, { stdio: "inherit" });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`codesign failed for Computer Use helper app with status ${result.status}`);
}
}
function copyExecutableTargetToAlias(sidecarsDir, targetName, aliasName) {
const targetPath = path.join(sidecarsDir, targetName);
if (!fs.existsSync(targetPath)) {
throw new Error(`Missing packaged sidecar for target: ${targetName}`);
}
const aliasPath = path.join(sidecarsDir, aliasName);
fs.copyFileSync(targetPath, aliasPath);
try {
fs.chmodSync(aliasPath, 0o755);
} catch {
// Windows and some filesystems may ignore chmod.
}
}
async function afterPack(context) {
verifyRuntimeDependencies(context);
const triple = targetTriple(context.electronPlatformName, context.arch);
if (!triple) return;
const sidecarsDir = resolveSidecarsDir(context);
if (!sidecarsDir || !fs.existsSync(sidecarsDir)) return;
const isWindows = context.electronPlatformName === "win32";
const executableSuffix = isWindows ? ".exe" : "";
const keep = new Set();
for (const base of sidecarBases) {
const aliasName = `${base}${executableSuffix}`;
const targetName = `${base}-${triple}${executableSuffix}`;
copyExecutableTargetToAlias(sidecarsDir, targetName, aliasName);
keep.add(aliasName);
keep.add(targetName);
}
const versionsAlias = "versions.json";
const versionsTarget = `versions.json-${triple}${executableSuffix}`;
const versionsTargetPath = path.join(sidecarsDir, versionsTarget);
if (!fs.existsSync(versionsTargetPath)) {
throw new Error(`Missing packaged sidecar metadata for target: ${versionsTarget}`);
}
fs.copyFileSync(versionsTargetPath, path.join(sidecarsDir, versionsAlias));
keep.add(versionsAlias);
keep.add(versionsTarget);
for (const entry of fs.readdirSync(sidecarsDir)) {
if (!keep.has(entry)) {
fs.rmSync(path.join(sidecarsDir, entry), { force: true, recursive: true });
}
}
signComputerUseHelper(context);
}
module.exports = afterPack;
module.exports.default = afterPack;
module.exports.normalizeAsarEntryPath = normalizeAsarEntryPath;