Files
openswarm/electron/scripts/notarize.js
T

101 lines
3.5 KiB
JavaScript

// Hand-rolled notarization: `notarytool submit --wait` SIGBUSes on this macOS (its progress
// printer dies ~30s after upload, crash dumps 2026-07-13), so we submit WITHOUT --wait and poll
// `notarytool info` with short-lived calls that never reach the crashing code path. We staple
// here too because @electron/notarize (which normally staples) is out of the loop.
const path = require('path');
const os = require('os');
const fs = require('fs');
const { execFile } = require('child_process');
const { promisify } = require('util');
const execFileAsync = promisify(execFile);
const POLL_INTERVAL_MS = 30 * 1000;
const POLL_DEADLINE_MS = 90 * 60 * 1000;
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function xcrun(args) {
const { stdout } = await execFileAsync('xcrun', args, { maxBuffer: 32 * 1024 * 1024 });
return stdout;
}
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== 'darwin') return;
if (process.env.CSC_IDENTITY_AUTO_DISCOVERY === 'false') {
console.log('Skipping notarization (CSC_IDENTITY_AUTO_DISCOVERY=false)');
return;
}
// Prefer a stored notarytool keychain profile: an app-specific password in env
// silently 401s the day Apple rotates it; the keychain profile is durable.
const keychainProfile = process.env.APPLE_KEYCHAIN_PROFILE;
const hasEnvCreds =
process.env.APPLE_ID && process.env.APPLE_APP_SPECIFIC_PASSWORD && process.env.APPLE_TEAM_ID;
if (!keychainProfile && !hasEnvCreds) {
console.log('Skipping notarization (no APPLE_KEYCHAIN_PROFILE and no APPLE_ID/password/team)');
return;
}
const authArgs = keychainProfile
? ['--keychain-profile', keychainProfile]
: [
'--apple-id',
process.env.APPLE_ID,
'--password',
process.env.APPLE_APP_SPECIFIC_PASSWORD,
'--team-id',
process.env.APPLE_TEAM_ID,
];
const appName = context.packager.appInfo.productFilename;
const appPath = `${appOutDir}/${appName}.app`;
const zipPath = path.join(os.tmpdir(), `openswarm-notarize-${Date.now()}.zip`);
console.log(`Notarizing ${appPath} (submit, then poll)...`);
try {
await execFileAsync('ditto', ['-c', '-k', '--sequesterRsrc', '--keepParent', appPath, zipPath]);
const submitted = JSON.parse(
await xcrun(['notarytool', 'submit', zipPath, ...authArgs, '--output-format', 'json'])
);
if (!submitted.id) {
throw new Error(`notarytool submit returned no id: ${JSON.stringify(submitted)}`);
}
console.log(`Notarization submitted (id ${submitted.id}); polling until Apple decides...`);
const deadline = Date.now() + POLL_DEADLINE_MS;
let status = 'In Progress';
while (status === 'In Progress' && Date.now() < deadline) {
await delay(POLL_INTERVAL_MS);
try {
const info = JSON.parse(
await xcrun(['notarytool', 'info', submitted.id, ...authArgs, '--output-format', 'json'])
);
status = info.status;
} catch {
// One flaky poll is fine; the next tick asks again.
}
}
if (status !== 'Accepted') {
let notaryLog = '';
try {
notaryLog = await xcrun(['notarytool', 'log', submitted.id, ...authArgs]);
} catch {}
throw new Error(`Notarization ended ${status} (id ${submitted.id})\n${notaryLog}`);
}
await xcrun(['stapler', 'staple', appPath]);
console.log('Notarization complete (Accepted + stapled).');
} finally {
try {
fs.unlinkSync(zipPath);
} catch {}
}
};