根因: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>
155 lines
5.6 KiB
JavaScript
155 lines
5.6 KiB
JavaScript
/* 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);
|
|
}
|
|
};
|