Skip to content

Commit 3359b3b

Browse files
7418claude
andcommitted
fix: macOS 热更新 code signature 校验失败(改用逐组件签名)
根因:afterPack 中使用 codesign --force --deep -s - 签名,但 --deep 标志 不可靠,无法保证正确的签名顺序,导致 Electron 内嵌的 helper apps、 frameworks、.node 原生模块未被正确签名,ShipIt 在 kSecCSStrictValidate 严格校验时失败。 修复方案: - scripts/after-pack.js: 移除 Step 4 的 --deep 签名 - scripts/after-sign.js (新增): afterSign 钩子中从内到外逐个签名: 1. 所有 .node/.dylib/.so 原生二进制 2. 所有 .framework 目录 3. 所有 Electron Helper .app 4. 主 .app bundle 签名完成后执行 codesign --verify --strict 验证 - electron-builder.yml: 添加 afterSign 钩子引用 使用 afterSign 而非 afterPack 的原因:afterSign 在 electron-builder 自身 签名步骤之后执行(CSC_IDENTITY_AUTO_DISCOVERY=false 时为 no-op),确保 ad-hoc 签名是创建 DMG/ZIP 产物前的最后一步,不会被后续处理覆盖或失效。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 80b0fc9 commit 3359b3b

3 files changed

Lines changed: 159 additions & 28 deletions

File tree

electron-builder.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ extraResources:
4040
to: .
4141
filter: ["icon.*"]
4242
afterPack: scripts/after-pack.js
43+
afterSign: scripts/after-sign.js
4344
asarUnpack:
4445
- "**/*.node"
4546
- "**/better-sqlite3/**"

scripts/after-pack.js

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -119,32 +119,8 @@ module.exports = async function afterPack(context) {
119119
}
120120
}
121121

122-
// Step 4: Ad-hoc code sign on macOS for auto-update compatibility.
123-
// electron-updater's ShipIt process validates code signatures when applying
124-
// updates. Without at least an ad-hoc signature, the update fails with:
125-
// "Code signature did not pass validation: 代码未能满足指定的代码要求"
126-
// Ad-hoc signing (codesign -s -) creates a valid local signature without
127-
// requiring an Apple Developer certificate.
128-
// This runs AFTER all file modifications (better-sqlite3 replacement above)
129-
// so the signature covers the final app state. If a real certificate is
130-
// available, electron-builder's signing step will override this.
131-
if (platform === 'mac') {
132-
const appName = context.packager.appInfo.productFilename;
133-
const appPath = path.join(appOutDir, `${appName}.app`);
134-
135-
if (fs.existsSync(appPath)) {
136-
console.log(`[afterPack] Ad-hoc signing ${appPath} for auto-update compatibility...`);
137-
try {
138-
execSync(`codesign --force --deep -s - "${appPath}"`, {
139-
stdio: 'inherit',
140-
timeout: 120000,
141-
});
142-
console.log('[afterPack] Ad-hoc signing completed successfully');
143-
} catch (err) {
144-
console.warn('[afterPack] Ad-hoc signing failed (non-fatal):', err.message);
145-
}
146-
} else {
147-
console.warn(`[afterPack] macOS app not found at ${appPath}, skipping ad-hoc signing`);
148-
}
149-
}
122+
// Note: Ad-hoc code signing moved to scripts/after-sign.js (afterSign hook).
123+
// afterSign runs after electron-builder's own signing step (which is a no-op
124+
// with CSC_IDENTITY_AUTO_DISCOVERY=false), ensuring the signature is the last
125+
// modification before DMG/ZIP creation.
150126
};

