| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285 |
- /** Build one release target with matching Electron, Node.js, and seed architecture. */
- import { spawn } from 'node:child_process'
- import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
- import { parseArgs } from 'node:util'
- import { join, resolve } from 'node:path'
- import {
- desktopBuildRecordFilename,
- resolveDesktopAutoUpdateConfig,
- } from './desktop-auto-update-environment.mjs'
- import { desktopTargetBuildPaths } from './desktop-build-paths.mjs'
- const APP_ROOT = resolve(import.meta.dirname, '..')
- const REPOSITORY_ROOT = resolve(APP_ROOT, '..', '..')
- const WINDOWS_SIGNING_ENV_PREFIX = 'DSH_DESKTOP_WINDOWS_'
- const WINDOWS_SIGNING_ENV_NAMES = [
- 'DSH_DESKTOP_WINDOWS_CER_FILE',
- 'DSH_DESKTOP_WINDOWS_KEY_CONTAINER',
- 'DSH_DESKTOP_WINDOWS_SIGNTOOL',
- 'DSH_DESKTOP_WINDOWS_TOKEN_PIN',
- ] as const
- const DESKTOP_UPLOAD_CREDENTIAL_ENV_NAMES = new Set([
- 'DOWNLOAD_TEST_COS_SECRET_ID',
- 'DOWNLOAD_TEST_COS_SECRET_KEY',
- 'DOWNLOAD_PROD_COS_SECRET_ID',
- 'DOWNLOAD_PROD_COS_SECRET_KEY',
- ])
- /** Fixed platform and architecture identifiers exposed by package scripts. */
- export type DesktopPackageTargetName = 'mac-arm64' | 'mac-x64' | 'win-x64'
- /** One supported release target and its electron-builder selectors. */
- export interface DesktopPackageTarget {
- readonly name: DesktopPackageTargetName
- readonly platform: 'darwin' | 'win32'
- readonly arch: 'arm64' | 'x64'
- readonly builderPlatform: '--mac' | '--win'
- readonly builderArch: '--arm64' | '--x64'
- }
- const TARGETS: Record<DesktopPackageTargetName, DesktopPackageTarget> = {
- 'mac-arm64': {
- name: 'mac-arm64',
- platform: 'darwin',
- arch: 'arm64',
- builderPlatform: '--mac',
- builderArch: '--arm64',
- },
- 'mac-x64': {
- name: 'mac-x64',
- platform: 'darwin',
- arch: 'x64',
- builderPlatform: '--mac',
- builderArch: '--x64',
- },
- 'win-x64': {
- name: 'win-x64',
- platform: 'win32',
- arch: 'x64',
- builderPlatform: '--win',
- builderArch: '--x64',
- },
- }
- /**
- * Remove Windows signing configuration from package preparation subprocesses.
- * @param environment - Packaging command environment.
- * @returns A copy without Windows signing fields.
- */
- export function withoutWindowsSigningEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
- return Object.fromEntries(Object.entries(environment)
- .filter(([name]) => !name.startsWith(WINDOWS_SIGNING_ENV_PREFIX)))
- }
- /**
- * Remove upload-only COS credentials from every packaging subprocess.
- * @param environment - Packaging command environment.
- * @returns A copy without Desktop upload credentials.
- */
- export function withoutDesktopUploadCredentials(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
- return Object.fromEntries(Object.entries(environment)
- .filter(([name]) => !DESKTOP_UPLOAD_CREDENTIAL_ENV_NAMES.has(name)))
- }
- function isTargetName(value: string): value is DesktopPackageTargetName {
- return Object.hasOwn(TARGETS, value)
- }
- function packageVersion(path: string, label: string): string {
- const manifest = JSON.parse(readFileSync(path, 'utf8')) as { version?: unknown }
- if (typeof manifest.version !== 'string' || manifest.version === '') {
- throw new Error(`desktop package: ${label} has no version`)
- }
- return manifest.version
- }
- function writeReleaseRecord(
- target: DesktopPackageTarget,
- environment: NodeJS.ProcessEnv,
- artifactsRoot: string,
- ): void {
- const desktopVersion = packageVersion(join(APP_ROOT, 'package.json'), 'desktop package')
- const dshVersion = packageVersion(join(REPOSITORY_ROOT, 'package.json'), 'dsh package')
- if (desktopVersion !== dshVersion) {
- throw new Error(`desktop package: desktop version ${desktopVersion} does not match dsh version ${dshVersion}`)
- }
- const update = resolveDesktopAutoUpdateConfig(environment, target.platform, target.arch)
- const recordPath = join(artifactsRoot, desktopBuildRecordFilename(target.name))
- const temporaryPath = `${recordPath}.tmp`
- writeFileSync(temporaryPath, `${JSON.stringify({
- schemaVersion: 1,
- target: target.name,
- version: dshVersion,
- environment: update.environment,
- publicUrl: update.publicUrl,
- }, null, 2)}\n`)
- renameSync(temporaryPath, recordPath)
- }
- /**
- * Resolve a named release target and reject hosts that cannot execute its packaged runtime.
- * @param name - One of the fixed Desktop release target names.
- * @param hostPlatform - Build-host Node.js platform.
- * @param hostArch - Build-host Node.js architecture.
- * @returns The target selectors shared by runtime preparation and electron-builder.
- */
- export function resolveDesktopPackageTarget(
- name: string,
- hostPlatform: NodeJS.Platform = process.platform,
- hostArch: string = process.arch,
- ): DesktopPackageTarget {
- if (!isTargetName(name)) {
- throw new Error(`desktop package: unsupported target ${JSON.stringify(name)}; expected ${Object.keys(TARGETS).join(', ')}`)
- }
- const target = TARGETS[name]
- if (target.platform === 'win32' && (hostPlatform !== 'win32' || hostArch !== 'x64')) {
- throw new Error('desktop package: win-x64 requires a Windows x64 build host')
- }
- if (target.platform === 'darwin' && hostPlatform !== 'darwin') {
- throw new Error(`desktop package: ${name} requires a macOS build host`)
- }
- if (name === 'mac-arm64' && hostArch !== 'arm64') {
- throw new Error('desktop package: mac-arm64 requires an Apple Silicon build host')
- }
- if (name === 'mac-x64' && hostArch !== 'arm64' && hostArch !== 'x64') {
- throw new Error('desktop package: mac-x64 requires an Intel Mac or Apple Silicon with Rosetta')
- }
- return target
- }
- interface DesktopPackageInvocation {
- readonly target: DesktopPackageTarget
- readonly directory: boolean
- readonly prepareOnly: boolean
- }
- function hostTargetName(platform: NodeJS.Platform, arch: string): DesktopPackageTargetName {
- const name = `${platform === 'darwin' ? 'mac' : platform === 'win32' ? 'win' : platform}-${arch}`
- if (!isTargetName(name)) throw new Error(`desktop package: unsupported build host ${platform}-${arch}`)
- return name
- }
- /**
- * Parse the fixed-target packaging command line.
- * @param argv - Arguments after the script entry point.
- * @param hostPlatform - Build-host Node.js platform.
- * @param hostArch - Build-host Node.js architecture.
- * @returns The validated target and whether to emit an unpacked directory.
- */
- export function parseDesktopPackageInvocation(
- argv: readonly string[],
- hostPlatform: NodeJS.Platform = process.platform,
- hostArch: string = process.arch,
- ): DesktopPackageInvocation {
- const { values, positionals } = parseArgs({
- args: [...argv],
- allowPositionals: true,
- options: {
- dir: { type: 'boolean', default: false },
- 'prepare-only': { type: 'boolean', default: false },
- },
- })
- if (positionals.length > 1) throw new Error('desktop package: expected at most one target')
- const name = positionals[0] ?? hostTargetName(hostPlatform, hostArch)
- return {
- target: resolveDesktopPackageTarget(name, hostPlatform, hostArch),
- directory: values.dir,
- prepareOnly: values['prepare-only'],
- }
- }
- /**
- * Build the electron-builder command arguments for one validated target.
- * @param target - Supported release target.
- * @param directory - Whether to stop at an unpacked application directory.
- * @returns Arguments that keep publishing under the separate validated upload command.
- */
- export function desktopElectronBuilderArguments(
- target: DesktopPackageTarget,
- directory: boolean,
- ): readonly string[] {
- return [
- 'exec',
- 'electron-builder',
- '--config',
- 'electron-builder.config.mjs',
- target.builderPlatform,
- target.builderArch,
- '--publish',
- 'never',
- ...(directory ? ['--dir'] : []),
- ]
- }
- function runPnpm(
- args: readonly string[],
- env: NodeJS.ProcessEnv = process.env,
- cwd: string = APP_ROOT,
- ): Promise<void> {
- const pnpmEntry = process.env.npm_execpath
- if (pnpmEntry === undefined || pnpmEntry === '') {
- throw new Error('desktop package: invoke this script through a pnpm package command')
- }
- return new Promise((resolvePromise, reject) => {
- const child = spawn(process.execPath, [pnpmEntry, ...args], {
- cwd,
- env,
- stdio: 'inherit',
- })
- child.once('error', reject)
- child.once('close', (code, signal) => {
- if (code === 0) resolvePromise()
- else reject(new Error(`desktop package: pnpm ${args.join(' ')} exited with ${String(code ?? signal)}`))
- })
- })
- }
- async function main(): Promise<void> {
- const invocation = parseDesktopPackageInvocation(process.argv.slice(2))
- const { target } = invocation
- const buildPaths = desktopTargetBuildPaths(target.name)
- const releaseRecordPath = join(buildPaths.artifacts, desktopBuildRecordFilename(target.name))
- if (!invocation.prepareOnly) {
- rmSync(releaseRecordPath, { force: true })
- rmSync(`${releaseRecordPath}.tmp`, { force: true })
- }
- const buildEnv = withoutWindowsSigningEnvironment(withoutDesktopUploadCredentials(process.env))
- const targetEnv: NodeJS.ProcessEnv = {
- ...buildEnv,
- DSH_DESKTOP_TARGET_PLATFORM: target.platform,
- DSH_DESKTOP_TARGET_ARCH: target.arch,
- }
- const electronBuilderEnv = { ...targetEnv }
- for (const name of WINDOWS_SIGNING_ENV_NAMES) {
- if (process.env[name] !== undefined) electronBuilderEnv[name] = process.env[name]
- }
- await runPnpm(['run', 'build:official'], buildEnv, REPOSITORY_ROOT)
- await runPnpm(['run', 'release:pack', '--family', 'dsh', '--out', buildPaths.packedDsh], buildEnv, REPOSITORY_ROOT)
- await runPnpm([
- '--dir',
- 'apps/desktop-host',
- 'pack',
- '--pack-destination',
- buildPaths.packedDsh,
- ], buildEnv, REPOSITORY_ROOT)
- await runPnpm(['run', 'release:pack', '--family', 'vendor', '--out', buildPaths.packedVendor], buildEnv, REPOSITORY_ROOT)
- rmSync(buildPaths.packedLandlock, { recursive: true, force: true })
- mkdirSync(buildPaths.packedLandlock, { recursive: true })
- await runPnpm(['--dir', 'native/system', 'run', 'build:ts'], buildEnv, REPOSITORY_ROOT)
- await runPnpm([
- '--dir',
- 'native/system/packages/entry',
- 'pack',
- '--pack-destination',
- buildPaths.packedLandlock,
- ], buildEnv, REPOSITORY_ROOT)
- await runPnpm(['run', 'prepare:runtime'], targetEnv)
- await runPnpm(['run', 'prepare:packages'], targetEnv)
- await runPnpm(['run', 'prepare:seed'], targetEnv)
- if (invocation.prepareOnly) return
- await runPnpm(desktopElectronBuilderArguments(target, invocation.directory), electronBuilderEnv)
- if (!invocation.directory) writeReleaseRecord(target, electronBuilderEnv, buildPaths.artifacts)
- }
- if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) await main()
|