import { execFileSync, spawnSync } from 'node:child_process' import { existsSync, readdirSync } from 'node:fs' import { join } from 'node:path' import { assertWithinSize, readPeMachine } from './release-tools.mjs' const platform = process.argv[2] ?? (process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'windows' : process.platform === 'linux' ? 'linux' : '') const root = process.argv[3] ?? (platform === 'macos' ? 'src-tauri/target/aarch64-apple-darwin/release/bundle' : 'src-tauri/target/release/bundle') const requireSigning = process.env.REQUIRE_SIGNING === 'true' const maxBinaryBytes = Number(process.env.MAX_RELEASE_BINARY_MB ?? 40) * 1024 * 1024 const maxInstallerBytes = Number(process.env.MAX_RELEASE_INSTALLER_MB ?? 40) * 1024 * 1024 function walk(dir) { if (!existsSync(dir)) return [] return readdirSync(dir, { withFileTypes: true }).flatMap(entry => { const path = join(dir, entry.name) return entry.isDirectory() ? walk(path) : [path] }) } function fail(message) { console.error(`[release] ${message}`) process.exit(1) } const files = walk(root) if (files.length === 0) fail(`No release artifacts found under ${root}`) if (platform === 'macos') { const appExecutable = files.find(path => /\.app\/Contents\/MacOS\/[^/]+$/.test(path)) const dmg = files.find(path => path.includes('/dmg/') && path.endsWith('.dmg')) if (!appExecutable || !dmg) fail('macOS release must contain an .app executable and a .dmg') assertWithinSize(appExecutable, maxBinaryBytes) assertWithinSize(dmg, maxInstallerBytes) const architectures = execFileSync('lipo', ['-archs', appExecutable], { encoding: 'utf8' }).trim().split(/\s+/) if (!architectures.includes('arm64')) fail(`Expected arm64 app, found: ${architectures.join(', ')}`) if (requireSigning) { execFileSync('codesign', ['--verify', '--deep', '--strict', appExecutable.slice(0, appExecutable.indexOf('.app/') + 4)], { stdio: 'inherit' }) const inspection = spawnSync('codesign', ['-dv', '--verbose=4', appExecutable.slice(0, appExecutable.indexOf('.app/') + 4)], { encoding: 'utf8' }) if (inspection.status !== 0) fail(`Unable to inspect macOS signature: ${inspection.stderr}`) const detail = `${inspection.stdout}\n${inspection.stderr}` if (/Signature=adhoc/.test(detail)) fail('macOS application is only ad-hoc signed') execFileSync('spctl', ['--assess', '--type', 'execute', '--verbose=2', appExecutable.slice(0, appExecutable.indexOf('.app/') + 4)], { stdio: 'inherit' }) } console.log(`[release] macOS arm64 artifacts verified: ${root}`) } else if (platform === 'windows') { const executables = files.filter(path => path.toLowerCase().endsWith('.exe')) const installers = files.filter(path => /\.(?:msi|exe)$/i.test(path)) if (executables.length === 0 || installers.length === 0) fail('Windows release must contain an x64 executable or installer') for (const executable of executables) { assertWithinSize(executable, maxBinaryBytes) if (readPeMachine(executable) !== 0x8664) fail(`${executable} is not an x86_64 PE executable`) if (requireSigning) { execFileSync('powershell', ['-NoProfile', '-Command', `$s = Get-AuthenticodeSignature -LiteralPath '${executable.replaceAll("'", "''")}'; if ($s.Status -ne 'Valid') { throw \"Invalid signature: $($s.Status)\" }`], { stdio: 'inherit' }) } } for (const installer of installers) assertWithinSize(installer, maxInstallerBytes) console.log(`[release] Windows x64 artifacts verified: ${root}`) } else if (platform === 'linux') { const installers = files.filter((path) => /\.(?:deb|rpm|appimage)$/i.test(path)) if (installers.length === 0) fail('Linux release must contain a .deb, .rpm, or .AppImage') for (const installer of installers) assertWithinSize(installer, maxInstallerBytes) console.log(`[release] Linux artifacts verified: ${root}`) } else { fail(`Unsupported platform: ${platform}`) }