package-target.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. /** Build one release target with matching Electron, Node.js, and seed architecture. */
  2. import { spawn } from 'node:child_process'
  3. import { mkdirSync, rmSync } from 'node:fs'
  4. import { parseArgs } from 'node:util'
  5. import { join, resolve } from 'node:path'
  6. const APP_ROOT = resolve(import.meta.dirname, '..')
  7. const REPOSITORY_ROOT = resolve(APP_ROOT, '..', '..')
  8. const DSH_PACK_ROOT = join(REPOSITORY_ROOT, 'dist', 'npm')
  9. const VENDOR_PACK_ROOT = join(REPOSITORY_ROOT, 'dist', 'npm-vendor')
  10. const LANDLOCK_PACK_ROOT = join(REPOSITORY_ROOT, 'dist', 'npm-landlock')
  11. const WINDOWS_SIGNING_ENV_PREFIX = 'DSH_DESKTOP_WINDOWS_'
  12. const WINDOWS_SIGNING_ENV_NAMES = [
  13. 'DSH_DESKTOP_WINDOWS_CER_FILE',
  14. 'DSH_DESKTOP_WINDOWS_KEY_CONTAINER',
  15. 'DSH_DESKTOP_WINDOWS_SIGNTOOL',
  16. 'DSH_DESKTOP_WINDOWS_TOKEN_PIN',
  17. ] as const
  18. /** Fixed platform and architecture identifiers exposed by package scripts. */
  19. export type DesktopPackageTargetName = 'mac-arm64' | 'mac-x64' | 'win-x64'
  20. /** One supported release target and its electron-builder selectors. */
  21. export interface DesktopPackageTarget {
  22. readonly name: DesktopPackageTargetName
  23. readonly platform: 'darwin' | 'win32'
  24. readonly arch: 'arm64' | 'x64'
  25. readonly builderPlatform: '--mac' | '--win'
  26. readonly builderArch: '--arm64' | '--x64'
  27. }
  28. const TARGETS: Record<DesktopPackageTargetName, DesktopPackageTarget> = {
  29. 'mac-arm64': {
  30. name: 'mac-arm64',
  31. platform: 'darwin',
  32. arch: 'arm64',
  33. builderPlatform: '--mac',
  34. builderArch: '--arm64',
  35. },
  36. 'mac-x64': {
  37. name: 'mac-x64',
  38. platform: 'darwin',
  39. arch: 'x64',
  40. builderPlatform: '--mac',
  41. builderArch: '--x64',
  42. },
  43. 'win-x64': {
  44. name: 'win-x64',
  45. platform: 'win32',
  46. arch: 'x64',
  47. builderPlatform: '--win',
  48. builderArch: '--x64',
  49. },
  50. }
  51. /**
  52. * Remove Windows signing configuration from package preparation subprocesses.
  53. * @param environment - Packaging command environment.
  54. * @returns A copy without Windows signing fields.
  55. */
  56. export function withoutWindowsSigningEnvironment(environment: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
  57. return Object.fromEntries(Object.entries(environment)
  58. .filter(([name]) => !name.startsWith(WINDOWS_SIGNING_ENV_PREFIX)))
  59. }
  60. function isTargetName(value: string): value is DesktopPackageTargetName {
  61. return Object.hasOwn(TARGETS, value)
  62. }
  63. /**
  64. * Resolve a named release target and reject hosts that cannot execute its packaged runtime.
  65. * @param name - One of the fixed Desktop release target names.
  66. * @param hostPlatform - Build-host Node.js platform.
  67. * @param hostArch - Build-host Node.js architecture.
  68. * @returns The target selectors shared by runtime preparation and electron-builder.
  69. */
  70. export function resolveDesktopPackageTarget(
  71. name: string,
  72. hostPlatform: NodeJS.Platform = process.platform,
  73. hostArch: string = process.arch,
  74. ): DesktopPackageTarget {
  75. if (!isTargetName(name)) {
  76. throw new Error(`desktop package: unsupported target ${JSON.stringify(name)}; expected ${Object.keys(TARGETS).join(', ')}`)
  77. }
  78. const target = TARGETS[name]
  79. if (target.platform === 'win32' && (hostPlatform !== 'win32' || hostArch !== 'x64')) {
  80. throw new Error('desktop package: win-x64 requires a Windows x64 build host')
  81. }
  82. if (target.platform === 'darwin' && hostPlatform !== 'darwin') {
  83. throw new Error(`desktop package: ${name} requires a macOS build host`)
  84. }
  85. if (name === 'mac-arm64' && hostArch !== 'arm64') {
  86. throw new Error('desktop package: mac-arm64 requires an Apple Silicon build host')
  87. }
  88. if (name === 'mac-x64' && hostArch !== 'arm64' && hostArch !== 'x64') {
  89. throw new Error('desktop package: mac-x64 requires an Intel Mac or Apple Silicon with Rosetta')
  90. }
  91. return target
  92. }
  93. interface DesktopPackageInvocation {
  94. readonly target: DesktopPackageTarget
  95. readonly directory: boolean
  96. readonly prepareOnly: boolean
  97. }
  98. function hostTargetName(platform: NodeJS.Platform, arch: string): DesktopPackageTargetName {
  99. const name = `${platform === 'darwin' ? 'mac' : platform === 'win32' ? 'win' : platform}-${arch}`
  100. if (!isTargetName(name)) throw new Error(`desktop package: unsupported build host ${platform}-${arch}`)
  101. return name
  102. }
  103. /**
  104. * Parse the fixed-target packaging command line.
  105. * @param argv - Arguments after the script entry point.
  106. * @param hostPlatform - Build-host Node.js platform.
  107. * @param hostArch - Build-host Node.js architecture.
  108. * @returns The validated target and whether to emit an unpacked directory.
  109. */
  110. export function parseDesktopPackageInvocation(
  111. argv: readonly string[],
  112. hostPlatform: NodeJS.Platform = process.platform,
  113. hostArch: string = process.arch,
  114. ): DesktopPackageInvocation {
  115. const { values, positionals } = parseArgs({
  116. args: [...argv],
  117. allowPositionals: true,
  118. options: {
  119. dir: { type: 'boolean', default: false },
  120. 'prepare-only': { type: 'boolean', default: false },
  121. },
  122. })
  123. if (positionals.length > 1) throw new Error('desktop package: expected at most one target')
  124. const name = positionals[0] ?? hostTargetName(hostPlatform, hostArch)
  125. return {
  126. target: resolveDesktopPackageTarget(name, hostPlatform, hostArch),
  127. directory: values.dir,
  128. prepareOnly: values['prepare-only'],
  129. }
  130. }
  131. function runPnpm(
  132. args: readonly string[],
  133. env: NodeJS.ProcessEnv = process.env,
  134. cwd: string = APP_ROOT,
  135. ): Promise<void> {
  136. const pnpmEntry = process.env.npm_execpath
  137. if (pnpmEntry === undefined || pnpmEntry === '') {
  138. throw new Error('desktop package: invoke this script through a pnpm package command')
  139. }
  140. return new Promise((resolvePromise, reject) => {
  141. const child = spawn(process.execPath, [pnpmEntry, ...args], {
  142. cwd,
  143. env,
  144. stdio: 'inherit',
  145. })
  146. child.once('error', reject)
  147. child.once('close', (code, signal) => {
  148. if (code === 0) resolvePromise()
  149. else reject(new Error(`desktop package: pnpm ${args.join(' ')} exited with ${String(code ?? signal)}`))
  150. })
  151. })
  152. }
  153. async function main(): Promise<void> {
  154. const invocation = parseDesktopPackageInvocation(process.argv.slice(2))
  155. const { target } = invocation
  156. const buildEnv = withoutWindowsSigningEnvironment(process.env)
  157. const targetEnv: NodeJS.ProcessEnv = {
  158. ...buildEnv,
  159. DSH_DESKTOP_TARGET_PLATFORM: target.platform,
  160. DSH_DESKTOP_TARGET_ARCH: target.arch,
  161. }
  162. const electronBuilderEnv = { ...targetEnv }
  163. for (const name of WINDOWS_SIGNING_ENV_NAMES) {
  164. if (process.env[name] !== undefined) electronBuilderEnv[name] = process.env[name]
  165. }
  166. await runPnpm(['run', 'build:official'], buildEnv, REPOSITORY_ROOT)
  167. await runPnpm(['run', 'release:pack', '--family', 'dsh', '--out', DSH_PACK_ROOT], buildEnv, REPOSITORY_ROOT)
  168. await runPnpm(['run', 'release:pack', '--family', 'vendor', '--out', VENDOR_PACK_ROOT], buildEnv, REPOSITORY_ROOT)
  169. rmSync(LANDLOCK_PACK_ROOT, { recursive: true, force: true })
  170. mkdirSync(LANDLOCK_PACK_ROOT, { recursive: true })
  171. await runPnpm(['--dir', 'native/landlock-run', 'run', 'build:ts'], buildEnv, REPOSITORY_ROOT)
  172. await runPnpm([
  173. '--dir',
  174. 'native/landlock-run/packages/entry',
  175. 'pack',
  176. '--pack-destination',
  177. LANDLOCK_PACK_ROOT,
  178. ], buildEnv, REPOSITORY_ROOT)
  179. await runPnpm(['run', 'prepare:runtime'], targetEnv)
  180. await runPnpm(['run', 'prepare:packages'], targetEnv)
  181. await runPnpm(['run', 'prepare:seed'], targetEnv)
  182. if (invocation.prepareOnly) return
  183. await runPnpm([
  184. 'exec',
  185. 'electron-builder',
  186. '--config',
  187. 'electron-builder.config.mjs',
  188. target.builderPlatform,
  189. target.builderArch,
  190. ...(invocation.directory ? ['--dir'] : []),
  191. ], electronBuilderEnv)
  192. }
  193. if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) await main()