@@ -12,13 +12,12 @@ import { mkdir } from 'node:fs/promises'
1212import { dirname , join } from 'node:path'
1313import { fileURLToPath } from 'node:url'
1414
15- import ensureCustomNodeInCache from './ensure-node-in-cache.mjs'
15+ import { default as ensureCustomNodeInCache } from './ensure-node-in-cache.mjs'
1616import syncPatches from './stub/sync-yao-patches.mjs'
1717
1818const __filename = fileURLToPath ( import . meta. url )
1919const __dirname = dirname ( __filename )
2020const ROOT_DIR = join ( __dirname , '../..' )
21- const BUILD_DIR = join ( ROOT_DIR , 'build' )
2221const STUB_DIR = join ( ROOT_DIR , 'binaries' , 'stub' )
2322const DIST_DIR = join ( ROOT_DIR , 'dist' )
2423const PKG_CONFIG = join ( ROOT_DIR , '.config' , 'pkg.json' )
@@ -114,8 +113,91 @@ export async function buildStub(options = {}) {
114113 const child = spawn ( 'pnpm' , pkgArgs , {
115114 cwd : ROOT_DIR ,
116115 env,
117- stdio : quiet ? 'pipe' : 'inherit '
116+ stdio : 'pipe'
118117 } )
118+
119+ // Filter out signing warnings on macOS since we handle signing ourselves
120+ const shouldFilterSigningWarnings = platform === 'darwin'
121+ let inSigningWarning = false
122+ let _signingWarningBuffer = [ ]
123+
124+ if ( ! quiet ) {
125+ if ( child . stdout ) {
126+ child . stdout . on ( 'data' , ( data ) => {
127+ const lines = data . toString ( ) . split ( '\n' )
128+ for ( let i = 0 ; i < lines . length ; i ++ ) {
129+ const line = lines [ i ]
130+
131+ // Check if this is signing-related content we want to filter
132+ if ( shouldFilterSigningWarnings ) {
133+ // Start of signing warning
134+ if ( ( line . includes ( 'Warning' ) && line . includes ( 'Unable to sign' ) ) ||
135+ ( line . includes ( 'Due to the mandatory code signing' ) ) ) {
136+ inSigningWarning = true
137+ _signingWarningBuffer = [ ]
138+ continue
139+ }
140+
141+ // Common signing-related lines to filter
142+ const signingPhrases = [
143+ 'executable is distributed to end users' ,
144+ 'Otherwise, it will be immediately killed' ,
145+ 'An ad-hoc signature is sufficient' ,
146+ 'To do that, run pkg on a Mac' ,
147+ 'and run "codesign --sign -' ,
148+ 'install "ldid" utility to PATH'
149+ ]
150+
151+ if ( signingPhrases . some ( phrase => line . includes ( phrase ) ) ) {
152+ inSigningWarning = true
153+ continue
154+ }
155+
156+ // If we're in a signing warning, buffer lines
157+ if ( inSigningWarning ) {
158+ if ( line . trim ( ) === '' ) {
159+ // Empty line might end the warning
160+ inSigningWarning = false
161+ signingWarningBuffer = [ ]
162+ } else if ( line . startsWith ( '>' ) || line . startsWith ( '[' ) || line . includes ( '✅' ) || line . includes ( '📦' ) ) {
163+ // New content started, warning is over
164+ inSigningWarning = false
165+ signingWarningBuffer = [ ]
166+ process . stdout . write ( line + ( i < lines . length - 1 ? '\n' : '' ) )
167+ }
168+ continue
169+ }
170+ }
171+
172+ // Output non-filtered lines
173+ if ( i < lines . length - 1 || line !== '' ) {
174+ process . stdout . write ( line + ( i < lines . length - 1 ? '\n' : '' ) )
175+ }
176+ }
177+ } )
178+ }
179+
180+ if ( child . stderr ) {
181+ child . stderr . on ( 'data' , ( data ) => {
182+ const text = data . toString ( )
183+ // Filter out codesign errors that we handle ourselves
184+ if ( shouldFilterSigningWarnings ) {
185+ const signingErrors = [
186+ 'replacing existing signature' ,
187+ 'internal error in Code Signing subsystem' ,
188+ 'code object is not signed at all'
189+ ]
190+ // Check if this stderr contains signing-related errors we want to suppress
191+ if ( signingErrors . some ( err => text . includes ( err ) ) ) {
192+ // Don't output these errors
193+ return
194+ }
195+ }
196+ process . stderr . write ( data )
197+ } )
198+ }
199+ }
200+
119201 child . on ( 'exit' , ( code ) => resolve ( code || 0 ) )
120202 child . on ( 'error' , ( ) => resolve ( 1 ) )
121203 } )
@@ -125,7 +207,19 @@ export async function buildStub(options = {}) {
125207 return 1
126208 }
127209
128- // Step 5: Verify and report
210+ // Step 5: Sign the binary on macOS
211+ if ( platform === 'darwin' && existsSync ( outputPath ) ) {
212+ console . log ( '🔏 Signing macOS binary...' )
213+ const signExitCode = await signMacOSBinary ( outputPath , quiet )
214+ if ( signExitCode !== 0 ) {
215+ console . error ( '⚠️ Warning: Failed to sign macOS binary' )
216+ console . error ( ' The binary may not run properly without signing' )
217+ } else {
218+ console . log ( '✅ Binary signed successfully\n' )
219+ }
220+ }
221+
222+ // Step 6: Verify and report
129223 if ( existsSync ( outputPath ) ) {
130224 const { stat } = await import ( 'node:fs/promises' )
131225 const stats = await stat ( outputPath )
@@ -145,6 +239,84 @@ export async function buildStub(options = {}) {
145239 return 0
146240}
147241
242+ /**
243+ * Sign macOS binary using codesign
244+ */
245+ async function signMacOSBinary ( binaryPath , quiet = false ) {
246+ // First check if already signed
247+ const checkSigned = await new Promise ( ( resolve ) => {
248+ const child = spawn ( 'codesign' , [ '-dv' , binaryPath ] , {
249+ stdio : 'pipe'
250+ } )
251+
252+ let stderr = ''
253+ child . stderr . on ( 'data' , ( data ) => {
254+ stderr += data . toString ( )
255+ } )
256+
257+ child . on ( 'exit' , ( code ) => {
258+ // Exit code 0 means it's already signed
259+ resolve ( { signed : code === 0 , output : stderr } )
260+ } )
261+ child . on ( 'error' , ( ) => resolve ( { signed : false , output : '' } ) )
262+ } )
263+
264+ if ( checkSigned . signed ) {
265+ if ( ! quiet ) {
266+ console . log ( ' Binary is already signed' )
267+ }
268+ return 0
269+ }
270+
271+ // Sign the binary
272+ return new Promise ( ( resolve ) => {
273+ const child = spawn ( 'codesign' , [ '--sign' , '-' , '--force' , binaryPath ] , {
274+ // Always pipe to prevent stderr leakage
275+ stdio : 'pipe'
276+ } )
277+
278+ let stderr = ''
279+ if ( child . stderr ) {
280+ child . stderr . on ( 'data' , ( data ) => {
281+ stderr += data . toString ( )
282+ } )
283+ }
284+
285+ child . on ( 'exit' , ( code ) => {
286+ // Even if codesign reports an error, verify if the binary got signed
287+ if ( code !== 0 ) {
288+ // Check again if it's signed despite the error
289+ const verifyChild = spawn ( 'codesign' , [ '-dv' , binaryPath ] , {
290+ stdio : 'pipe'
291+ } )
292+
293+ verifyChild . on ( 'exit' , ( verifyCode ) => {
294+ if ( verifyCode === 0 ) {
295+ // Binary is signed despite the error
296+ resolve ( 0 )
297+ } else {
298+ // Only show error if not quiet and signing actually failed
299+ if ( ! quiet && stderr && ! stderr . includes ( 'replacing existing signature' ) ) {
300+ console . error ( ` codesign output: ${ stderr } ` )
301+ }
302+ resolve ( code )
303+ }
304+ } )
305+ verifyChild . on ( 'error' , ( ) => resolve ( code ) )
306+ } else {
307+ resolve ( 0 )
308+ }
309+ } )
310+
311+ child . on ( 'error' , ( error ) => {
312+ if ( ! quiet ) {
313+ console . error ( ` codesign error: ${ error . message } ` )
314+ }
315+ resolve ( 1 )
316+ } )
317+ } )
318+ }
319+
148320/**
149321 * Get pkg target string
150322 */
@@ -242,21 +414,29 @@ async function main() {
242414
243415 if ( options . help ) {
244416 showHelp ( )
245- process . exit ( 0 )
417+ return 0
246418 }
247419
248420 try {
249421 const exitCode = await buildStub ( options )
250- process . exit ( exitCode )
422+ if ( exitCode !== 0 ) {
423+ throw new Error ( `Build failed with exit code ${ exitCode } ` )
424+ }
425+ return 0
251426 } catch ( error ) {
252427 console . error ( '❌ Build failed:' , error . message )
253- process . exit ( 1 )
428+ throw error
254429 }
255430}
256431
257432// Run if called directly
258433if ( process . argv [ 1 ] === fileURLToPath ( import . meta. url ) ) {
259- main ( ) . catch ( console . error )
434+ main ( ) . then ( exitCode => {
435+ process . exitCode = exitCode || 0
436+ } ) . catch ( error => {
437+ console . error ( error )
438+ process . exitCode = 1
439+ } )
260440}
261441
262442export default buildStub
0 commit comments