Skip to content

Commit 7812240

Browse files
committed
Fix runtime packaging and MCP binary launch on macOS
1 parent 717c2d9 commit 7812240

31 files changed

Lines changed: 2047 additions & 111 deletions

.github/workflows/release-core.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ jobs:
101101
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
102102
run: cd apps/desktop && npm run notarize:mac:dmg
103103

104-
- name: Validate macOS release artifacts
104+
- name: Validate macOS release artifacts and runtime payloads
105105
run: cd apps/desktop && npm run validate:mac:artifacts
106106

107107
- name: Upload validated artifacts to workflow run

apps/desktop/package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
"author": "Arul Sharma",
88
"license": "AGPL-3.0",
99
"scripts": {
10-
"predev": "node ./scripts/clear-vite-cache.cjs",
10+
"predev": "node ./scripts/clear-vite-cache.cjs && node ./scripts/normalize-runtime-binaries.cjs",
11+
"prebuild": "node ./scripts/normalize-runtime-binaries.cjs",
1112
"dev": "node ./scripts/ensure-electron.cjs && node ./scripts/dev.cjs",
1213
"build": "tsup && vite build",
1314
"dist:mac": "npm run build && electron-builder --mac --publish never",
@@ -130,13 +131,16 @@
130131
"electron.cjs",
131132
"package.json",
132133
"vendor/**/*",
133-
"!node_modules/onnxruntime-node/**",
134-
"!node_modules/node-pty/build/**"
134+
"!node_modules/onnxruntime-node/**"
135135
],
136136
"asarUnpack": [
137+
"dist/main/adeMcpProxy.cjs",
138+
"dist/main/packagedRuntimeSmoke.cjs",
139+
"node_modules/node-pty/**/*",
137140
"node_modules/@huggingface/transformers/node_modules/onnxruntime-node/**",
138141
"vendor/crsqlite/**"
139142
],
143+
"afterPack": "./scripts/after-pack-runtime-fixes.cjs",
140144
"publish": {
141145
"provider": "github",
142146
"owner": "arul28",
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const fs = require("node:fs");
2+
const path = require("node:path");
3+
const {
4+
normalizeDesktopRuntimeBinaries,
5+
resolvePackagedRuntimeRoot,
6+
} = require("./runtimeBinaryPermissions.cjs");
7+
8+
module.exports = async function afterPack(context) {
9+
const productFilename = context?.packager?.appInfo?.productFilename || "ADE";
10+
const appBundlePath = path.join(context?.appOutDir || "", `${productFilename}.app`);
11+
if (!appBundlePath || !fs.existsSync(appBundlePath)) {
12+
throw new Error(`[afterPack] Missing packaged app bundle: ${String(appBundlePath)}`);
13+
}
14+
15+
const runtimeRoot = resolvePackagedRuntimeRoot(appBundlePath);
16+
if (!fs.existsSync(runtimeRoot)) {
17+
throw new Error(`[afterPack] Missing unpacked runtime payload: ${runtimeRoot}`);
18+
}
19+
20+
const normalized = normalizeDesktopRuntimeBinaries(runtimeRoot);
21+
for (const entry of normalized) {
22+
console.log(`[afterPack] Restored executable mode: ${entry.label} -> ${path.relative(appBundlePath, entry.filePath)}`);
23+
}
24+
25+
const requiredScripts = [
26+
path.join(runtimeRoot, "dist", "main", "adeMcpProxy.cjs"),
27+
path.join(runtimeRoot, "dist", "main", "packagedRuntimeSmoke.cjs"),
28+
];
29+
30+
for (const scriptPath of requiredScripts) {
31+
if (!fs.existsSync(scriptPath)) {
32+
throw new Error(`[afterPack] Missing unpacked runtime entry: ${scriptPath}`);
33+
}
34+
}
35+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const path = require("node:path");
2+
const { normalizeDesktopRuntimeBinaries } = require("./runtimeBinaryPermissions.cjs");
3+
4+
const appDir = path.resolve(__dirname, "..");
5+
const normalized = normalizeDesktopRuntimeBinaries(appDir);
6+
7+
if (normalized.length === 0) {
8+
console.log("[runtime-binaries] No executable mode fixes were needed.");
9+
process.exit(0);
10+
}
11+
12+
for (const entry of normalized) {
13+
console.log(`[runtime-binaries] Restored executable mode: ${entry.label} -> ${path.relative(appDir, entry.filePath)}`);
14+
}

apps/desktop/scripts/prepare-universal-mac-inputs.mjs

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,50 @@ async function assertPathExists(targetPath, description) {
4343
}
4444
}
4545

46+
async function findFirstNodeAddon(rootPath) {
47+
const entries = await fs.readdir(rootPath, { withFileTypes: true });
48+
49+
for (const entry of entries) {
50+
const entryPath = path.join(rootPath, entry.name);
51+
52+
if (entry.isDirectory()) {
53+
const nestedMatch = await findFirstNodeAddon(entryPath);
54+
if (nestedMatch) {
55+
return nestedMatch;
56+
}
57+
continue;
58+
}
59+
60+
if (entry.isFile() && entry.name.endsWith(".node")) {
61+
return entryPath;
62+
}
63+
}
64+
65+
return null;
66+
}
67+
68+
async function findNodePtyAddon(moduleRootPath) {
69+
const candidateRoots = [
70+
path.join(moduleRootPath, "build", "Release"),
71+
path.join(moduleRootPath, "build", "Debug"),
72+
path.join(moduleRootPath, "prebuilds", "darwin-arm64"),
73+
path.join(moduleRootPath, "prebuilds", "darwin-x64"),
74+
];
75+
76+
for (const candidateRoot of candidateRoots) {
77+
if (!(await pathExists(candidateRoot))) {
78+
continue;
79+
}
80+
81+
const addonPath = await findFirstNodeAddon(candidateRoot);
82+
if (addonPath) {
83+
return addonPath;
84+
}
85+
}
86+
87+
return null;
88+
}
89+
4690
async function loadPackageLock() {
4791
return JSON.parse(await fs.readFile(packageLockPath, "utf8"));
4892
}
@@ -246,6 +290,14 @@ async function assertUniversalInputsReady() {
246290
path.join(appDir, "vendor", "crsqlite", "darwin-x64", "crsqlite.dylib"),
247291
"x64 crsqlite dylib",
248292
);
293+
await assertPathExists(path.join(appDir, "node_modules", "node-pty"), "node-pty package");
294+
const nodePtyAddon = await findNodePtyAddon(path.join(appDir, "node_modules", "node-pty"));
295+
if (!nodePtyAddon) {
296+
throw new Error(
297+
"[release:mac] Missing node-pty native addon under node_modules/node-pty/build or node_modules/node-pty/prebuilds",
298+
);
299+
}
300+
console.log(`[release:mac] Verified node-pty native addon: ${path.relative(appDir, nodePtyAddon)}`);
249301
}
250302

