fix(desktop): an incomplete native tree restages on every build
Skipping stage-native-deps on an admitted receipt kept a tree staged without a piece forever: get-windows (or its win32 x64 binding, which only the native installer inside staging can fetch), the macOS get-windows helper, or the host HUD helper (no X11 toolchain on Linux). The receipt identity cannot see the host fix that would complete it. The receipt now records `degraded` from the staged tree itself (nativeTreeComplete: every piece this host can produce for the target is present), and build.mjs reuses only a complete one (nativeInputsComplete). Gaps no host fix closes — win32-arm64 has no get-windows prebuild, a cross-target pack never builds the helpers — do not count. Judging the tree rather than each builder's return value covers soft-fail paths that still return a path (the win32 binding).
This commit is contained in:
@@ -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' } } })
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user