From 0b32df57f0a5e2c939fb52e62dc1b3a9274e88fc Mon Sep 17 00:00:00 2001 From: CRT Date: Sun, 18 Oct 2026 02:43:59 +0800 Subject: [PATCH] desktop-release: tauri only signs; the app is notarized with notarytool directly (up to 3 tries, later ones without S3 acceleration) and stapled, because the upload inside tauri bundle was cut off twice for 0.8.12 with no retry --- tools/desktop-release.ts | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/tools/desktop-release.ts b/tools/desktop-release.ts index 73ac7ab..e7cc300 100644 --- a/tools/desktop-release.ts +++ b/tools/desktop-release.ts @@ -38,7 +38,7 @@ import { spawnSync, type SpawnSyncOptions } from 'node:child_process'; import { createHash } from 'node:crypto'; import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs'; import { homedir, release as osRelease, tmpdir } from 'node:os'; -import { basename, join, resolve } from 'node:path'; +import { basename, dirname, join, resolve } from 'node:path'; import { type Download, type DownloadKey, type Manifest, manifestProblems } from '../cli/lib/desktop-dist'; import { verify as verifySig } from '../cli/lib/minisign'; import { buildOne, OUT_DIR as SIDECAR_DIR } from './build-sidecars'; @@ -170,6 +170,22 @@ function run(cmd: string[], opts: SpawnSyncOptions & { quiet?: boolean; allowFai /** A command in the background (notarytool waiting for Apple, the boot check) while this process checks other * artifacts. Output is collected; the caller decides what a failure means. */ +/** notarytool submit --wait with up to 3 tries when the upload is cut off before Apple has it (abortedUpload, a reset + * connection); the later tries without S3 transfer acceleration. Returns notarytool's status or the last error. */ +async function notarizeWithRetry(file: string, n: Record, what: string): Promise { + let last = ''; + for (let tries = 1; tries <= 3; tries++) { + const args = ['xcrun', 'notarytool', 'submit', file, '--key', n.APPLE_API_KEY_PATH!, '--key-id', n.APPLE_API_KEY!, '--issuer', n.APPLE_API_ISSUER!, ...(tries > 1 ? ['--no-s3-acceleration'] : []), '--wait', '--output-format', 'json']; + const r = await runAsync(args, { timeoutMs: 3_600_000 }); + const status = (() => { try { return (JSON.parse(r.out) as { status?: string }).status; } catch { return undefined; } })(); + if (status) return status; + last = (r.err.trim() || r.out.trim() || `notarytool exited ${r.code}`).slice(0, 300); + if (!/abortedUpload|connection (was )?reset|timed out|network connection was lost|NSURLErrorDomain/i.test(last)) return last; + say(`notarytool (${what}): the upload was cut off before Apple had it (try ${tries}); ${tries < 3 ? 'again without S3 acceleration' : 'giving up'}`); + } + return last; +} + function runAsync(cmd: string[], opts: { cwd?: string; env?: Record; timeoutMs?: number; inherit?: boolean } = {}): Promise<{ code: number; out: string; err: string; ms: number }> { const start = Date.now(); const p = Bun.spawn(cmd, { cwd: opts.cwd, env: (opts.env ?? process.env) as Record, stdin: 'ignore', stdout: opts.inherit ? 'inherit' : 'pipe', stderr: opts.inherit ? 'inherit' : 'pipe' }); @@ -463,11 +479,27 @@ async function mac(argv: string[]): Promise { // Object.prototype with old saved states (tools/desktop-boot-check.ts); a blank window never ships again say(`tauri bundle --target universal-apple-darwin --bundles app (${signed ? `sign${notarized ? ', notarize, staple' : ''}` : 'ad-hoc sign'}), boot check of the built window at the same time`); const bootP = runAsync(['bun', join(ROOT, 'tools/desktop-boot-check.ts')], { cwd: ROOT, timeoutMs: 600_000 }); - const tbs = await runAsync(['bunx', 'tauri', 'bundle', '--target', 'universal-apple-darwin', '--bundles', 'app', '--ci', '--config', overrideFile], { cwd: APP_DIR, env, inherit: true, timeoutMs: 3_600_000 }); + // tauri signs only: notarization runs below with notarytool directly (retries, no S3 acceleration), because the + // upload inside tauri bundle was cut off twice (abortedUpload) for 0.8.12 and tauri gives no retry + const signEnv: Record = { ...env }; + delete signEnv.APPLE_API_KEY; delete signEnv.APPLE_API_ISSUER; delete signEnv.APPLE_API_KEY_PATH; + const tbs = await runAsync(['bunx', 'tauri', 'bundle', '--target', 'universal-apple-darwin', '--bundles', 'app', '--ci', '--config', overrideFile], { cwd: APP_DIR, env: signEnv, inherit: true, timeoutMs: 3_600_000 }); say(`tauri bundle done in ${(tbs.ms / 1000).toFixed(0)} s`); const boot = await bootP; process.stdout.write(boot.out); if (tbs.code !== 0) fail(`tauri bundle (signing) failed (exit ${tbs.code})`); + if (notarized) { + const appPath = join(targetDir, 'universal-apple-darwin/release/bundle/macos', APP_NAME); + const zip = join(mkdtempSync(join(tmpdir(), 'sb-notary-')), 'storebound.zip'); + const z = spawnSync('ditto', ['-c', '-k', '--keepParent', appPath, zip]); + if (z.status !== 0) fail('ditto: could not zip the app for notarization'); + const n = await notarizeWithRetry(zip, notary!, 'app'); + rmSync(dirname(zip), { recursive: true, force: true }); + if (n !== 'Accepted') fail(`notarization of the app: ${n}`); + const st = spawnSync('xcrun', ['stapler', 'staple', appPath], { encoding: 'utf8' }); + if (st.status !== 0) fail(`stapler staple (app): ${(st.stderr || st.stdout).trim().slice(-300)}`); + say('app notarized and stapled'); + } if (boot.code !== 0) fail(`the built window does not boot cleanly (tools/desktop-boot-check.ts): ${boot.err.trim().split('\n').slice(-3).join(' / ')}`); say(`boot check passed in ${(boot.ms / 1000).toFixed(0)} s`); const bootCheck = boot.out.trim().split('\n').at(-1) ?? 'ok';