From 3359b3b4044a7d55a98cd252b7a374ee5a8355ac Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Sat, 28 Feb 2026 01:31:13 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20macOS=20=E7=83=AD=E6=9B=B4=E6=96=B0=20co?= =?UTF-8?q?de=20signature=20=E6=A0=A1=E9=AA=8C=E5=A4=B1=E8=B4=A5=EF=BC=88?= =?UTF-8?q?=E6=94=B9=E7=94=A8=E9=80=90=E7=BB=84=E4=BB=B6=E7=AD=BE=E5=90=8D?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因: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 --- electron-builder.yml | 1 + scripts/after-pack.js | 32 ++------- scripts/after-sign.js | 154 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 28 deletions(-) create mode 100644 scripts/after-sign.js diff --git a/electron-builder.yml b/electron-builder.yml index ea92e17..092d9c0 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -40,6 +40,7 @@ extraResources: to: . filter: ["icon.*"] afterPack: scripts/after-pack.js +afterSign: scripts/after-sign.js asarUnpack: - "**/*.node" - "**/better-sqlite3/**" diff --git a/scripts/after-pack.js b/scripts/after-pack.js index 130e07f..26a0924 100644 --- a/scripts/after-pack.js +++ b/scripts/after-pack.js @@ -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. }; diff --git a/scripts/after-sign.js b/scripts/after-sign.js new file mode 100644 index 0000000..3e010ee --- /dev/null +++ b/scripts/after-sign.js @@ -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); + } +};