scripts/after-sign.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/* eslint-disable @typescript-eslint/no-require-imports */
2+
/**
3+
* electron-builder afterSign hook — ad-hoc code signing for macOS.
4+
*
5+
* electron-updater's ShipIt process validates code signatures when applying
6+
* updates. Without a valid signature the update fails with:
7+
* "Code signature did not pass validation: 代码未能满足指定的代码要求"
8+
*
9+
* The previous approach used `codesign --force --deep -s -`, but --deep is
10+
* unreliable: it does not guarantee correct signing order and may miss nested
11+
* components, causing kSecCSStrictValidate failures.
12+
*
13+
* This script signs each component individually from the inside out:
14+
* 1. All native binaries (.node, .dylib, .so)
15+
* 2. All Frameworks (*.framework)
16+
* 3. All Helper apps (*.app inside Frameworks/)
17+
* 4. The main .app bundle
18+
*
19+
* Runs in the afterSign hook so it executes AFTER electron-builder's own
20+
* signing step (which is a no-op with CSC_IDENTITY_AUTO_DISCOVERY=false)
21+
* and right before DMG/ZIP artifact creation.
22+
*/
23+
const fs = require('fs');
24+
const path = require('path');
25+
const { execSync } = require('child_process');
26+
27+
/**
28+
* Ad-hoc sign a single path. Failures are logged but non-fatal to avoid
29+
* breaking builds on edge-case binaries (e.g. debug symbols).
30+
*/
31+
function codesign(targetPath) {
32+
try {
33+
execSync(`codesign --force --sign - "${targetPath}"`, {
34+
stdio: 'pipe',
35+
timeout: 30000,
36+
});
37+
} catch (err) {
38+
console.warn(`[afterSign] Failed to sign ${targetPath}: ${err.message}`);
39+
}
40+
}
41+
42+
/**
43+
* Recursively collect all files matching the given extensions.
44+
*/
45+
function collectFiles(dir, extensions) {
46+
const results = [];
47+
if (!fs.existsSync(dir)) return results;
48+
49+
const entries = fs.readdirSync(dir, { withFileTypes: true });
50+
for (const entry of entries) {
51+
const fullPath = path.join(dir, entry.name);
52+
if (entry.isDirectory()) {
53+
// Don't descend into .app or .framework bundles — they are signed as a unit
54+
if (entry.name.endsWith('.app') || entry.name.endsWith('.framework')) {
55+
continue;
56+
}
57+
results.push(...collectFiles(fullPath, extensions));
58+
} else if (entry.isFile()) {
59+
const ext = path.extname(entry.name);
60+
if (extensions.includes(ext)) {
61+
results.push(fullPath);
62+
}
63+
}
64+
}
65+
return results;
66+
}
67+
68+
/**
69+
* Collect bundle directories (.app, .framework) at a given depth.
70+
*/
71+
function collectBundles(dir, extension) {
72+
const results = [];
73+
if (!fs.existsSync(dir)) return results;
74+
75+
const entries = fs.readdirSync(dir, { withFileTypes: true });
76+
for (const entry of entries) {
77+
if (entry.isDirectory() && entry.name.endsWith(extension)) {
78+
results.push(path.join(dir, entry.name));
79+
}
80+
}
81+
return results;
82+
}
83+
84+
module.exports = async function afterSign(context) {
85+
const platform = context.packager.platform.name;
86+
if (platform !== 'mac') return;
87+
88+
const appOutDir = context.appOutDir;
89+
const appName = context.packager.appInfo.productFilename;
90+
const appPath = path.join(appOutDir, `${appName}.app`);
91+
92+
if (!fs.existsSync(appPath)) {
93+
console.warn(`[afterSign] macOS app not found at ${appPath}, skipping ad-hoc signing`);
94+
return;
95+
}
96+
97+
console.log(`[afterSign] Ad-hoc signing ${appPath} (individual component signing)...`);
98+
99+
const contentsPath = path.join(appPath, 'Contents');
100+
const frameworksPath = path.join(contentsPath, 'Frameworks');
101+
let signed = 0;
102+
103+
// ── Step 1: Sign all native binaries (.node, .dylib, .so) ─────────────
104+
// These are the innermost signable items. Must be signed before their
105+
// enclosing bundles.
106+
const nativeBinaries = collectFiles(contentsPath, ['.node', '.dylib', '.so']);
107+
for (const bin of nativeBinaries) {
108+
codesign(bin);
109+
signed++;
110+
}
111+
if (nativeBinaries.length > 0) {
112+
console.log(`[afterSign] Signed ${nativeBinaries.length} native binaries (.node/.dylib/.so)`);
113+
}
114+
115+
// ── Step 2: Sign all Frameworks ───────────────────────────────────────
116+
// Frameworks contain nested code that was already signed in step 1 (if any
117+
// .dylib/.so lived outside the framework) or that --sign covers here.
118+
const frameworks = collectBundles(frameworksPath, '.framework');
119+
for (const fw of frameworks) {
120+
codesign(fw);
121+
signed++;
122+
}
123+
if (frameworks.length > 0) {
124+
console.log(`[afterSign] Signed ${frameworks.length} frameworks`);
125+
}
126+
127+
// ── Step 3: Sign all Helper apps ──────────────────────────────────────
128+
// Electron ships multiple helper apps (GPU, Plugin, Renderer, etc.)
129+
const helperApps = collectBundles(frameworksPath, '.app');
130+
for (const helper of helperApps) {
131+
codesign(helper);
132+
signed++;
133+
}
134+
if (helperApps.length > 0) {
135+
console.log(`[afterSign] Signed ${helperApps.length} helper apps`);
136+
}
137+
138+
// ── Step 4: Sign the main app bundle ──────────────────────────────────
139+
codesign(appPath);
140+
signed++;
141+
142+
console.log(`[afterSign] Ad-hoc signing complete — ${signed} components signed`);
143+
144+
// ── Verify ────────────────────────────────────────────────────────────
145+
try {
146+
execSync(`codesign --verify --strict "${appPath}"`, {
147+
stdio: 'pipe',
148+
timeout: 30000,
149+
});
150+
console.log('[afterSign] Signature verification passed (--strict)');
151+
} catch (err) {
152+
console.error('[afterSign] WARNING: Signature verification FAILED:', err.stderr?.toString() || err.message);
153+
}
154+
};

0 commit comments

Comments
 (0)