build-exe-for-python-sdk.spec.ts 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import { spawnSync } from 'node:child_process'
  2. import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { dirname, join, resolve } from 'node:path'
  5. import { afterEach, describe, expect, it } from 'vitest'
  6. const root = resolve(import.meta.dirname, '..')
  7. const script = resolve(root, 'scripts/build-exe-for-python-sdk.ts')
  8. const temporaryDirectories: string[] = []
  9. afterEach(() => {
  10. for (const directory of temporaryDirectories.splice(0)) {
  11. rmSync(directory, { recursive: true, force: true })
  12. }
  13. })
  14. function run(env: NodeJS.ProcessEnv, ...args: string[]) {
  15. return spawnSync(process.execPath, ['--import', 'tsx/esm', script, ...args], {
  16. cwd: root,
  17. encoding: 'utf8',
  18. env: isolatedPnpmEnvironment(env),
  19. })
  20. }
  21. describe('Python runtime executable builder CLI', () => {
  22. it('keeps the single-file dispatcher on the Python packaging surface', () => {
  23. const bootstrapPath = resolve(root, 'python/sdk-runtime/runtime-bootstrap.mjs')
  24. const bootstrap = readFileSync(bootstrapPath, 'utf8')
  25. const cliConfig = readFileSync(resolve(root, 'apps/cli/tsdown.config.ts'), 'utf8')
  26. const cliTsconfig = readFileSync(resolve(root, 'apps/cli/tsconfig.json'), 'utf8')
  27. const cliManifest = JSON.parse(readFileSync(resolve(root, 'apps/cli/package.json'), 'utf8')) as {
  28. dependencies?: Record<string, string>
  29. devDependencies?: Record<string, string>
  30. }
  31. const runtimeManifest = JSON.parse(readFileSync(resolve(root, 'python/sdk-runtime/package.json'), 'utf8')) as {
  32. dependencies?: Record<string, string>
  33. }
  34. expect(existsSync(resolve(root, 'apps/cli/src/runtime-bootstrap.ts'))).toBe(false)
  35. expect(cliConfig).not.toContain('runtime-bootstrap')
  36. expect(cliConfig).toContain("clean: ['lib/*.js']")
  37. expect(cliTsconfig).not.toContain('packages/subprocess/subprocess-local')
  38. expect(cliManifest.dependencies).not.toHaveProperty('@deepseek-ai/dsh-subprocess-local')
  39. expect(cliManifest.devDependencies).toHaveProperty('@deepseek-ai/dsh-subprocess-local')
  40. expect(runtimeManifest.dependencies).toHaveProperty('@deepseek-ai/dsh-subprocess-local')
  41. expect(bootstrap).toContain("import('@deepseek-ai/dsh/lib/bin.js')")
  42. expect(bootstrap).toContain('await runCli()')
  43. expect(bootstrap).toContain("import('@deepseek-ai/dsh-subprocess-local/runner')")
  44. expect(bootstrap).toContain('await runSelectedSubprocessRunner(selection)')
  45. })
  46. it('runs pnpm through its JavaScript entrypoint without a command shell', () => {
  47. const result = run(
  48. { npm_execpath: 'C:\\tools\\pnpm.cjs' },
  49. '--skip-build',
  50. '--dry-run',
  51. '--targets=node24-macos-arm64',
  52. )
  53. expect(result.status).toBe(0)
  54. expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs run verify-runtime-closure`)
  55. expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs --filter dsh-python-runtime-closure deploy`)
  56. expect(result.stdout).not.toContain(resolve(root, 'python/sdk-runtime/runtime-bootstrap.mjs'))
  57. expect(result.stdout).toContain('"bin":"runtime-bootstrap.mjs"')
  58. expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs exec pkg`)
  59. expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
  60. })
  61. it('resolves the pnpm package behind a Windows command shim', () => {
  62. const setup = mkdtempSync(join(tmpdir(), 'dsh-pnpm-home-'))
  63. temporaryDirectories.push(setup)
  64. const home = join(setup, 'node_modules', '.bin')
  65. const entrypoint = join(setup, 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')
  66. mkdirSync(home, { recursive: true })
  67. mkdirSync(dirname(entrypoint), { recursive: true })
  68. writeFileSync(entrypoint, '')
  69. const result = run(
  70. { npm_execpath: 'C:\\tools\\pnpm.cmd', PNPM_HOME: home },
  71. '--skip-build',
  72. '--dry-run',
  73. '--targets=node24-macos-arm64',
  74. )
  75. expect(result.status).toBe(0)
  76. expect(result.stdout).toContain(`${process.execPath} ${entrypoint} run verify-runtime-closure`)
  77. expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
  78. })
  79. it('accepts the macOS x64 pkg target', () => {
  80. const result = run(
  81. { npm_execpath: 'C:\\tools\\pnpm.cjs' },
  82. '--skip-build',
  83. '--dry-run',
  84. '--targets=node24-macos-x64',
  85. )
  86. expect(result.status).toBe(0)
  87. expect(result.stdout).toContain('exec pkg')
  88. expect(result.stdout).toContain('--sea --targets node24-macos-x64')
  89. })
  90. it('rejects a Windows arm64 product before any build step', () => {
  91. const result = run(
  92. { npm_execpath: 'C:\\tools\\pnpm.cjs' },
  93. '--skip-build',
  94. '--dry-run',
  95. '--targets=node24-win-arm64',
  96. )
  97. expect(result.status).not.toBe(0)
  98. expect(result.stderr).toContain('Windows supports x64 only')
  99. expect(result.stdout).toBe('')
  100. })
  101. })
  102. function isolatedPnpmEnvironment(overrides: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
  103. const environment = Object.fromEntries(
  104. Object.entries(process.env).filter(([key]) => !['npm_execpath', 'pnpm_home'].includes(key.toLowerCase())),
  105. )
  106. return { ...environment, ...overrides }
  107. }