rendererCurrent and productCurrent share hostReceipt() for the schema/product/host checks. The stamped outputs come from bundleElectronMain's own electronOutputs list, so a renamed or added bundle can never be copied stale from the previous dist. The reuse copy skips node_modules/ and native/, which copyNativeTree recopies from nativeDeps right after, and a --typecheck build never reuses a renderer that skipped tsc.
97 lines
5.5 KiB
JavaScript
97 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
import { execFileSync } from 'node:child_process'
|
|
import { cpSync, existsSync, readdirSync } from 'node:fs'
|
|
import { dirname, join, relative, resolve, sep } from 'node:path'
|
|
import { pathToFileURL } from 'node:url'
|
|
import { bundleElectronMain, electronOutputs } from '../../apps/desktop/scripts/bundle-electron-main.mjs'
|
|
import { checkDistBuilt } from '../../apps/desktop/scripts/assert-dist-built.mjs'
|
|
import { classifyNativeBinary } from '../../apps/desktop/scripts/stage-native-deps.mjs'
|
|
import { copyNativeTree } from '../../apps/desktop/scripts/prepared-native-deps.mjs'
|
|
import { frontendArgs, isMain, productOutput, withProduct, workspaceTool } from './frontend-common.mjs'
|
|
import { recordProduct, buildInputs, rendererCurrent } from './freshness.mjs'
|
|
|
|
function validateNativeTree(nativeDeps, platform) {
|
|
const pty = join(nativeDeps, 'node-pty')
|
|
if (!existsSync(join(pty, 'package.json'))) throw new Error(`Missing prepared node-pty: ${pty}`)
|
|
const bindings = readdirSync(pty, { recursive: true }).filter(name => name.endsWith('.node'))
|
|
if (!bindings.length) throw new Error(`Prepared node-pty has no native binding: ${pty}`)
|
|
for (const binding of bindings) {
|
|
if (classifyNativeBinary(join(pty, binding)) !== platform) {
|
|
throw new Error(`Prepared node-pty binding does not match ${platform}: ${binding}`)
|
|
}
|
|
}
|
|
if (platform === 'darwin' && !existsSync(join(nativeDeps, 'get-windows/main'))) {
|
|
throw new Error('Prepared get-windows is missing its macOS helper')
|
|
}
|
|
}
|
|
|
|
// Only main/preload bake the install stamp (bundleElectronMain); the renderer is a function of
|
|
// the source tree, icons and native inputs alone. A new commit with no desktop change therefore
|
|
// leaves the published renderer valid: reuse it when the receipt proves those inputs and the
|
|
// output bytes are unchanged, and rebuild only what carries the stamp. The native trees are
|
|
// recopied from nativeDeps below either way.
|
|
const notReused = new Set([...Object.values(electronOutputs), 'node_modules', 'native'])
|
|
|
|
/** Pure desktop compilation. out is the dist directory, not the source app. */
|
|
export async function buildDesktop({ source, out, icons, stamp, nativeDeps, typecheck = false, platform = process.platform }) {
|
|
if (!icons || !stamp || !nativeDeps) throw new Error('icons, stamp and nativeDeps are required prepared inputs')
|
|
const app = 'apps/desktop'
|
|
;({ source, out } = productOutput(source, out, [
|
|
// Source children can be symlinks outside the checkout. Protect their
|
|
// canonical paths, but leave generated dist/build trees to productOutput.
|
|
...readdirSync(join(resolve(source), app)).filter(name => !['dist', 'build'].includes(name)).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))),
|
|
]))
|
|
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')}`)
|
|
validateNativeTree(resolve(nativeDeps), platform)
|
|
const inputs = buildInputs(source, 'desktop', { icons: publicIcons, stamp, nativeDeps })
|
|
// A typechecked build must run the check: a reused renderer never passed through tsc here.
|
|
const reuse = !typecheck && rendererCurrent(out, inputs)
|
|
await withProduct(out, async (product, scratch) => {
|
|
if (reuse) {
|
|
cpSync(out, product, { recursive: true, filter: file => !notReused.has(relative(out, file).split(sep)[0]) })
|
|
} else await compileRenderer(product, scratch)
|
|
// The receipt must carry the clock this output BAKED, not whatever the live
|
|
// stamp says by now: electron-builder copies that live file into the bundle
|
|
// after the receipt is written, and productCurrent must be able to tell that
|
|
// the two disagree (#123308).
|
|
const { stampClock } = await bundleElectronMain({ source, out: product, stamp })
|
|
copyNativeTree({ nativeDeps, out: join(product, 'node_modules') })
|
|
const result = checkDistBuilt(product)
|
|
if (!result.ok) throw new Error(result.error)
|
|
recordProduct({ source, product: 'desktop', out: product, inputs, stampClock })
|
|
}, { source })
|
|
return { out, reusedRenderer: reuse }
|
|
|
|
async function compileRenderer(product, scratch) {
|
|
const publicDir = join(scratch, 'public')
|
|
const sourcePublic = join(source, app, 'public')
|
|
if (existsSync(sourcePublic)) cpSync(sourcePublic, publicDir, { recursive: true })
|
|
cpSync(publicIcons, publicDir, { recursive: true })
|
|
if (typecheck) {
|
|
const ts = workspaceTool(source, app, 'typescript')
|
|
execFileSync(process.execPath, [join(dirname(ts), 'tsc.js'), '-p', join(source, app, 'tsconfig.json'),
|
|
'--noEmit', '--incremental', '--tsBuildInfoFile', join(scratch, 'renderer.tsbuildinfo')], { cwd: join(source, app), stdio: 'inherit' })
|
|
}
|
|
const { build } = await import(pathToFileURL(workspaceTool(source, app, 'vite')).href)
|
|
await build({
|
|
root: join(source, app),
|
|
configLoader: 'runner',
|
|
publicDir,
|
|
cacheDir: join(scratch, 'vite-cache'),
|
|
build: { outDir: product, emptyOutDir: true },
|
|
})
|
|
}
|
|
}
|
|
|
|
if (isMain(import.meta.url)) {
|
|
const args = frontendArgs(process.argv.slice(2), {
|
|
icons: { type: 'string' }, stamp: { type: 'string' }, 'native-deps': { type: 'string' },
|
|
typecheck: { type: 'boolean' }, platform: { type: 'string' },
|
|
})
|
|
await buildDesktop({ ...args, nativeDeps: args['native-deps'] })
|
|
}
|