mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
162 lines
5.5 KiB
JavaScript
162 lines
5.5 KiB
JavaScript
import { execFileSync } from 'node:child_process'
|
|
import { createRequire } from 'node:module'
|
|
import { join, resolve } from 'node:path'
|
|
|
|
const ARCHITECTURES = [1, 3] // electron-builder: x64, arm64.
|
|
|
|
export async function buildWithOverlappingNotarization({
|
|
config,
|
|
build,
|
|
notarize,
|
|
verify,
|
|
targets,
|
|
publish,
|
|
credentials,
|
|
log = console.log
|
|
}) {
|
|
if (!config.forceCodeSigning || !config.mac?.hardenedRuntime || config.mac.notarize !== true) {
|
|
throw new Error('Overlapping notarization requires the complete macOS release signing policy')
|
|
}
|
|
for (const hook of ['afterSign', 'artifactBuildStarted']) {
|
|
if (config[hook] !== undefined && typeof config[hook] !== 'function') {
|
|
throw new Error(`Unsupported ${hook} hook: expected a function`)
|
|
}
|
|
}
|
|
const tasks = new Map()
|
|
const paths = new Set()
|
|
let aborted = false
|
|
let resolvePrepared
|
|
let rejectPrepared
|
|
const prepared = new Promise((resolve, reject) => {
|
|
resolvePrepared = resolve
|
|
rejectPrepared = reject
|
|
})
|
|
// The first target can be waiting while the second architecture is still signing.
|
|
prepared.catch(() => {})
|
|
const waitForNotarization = async () => {
|
|
await prepared
|
|
const results = await Promise.all(tasks.values())
|
|
const failure = results.find((result) => 'error' in result)
|
|
if (failure) {
|
|
throw failure.error
|
|
}
|
|
if (aborted) {
|
|
throw new Error('macOS packaging was aborted')
|
|
}
|
|
}
|
|
try {
|
|
const artifacts = await build({
|
|
targets,
|
|
publish,
|
|
config: {
|
|
...config,
|
|
// Only this invocation replaces builder's blocking notary wait with the guarded hooks.
|
|
mac: { ...config.mac, notarize: false },
|
|
concurrency: { ...config.concurrency, jobs: 1 },
|
|
afterSign: async (context) => {
|
|
await config.afterSign?.(context)
|
|
if (context.electronPlatformName !== 'darwin' || !ARCHITECTURES.includes(context.arch)) {
|
|
throw new Error('Unexpected platform or architecture in macOS signing hook')
|
|
}
|
|
const appPath = join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`)
|
|
if (tasks.has(context.arch) || paths.has(appPath)) {
|
|
throw new Error('macOS architectures must have separate, freshly signed app bundles')
|
|
}
|
|
paths.add(appPath)
|
|
const task = Promise.resolve()
|
|
.then(async () => {
|
|
log(`[mac-notary] submitting ${appPath}`)
|
|
await notarize({ appPath, ...credentials })
|
|
await verify(appPath)
|
|
log(`[mac-notary] accepted, stapled and verified ${appPath}`)
|
|
return {}
|
|
})
|
|
.catch((error) => {
|
|
rejectPrepared(error)
|
|
return { error }
|
|
})
|
|
tasks.set(context.arch, task)
|
|
if (tasks.size === ARCHITECTURES.length) {
|
|
resolvePrepared()
|
|
}
|
|
},
|
|
artifactBuildStarted: async (event) => {
|
|
if (!ARCHITECTURES.includes(event.arch)) {
|
|
throw new Error('Unexpected architecture in macOS artifact hook')
|
|
}
|
|
await waitForNotarization()
|
|
await config.artifactBuildStarted?.(event)
|
|
}
|
|
}
|
|
})
|
|
if (tasks.size !== ARCHITECTURES.length) {
|
|
throw new Error('Both macOS architectures must be signed before artifacts can succeed')
|
|
}
|
|
await waitForNotarization()
|
|
return artifacts
|
|
} catch (error) {
|
|
aborted = true
|
|
rejectPrepared(error)
|
|
throw error
|
|
} finally {
|
|
// Drain outstanding notary work before a failed invocation returns to retry.
|
|
await Promise.all(tasks.values())
|
|
}
|
|
}
|
|
|
|
export async function main(args = process.argv.slice(2)) {
|
|
if (process.platform !== 'darwin') {
|
|
throw new Error('macOS release packaging requires macOS')
|
|
}
|
|
const publish = args[0] === '--publish' && ['always', 'never'].includes(args[1]) ? args[1] : null
|
|
if (!publish || args.slice(2).some((arg) => arg !== '--draft') || args.length > 3) {
|
|
throw new Error('Usage: build-mac-notarization-overlap.mjs --publish always|never [--draft]')
|
|
}
|
|
const require = createRequire(import.meta.url)
|
|
const { build, Platform, Arch } = require('electron-builder')
|
|
// Use the same pinned notarization implementation as electron-builder, including its retries.
|
|
const builderRequire = createRequire(require.resolve('electron-builder'))
|
|
const { notarize } = createRequire(builderRequire.resolve('app-builder-lib'))(
|
|
'@electron/notarize'
|
|
)
|
|
const config = require('../electron-builder.config.cjs')
|
|
if (args.includes('--draft')) {
|
|
config.publish = { ...config.publish, releaseType: 'draft' }
|
|
}
|
|
const credentials = {
|
|
appleId: process.env.APPLE_ID,
|
|
appleIdPassword: process.env.APPLE_APP_SPECIFIC_PASSWORD,
|
|
teamId: process.env.APPLE_TEAM_ID
|
|
}
|
|
if (Object.values(credentials).some((value) => !value?.trim())) {
|
|
throw new Error('macOS notarization credentials are incomplete')
|
|
}
|
|
return buildWithOverlappingNotarization({
|
|
config,
|
|
build,
|
|
notarize,
|
|
verify: (appPath) => {
|
|
execFileSync('codesign', ['--verify', '--deep', '--strict', appPath], { stdio: 'inherit' })
|
|
execFileSync('xcrun', ['stapler', 'validate', appPath], { stdio: 'inherit' })
|
|
},
|
|
targets: new Map([
|
|
[
|
|
Platform.MAC,
|
|
new Map([
|
|
[Arch.x64, ['dmg', 'zip']],
|
|
[Arch.arm64, ['dmg', 'zip']]
|
|
])
|
|
]
|
|
]),
|
|
publish,
|
|
credentials
|
|
})
|
|
}
|
|
|
|
if (process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename)) {
|
|
main().catch((error) => {
|
|
console.error(error)
|
|
process.exitCode = 1
|
|
})
|
|
}
|