2026-07-11 14:08:26 +08:00
/**
* Build the single-file SDK runtime executables
* (docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
*
* Every settled decision is hardcoded — the PoC judged @yao-pkg/pkg's
* standard mode unusable for this architecture (its ESM→CJS transform breaks
* every runtime `import()`), so the pipeline is fixed on `--sea` mode, plain
* ESM entry, plain-source assets, and a hoisted (symlink-free) staged tree.
*
* Pipeline — every step fails loud with the command it ran:
*
* 1. `pnpm run build` — all packages emit `lib/` (skippable via --skip-build).
* 2. `pnpm --filter dsh-jsonrpc-agent-pkg deploy` — materialize the
* closure-manifest package (python/sdk-runtime/package.json — the single
* source of truth for the exe's plugin set) into the staging dir
* (cleared first; pnpm refuses a non-empty deploy target). Flags, all
* verified against pnpm 11.7: `--legacy` because the workspace does not
* set `inject-workspace-packages=true`; `node-linker=hoisted` for a plain
* file tree with zero symlinks (the safe shape for pkg's VFS, and it
* physically guarantees a single cordis copy); `auto-install-peers=false`
* so transitive `^0.0.x` peers on unpublished packages never hit the
* registry; `link-workspace-packages=true` so the closure resolves to
* workspace/vendor sources.
* 3. Inject the pkg config into the staged package.json: `bin` = the ESM
* `node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js` (SEA mode
* hands it to Node's default ESM loader — no CJS shim), plus whole-tree
* asset globs. The cordis Loader resolves plugins
* through runtime dynamic `import()` of bare package names, so pkg's
* static analysis discovers none of them — the entire staged tree must be
* globbed in explicitly.
* 4. `pnpm dlx @yao-pkg/pkg@<pinned> <staging> --sea --targets <t> --output
* <out>/dsh-jsonrpc-agent-pkg-<platform>-<arch>` — once per target (SEA mode
* packs a single target per invocation), so each product gets its
* canonical name directly.
* 5. Sync into the Python runtime package
* (python/sdk-runtime/src/deepseek_harness_runtime/runtime/,
* created if missing): each product under its canonical filename (exe
* mode), plus the whole staged closure into runtime/node/ (node mode —
* `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js`
* runs it directly; the injected pkg
* fields are harmless to node). dist-exe/ keeps the originals for CI
* artifact upload.
*
* `pnpm exec tsx scripts/build-exe-for-python-sdk.ts` → host-platform exe into dist-exe/
* `pnpm exec tsx scripts/build-exe-for-python-sdk.ts --targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64`
* `pnpm exec tsx scripts/build-exe-for-python-sdk.ts --dry-run` → print the plan without executing
*/
import { spawn } from 'node:child_process'
import { existsSync , mkdirSync , statSync } from 'node:fs'
import { copyFile , readFile , rm , writeFile } from 'node:fs/promises'
import { basename , join , resolve , sep } from 'node:path'
import { parseArgs } from 'node:util'
const root = resolve ( import . meta . dirname , '..' )
/**
* The deploy root: the closure-manifest package (python/sdk-runtime) whose
* dependencies define the exe's contents; the runnable entry inside the
* closure is {@link ENTRY_BIN}.
*/
const DEPLOY_ROOT_PACKAGE = 'dsh-jsonrpc-agent-pkg'
/** The bin entry inside the deployed closure (the dsh-jsonrpc-agent app bin). */
const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-jsonrpc-agent/lib/bin.js'
/** Basename of every product; the canonical name appends `-<platform>-<arch>`. */
const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg'
/** Default exe Node major; SEA mode requires >= node22, the repo tracks node24. */
const DEFAULT_NODE_RANGE = 'node24'
/** Pinned pkg version (the one the PoC and acceptance ran on) for reproducible builds. */
const PKG_SPEC = '@yao-pkg/pkg@6.21.0'
/** Staging dir for the deployed closure — cleared on every run (gitignored). */
// (No external staging dir: the deploy target IS the Python runtime's
// node-mode carrier — see PYTHON_RUNTIME_DIR/PYTHON_NODE_SUBDIR.)
/** Product output dir (gitignored). */
const OUT_DIR = 'dist-exe'
/**
* Python runtime package dir the products are synced into. A parallel change
* owns the directory and its .gitignore; this script's only contract is the
* destination path, so a missing dir is created, never an error.
*/
const PYTHON_RUNTIME_DIR = 'python/sdk-runtime/src/deepseek_harness_runtime/runtime'
/** Subdir of {@link PYTHON_RUNTIME_DIR} carrying the staged closure for node-mode execution. */
const PYTHON_NODE_SUBDIR = 'node'
2026-07-13 16:34:09 +08:00
/** Deploy-root documentation is not runtime input and violates the generated-directory i18n exclusion if retained. */
const DEPLOY_ONLY_DOCS = [ 'README.md' , 'README.zh.md' , 'README.i18n.yaml' ]
2026-07-11 14:08:26 +08:00
/**
* Whole-tree asset globs. The cordis Loader dynamic-imports bare package names
* at runtime, invisible to pkg's static analysis, so every runtime file in the
* closure is listed; SEA mode ships them as plain source in the VFS. Every
* package.json must ride along — bare-name resolution dies without them (the
* json glob would already match, but the manifests are resolution-critical, so
* they get their own explicit entry).
*/
const ASSET_GLOBS = [
'package.json' ,
'node_modules/**/*.js' ,
'node_modules/**/*.cjs' ,
'node_modules/**/*.mjs' ,
'node_modules/**/package.json' ,
'node_modules/**/*.json' ,
'node_modules/**/*.node' ,
'node_modules/**/*.wasm' ,
]
const PLATFORMS = [ 'linux' , 'macos' ] as const
const ARCHES = [ 'x64' , 'arm64' ] as const
type Platform = ( typeof PLATFORMS ) [ number ]
type Arch = ( typeof ARCHES ) [ number ]
/** True when `value` is a supported pkg platform tag. */
function isPlatform ( value : string ) : value is Platform {
return ( PLATFORMS as readonly string [ ] ) . includes ( value )
}
/** True when `value` is a supported pkg CPU tag. */
function isArch ( value : string ) : value is Arch {
return ( ARCHES as readonly string [ ] ) . includes ( value )
}
/**
* One pkg target triple, e.g. `node24-linux-x64`, as an immutable value.
* Construction goes through {@link Target.parse} (a `--targets` entry) or
* {@link Target.host} (the default), which own all validation.
*/
class Target {
private constructor (
/** pkg Node range (`node<major>`); pins the official base binary pkg pulls. */
readonly nodeRange : string ,
/**
* pkg platform tag. Windows is a documented non-goal
* (docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
*/
readonly platform : Platform ,
/** pkg CPU tag. */
readonly arch : Arch ,
) { }
/** The pkg `--targets` spec string `<nodeRange>-<platform>-<arch>`. */
get spec ( ) : string {
return ` ${ this . nodeRange } - ${ this . platform } - ${ this . arch } `
}
/**
* Parse and validate one target spec; throws on any malformed component.
* @param spec - the raw triple, e.g. `node24-linux-x64`.
* @returns the parsed target.
*/
static parse ( spec : string ) : Target {
const parts = spec . split ( '-' )
const [ nodeRange , platform , arch ] = parts
if ( parts . length !== 3 || nodeRange === undefined || platform === undefined || arch === undefined ) {
throw new Error ( ` build-exe-for-python-sdk: target ${ JSON . stringify ( spec ) } must be <nodeRange>-<platform>-<arch>, e.g. node24-linux-x64. ` )
}
if ( ! /^node\d+$/ . test ( nodeRange ) ) {
throw new Error ( ` build-exe-for-python-sdk: target ${ JSON . stringify ( spec ) } : node range must look like node24, got ${ JSON . stringify ( nodeRange ) } . ` )
}
if ( ! isPlatform ( platform ) ) {
throw new Error ( ` build-exe-for-python-sdk: target ${ JSON . stringify ( spec ) } : platform must be one of ${ PLATFORMS . join ( ', ' ) } (Windows is a docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md non-goal), got ${ JSON . stringify ( platform ) } . ` )
}
if ( ! isArch ( arch ) ) {
throw new Error ( ` build-exe-for-python-sdk: target ${ JSON . stringify ( spec ) } : arch must be one of ${ ARCHES . join ( ', ' ) } , got ${ JSON . stringify ( arch ) } . ` )
}
return new Target ( nodeRange , platform , arch )
}
/**
* The default target when --targets is omitted: the host platform on node24.
* @returns the host target; throws on an unsupported host platform or arch.
*/
static host ( ) : Target {
const platform = process . platform === 'darwin' ? 'macos' : process . platform === 'linux' ? 'linux' : undefined
if ( platform === undefined ) {
throw new Error ( ` build-exe-for-python-sdk: unsupported host platform ${ process . platform } ; pass --targets explicitly. ` )
}
const arch = process . arch === 'x64' || process . arch === 'arm64' ? process.arch : undefined
if ( arch === undefined ) {
throw new Error ( ` build-exe-for-python-sdk: unsupported host arch ${ process . arch } ; pass --targets explicitly. ` )
}
return new Target ( DEFAULT_NODE_RANGE , platform , arch )
}
}
/**
* Parsed CLI configuration. {@link BuildCli.parse} is the only constructor
* path — it owns flag parsing, target validation, and the --help / bad-flag
* process exits, so an instance always holds a valid plan.
*/
class BuildCli {
private constructor (
/** Build targets; defaults to the host platform only. */
readonly targets : readonly Target [ ] ,
/** Skip step 1 (`pnpm run build`); lib/ artifacts must already exist. */
readonly skipBuild : boolean ,
/** Print every command and config patch instead of executing. */
readonly dryRun : boolean ,
) { }
/**
* Parse argv into a validated configuration. Exits the process for --help
* (code 0, usage) and for unknown/malformed flags (code 1, usage on
* stderr); throws on invalid or colliding targets.
* @param argv - the raw arguments (`process.argv.slice(2)`).
* @returns the parsed, validated configuration.
*/
static parse ( argv : string [ ] ) : BuildCli {
let values : ReturnType < typeof BuildCli.parseRaw >
try {
values = BuildCli . parseRaw ( argv )
} catch ( error ) {
console . error ( ` build-exe-for-python-sdk: ${ error instanceof Error ? error.message : String ( error ) } \ n ` )
console . error ( BuildCli . usage ( ) )
process . exit ( 1 )
}
if ( values . help ) {
console . log ( BuildCli . usage ( ) )
process . exit ( 0 )
}
const targets = values . targets === undefined
? [ Target . host ( ) ]
: values . targets . split ( ',' ) . map ( part = > part . trim ( ) ) . filter ( part = > part !== '' ) . map ( spec = > Target . parse ( spec ) )
if ( targets . length === 0 ) throw new Error ( 'build-exe-for-python-sdk: --targets is empty.' )
const seen = new Set < string > ( )
for ( const target of targets ) {
const key = ` ${ target . platform } - ${ target . arch } `
if ( seen . has ( key ) ) {
throw new Error ( ` build-exe-for-python-sdk: duplicate platform-arch ${ key } in --targets; canonical product names would collide. ` )
}
seen . add ( key )
}
return new BuildCli ( targets , values [ 'skip-build' ] , values [ 'dry-run' ] )
}
/** The flag grammar in one place; parseArgs throws on any unknown flag. */
private static parseRaw ( argv : string [ ] ) {
return parseArgs ( {
args : argv ,
options : {
'targets' : { type : 'string' } ,
'skip-build' : { type : 'boolean' , default : false } ,
'dry-run' : { type : 'boolean' , default : false } ,
'help' : { type : 'boolean' , default : false } ,
} ,
} ) . values
}
/** The --help text; also printed under flag-parse errors. */
private static usage ( ) : string {
return [
'Usage: pnpm exec tsx scripts/build-exe-for-python-sdk.ts [flags]' ,
'' ,
' --targets=<t1,t2,...> pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64.' ,
' Default: the host platform only (on node24).' ,
' --skip-build skip `pnpm run build` (lib/ artifacts must already exist).' ,
' --dry-run print every command and config patch without executing.' ,
' --help print this help.' ,
'' ,
'Settled decisions are hardcoded (docs/rfc/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md): pkg runs in --sea mode' ,
` (standard mode breaks runtime import()), pinned to ${ PKG_SPEC } ; the deploy tree is ` ,
` hoisted/symlink-free; the closure deploys straight into ${ PYTHON_RUNTIME_DIR } / ${ PYTHON_NODE_SUBDIR } and products land in ${ OUT_DIR } /. ` ,
] . join ( '\n' )
}
}
/** The pnpm executable name for the host OS. */
function pnpmBin ( ) : string {
return process . platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
}
/**
* Render a command line for logs and error messages, quoting arguments that
* contain spaces.
* @param command - the executable.
* @param args - its arguments.
* @returns the printable command line.
*/
function formatCommand ( command : string , args : string [ ] ) : string {
return [ command , . . . args ] . map ( part = > ( part . includes ( ' ' ) ? JSON . stringify ( part ) : part ) ) . join ( ' ' )
}
/**
* The four-step build pipeline over one parsed CLI. Steps are sequential
* async methods; every subprocess inherits stdio and fails loud with the
* exact command it ran. In --dry-run the command/filesystem layer prints
* what it would do instead of executing.
*/
class SingleExeBuild {
/**
* Absolute staging dir — the Python runtime's node-mode carrier: step 2
* deploys the closure DIRECTLY here (cleared first; it is a pure build
* product, the checked-in default `cordis.yml` lives one level up), step 4
* reads it as the pkg input, and node mode runs it in place.
*/
readonly staging = resolve ( root , PYTHON_RUNTIME_DIR , PYTHON_NODE_SUBDIR )
/** Absolute product output dir. */
private readonly outDir = resolve ( root , OUT_DIR )
constructor ( private readonly cli : BuildCli ) { }
2026-07-13 16:34:09 +08:00
/** Gate the manifest before spending time compiling or packaging it. */
async verifyClosure ( ) : Promise < void > {
await this . run ( 'runtime dependency closure' , pnpmBin ( ) , [ 'run' , 'verify-runtime-closure' ] )
}
2026-07-11 14:08:26 +08:00
/** Step 1: `pnpm run build` — all packages emit `lib/` (skipped via --skip-build). */
async build ( ) : Promise < void > {
if ( this . cli . skipBuild ) {
console . log ( 'build-exe-for-python-sdk: skipping pnpm run build (--skip-build)' )
return
}
await this . run ( 'build' , pnpmBin ( ) , [ 'run' , 'build' ] )
}
/** Step 2: clear the staging dir and deploy the bridge closure into it. */
async deployStaging ( ) : Promise < void > {
if ( this . staging === root || root . startsWith ( this . staging + sep ) ) {
throw new Error ( ` build-exe-for-python-sdk: refusing to clear staging dir ${ this . staging } : it contains the repo root. ` )
}
if ( this . cli . dryRun ) console . log ( ` build-exe-for-python-sdk: [dry-run] rm -rf ${ this . staging } ` )
else await rm ( this . staging , { recursive : true , force : true } )
await this . run ( 'deploy' , pnpmBin ( ) , [
'--filter' ,
DEPLOY_ROOT_PACKAGE ,
'deploy' ,
'--legacy' ,
'--prod' ,
'--config.node-linker=hoisted' ,
'--config.auto-install-peers=false' ,
'--config.link-workspace-packages=true' ,
this . staging ,
] )
2026-07-13 16:34:09 +08:00
if ( this . cli . dryRun ) {
for ( const name of DEPLOY_ONLY_DOCS ) console . log ( ` build-exe-for-python-sdk: [dry-run] rm -f ${ join ( this . staging , name ) } ` )
} else {
await Promise . all ( DEPLOY_ONLY_DOCS . map ( name = > rm ( join ( this . staging , name ) , { force : true } ) ) )
}
2026-07-11 14:08:26 +08:00
}
/** Step 3: patch the staged package.json with the bin entry + pkg asset globs. */
async injectPkgConfig ( ) : Promise < void > {
const patch = { bin : ENTRY_BIN , pkg : { assets : ASSET_GLOBS } }
const manifestPath = join ( this . staging , 'package.json' )
if ( this . cli . dryRun ) {
console . log ( ` build-exe-for-python-sdk: [dry-run] patch ${ manifestPath } with ${ JSON . stringify ( patch ) } ` )
return
}
if ( ! existsSync ( manifestPath ) ) {
throw new Error ( ` build-exe-for-python-sdk: ${ manifestPath } missing — pnpm deploy did not produce a staged package. ` )
}
if ( ! existsSync ( join ( this . staging , ENTRY_BIN ) ) ) {
throw new Error ( ` build-exe-for-python-sdk: ${ join ( this . staging , ENTRY_BIN ) } missing — run without --skip-build so lib/ artifacts exist. ` )
}
const manifest = JSON . parse ( await readFile ( manifestPath , 'utf8' ) ) as Record < string , unknown >
await writeFile ( manifestPath , ` ${ JSON . stringify ( { . . . manifest , . . . patch } , null, 2)} \ n ` )
console . log ( ` build-exe-for-python-sdk: injected pkg config into ${ manifestPath } ` )
}
/**
* Step 4: run @yao-pkg/pkg over the staged tree for ONE target (SEA mode
* packs a single target per invocation) and return the product path.
* @param target - the pkg target triple to build.
* @returns the canonical product path `<out>/dsh-jsonrpc-agent-pkg-<platform>-<arch>`.
*/
async pack ( target : Target ) : Promise < string > {
const product = join ( this . outDir , ` ${ OUTPUT_BASENAME } - ${ target . platform } - ${ target . arch } ` )
if ( ! this . cli . dryRun ) mkdirSync ( this . outDir , { recursive : true } )
await this . run ( ` pkg ${ target . spec } ` , pnpmBin ( ) , [
'dlx' ,
PKG_SPEC ,
this . staging ,
'--sea' ,
'--targets' ,
target . spec ,
'--output' ,
product ,
] )
if ( ! this . cli . dryRun && ! existsSync ( product ) ) {
throw new Error ( ` build-exe-for-python-sdk: product ${ product } is missing after the pkg run; inspect ${ this . outDir } . ` )
}
return product
}
/**
* Print each product path (and size, when it exists on disk).
* @param products - the product paths returned by {@link pack}.
*/
printProducts ( products : string [ ] ) : void {
console . log ( this . cli . dryRun ? 'build-exe-for-python-sdk: [dry-run] would produce:' : 'build-exe-for-python-sdk: products:' )
for ( const product of products ) {
if ( this . cli . dryRun ) {
console . log ( ` ${ product } ` )
continue
}
const megabytes = statSync ( product ) . size / ( 1024 * 1024 )
console . log ( ` ${ product } ( ${ megabytes . toFixed ( 1 ) } MB) ` )
}
}
/**
* Step 5: copy every product into the Python runtime package under its
* canonical filename (exe mode). The node-mode carrier needs no sync — step
* 2 deployed the closure into it directly. dist-exe/ keeps the originals
* for CI artifact upload; the destination dir is created if missing.
* @param products - the product paths returned by {@link pack}.
*/
async syncToPythonRuntime ( products : string [ ] ) : Promise < void > {
const destDir = resolve ( root , PYTHON_RUNTIME_DIR )
if ( this . cli . dryRun ) {
for ( const product of products ) {
console . log ( ` build-exe-for-python-sdk: [dry-run] cp ${ product } ${ join ( destDir , basename ( product ) ) } ` )
}
return
}
mkdirSync ( destDir , { recursive : true } )
for ( const product of products ) {
const destination = join ( destDir , basename ( product ) )
await copyFile ( product , destination )
console . log ( ` build-exe-for-python-sdk: synced ${ destination } ` )
}
}
/**
* Run one pipeline step as a subprocess with inherited stdio; reject —
* carrying the printable command — on spawn failure and non-zero exit
* alike. In --dry-run, print the command instead of executing.
* @param label - the step name used in logs and error messages.
* @param command - the executable.
* @param args - its arguments.
*/
private async run ( label : string , command : string , args : string [ ] ) : Promise < void > {
const printable = formatCommand ( command , args )
if ( this . cli . dryRun ) {
console . log ( ` build-exe-for-python-sdk: [dry-run] ${ printable } ` )
return
}
console . log ( ` build-exe-for-python-sdk: ${ label } : ${ printable } ` )
await new Promise < void > ( ( resolvePromise , reject ) = > {
const child = spawn ( command , args , { cwd : root , stdio : 'inherit' } )
child . once ( 'error' , ( error ) = > {
reject ( new Error ( ` build-exe-for-python-sdk: ${ label } failed to spawn: ${ error . message } ( ${ printable } ) ` ) )
} )
child . once ( 'exit' , ( code , signal ) = > {
if ( code === 0 ) {
resolvePromise ( )
return
}
const cause = code === null ? ` signal ${ signal ? ? 'unknown' } ` : ` exit code ${ code } `
reject ( new Error ( ` build-exe-for-python-sdk: ${ label } failed ( ${ cause } ): ${ printable } ` ) )
} )
} )
}
}
/** Entry point: parse the CLI, then await each pipeline step in order. */
async function main ( ) : Promise < void > {
const cli = BuildCli . parse ( process . argv . slice ( 2 ) )
const pipeline = new SingleExeBuild ( cli )
console . log ( ` build-exe-for-python-sdk: targets: ${ cli . targets . map ( target = > target . spec ) . join ( ', ' ) } ` )
console . log ( ` build-exe-for-python-sdk: staging: ${ pipeline . staging } ` )
2026-07-13 16:34:09 +08:00
await pipeline . verifyClosure ( )
2026-07-11 14:08:26 +08:00
await pipeline . build ( )
await pipeline . deployStaging ( )
await pipeline . injectPkgConfig ( )
const products : string [ ] = [ ]
for ( const target of cli . targets ) products . push ( await pipeline . pack ( target ) )
pipeline . printProducts ( products )
await pipeline . syncToPythonRuntime ( products )
}
await main ( )