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>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-02-28 01:31:13 +08:00
1 parent 80b0fc9509
commit 3359b3b404
3 files changed
+159 -28

No files matched your search

+4 -28
View File
@@ -119,32 +119,8 @@ module.exports = async function afterPack(context) {
}
}
// Step 4: Ad-hoc code sign on macOS for auto-update compatibility.
// electron-updater's ShipIt process validates code signatures when applying
// updates. Without at least an ad-hoc signature, the update fails with:
// "Code signature did not pass validation: 代码未能满足指定的代码要求"
// Ad-hoc signing (codesign -s -) creates a valid local signature without
// requiring an Apple Developer certificate.
// This runs AFTER all file modifications (better-sqlite3 replacement above)
// so the signature covers the final app state. If a real certificate is
// available, electron-builder's signing step will override this.
if (platform === 'mac') {
const appName = context.packager.appInfo.productFilename;
const appPath = path.join(appOutDir, `${appName}.app`);
if (fs.existsSync(appPath)) {
console.log(`[afterPack] Ad-hoc signing ${appPath} for auto-update compatibility...`);
try {
execSync(`codesign --force --deep -s - "${appPath}"`, {
stdio: 'inherit',
timeout: 120000,
});
console.log('[afterPack] Ad-hoc signing completed successfully');
} catch (err) {
console.warn('[afterPack] Ad-hoc signing failed (non-fatal):', err.message);
}
} else {
console.warn(`[afterPack] macOS app not found at ${appPath}, skipping ad-hoc signing`);
}
}
// Note: Ad-hoc code signing moved to scripts/after-sign.js (afterSign hook).
// afterSign runs after electron-builder's own signing step (which is a no-op
// with CSC_IDENTITY_AUTO_DISCOVERY=false), ensuring the signature is the last
// modification before DMG/ZIP creation.
};
+154
View File
@@ -0,0 +1,154 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/**
* electron-builder afterSign hook — ad-hoc code signing for macOS.
*
* electron-updater's ShipIt process validates code signatures when applying
* updates. Without a valid signature the update fails with:
* "Code signature did not pass validation: 代码未能满足指定的代码要求"
*
* The previous approach used `codesign --force --deep -s -`, but --deep is
* unreliable: it does not guarantee correct signing order and may miss nested
* components, causing kSecCSStrictValidate failures.
*
* This script signs each component individually from the inside out:
* 1. All native binaries (.node, .dylib, .so)
* 2. All Frameworks (*.framework)
* 3. All Helper apps (*.app inside Frameworks/)
* 4. The main .app bundle
*
* Runs in the afterSign hook so it executes AFTER electron-builder's own
* signing step (which is a no-op with CSC_IDENTITY_AUTO_DISCOVERY=false)
* and right before DMG/ZIP artifact creation.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
/**
* Ad-hoc sign a single path. Failures are logged but non-fatal to avoid
* breaking builds on edge-case binaries (e.g. debug symbols).
*/
function codesign(targetPath) {
try {
execSync(`codesign --force --sign - "${targetPath}"`, {
stdio: 'pipe',
timeout: 30000,
});
} catch (err) {
console.warn(`[afterSign] Failed to sign ${targetPath}: ${err.message}`);
}
}
/**
* Recursively collect all files matching the given extensions.
*/
function collectFiles(dir, extensions) {
const results = [];
if (!fs.existsSync(dir)) return results;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
// Don't descend into .app or .framework bundles — they are signed as a unit
if (entry.name.endsWith('.app') || entry.name.endsWith('.framework')) {
continue;
}
results.push(...collectFiles(fullPath, extensions));
} else if (entry.isFile()) {
const ext = path.extname(entry.name);
if (extensions.includes(ext)) {
results.push(fullPath);
}
}
}
return results;
}
/**
* Collect bundle directories (.app, .framework) at a given depth.
*/
function collectBundles(dir, extension) {
const results = [];
if (!fs.existsSync(dir)) return results;
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.endsWith(extension)) {
results.push(path.join(dir, entry.name));
}
}
return results;
}
module.exports = async function afterSign(context) {
const platform = context.packager.platform.name;
if (platform !== 'mac') return;
const appOutDir = context.appOutDir;
const appName = context.packager.appInfo.productFilename;
const appPath = path.join(appOutDir, `${appName}.app`);
if (!fs.existsSync(appPath)) {
console.warn(`[afterSign] macOS app not found at ${appPath}, skipping ad-hoc signing`);
return;
}
console.log(`[afterSign] Ad-hoc signing ${appPath} (individual component signing)...`);
const contentsPath = path.join(appPath, 'Contents');
const frameworksPath = path.join(contentsPath, 'Frameworks');
let signed = 0;
// ── Step 1: Sign all native binaries (.node, .dylib, .so) ─────────────
// These are the innermost signable items. Must be signed before their
// enclosing bundles.
const nativeBinaries = collectFiles(contentsPath, ['.node', '.dylib', '.so']);
for (const bin of nativeBinaries) {
codesign(bin);
signed++;
}
if (nativeBinaries.length > 0) {
console.log(`[afterSign] Signed ${nativeBinaries.length} native binaries (.node/.dylib/.so)`);
}
// ── Step 2: Sign all Frameworks ───────────────────────────────────────
// Frameworks contain nested code that was already signed in step 1 (if any
// .dylib/.so lived outside the framework) or that --sign covers here.
const frameworks = collectBundles(frameworksPath, '.framework');
for (const fw of frameworks) {
codesign(fw);
signed++;
}
if (frameworks.length > 0) {
console.log(`[afterSign] Signed ${frameworks.length} frameworks`);
}
// ── Step 3: Sign all Helper apps ──────────────────────────────────────
// Electron ships multiple helper apps (GPU, Plugin, Renderer, etc.)
const helperApps = collectBundles(frameworksPath, '.app');
for (const helper of helperApps) {
codesign(helper);
signed++;
}
if (helperApps.length > 0) {
console.log(`[afterSign] Signed ${helperApps.length} helper apps`);
}
// ── Step 4: Sign the main app bundle ──────────────────────────────────
codesign(appPath);
signed++;
console.log(`[afterSign] Ad-hoc signing complete — ${signed} components signed`);
// ── Verify ────────────────────────────────────────────────────────────
try {
execSync(`codesign --verify --strict "${appPath}"`, {
stdio: 'pipe',
timeout: 30000,
});
console.log('[afterSign] Signature verification passed (--strict)');
} catch (err) {
console.error('[afterSign] WARNING: Signature verification FAILED:', err.stderr?.toString() || err.message);
}
};