diff --git a/apps/desktop/scripts/build.mjs b/apps/desktop/scripts/build.mjs index fd1ecacb7e..8ba6bbf1e0 100644 --- a/apps/desktop/scripts/build.mjs +++ b/apps/desktop/scripts/build.mjs @@ -4,7 +4,7 @@ import { cpSync } from 'node:fs' import { join, resolve } from 'node:path' import { parseArgs } from 'node:util' import { isMain, repoRoot } from '../../../scripts/build/frontend-common.mjs' -import { readNativeInputs } from './prepared-native-deps.mjs' +import { nativeInputsComplete } from './prepared-native-deps.mjs' export function buildSourceDesktop({ source = repoRoot, icons, run = execFileSync } = {}) { source = resolve(source) @@ -26,19 +26,11 @@ export function buildSourceDesktop({ source = repoRoot, icons, run = execFileSyn // The native inputs carry their own receipt (inputs identity + tree digest): restaging the // same sources recompiles two universal helpers for identical bytes on every update. const nativeDeps = join(app, 'build/native-deps') - if (!nativeCurrent({ source, nativeDeps })) step('apps/desktop/scripts/stage-native-deps.mjs') + if (!nativeInputsComplete({ source, nativeDeps })) step('apps/desktop/scripts/stage-native-deps.mjs') step('scripts/build/desktop.mjs', ['--source', source, '--icons', icons, '--stamp', join(app, 'build/install-stamp.json'), '--native-deps', nativeDeps, '--out', join(app, 'dist')]) } -function nativeCurrent(inputs) { - try { - readNativeInputs(inputs) - return true - } catch { - return false - } -} if (isMain(import.meta.url)) { const { values } = parseArgs({ options: { icons: { type: 'string' } } }) diff --git a/apps/desktop/scripts/prepared-native-deps.mjs b/apps/desktop/scripts/prepared-native-deps.mjs index 1fed773a9a..117bb3c415 100644 --- a/apps/desktop/scripts/prepared-native-deps.mjs +++ b/apps/desktop/scripts/prepared-native-deps.mjs @@ -21,16 +21,29 @@ function nativeIdentity(source) { /** * The sidecar stays outside node_modules so it never ships in the application. - * @param {{ source: string, out: string, platform: string, arch: string, nativeToolchain?: string }} inputs + * ``degraded`` records a soft-failed component (no get-windows, no X11 toolchain): the tree is + * consumable, but the identity cannot see the host fix that would complete it. + * @param {{ source: string, out: string, platform: string, arch: string, nativeToolchain?: string, degraded?: boolean }} inputs * @returns {void} */ -export function recordNativeInputs({ source, out, platform, arch, nativeToolchain }) { +export function recordNativeInputs({ source, out, platform, arch, nativeToolchain, degraded = false }) { fs.writeFileSync(`${out}.prepared.json`, JSON.stringify({ schema: 1, source: fs.realpathSync(source), out: fs.realpathSync(out), - platform, arch, nativeToolchain, identity: nativeIdentity(source), digest: treeDigest(out), + platform, arch, nativeToolchain, degraded, identity: nativeIdentity(source), digest: treeDigest(out), }) + '\n') } +/** A local rebuild may skip restaging: the receipt is admitted and nothing in it soft-failed. + * @param {NativeSelection} inputs @returns {boolean} */ +export function nativeInputsComplete(inputs) { + try { + readNativeInputs(inputs) + return JSON.parse(fs.readFileSync(`${inputs.nativeDeps}.prepared.json`, 'utf8')).degraded === false + } catch { + return false + } +} + /** @param {NativeSelection} inputs @returns {string} */ export function readNativeInputs({ source, nativeDeps, platform = process.platform, arch = process.arch, nativeToolchain }) { try { diff --git a/apps/desktop/scripts/stage-native-deps.mjs b/apps/desktop/scripts/stage-native-deps.mjs index b843222850..a018923fb6 100644 --- a/apps/desktop/scripts/stage-native-deps.mjs +++ b/apps/desktop/scripts/stage-native-deps.mjs @@ -29,7 +29,7 @@ import { spawnSync } from 'node:child_process' import { isMain } from './utils.mjs' import { recordNativeInputs } from './prepared-native-deps.mjs' import { buildCommandScreenshotMonitor } from './build-command-screenshot-monitor.mjs' -import { buildHudModifierMonitor } from './build-hud-modifier-monitor.mjs' +import { buildHudModifierMonitor, hudModifierBinaryRelativePath } from './build-hud-modifier-monitor.mjs' import { parseArgs } from 'node:util' import { productOutput, withProduct, workspaceTool } from '../../../scripts/build/frontend-common.mjs' @@ -727,6 +727,23 @@ export function stageGetWindows( return stageGetWindowsInto(srcRoot, destRoot, { platform, arch, install }) } +/** + * Whether ``out`` holds every native piece a build on THIS host can stage for the target. + * Cross-target packs never build helpers and win32-arm64 has no get-windows prebuild, so neither + * counts as missing: nothing a host fix could add. + * @param {string} out @param {string} platform @param {string} arch @returns {boolean} + */ +export function nativeTreeComplete(out, platform, arch) { + const native = platform === process.platform && (platform !== 'linux' || arch === process.arch) + const required = ['get-windows/lib/windows.js'] + if (platform === 'darwin') required.push('get-windows/main') + if (platform === 'win32' && arch !== 'arm64') required.push('get-windows/lib/binding') + if (native && ['darwin', 'linux', 'win32'].includes(platform)) { + required.push(hudModifierBinaryRelativePath(platform, arch)) + } + return required.every(relative => existsSync(join(out, relative))) +} + /** * Preparation may rebuild/download native bindings; compilation only consumes them. * @param {{ source: string, out: string, platform?: string, arch?: string, nativeToolchain?: string }} inputs @@ -741,7 +758,10 @@ export async function prepareDesktopNativeDependencies({ source, out, platform = buildCommandScreenshotMonitor({ source, distDir: product, platform }) buildHudModifierMonitor({ source, distDir: product, platform, arch }) }, { source }) - recordNativeInputs({ source, out, platform, arch, nativeToolchain }) + // Each component fails soft (no get-windows or its win32 binding, no X11 toolchain). Judge the + // staged tree, not each builder's return shape: a tree missing a piece this host could produce + // is degraded, and build.mjs restages it on every build, so fixing the host repairs the next update. + recordNativeInputs({ source, out, platform, arch, nativeToolchain, degraded: !nativeTreeComplete(out, platform, arch) }) return { out } } diff --git a/apps/desktop/scripts/stage-native-deps.test.mjs b/apps/desktop/scripts/stage-native-deps.test.mjs index 6f2fc8d825..ca94fad871 100644 --- a/apps/desktop/scripts/stage-native-deps.test.mjs +++ b/apps/desktop/scripts/stage-native-deps.test.mjs @@ -12,7 +12,8 @@ import { stageGetWindows, stageGetWindowsInto, stageNodePtyInto, - classifyNativeBinary + classifyNativeBinary, + nativeTreeComplete } from '../scripts/stage-native-deps.mjs' const { join } = path @@ -712,3 +713,33 @@ test('a half-installed get-windows dir is found and named in a repair hint', () fs.rmSync(tmp, { recursive: true, force: true }) } }) + +test('a native tree missing a piece this host could stage is incomplete; unfixable gaps are not', () => { + const tmp = fs.mkdtempSync(join(os.tmpdir(), 'native-complete-')) + const put = relative => { fs.mkdirSync(path.dirname(join(tmp, relative)), { recursive: true }); fs.writeFileSync(join(tmp, relative), '') } + try { + // A cross-target pack (here: win32 packed off Windows) never builds the HUD helper. + const crossWin = (arch) => nativeTreeComplete(tmp, 'win32', arch) + put('get-windows/lib/windows.js') + if (process.platform !== 'win32') { + assert.equal(crossWin('x64'), false) // x64 can still get its prebuilt binding + assert.equal(crossWin('arm64'), true) // arm64 has no prebuild: nothing a host fix adds + fs.mkdirSync(join(tmp, 'get-windows/lib/binding'), { recursive: true }) + assert.equal(crossWin('x64'), true) + } + if (process.platform === 'darwin') { + assert.equal(nativeTreeComplete(tmp, 'darwin', process.arch), false) // no get-windows helper + put('get-windows/main') + assert.equal(nativeTreeComplete(tmp, 'darwin', process.arch), false) // HUD helper did not build + put('native/darwin-universal/hud-modifier-monitor') + assert.equal(nativeTreeComplete(tmp, 'darwin', process.arch), true) + } + if (process.platform === 'linux') { + assert.equal(nativeTreeComplete(tmp, 'linux', process.arch), false) // no X11 toolchain at stage time + put(`native/linux-${process.arch}/hud-modifier-monitor`) + assert.equal(nativeTreeComplete(tmp, 'linux', process.arch), true) + } + } finally { + fs.rmSync(tmp, { recursive: true, force: true }) + } +}) diff --git a/tests-js/desktop-builder.test.mjs b/tests-js/desktop-builder.test.mjs index 81f3f69003..6c17ec5d83 100644 --- a/tests-js/desktop-builder.test.mjs +++ b/tests-js/desktop-builder.test.mjs @@ -41,6 +41,8 @@ test('desktop development composition restages native inputs only when their rec expect(staged()).toBe(false) put(join(nativeDeps, 'native/helper-fixture'), 'tampered') expect(staged()).toBe(true) // the tree no longer matches its digest + recordNativeInputs({ source: input.source, out: nativeDeps, platform: process.platform, arch: process.arch, degraded: true }) + expect(staged()).toBe(true) // a soft-failed component restages until the host can complete it recordNativeInputs({ source: input.source, out: nativeDeps, platform: process.platform, arch: process.arch }) put(join(input.source, 'package-lock.json'), '{"lockfileVersion":3}') expect(staged()).toBe(true) // a dependency pin moved