11/* eslint-disable @typescript-eslint/no-require-imports */
22/**
3- * electron-builder afterSign hook — ad-hoc code signing for macOS.
3+ * electron-builder afterSign hook — code signing for macOS.
44 *
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: 代码未能满足指定的代码要求"
5+ * When a real Developer ID certificate is available (CSC_LINK or CSC_NAME env
6+ * vars are set), electron-builder handles signing automatically. This hook only
7+ * runs a strict verification to confirm the signature is intact.
88 *
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 .
9+ * When no certificate is available (local dev builds), falls back to ad-hoc
10+ * signing so that electron-updater's ShipIt process can still validate the
11+ * code signature .
1212 *
13- * This script signs each component individually from the inside out:
13+ * Ad-hoc signing order ( inside- out) :
1414 * 1. All native binaries (.node, .dylib, .so)
1515 * 2. All Frameworks (*.framework)
1616 * 3. All Helper apps (*.app inside Frameworks/)
1717 * 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.
2218 */
2319const fs = require ( 'fs' ) ;
2420const path = require ( 'path' ) ;
@@ -90,19 +86,55 @@ module.exports = async function afterSign(context) {
9086 const appPath = path . join ( appOutDir , `${ appName } .app` ) ;
9187
9288 if ( ! fs . existsSync ( appPath ) ) {
93- console . warn ( `[afterSign] macOS app not found at ${ appPath } , skipping ad-hoc signing` ) ;
89+ console . warn ( `[afterSign] macOS app not found at ${ appPath } , skipping` ) ;
90+ return ;
91+ }
92+
93+ // ── Detect real (non-ad-hoc) code signature ───────────────────────────
94+ // Check env vars first (CI path), then probe the actual signature on the
95+ // .app bundle (covers the case where electron-builder auto-discovered a
96+ // Developer ID certificate from the local Keychain).
97+ let hasRealSignature = ! ! ( process . env . CSC_LINK || process . env . CSC_NAME ) ;
98+
99+ if ( ! hasRealSignature ) {
100+ try {
101+ const info = execSync ( `codesign -d --verbose=2 "${ appPath } " 2>&1` , {
102+ stdio : 'pipe' ,
103+ timeout : 15000 ,
104+ encoding : 'utf-8' ,
105+ } ) ;
106+ if ( / A u t h o r i t y = D e v e l o p e r I D A p p l i c a t i o n / . test ( info ) ) {
107+ hasRealSignature = true ;
108+ }
109+ } catch {
110+ // codesign -d fails if the bundle is unsigned — that's fine
111+ }
112+ }
113+
114+ if ( hasRealSignature ) {
115+ console . log ( '[afterSign] Real code signing certificate detected (CSC_LINK/CSC_NAME set or Developer ID signature found).' ) ;
116+ console . log ( '[afterSign] Skipping ad-hoc signing to preserve Developer ID signature.' ) ;
117+
118+ try {
119+ execSync ( `codesign --verify --deep --strict --verbose=4 "${ appPath } "` , {
120+ stdio : 'pipe' ,
121+ timeout : 60000 ,
122+ } ) ;
123+ console . log ( '[afterSign] Developer ID signature verification passed.' ) ;
124+ } catch ( err ) {
125+ console . error ( '[afterSign] WARNING: Developer ID signature verification FAILED:' , err . stderr ?. toString ( ) || err . message ) ;
126+ }
94127 return ;
95128 }
96129
130+ // ── No certificate — ad-hoc signing fallback ─────────────────────────
97131 console . log ( `[afterSign] Ad-hoc signing ${ appPath } (individual component signing)...` ) ;
98132
99133 const contentsPath = path . join ( appPath , 'Contents' ) ;
100134 const frameworksPath = path . join ( contentsPath , 'Frameworks' ) ;
101135 let signed = 0 ;
102136
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.
137+ // Step 1: Sign all native binaries (.node, .dylib, .so)
106138 const nativeBinaries = collectFiles ( contentsPath , [ '.node' , '.dylib' , '.so' ] ) ;
107139 for ( const bin of nativeBinaries ) {
108140 codesign ( bin ) ;
@@ -112,9 +144,7 @@ module.exports = async function afterSign(context) {
112144 console . log ( `[afterSign] Signed ${ nativeBinaries . length } native binaries (.node/.dylib/.so)` ) ;
113145 }
114146
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.
147+ // Step 2: Sign all Frameworks
118148 const frameworks = collectBundles ( frameworksPath , '.framework' ) ;
119149 for ( const fw of frameworks ) {
120150 codesign ( fw ) ;
@@ -124,8 +154,7 @@ module.exports = async function afterSign(context) {
124154 console . log ( `[afterSign] Signed ${ frameworks . length } frameworks` ) ;
125155 }
126156
127- // ── Step 3: Sign all Helper apps ──────────────────────────────────────
128- // Electron ships multiple helper apps (GPU, Plugin, Renderer, etc.)
157+ // Step 3: Sign all Helper apps
129158 const helperApps = collectBundles ( frameworksPath , '.app' ) ;
130159 for ( const helper of helperApps ) {
131160 codesign ( helper ) ;
@@ -135,19 +164,19 @@ module.exports = async function afterSign(context) {
135164 console . log ( `[afterSign] Signed ${ helperApps . length } helper apps` ) ;
136165 }
137166
138- // ── Step 4: Sign the main app bundle ──────────────────────────────────
167+ // Step 4: Sign the main app bundle
139168 codesign ( appPath ) ;
140169 signed ++ ;
141170
142171 console . log ( `[afterSign] Ad-hoc signing complete — ${ signed } components signed` ) ;
143172
144- // ── Verify ────────────────────────────────────────────────────────────
173+ // Verify
145174 try {
146- execSync ( `codesign --verify --strict "${ appPath } "` , {
175+ execSync ( `codesign --verify --deep -- strict "${ appPath } "` , {
147176 stdio : 'pipe' ,
148177 timeout : 30000 ,
149178 } ) ;
150- console . log ( '[afterSign] Signature verification passed (--strict)' ) ;
179+ console . log ( '[afterSign] Signature verification passed (--deep -- strict)' ) ;
151180 } catch ( err ) {
152181 console . error ( '[afterSign] WARNING: Signature verification FAILED:' , err . stderr ?. toString ( ) || err . message ) ;
153182 }
0 commit comments