2026-07-11 14:08:26 +08:00
/**
2026-07-14 00:40:36 +08:00
* Build the SDK runtime executables and Python node carrier. The fixed
* `@yao-pkg/pkg --sea` route, deploy flags, and artifact layout are owned by
2026-07-19 22:50:49 +08:00
* .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md.
2026-07-14 00:40:36 +08:00
* The staged closure is symlink-free, and whole-tree assets cover Cordis's
* runtime imports that pkg cannot discover statically.
2026-07-11 14:08:26 +08:00
*/
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 , '..' )
2026-07-14 00:40:36 +08:00
/** The closure manifest whose dependencies define the executable. */
2026-07-11 14:08:26 +08:00
const DEPLOY_ROOT_PACKAGE = 'dsh-jsonrpc-agent-pkg'
2026-07-14 00:40:36 +08:00
/** The app entry inside the deployed closure. */
2026-07-15 15:57:57 +08:00
const ENTRY_BIN = 'node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js'
2026-07-11 14:08:26 +08:00
const OUTPUT_BASENAME = 'dsh-jsonrpc-agent-pkg'
2026-07-14 00:40:36 +08:00
/** Default Node major; SEA mode requires at least Node 22. */
2026-07-11 14:08:26 +08:00
const DEFAULT_NODE_RANGE = 'node24'
2026-07-14 00:40:36 +08:00
/** Pinned for reproducible builds. */
2026-07-11 14:08:26 +08:00
const PKG_SPEC = '@yao-pkg/pkg@6.21.0'
const OUT_DIR = 'dist-exe'
2026-07-14 00:40:36 +08:00
/** Python package destination; created when absent. */
2026-07-11 14:08:26 +08:00
const PYTHON_RUNTIME_DIR = 'python/sdk-runtime/src/deepseek_harness_runtime/runtime'
2026-07-14 00:40:36 +08:00
/** The deployed closure doubles as the node-mode carrier. */
2026-07-11 14:08:26 +08:00
const PYTHON_NODE_SUBDIR = 'node'
2026-07-14 00:40:36 +08:00
/** Documentation excluded from the generated runtime directory. */
2026-07-13 16:34:09 +08:00
const DEPLOY_ONLY_DOCS = [ 'README.md' , 'README.zh.md' , 'README.i18n.yaml' ]
2026-07-11 14:08:26 +08:00
/**
2026-07-14 00:40:36 +08:00
* Whole-tree assets cover Cordis's runtime bare-package imports, which pkg's
* static analysis cannot see. Package manifests are explicit because bare-name
* resolution depends on them.
2026-07-11 14:08:26 +08:00
*/
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 ]
function isPlatform ( value : string ) : value is Platform {
return ( PLATFORMS as readonly string [ ] ) . includes ( value )
}
function isArch ( value : string ) : value is Arch {
return ( ARCHES as readonly string [ ] ) . includes ( value )
}
/**
2026-07-14 16:21:41 +08:00
* A parsed pkg target triple, constructed from `--targets` or the host.
2026-07-11 14:08:26 +08:00
*/
class Target {
private constructor (
2026-07-14 00:40:36 +08:00
/** pkg Node range (`node<major>`). */
2026-07-11 14:08:26 +08:00
readonly nodeRange : string ,
/**
* pkg platform tag. Windows is a documented non-goal
2026-07-19 22:50:49 +08:00
* (.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
2026-07-11 14:08:26 +08:00
*/
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 } `
}
/**
2026-07-14 16:21:41 +08:00
* Parse one target spec, rejecting malformed triples and unsupported platform or architecture.
2026-07-11 14:08:26 +08:00
* @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 ) ) {
2026-07-14 00:40:36 +08:00
throw new Error ( ` build-exe-for-python-sdk: target ${ JSON . stringify ( spec ) } : platform must be one of ${ PLATFORMS . join ( ', ' ) } , got ${ JSON . stringify ( platform ) } . ` )
2026-07-11 14:08:26 +08:00
}
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 )
}
/**
2026-07-14 00:40:36 +08:00
* Resolve the host-platform default on Node 24.
2026-07-11 14:08:26 +08:00
* @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 )
}
}
/**
2026-07-14 00:40:36 +08:00
* Validated CLI configuration; construction owns help and parse-error exits.
2026-07-11 14:08:26 +08:00
*/
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 ,
) { }
/**
2026-07-14 00:40:36 +08:00
* Parse argv. Help exits 0; malformed flags exit 1; invalid or colliding
* targets throw.
2026-07-11 14:08:26 +08:00
* @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' ] )
}
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
}
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.' ,
'' ,
2026-07-19 22:50:49 +08:00
` Build route: ${ PKG_SPEC } --sea; see .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md. ` ,
2026-07-14 00:40:36 +08:00
` Stages the node carrier in ${ PYTHON_RUNTIME_DIR } / ${ PYTHON_NODE_SUBDIR } and writes executables to ${ OUT_DIR } /. ` ,
2026-07-11 14:08:26 +08:00
] . join ( '\n' )
}
}
function pnpmBin ( ) : string {
return process . platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
}
/**
2026-07-14 00:40:36 +08:00
* Render a command for logs and errors, quoting arguments with spaces.
2026-07-11 14:08:26 +08:00
* @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 ( ' ' )
}
/**
2026-07-14 00:40:36 +08:00
* Sequential build pipeline. Subprocesses inherit stdio and errors include
* the command; dry runs print commands and filesystem changes.
2026-07-11 14:08:26 +08:00
*/
class SingleExeBuild {
/**
2026-07-14 00:40:36 +08:00
* The cleared deploy target, pkg input, and Python node-mode carrier. The
* checked-in default `cordis.yml` remains in its parent directory.
2026-07-11 14:08:26 +08:00
*/
readonly staging = resolve ( root , PYTHON_RUNTIME_DIR , PYTHON_NODE_SUBDIR )
private readonly outDir = resolve ( root , OUT_DIR )
constructor ( private readonly cli : BuildCli ) { }
2026-07-14 00:40:36 +08:00
/** Verify the closure before compiling or packaging. */
2026-07-13 16:34:09 +08:00
async verifyClosure ( ) : Promise < void > {
await this . run ( 'runtime dependency closure' , pnpmBin ( ) , [ 'run' , 'verify-runtime-closure' ] )
}
2026-07-14 00:40:36 +08:00
/** Build all package artifacts unless `--skip-build` was passed. */
2026-07-11 14:08:26 +08:00
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' ] )
}
2026-07-14 00:40:36 +08:00
/** Clear and deploy the runtime closure into the node carrier. */
2026-07-11 14:08:26 +08:00
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
}
2026-07-14 00:40:36 +08:00
/** Add the executable entry and pkg assets to the staged manifest. */
2026-07-11 14:08:26 +08:00
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 } ` )
}
/**
2026-07-14 00:40:36 +08:00
* Package one target; SEA mode accepts one target per invocation.
2026-07-11 14:08:26 +08:00
* @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
}
/**
2026-07-14 00:40:36 +08:00
* Print each product path and, outside dry-run mode, its size.
2026-07-11 14:08:26 +08:00
* @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) ` )
}
}
/**
2026-07-14 00:40:36 +08:00
* Copy each executable into the Python runtime package. The deployed node
* carrier is already in place, and `dist-exe/` retains upload copies.
2026-07-11 14:08:26 +08:00
* @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 } ` )
}
}
/**
2026-07-14 00:40:36 +08:00
* Run one subprocess with inherited stdio. Spawn and non-zero-exit errors
* include the command; dry runs only print it.
2026-07-11 14:08:26 +08:00
* @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 } ` ) )
} )
} )
}
}
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 ( )