build-exe-for-python-sdk.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. /**
  2. * Build the SDK runtime executables and Python node carrier. The fixed
  3. * `@yao-pkg/pkg --sea` route, deploy flags, and artifact layout are owned by
  4. * .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
  5. * The staged closure is symlink-free, and whole-tree assets cover Cordis's
  6. * runtime imports that pkg cannot discover statically.
  7. */
  8. import { spawn } from 'node:child_process'
  9. import { existsSync, statSync } from 'node:fs'
  10. import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
  11. import { basename, dirname, join, resolve, sep } from 'node:path'
  12. import { parseArgs } from 'node:util'
  13. const root = resolve(import.meta.dirname, '..')
  14. /** The closure manifest whose dependencies define the executable. */
  15. const DEPLOY_ROOT_PACKAGE = 'dsh-jsonrpc-agent-pkg'
  16. /** The closed-runtime app entry inside the deployed closure. */
  17. const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-sdk-jsonrpc-demo/lib/packaged-bin.js'
  18. const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg'
  19. /** Default Node major; SEA mode requires at least Node 22. */
  20. const DEFAULT_NODE_RANGE = 'node24'
  21. /** Pinned for reproducible builds. */
  22. const PKG_SPEC = '@yao-pkg/pkg@6.21.0'
  23. const OUT_DIR = 'dist-exe'
  24. /** Python package destination; created when absent. */
  25. const PYTHON_RUNTIME_DIR = 'python/sdk-runtime/src/deepseek_harness_runtime/runtime'
  26. /** The deployed closure doubles as the node-mode carrier. */
  27. const PYTHON_NODE_SUBDIR = 'node'
  28. /** Legacy deploy may hoist peer-specialized workspace packages back here. */
  29. const DEPLOY_SOURCE_NODE_MODULES = 'python/sdk-runtime/node_modules'
  30. /** Documentation excluded from the generated runtime directory. */
  31. const DEPLOY_ONLY_DOCS = ['README.md', 'README.zh.md', 'README.i18n.yaml']
  32. /**
  33. * Whole-tree assets cover Cordis's runtime bare-package imports, which pkg's
  34. * static analysis cannot see. Package manifests are explicit because bare-name
  35. * resolution depends on them.
  36. */
  37. const ASSET_GLOBS = [
  38. 'package.json',
  39. 'node_modules/**/*.js',
  40. 'node_modules/**/*.cjs',
  41. 'node_modules/**/*.mjs',
  42. 'node_modules/**/package.json',
  43. 'node_modules/**/*.json',
  44. 'node_modules/**/*.node',
  45. 'node_modules/**/*.wasm',
  46. ]
  47. const PLATFORMS = ['linux', 'macos'] as const
  48. const ARCHES = ['x64', 'arm64'] as const
  49. type Platform = (typeof PLATFORMS)[number]
  50. type Arch = (typeof ARCHES)[number]
  51. function isPlatform(value: string): value is Platform {
  52. return (PLATFORMS as readonly string[]).includes(value)
  53. }
  54. function isArch(value: string): value is Arch {
  55. return (ARCHES as readonly string[]).includes(value)
  56. }
  57. /**
  58. * A parsed pkg target triple, constructed from `--targets` or the host.
  59. */
  60. class Target {
  61. private constructor(
  62. /** pkg Node range (`node<major>`). */
  63. readonly nodeRange: string,
  64. /**
  65. * pkg platform tag. Windows is a documented non-goal
  66. * (.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
  67. */
  68. readonly platform: Platform,
  69. /** pkg CPU tag. */
  70. readonly arch: Arch,
  71. ) {}
  72. /** The pkg `--targets` spec string `<nodeRange>-<platform>-<arch>`. */
  73. get spec(): string {
  74. return `${this.nodeRange}-${this.platform}-${this.arch}`
  75. }
  76. /**
  77. * Parse one target spec, rejecting malformed triples and unsupported platform or architecture.
  78. * @param spec - the raw triple, e.g. `node24-linux-x64`.
  79. * @returns the parsed target.
  80. */
  81. static parse(spec: string): Target {
  82. const parts = spec.split('-')
  83. const [nodeRange, platform, arch] = parts
  84. if (parts.length !== 3 || nodeRange === undefined || platform === undefined || arch === undefined) {
  85. throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)} must be <nodeRange>-<platform>-<arch>, e.g. node24-linux-x64.`)
  86. }
  87. if (!/^node\d+$/.test(nodeRange)) {
  88. throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: node range must look like node24, got ${JSON.stringify(nodeRange)}.`)
  89. }
  90. if (!isPlatform(platform)) {
  91. throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: platform must be one of ${PLATFORMS.join(', ')}, got ${JSON.stringify(platform)}.`)
  92. }
  93. if (!isArch(arch)) {
  94. throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: arch must be one of ${ARCHES.join(', ')}, got ${JSON.stringify(arch)}.`)
  95. }
  96. return new Target(nodeRange, platform, arch)
  97. }
  98. /**
  99. * Resolve the host-platform default on Node 24.
  100. * @returns the host target; throws on an unsupported host platform or arch.
  101. */
  102. static host(): Target {
  103. const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'linux' ? 'linux' : undefined
  104. if (platform === undefined) {
  105. throw new Error(`build-exe-for-python-sdk: unsupported host platform ${process.platform}; pass --targets explicitly.`)
  106. }
  107. const arch = process.arch === 'x64' || process.arch === 'arm64' ? process.arch : undefined
  108. if (arch === undefined) {
  109. throw new Error(`build-exe-for-python-sdk: unsupported host arch ${process.arch}; pass --targets explicitly.`)
  110. }
  111. return new Target(DEFAULT_NODE_RANGE, platform, arch)
  112. }
  113. }
  114. /**
  115. * Validated CLI configuration; construction owns help and parse-error exits.
  116. */
  117. class BuildCli {
  118. private constructor(
  119. /** Build targets; defaults to the host platform only. */
  120. readonly targets: readonly Target[],
  121. /** Skip step 1 (`pnpm run build`); lib/ artifacts must already exist. */
  122. readonly skipBuild: boolean,
  123. /** Print every command and config patch instead of executing. */
  124. readonly dryRun: boolean,
  125. ) {}
  126. /**
  127. * Parse argv. Help exits 0; malformed flags exit 1; invalid or colliding
  128. * targets throw.
  129. * @param argv - the raw arguments (`process.argv.slice(2)`).
  130. * @returns the parsed, validated configuration.
  131. */
  132. static parse(argv: string[]): BuildCli {
  133. let values: ReturnType<typeof BuildCli.parseRaw>
  134. try {
  135. values = BuildCli.parseRaw(argv)
  136. } catch (error) {
  137. console.error(`build-exe-for-python-sdk: ${error instanceof Error ? error.message : String(error)}\n`)
  138. console.error(BuildCli.usage())
  139. process.exit(1)
  140. }
  141. if (values.help) {
  142. console.log(BuildCli.usage())
  143. process.exit(0)
  144. }
  145. const targets = values.targets === undefined
  146. ? [Target.host()]
  147. : values.targets.split(',').map(part => part.trim()).filter(part => part !== '').map(spec => Target.parse(spec))
  148. if (targets.length === 0) throw new Error('build-exe-for-python-sdk: --targets is empty.')
  149. const seen = new Set<string>()
  150. for (const target of targets) {
  151. const key = `${target.platform}-${target.arch}`
  152. if (seen.has(key)) {
  153. throw new Error(`build-exe-for-python-sdk: duplicate platform-arch ${key} in --targets; canonical product names would collide.`)
  154. }
  155. seen.add(key)
  156. }
  157. return new BuildCli(targets, values['skip-build'], values['dry-run'])
  158. }
  159. private static parseRaw(argv: string[]) {
  160. return parseArgs({
  161. args: argv,
  162. options: {
  163. 'targets': { type: 'string' },
  164. 'skip-build': { type: 'boolean', default: false },
  165. 'dry-run': { type: 'boolean', default: false },
  166. 'help': { type: 'boolean', default: false },
  167. },
  168. }).values
  169. }
  170. private static usage(): string {
  171. return [
  172. 'Usage: pnpm exec tsx scripts/build-exe-for-python-sdk.ts [flags]',
  173. '',
  174. ' --targets=<t1,t2,...> pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64.',
  175. ' Default: the host platform only (on node24).',
  176. ' --skip-build skip `pnpm run build` (lib/ artifacts must already exist).',
  177. ' --dry-run print every command and config patch without executing.',
  178. ' --help print this help.',
  179. '',
  180. `Build route: ${PKG_SPEC} --sea; see .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.`,
  181. `Stages the node carrier in ${PYTHON_RUNTIME_DIR}/${PYTHON_NODE_SUBDIR} and writes executables to ${OUT_DIR}/.`,
  182. ].join('\n')
  183. }
  184. }
  185. function pnpmBin(): string {
  186. return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
  187. }
  188. /**
  189. * Render a command for logs and errors, quoting arguments with spaces.
  190. * @param command - the executable.
  191. * @param args - its arguments.
  192. * @returns the printable command line.
  193. */
  194. function formatCommand(command: string, args: string[]): string {
  195. return [command, ...args].map(part => (part.includes(' ') ? JSON.stringify(part) : part)).join(' ')
  196. }
  197. /**
  198. * Sequential build pipeline. Subprocesses inherit stdio and errors include
  199. * the command; dry runs print commands and filesystem changes.
  200. */
  201. class SingleExeBuild {
  202. /**
  203. * The cleared deploy target, pkg input, and Python node-mode carrier. The
  204. * checked-in default `cordis.yml` remains in its parent directory.
  205. */
  206. readonly staging = resolve(root, PYTHON_RUNTIME_DIR, PYTHON_NODE_SUBDIR)
  207. private readonly outDir = resolve(root, OUT_DIR)
  208. constructor(private readonly cli: BuildCli) {}
  209. /** Verify the closure before compiling or packaging. */
  210. async verifyClosure(): Promise<void> {
  211. await this.run('runtime dependency closure', pnpmBin(), ['run', 'verify-runtime-closure'])
  212. }
  213. /** Build all package artifacts unless `--skip-build` was passed. */
  214. async build(): Promise<void> {
  215. if (this.cli.skipBuild) {
  216. console.log('build-exe-for-python-sdk: skipping pnpm run build (--skip-build)')
  217. return
  218. }
  219. await this.run('build', pnpmBin(), ['run', 'build'])
  220. }
  221. /** Clear and deploy the runtime closure into the node carrier. */
  222. async deployStaging(): Promise<void> {
  223. if (this.staging === root || root.startsWith(this.staging + sep)) {
  224. throw new Error(`build-exe-for-python-sdk: refusing to clear staging dir ${this.staging}: it contains the repo root.`)
  225. }
  226. if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${this.staging}`)
  227. else await rm(this.staging, { recursive: true, force: true })
  228. await this.run('deploy', pnpmBin(), [
  229. '--filter',
  230. DEPLOY_ROOT_PACKAGE,
  231. 'deploy',
  232. '--legacy',
  233. '--prod',
  234. '--config.node-linker=hoisted',
  235. '--config.auto-install-peers=false',
  236. '--config.link-workspace-packages=true',
  237. this.staging,
  238. ])
  239. await this.restoreLegacyHoists()
  240. await this.materializeStagedLinks()
  241. if (this.cli.dryRun) {
  242. for (const name of DEPLOY_ONLY_DOCS) console.log(`build-exe-for-python-sdk: [dry-run] rm -f ${join(this.staging, name)}`)
  243. } else {
  244. await Promise.all(DEPLOY_ONLY_DOCS.map(name => rm(join(this.staging, name), { force: true })))
  245. }
  246. }
  247. /**
  248. * Restore direct packages that pnpm's legacy hoister places beside the deploy
  249. * source instead of in the target. The runtime manifest supplies every peer,
  250. * so package-local node_modules trees are omitted to preserve one flat Cordis
  251. * instance and a symlink-free packaged payload.
  252. */
  253. private async restoreLegacyHoists(): Promise<void> {
  254. if (this.cli.dryRun) {
  255. console.log('build-exe-for-python-sdk: [dry-run] restore direct dependencies omitted by legacy deploy')
  256. return
  257. }
  258. const manifestPath = join(this.staging, 'package.json')
  259. const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as {
  260. dependencies?: Record<string, string>
  261. }
  262. const sourceNodeModules = resolve(root, DEPLOY_SOURCE_NODE_MODULES)
  263. const restored: string[] = []
  264. for (const dependency of Object.keys(manifest.dependencies ?? {}).sort()) {
  265. const destination = join(this.staging, 'node_modules', dependency)
  266. if (existsSync(destination)) continue
  267. const source = join(sourceNodeModules, dependency)
  268. if (!existsSync(source)) {
  269. throw new Error(
  270. `build-exe-for-python-sdk: deployed dependency ${dependency} is absent from both ${destination} and ${source}.`,
  271. )
  272. }
  273. await mkdir(dirname(destination), { recursive: true })
  274. const nestedNodeModules = join(source, 'node_modules')
  275. await cp(source, destination, {
  276. recursive: true,
  277. dereference: true,
  278. filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
  279. })
  280. restored.push(dependency)
  281. }
  282. const stillMissing = Object.keys(manifest.dependencies ?? {})
  283. .filter(dependency => !existsSync(join(this.staging, 'node_modules', dependency)))
  284. if (stillMissing.length > 0) {
  285. throw new Error(`build-exe-for-python-sdk: staged dependencies remain missing: ${stillMissing.join(', ')}.`)
  286. }
  287. if (restored.length > 0) {
  288. console.log(`build-exe-for-python-sdk: restored legacy deploy hoists: ${restored.join(', ')}`)
  289. }
  290. }
  291. /** Replace deploy-time package links with files and reject any remaining link. */
  292. private async materializeStagedLinks(): Promise<void> {
  293. if (this.cli.dryRun) {
  294. console.log('build-exe-for-python-sdk: [dry-run] materialize staged package links')
  295. return
  296. }
  297. const nodeModules = join(this.staging, 'node_modules')
  298. let remaining = await this.findSymlink(nodeModules)
  299. while (remaining !== undefined) {
  300. const segments = remaining.slice(nodeModules.length + 1).split(sep)
  301. const binIndex = segments.lastIndexOf('.bin')
  302. if (binIndex >= 0) {
  303. await rm(join(nodeModules, ...segments.slice(0, binIndex + 1)), { recursive: true, force: true })
  304. remaining = await this.findSymlink(nodeModules)
  305. continue
  306. }
  307. const destination = remaining
  308. const source = await realpath(destination)
  309. const nestedNodeModules = join(source, 'node_modules')
  310. await rm(destination, { recursive: true, force: true })
  311. await cp(source, destination, {
  312. recursive: true,
  313. dereference: true,
  314. filter: path => path !== nestedNodeModules && !path.startsWith(nestedNodeModules + sep),
  315. })
  316. remaining = await this.findSymlink(nodeModules)
  317. }
  318. }
  319. /** Return the first symbolic link below a directory, if one exists. */
  320. private async findSymlink(directory: string): Promise<string | undefined> {
  321. for (const entry of await readdir(directory, { withFileTypes: true })) {
  322. const path = join(directory, entry.name)
  323. const metadata = await lstat(path)
  324. if (metadata.isSymbolicLink()) return path
  325. if (metadata.isDirectory()) {
  326. const nested = await this.findSymlink(path)
  327. if (nested !== undefined) return nested
  328. }
  329. }
  330. return undefined
  331. }
  332. /** Add the executable entry and pkg assets to the staged manifest. */
  333. async injectPkgConfig(): Promise<void> {
  334. const patch = { bin: ENTRY_BIN, pkg: { assets: ASSET_GLOBS } }
  335. const manifestPath = join(this.staging, 'package.json')
  336. if (this.cli.dryRun) {
  337. console.log(`build-exe-for-python-sdk: [dry-run] patch ${manifestPath} with ${JSON.stringify(patch)}`)
  338. return
  339. }
  340. if (!existsSync(manifestPath)) {
  341. throw new Error(`build-exe-for-python-sdk: ${manifestPath} missing — pnpm deploy did not produce a staged package.`)
  342. }
  343. if (!existsSync(join(this.staging, ENTRY_BIN))) {
  344. throw new Error(`build-exe-for-python-sdk: ${join(this.staging, ENTRY_BIN)} missing — run without --skip-build so lib/ artifacts exist.`)
  345. }
  346. const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as Record<string, unknown>
  347. await writeFile(manifestPath, `${JSON.stringify({ ...manifest, ...patch }, null, 2)}\n`)
  348. console.log(`build-exe-for-python-sdk: injected pkg config into ${manifestPath}`)
  349. }
  350. /**
  351. * Package one target; SEA mode accepts one target per invocation.
  352. * @param target - the pkg target triple to build.
  353. * @returns the executable path and, on macOS, its helper path.
  354. */
  355. async pack(target: Target): Promise<string[]> {
  356. const product = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
  357. await this.prepareNativePty(target)
  358. if (!this.cli.dryRun) await mkdir(this.outDir, { recursive: true })
  359. await this.run(`pkg ${target.spec}`, pnpmBin(), [
  360. 'dlx',
  361. PKG_SPEC,
  362. this.staging,
  363. '--sea',
  364. '--targets',
  365. target.spec,
  366. '--output',
  367. product,
  368. ])
  369. if (!this.cli.dryRun && !existsSync(product)) {
  370. throw new Error(`build-exe-for-python-sdk: product ${product} is missing after the pkg run; inspect ${this.outDir}.`)
  371. }
  372. if (target.platform !== 'macos') return [product]
  373. const spawnHelper = `${product}-spawn-helper`
  374. const source = join(this.staging, 'node_modules', 'node-pty', 'prebuilds', `darwin-${target.arch}`, 'spawn-helper')
  375. if (this.cli.dryRun) {
  376. console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${spawnHelper}`)
  377. } else {
  378. await copyFile(source, spawnHelper)
  379. await chmod(spawnHelper, 0o755)
  380. }
  381. return [product, spawnHelper]
  382. }
  383. /**
  384. * Put the target node-pty addon in the staged closure. Linux npm installs
  385. * build it from source, but legacy deploy omits that side-effect directory.
  386. * @param target - the pkg target whose native addon is being staged.
  387. */
  388. private async prepareNativePty(target: Target): Promise<void> {
  389. const stagedBuild = join(this.staging, 'node_modules', 'node-pty', 'build')
  390. if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${stagedBuild}`)
  391. else await rm(stagedBuild, { recursive: true, force: true })
  392. if (target.platform !== 'linux') return
  393. const source = join(root, 'packages', 'subprocess', 'subprocess-local', 'node_modules', 'node-pty', 'build', 'Release', 'pty.node')
  394. const destination = join(stagedBuild, 'Release', 'pty.node')
  395. if (this.cli.dryRun) {
  396. console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
  397. return
  398. }
  399. const host = Target.host()
  400. if (target.platform !== host.platform || target.arch !== host.arch) {
  401. throw new Error(
  402. 'build-exe-for-python-sdk: build the Linux runtime on its target architecture; '
  403. + `target ${target.platform}-${target.arch} does not match host ${host.platform}-${host.arch}.`,
  404. )
  405. }
  406. await mkdir(dirname(destination), { recursive: true })
  407. await copyFile(source, destination)
  408. }
  409. /**
  410. * Print each product path and, outside dry-run mode, its size.
  411. * @param products - the product paths returned by {@link pack}.
  412. */
  413. printProducts(products: string[]): void {
  414. console.log(this.cli.dryRun ? 'build-exe-for-python-sdk: [dry-run] would produce:' : 'build-exe-for-python-sdk: products:')
  415. for (const path of products) {
  416. if (this.cli.dryRun) {
  417. console.log(` ${path}`)
  418. continue
  419. }
  420. const megabytes = statSync(path).size / (1024 * 1024)
  421. console.log(` ${path} (${megabytes.toFixed(1)} MB)`)
  422. }
  423. }
  424. /**
  425. * Copy each product into the Python runtime package. The deployed node
  426. * carrier is already in place, and `dist-exe/` retains upload copies.
  427. * @param products - the product paths returned by {@link pack}.
  428. */
  429. async syncToPythonRuntime(products: string[]): Promise<void> {
  430. const destDir = resolve(root, PYTHON_RUNTIME_DIR)
  431. if (this.cli.dryRun) {
  432. for (const path of products) {
  433. console.log(`build-exe-for-python-sdk: [dry-run] cp ${path} ${join(destDir, basename(path))}`)
  434. }
  435. return
  436. }
  437. await mkdir(destDir, { recursive: true })
  438. for (const path of products) {
  439. const destination = join(destDir, basename(path))
  440. await copyFile(path, destination)
  441. await chmod(destination, statSync(path).mode & 0o777)
  442. console.log(`build-exe-for-python-sdk: synced ${destination}`)
  443. }
  444. }
  445. /**
  446. * Run one subprocess with inherited stdio. Spawn and non-zero-exit errors
  447. * include the command; dry runs only print it.
  448. * @param label - the step name used in logs and error messages.
  449. * @param command - the executable.
  450. * @param args - its arguments.
  451. */
  452. private async run(label: string, command: string, args: string[]): Promise<void> {
  453. const printable = formatCommand(command, args)
  454. if (this.cli.dryRun) {
  455. console.log(`build-exe-for-python-sdk: [dry-run] ${printable}`)
  456. return
  457. }
  458. console.log(`build-exe-for-python-sdk: ${label}: ${printable}`)
  459. await new Promise<void>((resolvePromise, reject) => {
  460. const child = spawn(command, args, {
  461. cwd: root,
  462. stdio: 'inherit',
  463. // Artifact builds must not mutate or validate a developer's Git hooks.
  464. env: { ...process.env, CI: 'true' },
  465. })
  466. child.once('error', (error) => {
  467. reject(new Error(`build-exe-for-python-sdk: ${label} failed to spawn: ${error.message} (${printable})`))
  468. })
  469. child.once('exit', (code, signal) => {
  470. if (code === 0) {
  471. resolvePromise()
  472. return
  473. }
  474. const cause = code === null ? `signal ${signal ?? 'unknown'}` : `exit code ${code}`
  475. reject(new Error(`build-exe-for-python-sdk: ${label} failed (${cause}): ${printable}`))
  476. })
  477. })
  478. }
  479. }
  480. async function main(): Promise<void> {
  481. const cli = BuildCli.parse(process.argv.slice(2))
  482. const pipeline = new SingleExeBuild(cli)
  483. console.log(`build-exe-for-python-sdk: targets: ${cli.targets.map(target => target.spec).join(', ')}`)
  484. console.log(`build-exe-for-python-sdk: staging: ${pipeline.staging}`)
  485. await pipeline.verifyClosure()
  486. await pipeline.build()
  487. await pipeline.deployStaging()
  488. await pipeline.injectPkgConfig()
  489. const products: string[] = []
  490. for (const target of cli.targets) products.push(...await pipeline.pack(target))
  491. pipeline.printProducts(products)
  492. await pipeline.syncToPythonRuntime(products)
  493. }
  494. await main()