verify-runtime-closure.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. /**
  2. * Verify that the Python single-exe deploy manifest explicitly supplies every
  3. * required workspace peer of every workspace package in its dependency graph.
  4. *
  5. * `pnpm deploy --config.auto-install-peers=false` cannot repair an incomplete
  6. * runtime root. Keeping the peer at the root also prevents a successful build
  7. * from producing an executable that fails only when Cordis loads the plugin.
  8. */
  9. import { readFile, readdir } from 'node:fs/promises'
  10. import { join, resolve } from 'node:path'
  11. import { parseArgs } from 'node:util'
  12. interface PackageManifest {
  13. name?: string
  14. dependencies?: Record<string, string>
  15. optionalDependencies?: Record<string, string>
  16. peerDependencies?: Record<string, string>
  17. peerDependenciesMeta?: Record<string, { optional?: boolean }>
  18. }
  19. interface WorkspacePackage {
  20. path: string
  21. manifest: PackageManifest
  22. }
  23. const root = resolve(import.meta.dirname, '..')
  24. const { values } = parseArgs({
  25. args: process.argv.slice(2),
  26. options: { manifest: { type: 'string' } },
  27. })
  28. const runtimeManifestPath = resolve(root, values.manifest ?? 'python/sdk-runtime/package.json')
  29. const runtimeManifest = await loadManifest(runtimeManifestPath)
  30. const runtimeName = runtimeManifest.name ?? 'python/sdk-runtime'
  31. const workspace = await loadWorkspacePackages()
  32. const runtimeDependencies = runtimeManifest.dependencies ?? {}
  33. const parents = new Map<string, string | undefined>()
  34. const queue: string[] = []
  35. for (const dependency of Object.keys(runtimeDependencies).sort()) {
  36. if (!workspace.has(dependency)) continue
  37. parents.set(dependency, undefined)
  38. queue.push(dependency)
  39. }
  40. const failures: string[] = []
  41. for (let index = 0; index < queue.length; index += 1) {
  42. const packageName = queue[index]
  43. if (packageName === undefined) continue
  44. const current = workspace.get(packageName)
  45. if (current === undefined) continue
  46. const peers = current.manifest.peerDependencies ?? {}
  47. const peerMeta = current.manifest.peerDependenciesMeta ?? {}
  48. for (const peer of Object.keys(peers).sort()) {
  49. if (!workspace.has(peer) || peerMeta[peer]?.optional === true) continue
  50. if (runtimeDependencies[peer]?.startsWith('workspace:') === true) continue
  51. failures.push(`${formatChain(runtimeName, packageName, parents)} -> ${peer}`)
  52. }
  53. const dependencies = {
  54. ...current.manifest.dependencies,
  55. ...current.manifest.optionalDependencies,
  56. }
  57. for (const dependency of Object.keys(dependencies).sort()) {
  58. if (!workspace.has(dependency) || parents.has(dependency)) continue
  59. parents.set(dependency, packageName)
  60. queue.push(dependency)
  61. }
  62. }
  63. if (failures.length > 0) {
  64. console.error('verify-runtime-closure: required workspace peers are missing from python/sdk-runtime dependencies:')
  65. for (const failure of failures) console.error(` ${failure}`)
  66. process.exit(1)
  67. }
  68. console.log(`verify-runtime-closure: ${queue.length} workspace packages form a closed runtime dependency graph.`)
  69. async function loadWorkspacePackages(): Promise<Map<string, WorkspacePackage>> {
  70. const paths: string[] = []
  71. for (const group of await childDirectories(join(root, 'packages'))) {
  72. for (const packageDir of await childDirectories(join(root, 'packages', group))) {
  73. paths.push(join(root, 'packages', group, packageDir, 'package.json'))
  74. }
  75. }
  76. for (const packageDir of await childDirectories(join(root, 'vendor'))) {
  77. paths.push(join(root, 'vendor', packageDir, 'package.json'))
  78. }
  79. const result = new Map<string, WorkspacePackage>()
  80. for (const path of paths) {
  81. const manifest = await loadManifest(path)
  82. if (manifest.name !== undefined) result.set(manifest.name, { path, manifest })
  83. }
  84. return result
  85. }
  86. async function childDirectories(path: string): Promise<string[]> {
  87. const entries = await readdir(path, { withFileTypes: true })
  88. return entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort()
  89. }
  90. async function loadManifest(path: string): Promise<PackageManifest> {
  91. return JSON.parse(await readFile(path, 'utf8')) as PackageManifest
  92. }
  93. function formatChain(
  94. runtimeName: string,
  95. packageName: string,
  96. parents: ReadonlyMap<string, string | undefined>,
  97. ): string {
  98. const chain = [packageName]
  99. let parent = parents.get(packageName)
  100. while (parent !== undefined) {
  101. chain.unshift(parent)
  102. parent = parents.get(parent)
  103. }
  104. return [runtimeName, ...chain].join(' -> ')
  105. }