251303
const x64App = await resolveX64AppPath();
@@ -257,11 +309,6 @@ try {
257309
await seedFromLockfileAndPinnedArtifacts();
258310
}
259311

260-
await fs.rm(path.join(appDir, "node_modules", "node-pty", "build"), {
261-
recursive: true,
262-
force: true,
263-
});
264-
265312
await assertUniversalInputsReady();
266313
console.log("[release:mac] Universal macOS source tree now contains the required x64 runtime payloads");
267314
} finally {

apps/desktop/scripts/release-mac-local.mjs

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import fs from "node:fs/promises";
2-
import { mkdtempSync, writeFileSync, chmodSync, rmSync } from "node:fs";
2+
import { mkdtempSync, writeFileSync, chmodSync, rmSync, existsSync } from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
55
import { spawnSync } from "node:child_process";
@@ -76,13 +76,23 @@ function run(command, args, env) {
7676

7777
function maybeUseInstalledIdentity(env) {
7878
const hasImportedCertificate = Boolean(env.CSC_LINK && env.CSC_KEY_PASSWORD);
79+
const cscLinkPath = typeof env.CSC_LINK === "string" && env.CSC_LINK.startsWith("/") ? env.CSC_LINK : null;
80+
const missingImportedCertificate = Boolean(cscLinkPath && !existsSync(cscLinkPath));
7981

80-
if (hasImportedCertificate) {
82+
if (hasImportedCertificate && !missingImportedCertificate) {
8183
delete env.CSC_NAME;
8284
return;
8385
}
8486

8587
if (env.CSC_NAME) {
88+
if (missingImportedCertificate) {
89+
console.warn(
90+
`[release:mac] CSC_LINK points to a missing certificate file (${cscLinkPath}); ` +
91+
`falling back to installed identity ${env.CSC_NAME}.`,
92+
);
93+
delete env.CSC_LINK;
94+
delete env.CSC_KEY_PASSWORD;
95+
}
8696
return;
8797
}
8898

@@ -110,6 +120,11 @@ function maybeUseInstalledIdentity(env) {
110120
identities.find((identity) => authorName && identity.includes(authorName)) ?? identities[0] ?? null;
111121

112122
if (!preferredIdentity) {
123+
if (missingImportedCertificate) {
124+
throw new Error(
125+
`[release:mac] CSC_LINK points to a missing certificate file (${cscLinkPath}) and no installed Developer ID Application identity was found`,
126+
);
127+
}
113128
return;
114129
}
115130

apps/desktop/scripts/require-macos-release-secrets.cjs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#!/usr/bin/env node
22
"use strict";
33

4+
const fs = require("node:fs");
5+
46
function hasEnv(name) {
57
return Boolean(process.env[name] && String(process.env[name]).trim().length > 0);
68
}
@@ -13,8 +15,14 @@ const missing = [];
1315

1416
const hasImportedCertificate = ["CSC_LINK", "CSC_KEY_PASSWORD"].every(hasEnv);
1517
const hasInstalledIdentity = hasEnv("CSC_NAME");
18+
const cscLink = hasEnv("CSC_LINK") ? String(process.env.CSC_LINK).trim() : "";
19+
const cscLinkIsAbsolutePath = cscLink.startsWith("/");
20+
const cscLinkPathExists = !cscLinkIsAbsolutePath || fs.existsSync(cscLink);
1621

17-
if (!hasImportedCertificate && !hasInstalledIdentity) {
22+
if (hasImportedCertificate && cscLinkIsAbsolutePath && !cscLinkPathExists && !hasInstalledIdentity) {
23+
missing.push(`CSC_LINK points to a missing certificate file: ${cscLink}`);
24+
missing.push("Provide a valid CSC_LINK + CSC_KEY_PASSWORD pair or set CSC_NAME to an installed Developer ID identity");
25+
} else if (!hasImportedCertificate && !hasInstalledIdentity) {
1826
missing.push("Provide either CSC_LINK + CSC_KEY_PASSWORD or CSC_NAME");
1927
}
2028

@@ -67,8 +75,16 @@ if (matchingProfile.vars.includes("APPLE_API_KEY") && !String(process.env.APPLE_
6775
process.exit(1);
6876
}
6977

78+
if (hasImportedCertificate && cscLinkIsAbsolutePath && !cscLinkPathExists && hasInstalledIdentity) {
79+
process.stdout.write(
80+
`[release:mac] CSC_LINK points to a missing file (${cscLink}); continuing with installed identity ${process.env.CSC_NAME}.\n`
81+
);
82+
}
83+
7084
process.stdout.write(
7185
`[release:mac] macOS signing and notarization environment looks complete (` +
72-
`${hasImportedCertificate ? "imported Developer ID certificate" : `installed identity ${process.env.CSC_NAME}`}, ` +
86+
`${hasImportedCertificate && (!cscLinkIsAbsolutePath || cscLinkPathExists)
87+
? "imported Developer ID certificate"
88+
: `installed identity ${process.env.CSC_NAME}`}, ` +
7389
`${matchingProfile.label}).\n`
7490
);
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
const fs = require("node:fs");
2+
const path = require("node:path");
3+
4+
const EXECUTABLE_MASK = 0o111;
5+
const NODE_PTY_HELPER_PATH_PATCHES = [
6+
{
7+
from: "helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');",
8+
to: "helperPath = helperPath.replace(/app\\.asar(?!\\.unpacked)/, 'app.asar.unpacked');",
9+
},
10+
{
11+
from: "helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');",
12+
to: "helperPath = helperPath.replace(/node_modules\\.asar(?!\\.unpacked)/, 'node_modules.asar.unpacked');",
13+
},
14+
];
15+
16+
function pathExists(targetPath) {
17+
try {
18+
fs.accessSync(targetPath);
19+
return true;
20+
} catch {
21+
return false;
22+
}
23+
}
24+
25+
function listDirectories(rootPath) {
26+
if (!pathExists(rootPath)) return [];
27+
return fs.readdirSync(rootPath, { withFileTypes: true })
28+
.filter((entry) => entry.isDirectory())
29+
.map((entry) => path.join(rootPath, entry.name));
30+
}
31+
32+
function ensureExecutable(filePath) {
33+
const stat = fs.statSync(filePath);
34+
if (!stat.isFile()) return false;
35+
const currentMode = stat.mode & 0o777;
36+
if ((currentMode & EXECUTABLE_MASK) === EXECUTABLE_MASK) {
37+
return false;
38+
}
39+
fs.chmodSync(filePath, currentMode | EXECUTABLE_MASK);
40+
return true;
41+
}
42+
43+
function normalizeFileSet(filePaths, label) {
44+
const normalized = [];
45+
for (const filePath of filePaths) {
46+
if (!pathExists(filePath)) continue;
47+
if (ensureExecutable(filePath)) normalized.push(filePath);
48+
}
49+
return normalized.map((filePath) => ({ filePath, label }));
50+
}
51+
52+
function collectDesktopRuntimeExecutableCandidates(rootPath) {
53+
const candidates = [];
54+
55+
for (const prebuildDir of listDirectories(path.join(rootPath, "node_modules", "node-pty", "prebuilds"))) {
56+
candidates.push({
57+
filePath: path.join(prebuildDir, "spawn-helper"),
58+
label: "node-pty spawn helper",
59+
});
60+
}
61+
62+
for (const packageDir of listDirectories(path.join(rootPath, "node_modules", "@openai"))) {
63+
if (!path.basename(packageDir).startsWith("codex-darwin-")) continue;
64+
for (const vendorDir of listDirectories(path.join(packageDir, "vendor"))) {
65+
candidates.push({
66+
filePath: path.join(vendorDir, "codex", "codex"),
67+
label: "Codex CLI binary",
68+
});
69+
candidates.push({
70+
filePath: path.join(vendorDir, "path", "rg"),
71+
label: "Codex ripgrep helper",
72+
});
73+
}
74+
}
75+
76+
for (const vendorDir of listDirectories(path.join(rootPath, "node_modules", "@anthropic-ai", "claude-agent-sdk", "vendor", "ripgrep"))) {
77+
candidates.push({
78+
filePath: path.join(vendorDir, "rg"),
79+
label: "Claude ripgrep helper",
80+
});
81+
}
82+
83+
return candidates;
84+
}
85+
86+
function normalizeDesktopRuntimeBinaries(rootPath) {
87+
const normalized = [];
88+
for (const candidate of collectDesktopRuntimeExecutableCandidates(rootPath)) {
89+
if (!pathExists(candidate.filePath)) continue;
90+
if (ensureExecutable(candidate.filePath)) {
91+
normalized.push(candidate);
92+
}
93+
}
94+
95+
const helperPathFiles = [
96+
path.join(rootPath, "node_modules", "node-pty", "lib", "unixTerminal.js"),
97+
path.join(rootPath, "node_modules", "node-pty", "src", "unixTerminal.ts"),
98+
];
99+
100+
for (const filePath of helperPathFiles) {
101+
if (!pathExists(filePath)) continue;
102+
const original = fs.readFileSync(filePath, "utf8");
103+
let updated = original;
104+
for (const patch of NODE_PTY_HELPER_PATH_PATCHES) {
105+
updated = updated.replace(patch.from, patch.to);
106+
}
107+
if (updated === original) continue;
108+
fs.writeFileSync(filePath, updated, "utf8");
109+
normalized.push({
110+
filePath,
111+
label: "node-pty helper path patch",
112+
});
113+
}
114+
115+
return normalized;
116+
}
117+
118+
function resolvePackagedRuntimeRoot(appBundlePath) {
119+
return path.join(appBundlePath, "Contents", "Resources", "app.asar.unpacked");
120+
}
121+
122+
module.exports = {
123+
normalizeDesktopRuntimeBinaries,
124+
resolvePackagedRuntimeRoot,
125+
};

0 commit comments

Comments
 (0)