prepare-installed-update-runtime.ts 4.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. /** Copy a verified runtime into isolated, version-bound qualification resources without signing. */
  2. import { createHash } from 'node:crypto'
  3. import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'
  4. import { join } from 'node:path'
  5. import { runtimePath, verifyDesktopRuntime, writeDesktopRuntime } from '../src/runtime-tree.ts'
  6. import { readInstalledUpdateRun } from './installed-update-qualification.ts'
  7. interface PackageMetadata {
  8. name?: string
  9. version?: string
  10. dependencies?: Record<string, string>
  11. devDependencies?: Record<string, string>
  12. peerDependencies?: Record<string, string>
  13. optionalDependencies?: Record<string, string>
  14. }
  15. /**
  16. * Clone one verified source tree into two synthetic release versions, retaining source bytes unchanged.
  17. * @param manifest The existing test run manifest; both version directories must be absent.
  18. * @param sourceRoot Fresh prepared dsh runtime, never the user's installed application.
  19. * @returns Completion record with descriptor hashes; this is not signed or boot-tested artifact evidence.
  20. */
  21. export async function prepareInstalledUpdateRuntime(manifest: string, sourceRoot: string): Promise<object> {
  22. const run = await readInstalledUpdateRun(manifest)
  23. const receipt = join(run.root, 'runtime-preparation')
  24. await mkdir(receipt)
  25. await writeFile(join(receipt, 'started.json'), `${JSON.stringify({ sourceRoot, time: new Date().toISOString() })}\n`,
  26. { flag: 'wx', mode: 0o600, flush: true })
  27. const results: object[] = []
  28. try {
  29. const source = await verifyDesktopRuntime(sourceRoot, run.source.version)
  30. const sourceHash = createHash('sha256').update(await readFile(join(sourceRoot, 'desktop-runtime.json'))).digest('hex')
  31. await writeFile(join(receipt, 'source.json'), `${JSON.stringify({ sourceHash, version: run.source.version })}\n`,
  32. { flag: 'wx', mode: 0o600, flush: true })
  33. const releaseNames = new Set(source.sharedPackages.filter(entry => entry.version === run.source.version
  34. && (entry.name === '@deepseek-ai/dsh' || entry.name.startsWith('@deepseek-ai/dsh-'))).map(entry => entry.name))
  35. for (const version of run.versions) {
  36. const directory = join(run.root, version)
  37. await mkdir(directory)
  38. const runtime = join(directory, 'dsh')
  39. await cp(sourceRoot, runtime, { recursive: true, force: false, errorOnExist: true })
  40. const paths = [join(runtime, 'package.json'), ...source.sharedPackages.filter(entry => releaseNames.has(entry.name))
  41. .map(entry => join(runtimePath(runtime, entry.path), 'package.json'))]
  42. for (const path of paths) {
  43. const metadata = JSON.parse(await readFile(path, 'utf8')) as PackageMetadata
  44. metadata.version = version
  45. for (const field of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const) {
  46. for (const [name, value] of Object.entries(metadata[field] ?? {})) {
  47. if (releaseNames.has(name) && value === run.source.version) metadata[field]![name] = version
  48. }
  49. }
  50. await writeFile(path, `${JSON.stringify(metadata, null, 2)}\n`)
  51. }
  52. writeDesktopRuntime(runtime, { ...source.release, version }, source.sharedPackages.map(entry => entry.name), source)
  53. const verified = await verifyDesktopRuntime(runtime, version, source)
  54. results.push({ version, runtime, files: verified.files.length, sharedPackages: verified.sharedPackages.length,
  55. descriptorSha256: createHash('sha256').update(await readFile(join(runtime, 'desktop-runtime.json'))).digest('hex') })
  56. }
  57. await verifyDesktopRuntime(sourceRoot, run.source.version, source)
  58. if (createHash('sha256').update(await readFile(join(sourceRoot, 'desktop-runtime.json'))).digest('hex') !== sourceHash) {
  59. throw new Error('installed update: source runtime changed during qualification preparation')
  60. }
  61. const result = { schemaVersion: 1, sourceHash, versions: results, signed: false, bootTested: false }
  62. await writeFile(join(receipt, 'result.json'), `${JSON.stringify(result, null, 2)}\n`, { flag: 'wx', mode: 0o600, flush: true })
  63. return result
  64. } catch (error) {
  65. await writeFile(join(receipt, 'failed.json'), `${JSON.stringify({ failed: true, time: new Date().toISOString(),
  66. completedVersions: results.length, retryAllowed: false })}\n`, { flag: 'wx', mode: 0o600, flush: true })
  67. throw error
  68. }
  69. }