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

This commit is contained in:
CRTkafa 2026-10-18 02:43:59 +08:00
parent d2ae0fa765
commit 0b32df57f0

View file

@ -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<string, string | undefined>, what: string): Promise<string> {
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<string, string | undefined>; 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<string, string>, stdin: 'ignore', stdout: opts.inherit ? 'inherit' : 'pipe', stderr: opts.inherit ? 'inherit' : 'pipe' });
@ -463,11 +479,27 @@ async function mac(argv: string[]): Promise<void> {
// 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<string, string | undefined> = { ...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';