apple.macos listing / review notes per platform + iOS-only claim warnings; apple upload of a .pkg: bounded pkgutil, macOS processing wait, heartbeats; release 409 pattern

This commit is contained in:
CRTkafa 2026-11-03 23:30:59 +07:00
parent 53d57544bf
commit 1e47c2ee60
18 changed files with 417 additions and 63 deletions

View file

@ -271,29 +271,51 @@ export function readAppInfo(appDir: string): Record<string, PlistValue> {
return parsePlist(readFileSync(p)) as Record<string, PlistValue>;
}
/** What bounds and reports the .pkg reading (tests inject a fake spawnSync and short limits). */
export interface PkgInfoOptions { say?: (msg: string) => void; spawnSync?: typeof rt.spawnSync; checkMs?: number; expandMs?: number }
/** pkgutil --check-signature evaluates the certificate chain (revocation lookups over the network): never unbounded. */
export const PKG_CHECK_MS = 90_000;
/** pkgutil --expand copies the whole archive to a temp folder: minutes for a big app, never unbounded. */
export const PKG_EXPAND_MS = 5 * 60_000;
/** bundle id, version and build of a .pkg: pkgutil --expand on a Mac (PackageInfo), else the <pkg>.json next to it that
* `apple build --platform macos` wrote. Refuses an unsigned or wrongly signed .pkg when pkgutil is there. */
export function pkgInfo(pkg: string, expectBundleId?: string): { bundleId: string; version: string; build: string } {
if (rt.which('pkgutil')) {
const sig = parsePkgSignature(rt.spawnSync(['pkgutil', '--check-signature', pkg], { stdout: 'pipe', stderr: 'pipe' }).stdout.toString());
const bad = pkgProblems(sig);
if (bad.length) throw new Error(`${basename(pkg)}: not uploaded:\n${bad.map(x => ` ${x}`).join('\n')}`);
const tmp = join(process.env.TMPDIR ?? '/tmp', `sb-pkg-${process.pid}-${Date.now()}`);
try {
if (rt.spawnSync(['pkgutil', '--expand', pkg, tmp], { stdout: 'pipe', stderr: 'pipe' }).exitCode === 0) {
for (const d of readdirSync(tmp)) {
const p = join(tmp, d, 'PackageInfo');
const info = existsSync(p) ? parsePackageInfo(readFileSync(p, 'utf8')) : null;
if (info && (!expectBundleId || info.bundleId === expectBundleId)) return info;
}
}
} finally { try { rmSync(tmp, { recursive: true, force: true }); } catch { /* temp */ } }
}
* `apple build --platform macos` wrote. Refuses an unsigned or wrongly signed .pkg when pkgutil is there. Both pkgutil
* calls have a timeout (no stdin, so no prompt) and say what they do first; one that times out falls back to the
* .json file, else it fails with the reason (a silent hang over SSH, 10 min, was this read). */
export function pkgInfo(pkg: string, expectBundleId?: string, o: PkgInfoOptions = {}): { bundleId: string; version: string; build: string } {
const say = o.say ?? (() => {});
const spawnSync = o.spawnSync ?? rt.spawnSync;
const side = `${pkg}.json`;
let timedOut = '';
if (rt.which('pkgutil') || o.spawnSync) {
const checkMs = o.checkMs ?? PKG_CHECK_MS, expandMs = o.expandMs ?? PKG_EXPAND_MS;
say(`checking the signature of ${basename(pkg)} (pkgutil --check-signature, up to ${Math.round(checkMs / 1000)} s)`);
const chk = spawnSync(['pkgutil', '--check-signature', pkg], { stdout: 'pipe', stderr: 'pipe', timeout: checkMs });
if (chk.exitCode === null && chk.signalCode) timedOut = `pkgutil --check-signature gave no answer in ${Math.round(checkMs / 1000)} s (it checks the certificate chain over the network)`;
else {
const bad = pkgProblems(parsePkgSignature(chk.stdout.toString()));
if (bad.length) throw new Error(`${basename(pkg)}: not uploaded:\n${bad.map(x => ` ${x}`).join('\n')}`);
const tmp = join(process.env.TMPDIR ?? '/tmp', `sb-pkg-${process.pid}-${Date.now()}`);
say(`reading bundle id, version and build (pkgutil --expand, up to ${Math.round(expandMs / 1000)} s; a big package takes a while)`);
try {
const ex = spawnSync(['pkgutil', '--expand', pkg, tmp], { stdout: 'pipe', stderr: 'pipe', timeout: expandMs });
if (ex.exitCode === null && ex.signalCode) timedOut = `pkgutil --expand gave no answer in ${Math.round(expandMs / 1000)} s`;
else if (ex.exitCode === 0) {
for (const d of readdirSync(tmp)) {
const p = join(tmp, d, 'PackageInfo');
const info = existsSync(p) ? parsePackageInfo(readFileSync(p, 'utf8')) : null;
if (info && (!expectBundleId || info.bundleId === expectBundleId)) return info;
}
}
} finally { try { rmSync(tmp, { recursive: true, force: true }); } catch { /* temp */ } }
}
}
if (timedOut) say(`${timedOut}: using ${basename(side)} (written by apple build) instead`);
if (existsSync(side)) {
const j = JSON.parse(readFileSync(side, 'utf8')) as { bundleId?: string; version?: string; build?: string; unsigned?: boolean };
if (j.unsigned) throw new Error(`${basename(pkg)} was built with --unsigned: the Mac App Store refuses it`);
if (j.bundleId && j.version && j.build) return { bundleId: j.bundleId, version: j.version, build: j.build };
}
if (timedOut) throw new Error(`${basename(pkg)}: ${timedOut}, and there is no ${basename(side)} file from apple build to read instead: run it in the Mac's logged-in GUI session, or check the Mac's network`);
throw new Error(`${basename(pkg)}: cannot read bundle id / version / build (needs pkgutil on a Mac, or the ${basename(side)} file apple build wrote)`);
}