diff --git a/scripts/desktop-tauri-build.mjs b/scripts/desktop-tauri-build.mjs index c2196f9d78..fcf1ae263e 100644 --- a/scripts/desktop-tauri-build.mjs +++ b/scripts/desktop-tauri-build.mjs @@ -71,14 +71,23 @@ async function main() { }); const tauriBin = join(ROOT, 'node_modules', '.bin', 'tauri'); const tauriArgs = ['build', '--config', tauriConfig, ...forward]; - const buildStartedAtMs = Date.now(); + let attemptStartedAtMs = Date.now(); let r = runTauriBuild(tauriBin, tauriArgs, desktopDir); - if (!r.error && shouldRetryMacDmgBuild(r, forward, desktopDir, buildStartedAtMs)) { + const maxMacDmgBuildAttempts = 3; + for ( + let attempt = 1; + attempt < maxMacDmgBuildAttempts + && !r.error + && shouldRetryMacDmgBuild(r, forward, desktopDir, attemptStartedAtMs); + attempt += 1 + ) { + const retryDelaySeconds = attempt * 10; console.warn( - '[tauri-build] DMG bundling failed after the macOS app bundle was created; retrying once in 10 seconds.' + `[tauri-build] DMG bundling failed after the macOS app bundle was refreshed; retrying build attempt ${attempt + 1}/${maxMacDmgBuildAttempts} in ${retryDelaySeconds} seconds.` ); - await new Promise((resolveRetry) => setTimeout(resolveRetry, 10_000)); + await new Promise((resolveRetry) => setTimeout(resolveRetry, retryDelaySeconds * 1_000)); + attemptStartedAtMs = Date.now(); r = runTauriBuild(tauriBin, tauriArgs, desktopDir); } @@ -169,13 +178,28 @@ export function shouldRetryMacDmgBuild( 'macos' ); + const freshAfterMs = buildStartedAtMs - 1_000; try { - return readdirSync(bundleDir, { withFileTypes: true }).some( - (entry) => - entry.isDirectory() && - entry.name.endsWith('.app') && - statSync(join(bundleDir, entry.name)).mtimeMs >= buildStartedAtMs - 1_000 - ); + return readdirSync(bundleDir, { withFileTypes: true }).some((entry) => { + if (!entry.isDirectory() || !entry.name.endsWith('.app')) { + return false; + } + + const appDir = join(bundleDir, entry.name); + if (statSync(appDir).mtimeMs >= freshAfterMs) { + return true; + } + + // The Rust cache can restore an existing app directory without changing + // its own mtime. Tauri still refreshes the executable inside it before + // codesigning, so use that file as the reliable bundling boundary. + const executableDir = join(appDir, 'Contents', 'MacOS'); + return readdirSync(executableDir, { withFileTypes: true }).some( + (executable) => + executable.isFile() + && statSync(join(executableDir, executable.name)).mtimeMs >= freshAfterMs + ); + }); } catch { return false; } diff --git a/scripts/desktop-tauri-build.test.mjs b/scripts/desktop-tauri-build.test.mjs index 0826f10e22..40037fa28d 100644 --- a/scripts/desktop-tauri-build.test.mjs +++ b/scripts/desktop-tauri-build.test.mjs @@ -170,12 +170,16 @@ function retryFixture() { 'macos', 'OpenBitFun.app' ); + const executableDir = join(appDir, 'Contents', 'MacOS'); + const executablePath = join(executableDir, 'openbitfun-desktop'); mkdirSync(desktopDir, { recursive: true }); - mkdirSync(appDir, { recursive: true }); + mkdirSync(executableDir, { recursive: true }); + writeFileSync(executablePath, 'test executable'); return { appDir, desktopDir, + executablePath, runtime: { cargoTargetDir: targetDir, githubActions: 'true', @@ -204,6 +208,28 @@ test('retries a failed GitHub Actions DMG bundle after a fresh app bundle', () = } }); +test('retries when a restored app directory contains a freshly bundled executable', () => { + const fixture = retryFixture(); + try { + const buildStartedAt = Date.now(); + const staleTime = new Date(buildStartedAt - 60_000); + utimesSync(fixture.appDir, staleTime, staleTime); + + assert.equal( + shouldRetryMacDmgBuild( + FAILED_BUILD, + DMG_ARGS, + fixture.desktopDir, + buildStartedAt, + fixture.runtime + ), + true + ); + } finally { + fixture.cleanup(); + } +}); + test('does not retry failures outside the narrow DMG bundling boundary', () => { const fixture = retryFixture(); try { @@ -228,6 +254,7 @@ test('does not retry failures outside the narrow DMG bundling boundary', () => { const staleTime = new Date(Date.now() - 60_000); utimesSync(fixture.appDir, staleTime, staleTime); + utimesSync(fixture.executablePath, staleTime, staleTime); assert.equal( shouldRetryMacDmgBuild( FAILED_BUILD,