Skip to content

Commit e70e6ce

Browse files
committed
Add auto-signing for macOS stub binaries
- Automatically sign binaries after yao-pkg build on macOS - Check if binary is already signed before attempting to sign - Handle cases where codesign reports errors but signing succeeds - Filter yao-pkg signing warnings from output - Suppress codesign stderr messages about replacing signatures
1 parent dcde98c commit e70e6ce

File tree

1 file changed

+188
-8
lines changed

1 file changed

+188
-8
lines changed

scripts/build/build-stub.mjs

Lines changed: 188 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ import { mkdir } from 'node:fs/promises'
1212
import { dirname, join } from 'node:path'
1313
import { 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'
1616
import syncPatches from './stub/sync-yao-patches.mjs'
1717

1818
const __filename = fileURLToPath(import.meta.url)
1919
const __dirname = dirname(__filename)
2020
const ROOT_DIR = join(__dirname, '../..')
21-
const BUILD_DIR = join(ROOT_DIR, 'build')
2221
const STUB_DIR = join(ROOT_DIR, 'binaries', 'stub')
2322
const DIST_DIR = join(ROOT_DIR, 'dist')
2423
const 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
258433
if (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

262442
export default buildStub

0 commit comments

Comments
 (0)