prepare-runtime.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. /** Download and verify the upstream Node.js runtime and copy the pinned pnpm CLI. */
  2. import { createHash } from 'node:crypto'
  3. import { spawnSync } from 'node:child_process'
  4. import { cpSync, createReadStream, createWriteStream, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  5. import { chmod, readFile } from 'node:fs/promises'
  6. import { createRequire } from 'node:module'
  7. import { dirname, join } from 'node:path'
  8. import { pipeline } from 'node:stream/promises'
  9. import extractZip from 'extract-zip'
  10. import { extract } from 'tar'
  11. import { resolveDesktopTargetBuildPaths } from './desktop-build-paths.mjs'
  12. const NODE_VERSION = '24.17.0'
  13. const BUILD_PATHS = resolveDesktopTargetBuildPaths()
  14. const RUNTIME_ROOT = BUILD_PATHS.runtime
  15. const DOWNLOAD_ROOT = BUILD_PATHS.downloads
  16. type RuntimePlatform = 'darwin' | 'linux' | 'win'
  17. type RuntimeArch = 'arm64' | 'x64'
  18. function target(): { platform: RuntimePlatform; arch: RuntimeArch } {
  19. const rawPlatform = process.env.DSH_DESKTOP_TARGET_PLATFORM ?? process.env.npm_config_platform ?? process.platform
  20. const rawArch = process.env.DSH_DESKTOP_TARGET_ARCH ?? process.env.npm_config_arch ?? process.arch
  21. const platform = rawPlatform === 'win32' ? 'win' : rawPlatform
  22. if (platform !== 'darwin' && platform !== 'linux' && platform !== 'win') {
  23. throw new Error(`desktop runtime: unsupported platform ${rawPlatform}`)
  24. }
  25. if (rawArch !== 'arm64' && rawArch !== 'x64') throw new Error(`desktop runtime: unsupported architecture ${rawArch}`)
  26. return { platform, arch: rawArch }
  27. }
  28. async function download(url: string, path: string): Promise<void> {
  29. const response = await fetch(url)
  30. if (!response.ok) throw new Error(`desktop runtime: ${url} returned HTTP ${String(response.status)}`)
  31. writeFileSync(path, new Uint8Array(await response.arrayBuffer()), { mode: 0o600 })
  32. }
  33. async function prepareNode(platform: RuntimePlatform, arch: RuntimeArch): Promise<void> {
  34. const extension = platform === 'win' ? 'zip' : 'tar.gz'
  35. const folder = `node-v${NODE_VERSION}-${platform}-${arch}`
  36. const archiveName = `${folder}.${extension}`
  37. const releaseRoot = `https://nodejs.org/download/release/v${NODE_VERSION}`
  38. const archive = join(DOWNLOAD_ROOT, archiveName)
  39. const sums = join(DOWNLOAD_ROOT, `node-v${NODE_VERSION}-SHASUMS256.txt`)
  40. if (!existsSync(archive)) await download(`${releaseRoot}/${archiveName}`, archive)
  41. if (!existsSync(sums)) await download(`${releaseRoot}/SHASUMS256.txt`, sums)
  42. const line = (await readFile(sums, 'utf8')).split(/\r?\n/u)
  43. .find(candidate => candidate.endsWith(` ${archiveName}`))
  44. if (line === undefined) throw new Error(`desktop runtime: ${archiveName} is absent from Node.js SHASUMS256.txt`)
  45. const expected = line.split(/\s+/u)[0]
  46. const actual = createHash('sha256').update(await readFile(archive)).digest('hex')
  47. if (actual !== expected) throw new Error(`desktop runtime: checksum mismatch for ${archiveName}`)
  48. const extraction = BUILD_PATHS.nodeExtract
  49. rmSync(extraction, { recursive: true, force: true })
  50. mkdirSync(extraction, { recursive: true })
  51. if (platform === 'win') await extractZip(archive, { dir: extraction })
  52. else await extract({ cwd: extraction, file: archive })
  53. const source = join(extraction, folder, platform === 'win' ? 'node.exe' : 'bin/node')
  54. const destinationRoot = join(RUNTIME_ROOT, 'node')
  55. const destination = join(destinationRoot, platform === 'win' ? 'node.exe' : 'node')
  56. rmSync(destinationRoot, { recursive: true, force: true })
  57. mkdirSync(destinationRoot, { recursive: true })
  58. // A fresh write prevents macOS from retaining invalid code-signature vnode state from a tar-extracted Mach-O clone.
  59. await pipeline(createReadStream(source), createWriteStream(destination, { flags: 'wx' }))
  60. if (platform !== 'win') await chmod(destination, 0o755)
  61. const hostPlatform = process.platform === 'win32' ? 'win' : process.platform
  62. const hostCanExecute = platform === hostPlatform
  63. && (arch === process.arch || (platform === 'darwin' && arch === 'x64' && process.arch === 'arm64'))
  64. if (hostCanExecute) {
  65. const result = spawnSync(destination, ['--version'], { encoding: 'utf8' })
  66. if (result.error !== undefined || result.status !== 0 || result.stdout.trim() !== `v${NODE_VERSION}`) {
  67. const detail = result.error?.message ?? result.signal ?? result.stderr.trim()
  68. const outcome = detail === '' ? `exit ${String(result.status)}` : detail
  69. throw new Error(`desktop runtime: prepared Node.js ${NODE_VERSION} failed executable verification: ${outcome}`)
  70. }
  71. }
  72. rmSync(extraction, { recursive: true, force: true })
  73. }
  74. function preparePnpm(): string {
  75. const require = createRequire(import.meta.url)
  76. const manifestPath = require.resolve('pnpm')
  77. const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { version?: unknown }
  78. if (typeof manifest.version !== 'string') throw new Error('desktop runtime: pnpm manifest has no version')
  79. const packageDir = dirname(manifestPath)
  80. const destination = join(RUNTIME_ROOT, 'pnpm')
  81. rmSync(destination, { recursive: true, force: true })
  82. cpSync(packageDir, destination, { recursive: true })
  83. return manifest.version
  84. }
  85. async function main(): Promise<void> {
  86. const { platform, arch } = target()
  87. mkdirSync(DOWNLOAD_ROOT, { recursive: true })
  88. mkdirSync(RUNTIME_ROOT, { recursive: true })
  89. await prepareNode(platform, arch)
  90. const pnpmVersion = preparePnpm()
  91. writeFileSync(join(RUNTIME_ROOT, 'versions.json'), `${JSON.stringify({
  92. schemaVersion: 1,
  93. node: NODE_VERSION,
  94. pnpm: pnpmVersion,
  95. }, undefined, 2)}\n`)
  96. }
  97. await main()