Let the shared output guard classify desktop products

This commit is contained in:
ethernet
2026-09-12 18:57:54 -04:00
parent ea7299062c
commit 7417158acd
4 changed files with 37 additions and 4 deletions

View File

@@ -120,6 +120,10 @@ marker. Files, symlinks, and source directories are rejected. The exact npm
destinations (`ui-tui/dist`, `hermes_cli/web_dist`, `apps/desktop/dist`, and
`apps/desktop/build/native-deps`) remain rebuildable without a prior marker.
Other in-tree products live beneath `.build/` or `apps/desktop/build/products/`.
`frontend-common.mjs` classifies these destinations independently of which source
children already exist, so a warm desktop rebuild uses the same rule as a fresh
build. Explicit stamp, icon, native-tree and dependency inputs remain protected,
even when they live beneath a generated destination.
### Icons and native inputs

View File

@@ -29,9 +29,10 @@ export async function buildDesktop({ source, out, icons, stamp, nativeDeps, type
if (!icons || !stamp || !nativeDeps) throw new Error('icons, stamp and nativeDeps are required prepared inputs')
const app = 'apps/desktop'
;({ source, out } = productOutput(source, out, [
...readdirSync(join(resolve(source), app)).filter(name => name !== 'dist').map(name => `${app}/${name}`),
`${app}/scripts`, 'scripts/build', 'package.json', 'package-lock.json',
'apps/shared', 'node_modules', ...[join(resolve(icons), app, 'public'), stamp, nativeDeps].map(input => relative(resolve(source), resolve(input))),
// productOutput owns source/generated classification. Protect prepared
// inputs explicitly, including dependency symlinks outside the checkout.
`${app}/node_modules`, 'node_modules',
...[join(resolve(icons), app, 'public'), stamp, nativeDeps].map(input => relative(resolve(source), resolve(input))),
]))
const publicIcons = join(resolve(icons), app, 'public')
if (!existsSync(join(publicIcons, 'apple-touch-icon.png'))) throw new Error(`Missing desktop icon: ${join(publicIcons, 'apple-touch-icon.png')}`)

View File

@@ -50,6 +50,9 @@ function requireOwnedOutput(out, source) {
}
}
// Classify in-tree destinations here, not by enumerating existing workspace
// children: generated parents also exist after the first build. inputs names
// explicit prepared trees/files; their protection wins even in generated homes.
export function productOutput(source, out, inputs) {
if (!source || !out) throw new Error('source and output paths are required')
const src = realpathSync(path.resolve(source))

View File

@@ -97,6 +97,31 @@ test('desktop compiler consumes explicit immutable inputs, replaces variants, an
expect(files(input.source).some(([name]) => name.includes('.vite') || name.endsWith('tsbuildinfo'))).toBe(false)
}, 60000)
test('in-tree desktop products rebuild after build exists without replacing prepared inputs', async () => {
const { buildDesktop } = await import('../scripts/build/desktop.mjs')
const { productCurrent } = await import('../scripts/build/freshness.mjs')
const input = fixture()
input.out = join(input.source, 'apps/desktop/build/products/desktop')
await buildDesktop(input)
put(join(input.source, 'apps/desktop/src/index.js'), 'document.getElementById("app").textContent = "warm rebuild"')
await buildDesktop(input)
expect(productCurrent({ ...input, product: 'desktop' })).toBe(true)
const assets = files(join(input.out, 'assets')).map(([, bytes]) => Buffer.from(bytes, 'base64').toString()).join('')
expect(assets).toContain('warm rebuild')
// The generated-path allowance never overrides an explicitly prepared input,
// even when that input itself is an earlier builder-owned product.
const built = files(input.out)
for (const prepared of [
{ stamp: join(input.out, 'hermes-build.json') },
{ nativeDeps: join(input.out, 'node_modules') },
{ icons: input.out },
]) {
await expect(buildDesktop({ ...input, ...prepared })).rejects.toThrow(/overlap/)
expect(files(input.out)).toEqual(built)
}
}, 30000)
test('a prepared input changing during desktop compilation cannot publish a current receipt', async () => {
const { buildDesktop } = await import('../scripts/build/desktop.mjs')
const input = fixture()
@@ -137,7 +162,7 @@ test('typecheck uses scratch state and incomplete prepared inputs fail before pu
rmSync(join(input.icons, 'apps/desktop/public/apple-touch-icon.png'))
await expect(buildDesktop(input)).rejects.toThrow(/icon/i)
expect(files(input.out)).toEqual(built)
await expect(buildDesktop({ ...input, out: join(input.source, 'apps/desktop/scripts') })).rejects.toThrow(/overlap/)
await expect(buildDesktop({ ...input, out: join(input.source, 'apps/desktop/scripts') })).rejects.toThrow(/Output/)
put(join(input.icons, 'apps/desktop/public/apple-touch-icon.png'), 'fresh icon')
rmSync(join(input.nativeDeps, 'node-pty/build'), { recursive: true, force: true })
rmSync(join(input.nativeDeps, 'node-pty/prebuilds'), { recursive: true, force: true })