build-exe-for-python-sdk-office.ts 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. /** Keep LibreOffice workers, prebuilt engines, and their dependencies on the real filesystem. */
  2. import { existsSync } from 'node:fs'
  3. import { cp, mkdir, readFile, rm } from 'node:fs/promises'
  4. import { createRequire } from 'node:module'
  5. import { isAbsolute, join, relative, sep } from 'node:path'
  6. import { selectOfficeEngine } from './libreoffice-engine.ts'
  7. /** pkg applies these exclusions to dependency `files` as well as root asset globs. */
  8. export const OFFICE_ASSET_IGNORES = [
  9. '**/node_modules/@deepseek-ai/libreoffice-kit/**',
  10. '**/node_modules/@deepseek-ai/libreoffice-kit-*/**',
  11. ]
  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. os?: string[]
  19. cpu?: string[]
  20. }
  21. /**
  22. * Copy the installed Office dependency tree without changing package contents or executable modes.
  23. * Harness sidecars require the kit's declared target native engine, or WASM for other targets.
  24. * Missing target engines fail with their package name; missing required dependencies or paths outside the deployed closure also fail.
  25. * @param staging - Symlink-free deployed Node closure.
  26. * @param destination - Target-specific Office directory beside the executable; replaced when present.
  27. * @param target - Node platform and CPU of the executable.
  28. * @returns Relative package directories included in the sidecar.
  29. */
  30. export async function copyOfficeSidecar(
  31. staging: string,
  32. destination: string,
  33. target: { platform: string; arch: string },
  34. ): Promise<string[]> {
  35. const entry = join(staging, 'node_modules', '@deepseek-ai', 'libreoffice-kit')
  36. const manifest = JSON.parse(await readFile(join(entry, 'package.json'), 'utf8')) as PackageManifest
  37. const engineName = `@deepseek-ai/libreoffice-kit-${selectOfficeEngine(manifest, target)}`
  38. const packages = new Set<string>()
  39. async function visit(packageDirectory: string): Promise<void> {
  40. if (packages.has(packageDirectory)) return
  41. const relativeDirectory = relative(staging, packageDirectory)
  42. if (isAbsolute(relativeDirectory) || relativeDirectory === '..' || relativeDirectory.startsWith(`..${sep}`)) {
  43. throw new Error(`Python Office dependency is outside the deployed closure: ${packageDirectory}`)
  44. }
  45. const manifestPath = join(packageDirectory, 'package.json')
  46. const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as PackageManifest
  47. packages.add(packageDirectory)
  48. const require = createRequire(manifestPath)
  49. const dependencies = new Set([
  50. ...Object.keys(manifest.dependencies ?? {}),
  51. ...Object.keys(manifest.optionalDependencies ?? {}),
  52. ...Object.keys(manifest.peerDependencies ?? {}),
  53. ])
  54. for (const name of dependencies) {
  55. if (name.startsWith('@deepseek-ai/libreoffice-kit-')) continue
  56. const optional = manifest.optionalDependencies?.[name] !== undefined
  57. || manifest.peerDependenciesMeta?.[name]?.optional === true
  58. const dependencyDirectory = (require.resolve.paths(name) ?? [])
  59. .map(directory => join(directory, name))
  60. .find(directory => existsSync(directory))
  61. if (dependencyDirectory === undefined) {
  62. if (optional) continue
  63. throw new Error(`Python Office dependency ${name} required by ${manifest.name} is missing.`)
  64. }
  65. if (optional) {
  66. const dependency = JSON.parse(await readFile(join(dependencyDirectory, 'package.json'), 'utf8')) as PackageManifest
  67. if (!supports(dependency.os, target.platform) || !supports(dependency.cpu, target.arch)) continue
  68. }
  69. await visit(dependencyDirectory)
  70. }
  71. }
  72. await visit(entry)
  73. const engineDirectory = join(staging, 'node_modules', engineName)
  74. if (!existsSync(engineDirectory)) throw new Error(`Python Office engine ${engineName} required for ${target.platform}/${target.arch} is missing.`)
  75. await visit(engineDirectory)
  76. await rm(destination, { recursive: true, force: true })
  77. await mkdir(destination, { recursive: true })
  78. const directories = [...packages].sort()
  79. for (const source of directories) {
  80. const nestedModules = join(source, 'node_modules')
  81. await cp(source, join(destination, relative(staging, source)), {
  82. recursive: true,
  83. filter: path => path !== nestedModules && !path.startsWith(nestedModules + sep),
  84. })
  85. }
  86. return directories.map(directory => relative(staging, directory))
  87. }
  88. function supports(values: string[] | undefined, value: string): boolean {
  89. return values === undefined || (!values.includes(`!${value}`)
  90. && (values.every(item => item.startsWith('!')) || values.includes(value) || values.includes('any')))
  91. }