diff --git a/scripts/electron-builder/smokePackagedApp.cjs b/scripts/electron-builder/smokePackagedApp.cjs index 2c9d5d5f..9d60f448 100644 --- a/scripts/electron-builder/smokePackagedApp.cjs +++ b/scripts/electron-builder/smokePackagedApp.cjs @@ -16,6 +16,68 @@ const FAILURE_PATTERNS = [ /DeprecationWarning: fs\.Stats constructor is deprecated/i, ]; +function isMacBundle(candidatePath) { + return ( + candidatePath.endsWith('.app') && + fs.existsSync(path.join(candidatePath, 'Contents', 'MacOS')) && + fs.statSync(path.join(candidatePath, 'Contents', 'MacOS')).isDirectory() + ); +} + +function findMacBundles(searchRoot, maxDepth = 3) { + if (!fs.existsSync(searchRoot) || maxDepth < 0) { + return []; + } + + const stat = fs.statSync(searchRoot); + if (stat.isDirectory() && isMacBundle(searchRoot)) { + return [searchRoot]; + } + if (!stat.isDirectory()) { + return []; + } + + const bundles = []; + for (const entry of fs.readdirSync(searchRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + + const fullPath = path.join(searchRoot, entry.name); + if (isMacBundle(fullPath)) { + bundles.push(fullPath); + continue; + } + bundles.push(...findMacBundles(fullPath, maxDepth - 1)); + } + return bundles; +} + +function resolveBundlePath(bundlePath, platform) { + if (platform !== 'darwin' || isMacBundle(bundlePath)) { + return bundlePath; + } + + const searchRoots = [ + path.dirname(bundlePath), + path.dirname(path.dirname(bundlePath)), + path.resolve(process.cwd(), 'release'), + ]; + const bundles = [...new Set(searchRoots.flatMap((searchRoot) => findMacBundles(searchRoot)))]; + if (bundles.length === 1) { + return bundles[0]; + } + if (bundles.length > 1) { + const expectedName = path.basename(bundlePath); + const nameMatch = bundles.find((candidate) => path.basename(candidate) === expectedName); + if (nameMatch) { + return nameMatch; + } + } + + return bundlePath; +} + function fail(message, log = '') { console.error(`[smokePackagedApp] ${message}`); if (log.trim()) { @@ -105,7 +167,11 @@ async function terminateChild(child, exitPromise, platform) { const closed = await waitForProcessClose(child, exitPromise, SHUTDOWN_TIMEOUT_MS); if (!closed && child.exitCode === null && child.signalCode === null) { - throw new Error(`Timed out after ${SHUTDOWN_TIMEOUT_MS}ms waiting for packaged app to exit`); + child.kill('SIGKILL'); + const killed = await waitForProcessClose(child, exitPromise, SHUTDOWN_TIMEOUT_MS); + if (!killed && child.exitCode === null && child.signalCode === null) { + throw new Error(`Timed out after ${SHUTDOWN_TIMEOUT_MS}ms waiting for packaged app to exit`); + } } } @@ -115,10 +181,13 @@ async function main() { fail('Usage: node ./scripts/electron-builder/smokePackagedApp.cjs '); } - const bundlePath = path.resolve(bundlePathArg); + const bundlePath = resolveBundlePath(path.resolve(bundlePathArg), platform); const executable = findExecutable(bundlePath, platform); const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-teams-smoke-')); const args = [`--user-data-dir=${userDataDir}`]; + if (platform === 'linux') { + args.push('--no-sandbox'); + } const child = spawn(executable, args, { env: { ...process.env, diff --git a/scripts/electron-builder/verifyBundle.cjs b/scripts/electron-builder/verifyBundle.cjs index c4e68520..bb6f30f6 100644 --- a/scripts/electron-builder/verifyBundle.cjs +++ b/scripts/electron-builder/verifyBundle.cjs @@ -4,6 +4,69 @@ const afterPackModule = require('./afterPack.cjs'); const { validateNativeBinaries } = afterPackModule._internal; +function isMacBundle(candidatePath) { + return ( + candidatePath.endsWith('.app') && + require('node:fs').existsSync(path.join(candidatePath, 'Contents', 'MacOS')) && + require('node:fs').statSync(path.join(candidatePath, 'Contents', 'MacOS')).isDirectory() + ); +} + +function findMacBundles(searchRoot, maxDepth = 3) { + const fs = require('node:fs'); + if (!fs.existsSync(searchRoot) || maxDepth < 0) { + return []; + } + + const stat = fs.statSync(searchRoot); + if (stat.isDirectory() && isMacBundle(searchRoot)) { + return [searchRoot]; + } + if (!stat.isDirectory()) { + return []; + } + + const bundles = []; + for (const entry of fs.readdirSync(searchRoot, { withFileTypes: true })) { + if (!entry.isDirectory()) { + continue; + } + + const fullPath = path.join(searchRoot, entry.name); + if (isMacBundle(fullPath)) { + bundles.push(fullPath); + continue; + } + bundles.push(...findMacBundles(fullPath, maxDepth - 1)); + } + return bundles; +} + +function resolveBundlePath(bundlePath, platform) { + if (platform !== 'darwin' || isMacBundle(bundlePath)) { + return bundlePath; + } + + const searchRoots = [ + path.dirname(bundlePath), + path.dirname(path.dirname(bundlePath)), + path.resolve(process.cwd(), 'release'), + ]; + const bundles = [...new Set(searchRoots.flatMap((searchRoot) => findMacBundles(searchRoot)))]; + if (bundles.length === 1) { + return bundles[0]; + } + if (bundles.length > 1) { + const expectedName = path.basename(bundlePath); + const nameMatch = bundles.find((candidate) => path.basename(candidate) === expectedName); + if (nameMatch) { + return nameMatch; + } + } + + return bundlePath; +} + function isAllowedPostPackMismatch(mismatch, platform, arch) { const relativePath = mismatch.path.split(path.sep).join('/'); return ( @@ -24,7 +87,7 @@ async function main() { process.exit(1); } - const bundlePath = path.resolve(bundlePathArg); + const bundlePath = resolveBundlePath(path.resolve(bundlePathArg), platform); const mismatches = await validateNativeBinaries(bundlePath, platform, arch); const blockingMismatches = mismatches.filter( (mismatch) => !isAllowedPostPackMismatch(mismatch, platform, arch)