prepare-dsh.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. /** Materialize the complete production runtime before publishing Desktop resources. */
  2. import { spawn, execFile } from 'node:child_process'
  3. import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  4. import { tmpdir } from 'node:os'
  5. import { delimiter, join, relative, resolve } from 'node:path'
  6. import { desktopNodeEnvironment } from '../src/node-environment.ts'
  7. import { createRuntimeProjectMetadata } from '../src/project-manager.ts'
  8. import { DESKTOP_HOST_PROTOCOL_VERSION } from '../src/host-protocol.ts'
  9. import { parseDesktopRelease, type DesktopRelease } from '../src/release.ts'
  10. import {
  11. DESKTOP_HOST_PACKAGE,
  12. DESKTOP_HOST_RUNTIME_FILES,
  13. DESKTOP_PACKAGES_DIR,
  14. DESKTOP_PACKAGE_SET_FILE,
  15. readDesktopCorePackageSet,
  16. verifyDesktopCoreLockfile,
  17. } from '../src/core-package-set.ts'
  18. import { smokePrimaryRuntime } from './prepare-primary-runtime.ts'
  19. import { smokeDesktopRuntime } from './smoke-runtime.ts'
  20. import { writeDesktopRuntime, verifyDesktopRuntime } from '../src/runtime-tree.ts'
  21. import {
  22. resolveDesktopAppId,
  23. resolveMacOSSigningEnvironment,
  24. } from './desktop-release-environment.mjs'
  25. import {
  26. signMacOSRuntime,
  27. } from './macos-runtime.ts'
  28. import { resolveDesktopBuildTarget, resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
  29. import { desktopRuntimeFileExclusion } from './runtime-file-policy.ts'
  30. import { selectOfficeEngine } from '../../../scripts/libreoffice-engine.ts'
  31. const APP_ROOT = resolve(import.meta.dirname, '..')
  32. const BUILD_PATHS = resolveDesktopTargetBuildPaths()
  33. const DSH_OUTPUT_ROOT = BUILD_PATHS.dsh
  34. const BUILD_ROOT = mkdtempSync(join(tmpdir(), 'dsh-desktop-runtime-'))
  35. const STORE_ROOT = join(BUILD_ROOT, 'store')
  36. const RUNTIME_ROOT = BUILD_PATHS.runtime
  37. const PNPM_BUILD_STATE = BUILD_PATHS.dshPnpm
  38. const PACKAGE_SET_ROOT = BUILD_PATHS.packageSet
  39. const NODE = join(BUILD_PATHS.electron, process.platform === 'win32' ? 'electron.exe' : 'Electron.app/Contents/MacOS/Electron')
  40. const PNPM = join(RUNTIME_ROOT, 'pnpm', 'bin', 'pnpm.mjs')
  41. function manifestVersion(path: string, subject: string): string {
  42. const manifest = JSON.parse(readFileSync(path, 'utf8')) as { version?: unknown }
  43. if (typeof manifest.version !== 'string') throw new Error(`desktop runtime: ${subject} has no version`)
  44. return manifest.version
  45. }
  46. function desktopRelease(): DesktopRelease {
  47. const version = manifestVersion(join(APP_ROOT, 'package.json'), 'desktop package')
  48. const dshVersion = manifestVersion(resolve(APP_ROOT, '..', '..', 'package.json'), 'root dsh package')
  49. if (version !== dshVersion) {
  50. throw new Error(`desktop runtime: Electron ${version} must bind the same version of @deepseek-ai/dsh, found ${dshVersion}`)
  51. }
  52. const runtime = JSON.parse(readFileSync(join(RUNTIME_ROOT, 'versions.json'), 'utf8')) as Record<string, unknown>
  53. return parseDesktopRelease({
  54. schemaVersion: 1,
  55. version,
  56. hostProtocolVersion: DESKTOP_HOST_PROTOCOL_VERSION,
  57. nodeVersion: runtime.node,
  58. pnpmVersion: runtime.pnpm,
  59. })
  60. }
  61. function runPnpm(args: readonly string[]): Promise<void> {
  62. return new Promise((resolvePromise, reject) => {
  63. const [command, ...commandArgs] = args
  64. if (command === undefined) throw new Error('desktop runtime: pnpm command is required')
  65. const config = join(PNPM_BUILD_STATE, 'config')
  66. const userConfig = join(config, 'npmrc')
  67. mkdirSync(config, { recursive: true })
  68. writeFileSync(userConfig, '')
  69. const child = spawn(NODE, [
  70. '--expose-internals',
  71. PNPM,
  72. '--config.registry=https://registry.npmjs.org/',
  73. `--config.store-dir=${STORE_ROOT}`,
  74. '--config.enable-global-virtual-store=false',
  75. `--config.userconfig=${userConfig}`,
  76. command,
  77. ...commandArgs,
  78. ], {
  79. cwd: BUILD_ROOT,
  80. env: {
  81. ...Object.fromEntries(Object.entries(process.env).filter(([name]) => (
  82. name !== 'NODE_OPTIONS' && name !== 'NODE_PATH' && !/^DSH_DESKTOP_/u.test(name) && !/^(?:npm|pnpm|corepack)_/iu.test(name)
  83. ))),
  84. NPM_CONFIG_REGISTRY: 'https://registry.npmjs.org/',
  85. NPM_CONFIG_STORE_DIR: STORE_ROOT,
  86. NPM_CONFIG_USERCONFIG: userConfig,
  87. ...desktopNodeEnvironment(NODE, join(RUNTIME_ROOT, 'bin'), {}),
  88. PATH: `${join(RUNTIME_ROOT, 'bin')}${delimiter}${process.env.PATH ?? ''}`,
  89. XDG_CACHE_HOME: join(PNPM_BUILD_STATE, 'cache'),
  90. XDG_CONFIG_HOME: config,
  91. XDG_STATE_HOME: join(PNPM_BUILD_STATE, 'state'),
  92. },
  93. stdio: 'inherit',
  94. })
  95. child.once('error', reject)
  96. child.once('close', (code, signal) => {
  97. if (code === 0) resolvePromise()
  98. else reject(new Error(`desktop runtime: pnpm exited with ${String(code ?? signal)}`))
  99. })
  100. })
  101. }
  102. async function main(): Promise<void> {
  103. rmSync(DSH_OUTPUT_ROOT, { recursive: true, force: true })
  104. rmSync(PNPM_BUILD_STATE, { recursive: true, force: true })
  105. mkdirSync(STORE_ROOT, { recursive: true })
  106. try {
  107. const release = desktopRelease()
  108. copyFileSync(join(PACKAGE_SET_ROOT, DESKTOP_PACKAGE_SET_FILE), join(BUILD_ROOT, DESKTOP_PACKAGE_SET_FILE))
  109. cpSync(join(PACKAGE_SET_ROOT, DESKTOP_PACKAGES_DIR), join(BUILD_ROOT, DESKTOP_PACKAGES_DIR), { recursive: true })
  110. createRuntimeProjectMetadata(BUILD_ROOT, release)
  111. await runPnpm(['install', '--lockfile-only'])
  112. verifyDesktopCoreLockfile(
  113. readFileSync(join(BUILD_ROOT, 'pnpm-lock.yaml'), 'utf8'),
  114. readDesktopCorePackageSet(BUILD_ROOT, release.version),
  115. )
  116. await runPnpm(['install', '--prod', '--frozen-lockfile', '--trust-lockfile'])
  117. const packageSet = readDesktopCorePackageSet(BUILD_ROOT, release.version)
  118. const targetName = resolveDesktopBuildTarget()
  119. const target = { platform: process.platform, arch: targetName.endsWith('arm64') ? 'arm64' : 'x64' }
  120. const modules = join(BUILD_ROOT, 'node_modules')
  121. const officeManifest = JSON.parse(readFileSync(join(modules, '@deepseek-ai/libreoffice-kit/package.json'), 'utf8'))
  122. const officeEngine = selectOfficeEngine(officeManifest, target)
  123. mkdirSync(DSH_OUTPUT_ROOT, { recursive: true })
  124. cpSync(modules, join(DSH_OUTPUT_ROOT, 'node_modules'), {
  125. recursive: true, dereference: true,
  126. filter: source => desktopRuntimeFileExclusion(relative(modules, source), target, officeEngine) === undefined,
  127. })
  128. writeFileSync(join(DSH_OUTPUT_ROOT, 'package.json'), `${JSON.stringify({
  129. name: '@deepseek-ai/dsh-desktop-runtime', private: true, version: release.version, type: 'module',
  130. dependencies: Object.fromEntries(packageSet.packages.map(entry => [entry.name, entry.version])),
  131. }, undefined, 2)}\n`)
  132. for (const file of DESKTOP_HOST_RUNTIME_FILES) {
  133. if (!existsSync(join(DSH_OUTPUT_ROOT, 'node_modules', DESKTOP_HOST_PACKAGE, file))) {
  134. throw new Error(`desktop runtime: missing private Host file ${file}`)
  135. }
  136. }
  137. if (!existsSync(join(DSH_OUTPUT_ROOT, 'node_modules', '@deepseek-ai', `libreoffice-kit-${officeEngine}`, 'prebuilds.json'))) {
  138. throw new Error(`desktop runtime: missing required LibreOffice engine ${officeEngine}`)
  139. }
  140. if (process.platform === 'darwin') {
  141. await signMacOSRuntime(DSH_OUTPUT_ROOT, resolveDesktopAppId(process.env), resolveMacOSSigningEnvironment(process.env))
  142. await signMacOSRuntime(join(RUNTIME_ROOT, 'primary-runtime'), resolveDesktopAppId(process.env), resolveMacOSSigningEnvironment(process.env))
  143. }
  144. smokePrimaryRuntime(join(RUNTIME_ROOT, 'primary-runtime'))
  145. writeDesktopRuntime(DSH_OUTPUT_ROOT, release, packageSet.packages.map(entry => entry.name), target)
  146. const descriptor = await verifyDesktopRuntime(DSH_OUTPUT_ROOT, release.version, target)
  147. await new Promise<void>((accept, reject) => {
  148. execFile(NODE, ['--expose-internals', join(APP_ROOT, 'tests/fixtures/runtime-payload-smoke.mjs'), DSH_OUTPUT_ROOT],
  149. { timeout: 120_000, env: desktopNodeEnvironment(NODE, join(RUNTIME_ROOT, 'bin'), { ...process.env, NODE_OPTIONS: '' }) }, (error, stdout, stderr) => {
  150. if (error !== null) reject(new Error(`desktop native payload smoke failed: ${stderr}`, { cause: error }))
  151. else { process.stdout.write(stdout); accept() }
  152. })
  153. })
  154. await smokeDesktopRuntime(DSH_OUTPUT_ROOT, NODE, descriptor)
  155. await verifyDesktopRuntime(DSH_OUTPUT_ROOT, release.version, target)
  156. } catch (error) {
  157. rmSync(DSH_OUTPUT_ROOT, { recursive: true, force: true })
  158. throw error
  159. } finally {
  160. rmSync(BUILD_ROOT, { recursive: true, force: true })
  161. rmSync(PNPM_BUILD_STATE, { recursive: true, force: true })
  162. }
  163. }
  164. await main()