desktop-release: never run the x86_64 slice on the build Mac (Rosetta makes macOS warn the owner that the app will not open in the next macOS); every Mach-O in the bundle checked with lipo (arm64 + x86_64), self-test and sidecar version run natively only; pitfall 34
This commit is contained in:
parent
7e09947e62
commit
3952716ef8
5 changed files with 64 additions and 22 deletions
|
|
@ -8,8 +8,8 @@
|
|||
// identity in the keychain: hardened runtime, notarization (notarytool, App Store Connect API key) and stapling.
|
||||
// Without one: an ad-hoc signed build named `-unsigned`, and what the owner has to do. Must run in the owner's
|
||||
// local GUI session: codesign over SSH fails (the keychain is locked there), so SSH sessions are refused.
|
||||
// The built window is booted headless first (tools/desktop-boot-check.ts). Then, on the exact artifacts: codesign / Gatekeeper expectations, the self-test (sidecar handshake, both CPU
|
||||
// types) from the mounted DMG and from the unpacked updater bundle, the privacy scan, minisign signatures with
|
||||
// The built window is booted headless first (tools/desktop-boot-check.ts). Then, on the exact artifacts: codesign / Gatekeeper expectations, lipo (every executable universal; the x86_64 slice is
|
||||
// never run, Rosetta), the native self-test (sidecar handshake) from the mounted DMG and from the unpacked updater bundle, the privacy scan, minisign signatures with
|
||||
// the updater key (~/.storebound-keys/updater.key), SHA256SUMS, latest.json and evidence.json.
|
||||
// bun tools/desktop-release.ts add --out <dir> <files...> [--code-signed]
|
||||
// Adds Windows / Linux installers built by the Forgejo workflow (.forgejo/workflows/desktop.yml) to the version
|
||||
|
|
@ -217,23 +217,36 @@ function args(argv: string[]) {
|
|||
|
||||
interface SelfTest { arch: string; result: Record<string, unknown> }
|
||||
|
||||
/** The x86_64 slice is never run on this Mac: launching it starts Rosetta, and macOS then tells the owner that
|
||||
* "storebound" includes a component that will not open in the next major release. It is checked by structure only
|
||||
* (`lipo -archs` on every Mach-O in the bundle, each must carry arm64 and x86_64); the self-test runs natively. */
|
||||
export function sliceProblems(files: { file: string; archs: string[] }[]): string[] {
|
||||
return files.filter(f => !(f.archs.includes('arm64') && f.archs.includes('x86_64'))).map(f => `${f.file} (${f.archs.join(' ') || 'no slices'})`);
|
||||
}
|
||||
|
||||
function machOFiles(dir: string): string[] {
|
||||
const out: string[] = [];
|
||||
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
||||
const f = join(dir, e.name);
|
||||
if (e.isDirectory()) out.push(...machOFiles(f));
|
||||
else if (e.isFile() && /Mach-O/.test(run(['file', '-b', f]).out)) out.push(f);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function selfTest(appPath: string, label: string): SelfTest[] {
|
||||
const bin = join(appPath, 'Contents/MacOS', MAIN_BIN);
|
||||
const out: SelfTest[] = [];
|
||||
const arches = run(['lipo', '-archs', bin]).out.trim().split(/\s+/).sort();
|
||||
const sideArches = run(['lipo', '-archs', join(appPath, 'Contents/MacOS', PRODUCT)]).out.trim().split(/\s+/).sort();
|
||||
if (arches.join(' ') !== 'arm64 x86_64' || sideArches.join(' ') !== 'arm64 x86_64') fail(`${label}: not universal (app ${arches}, sidecar ${sideArches})`);
|
||||
const rosetta = run(['arch', '-x86_64', '/usr/bin/true'], { allowFail: true }).code === 0;
|
||||
for (const arch of rosetta ? ['arm64', 'x86_64'] : [process.arch === 'arm64' ? 'arm64' : 'x86_64']) {
|
||||
const r = run(['arch', `-${arch}`, bin, '--self-test'], { allowFail: true, timeout: 90_000 });
|
||||
if (r.code !== 0) fail(`${label}: self-test (${arch}) failed: ${r.err.trim() || r.out.trim()}`);
|
||||
const result = JSON.parse(r.out.trim().split('\n').at(-1)!) as Record<string, unknown>;
|
||||
if (result.ok !== true || result.protocol !== 'storebound-desktop/1') fail(`${label}: self-test (${arch}) answered ${r.out}`);
|
||||
say(`self-test ${label} (${arch}): sidecar ${result.sidecar}, ${result.methods} methods, ${result.ms} ms`);
|
||||
out.push({ arch, result });
|
||||
}
|
||||
if (!rosetta) say('self-test: Rosetta is not installed, x86_64 slice not run (softwareupdate --install-rosetta to cover it)');
|
||||
return out;
|
||||
const slices = machOFiles(appPath).map(f => ({ file: f.slice(appPath.length + 1), archs: run(['lipo', '-archs', f]).out.trim().split(/\s+/).filter(Boolean) }));
|
||||
const bad = sliceProblems(slices);
|
||||
if (!slices.length || bad.length) fail(`${label}: not universal: ${bad.join(', ') || 'no executables'}`);
|
||||
say(`${label}: every executable is universal (${slices.map(s => s.file.replace('Contents/MacOS/', '')).join(', ')}: arm64 x86_64); x86_64 checked by lipo, not run (no Rosetta)`);
|
||||
if (process.arch !== 'arm64') fail('the self-test runs the arm64 slice natively: build on Apple silicon');
|
||||
const r = run([bin, '--self-test'], { allowFail: true, timeout: 90_000 });
|
||||
if (r.code !== 0) fail(`${label}: self-test (arm64) failed: ${r.err.trim() || r.out.trim()}`);
|
||||
const result = JSON.parse(r.out.trim().split('\n').at(-1)!) as Record<string, unknown>;
|
||||
if (result.ok !== true || result.protocol !== 'storebound-desktop/1') fail(`${label}: self-test (arm64) answered ${r.out}`);
|
||||
say(`self-test ${label} (arm64): sidecar ${result.sidecar}, ${result.methods} methods, ${result.ms} ms`);
|
||||
return [{ arch: 'arm64', result }];
|
||||
}
|
||||
|
||||
export interface Gate { codesign: string; spctl: string; stapler: string; binaries: string[] }
|
||||
|
|
@ -344,9 +357,11 @@ async function mac(argv: string[]): Promise<void> {
|
|||
rmSync(uni, { force: true });
|
||||
run(['lipo', '-create', '-output', uni, arm, x64]);
|
||||
run(['codesign', '--force', '--sign', '-', uni]);
|
||||
for (const f of [arm, x64, uni]) {
|
||||
// native slices only: running the x64 sidecar would start Rosetta (see sliceProblems)
|
||||
if (run(['lipo', '-archs', uni]).out.trim().split(/\s+/).sort().join(' ') !== 'arm64 x86_64') fail(`${basename(uni)} is not universal`);
|
||||
for (const f of [arm, uni]) {
|
||||
const v = run([f, 'version'], { allowFail: true, timeout: 60_000 });
|
||||
if (f !== x64 && v.code !== 0) fail(`${basename(f)} does not run: ${v.err}`);
|
||||
if (v.code !== 0) fail(`${basename(f)} does not run: ${v.err}`);
|
||||
}
|
||||
|
||||
// the app: tauri build with the path-free environment, CARGO_TARGET_DIR outside home
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue