fix(build): stage platform-specific PTY artifacts

This commit is contained in:
Tianyi Cui
2026-07-29 22:44:19 +08:00
parent 7fdde06cf9
commit c0bd88430a
14 changed files with 141 additions and 105 deletions

View File

@@ -1,6 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write
2026-07-10-single-file-executable-sdk-runtime-distribution.md: 39cfb2999dea7767a18702ad7d160c9e88d7bf20
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: e1a21c40647e1418d4afd02c0bc6b44ef0d4a8cf
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
2026-07-10-single-file-executable-sdk-runtime-distribution.md: fac3d9527b496adaaffdbd8e78401a17c6f0cc0b
2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: ff6a8be16c5f8591efa9bf383cd47c8fe251fe39

View File

@@ -40,15 +40,15 @@ The deploy root is [`python/sdk-runtime/package.json`](../../../../python/sdk-ru
### Build pipeline and artifacts
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` land in `dist-exe/` and are copied back into the runtime directory. CI treats them as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources.
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts): runtime closure verification → `pnpm run build` → (after clearing) `pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **directly into** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → inject the pkg configuration (`bin` points at `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` inside the closure, `assets` is a full glob — dynamic import is invisible to pkg's static analysis, so everything must be packed in explicitly) → stage the target `node-pty` addon → one `pkg --sea` per target → the executables `dsh-jsonrpc-agent-pkg-<platform>-<arch>` land in `dist-exe/` and are copied back into the runtime directory. Linux installs build `pty.node` from source, so the builder copies it from the root install into the staged closure because legacy deploy omits that side-effect directory; macOS uses its target prebuild and emits the required `-spawn-helper` beside the executable. CI treats these products as intermediate test inputs and retains their platform wheels. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted yields a zero-symlink file tree (most stable for the pkg VFS, physically guaranteeing a single cordis instance); disabling automatic peer installation keeps unpublished package names from triggering registry resolution; link-workspace-packages points the closure at workspace/vendor sources.
CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml), triggered explicitly only — `workflow_dispatch`, or the `build-exe` label on a pull request; native builds on the three platforms linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64, with `~/.pkg-cache` cached; macOS ad-hoc signing is handled by pkg. Each leg drives a mock SSE model through the SDK with the default config and a custom `cordis.yml`, drives the exe directly over NDJSON JSON-RPC, verifies the JSONL and final response, and installs release-shaped wheels into a clean venv without `runtime_bin`; Linux additionally inspects GLIBC requirements and runs in a manylinux 2.28 container. A full three-target run retains four artifacts, each containing one release file: the platform-independent SDK wheel and three native runtime wheels; a subset dispatch retains the SDK wheel and selected runtime wheels. Bare executables and source bundles remain intermediate test inputs. [`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) accepts only `python-vX.Y.Z` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and three native runtime wheels, then a single serialized job checks and publishes all four to the project PyPI registry. Windows is a non-goal.
### Python SDK distribution: two carriers, exe for production, node for development
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds three kinds of content: the checked-in default `runtime/cordis.yml`, the build-injected platform exe, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` (the client) + `python/sdk-runtime` (the runtime carrier package). The runtime package's data directory holds the checked-in default `runtime/cordis.yml`, the build-injected platform exe and optional helper, and the build-injected `runtime/node/` closure tree. `resolve_bundled_launch_args()` automatic resolution **finds the exe only**; the node carrier is enabled only by an explicit `DSH_RUNTIME_MODE=node` (running `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`, requiring a system node ≥22.19), positioned as the development-verification channel for members of this repo, and does not enter wheel distributions.
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; the wheel-only runtime package contains exactly one exe and uses one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`. Its Hatch hook rejects sdists, universal tags, mixed executable payloads, and unsupported platforms.
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative stable `X.Y.Z` from the repository root `package.json` and stages both packages at that version, with the SDK depending exactly on `deepseek-harness-runtime-bin==X.Y.Z`. An optional `python-vX.Y.Z` release tag is a consistency assertion and is rejected when it differs from the repository version; the source `pyproject.toml` development sentinel never determines a release version. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe, and the macOS wheel also contains its architecture-matched helper. Runtime wheels use one of `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, or `py3-none-macosx_11_0_arm64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra helpers, and unsupported platforms.
The exe's "must be explicitly configured" hard semantic is unchanged; the zero-config experience is restored by the wrapper: when the caller gave no `cordis`, named no explicit runtime, and the environment has no `DSH_CORDIS_CONFIG`, the client explicitly injects the checked-in default `cordis.yml` (agent-core + preloaded llm-deepseek + JSONL persistence + bash-local + the `dsh-jsonrpc` serving entry, with `!!js` environment-variable fallbacks) via `DSH_CORDIS_CONFIG`.

View File

@@ -40,15 +40,15 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真
### 构建管线与产物
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js``assets` 使用全量 glob因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 写入 `dist-exe/`并拷回运行时目录。CI 将这些文件作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy``hoisted` 产出无符号链接的文件树(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码。
[`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-jsonrpc-agent-pkg deploy --legacy --prod --config.node-linker=hoisted --config.auto-install-peers=false --config.link-workspace-packages=true` **直接写入** `python/sdk-runtime/src/deepseek_harness_runtime/runtime/node/` → 注入 pkg 配置(`bin` 指向闭包内的 `node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js``assets` 使用全量 glob因为动态 `import()` 对 pkg 静态分析不可见,必须显式打入全部内容)→ 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 可执行文件 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 写入 `dist-exe/`,并拷回运行时目录。Linux 安装会从源码构建 `pty.node`,而 `--legacy` 部署会省略该副作用目录因此构建器会把它从根安装目录复制到暂存闭包macOS 使用对应目标的预构建产物,并在可执行文件旁生成所需的 `-spawn-helper`CI 将这些产物作为测试中间输入,只保留对应平台的 wheel 包。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy``hoisted` 产出无符号链接的文件树(对 pkg VFS 最稳定,并从物理上保证只有一个 Cordis 实例);关闭对等依赖自动安装可避免未发布包名触发注册表解析;`link-workspace-packages` 让闭包指向工作区/vendor 源码。
CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml),且只允许显式触发:手动派发 `workflow_dispatch`,或给 PR 添加 `build-exe` 标签。linux-x64、linux-arm64`ubuntu-24.04-arm`)和 macos-arm64 三个平台分别进行原生构建,并缓存 `~/.pkg-cache`macOS 的 ad-hoc 签名由 pkg 处理。每个平台都使用模拟 SSE 模型,分别通过默认配置和自定义 `cordis.yml` 驱动 SDK再通过 NDJSON JSON-RPC 直接驱动 exe校验 JSONL 与最终响应;最后把发布形态的 wheel 包安装到干净的 venv 中,并在不传 `runtime_bin` 的情况下运行。Linux 还会检查 GLIBC 依赖,并在 manylinux 2.28 容器中运行。完整构建三个目标时保留 4 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 3 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-vX.Y.Z` 标签流水线,构建一个 SDK wheel 包和 3 个原生运行时 wheel 包,再由单个串行任务校验并将这 4 个文件发布到项目的 PyPI 注册表。Windows 不在目标范围内。
### Python SDK 分发双载体exe 用于生产,`node` 用于开发
Python SDK 位于 [`python/`](../../../../python/README.md)`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含三类内容:检入的默认 `runtime/cordis.yml`、构建注入的平台 exe以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
Python SDK 位于 [`python/`](../../../../python/README.md)`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含检入的默认 `runtime/cordis.yml`、构建注入的平台 exe 与可选 helper,以及构建注入的 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 的自动解析**只查找 exe**`node` 载体仅在显式设置 `DSH_RUNTIME_MODE=node` 时启用(运行 `runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js`,需要系统 Node ≥22.19),定位为本仓库成员的开发验证通道,不随 wheel 包分发。
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;只提供 wheel 包的运行时包恰好包含一个 exe标签为 `py3-none-manylinux_2_28_x86_64``py3-none-manylinux_2_28_aarch64``py3-none-macosx_11_0_arm64`。其 Hatch 钩子拒绝 sdist、通用标签、混合可执行载荷以及不支持的平台。
[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的稳定版本 `X.Y.Z`,以该版本暂存两个包,并让 SDK 精确依赖 `deepseek-harness-runtime-bin==X.Y.Z`。可选的 `python-vX.Y.Z` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包包含一个 exemacOS wheel 包还包含与其架构匹配的 helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64``py3-none-manylinux_2_28_aarch64``py3-none-macosx_11_0_arm64` 三种标签之一;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、helper 缺失或多余,以及不支持的平台。
exe“必须显式配置”的硬语义不变零配置体验由包装层恢复调用方没有提供 `cordis`、没有显式指定运行时,且环境中没有 `DSH_CORDIS_CONFIG` 时,客户端将检入的默认 `cordis.yml``agent-core` + 预载的 `llm-deepseek` + JSONL 持久化 + `bash-local` + `dsh-jsonrpc` 对外服务条目,并通过 `!!js` 使用环境变量兜底)显式注入 `DSH_CORDIS_CONFIG`

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-29-persistent-bash-str-replace-editor.md
2026-07-29-persistent-bash-str-replace-editor.md: b1be9cc40e11b07b722877e666de0e0328636a05
2026-07-29-persistent-bash-str-replace-editor.zh.md: 0bf100514e971dfa759f44dcca15a7ad6a2fdd8a
2026-07-29-persistent-bash-str-replace-editor.md: 6e8a1df7f04340f4a97c0b799aaace0a78526ba5
2026-07-29-persistent-bash-str-replace-editor.zh.md: 256ffad7945cbb4367b9cc8bb92e9b2968ab4501

View File

@@ -16,7 +16,7 @@ Some deployments need a one-call Bash schema whose shell state survives across m
`dsh-system-prompt` accepts `includeHarnessIdentity: false`, while `dsh-agent-spine-demo` forwards that setting and accepts `toolBash: false`. A deployment can therefore own an exact persona and replace the spine's native Bash without duplicate prompt or tool registrations. Existing defaults remain unchanged.
Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper`, each packaged runtime executable ships with an architecture-matched `-spawn-helper` sibling. A pinned `node-pty` patch resolves that sibling only when present, preserving upstream lookup in ordinary Node runs. The explicit `DSH_NODE_PTY_SPAWN_HELPER` override remains for a current external consumer that supplies a non-sibling helper. The executable and runtime-wheel builders inspect ELF or thin Mach-O headers and fail before publication when the helper is absent, mismatched, or not executable.
Both plugins are included in the Python runtime closure. The persistent Bash closure also includes the PTY service/local backend and the sandbox services required by that backend. Because `node-pty` executes a native `spawn-helper` on macOS, each packaged macOS runtime executable ships with an architecture-matched `-spawn-helper` sibling; Linux uses `forkpty` directly. A pinned `node-pty` patch resolves the sibling only when present, preserving upstream lookup in ordinary Node runs. The explicit `DSH_NODE_PTY_SPAWN_HELPER` override remains for a current external consumer that supplies a non-sibling helper. The macOS executable and runtime-wheel builders inspect the thin Mach-O header and fail before publication when the helper is absent, mismatched, or not executable.
## Alternatives considered
@@ -30,4 +30,4 @@ Both plugins are included in the Python runtime closure. The persistent Bash clo
## Consequences
Profiles can reproduce an external agent by configuring persona and descriptions while the underlying packages remain general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. The editor delegates security and mutation policy to the mounted filesystem stack. Runtime-wheel consumers still need no Node installation, but the wheel now contains a main executable plus its private native helper rather than one physical file.
Profiles can reproduce an external agent by configuring persona and descriptions while the underlying packages remain general. Persistent Bash requires an owning Agent and real PTY backend. Shell exit, timeout, or cancellation loses state. The editor delegates security and mutation policy to the mounted filesystem stack. Runtime-wheel consumers still need no Node installation; Linux wheels contain one executable, while macOS wheels also contain its private native helper.

View File

@@ -16,7 +16,7 @@
`dsh-system-prompt` 接受 `includeHarnessIdentity: false``dsh-agent-spine-demo` 会转发该设置,并接受 `toolBash: false`。因此部署可以拥有精确 persona并替换 spine 的原生 Bash而不会重复注册提示词或工具。既有默认值不变。
两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 会执行原生 `spawn-helper`,每个打包后的运行时可执行文件都会携带一个架构匹配的 `-spawn-helper` 伴随文件。固定版本的 `node-pty` 补丁只在该伴随文件存在时解析它,普通 Node 运行仍保留上游查找方式。显式的 `DSH_NODE_PTY_SPAWN_HELPER` 覆盖仍予保留,供当前提供非伴随 helper 的外部消费方使用。可执行文件与运行时 wheel 包的构建器会检查 ELF 或 thin Mach-O 文件头;若 helper 缺失、架构不匹配或不可执行,构建会在发布前失败。
两个插件都进入 Python runtime 闭包。持久 Bash 的闭包还包含 PTY 服务/本地后端,以及该后端要求的沙箱服务。由于 `node-pty` 在 macOS 上会执行原生 `spawn-helper`,每个打包后的 macOS 运行时可执行文件都会携带一个架构匹配的 `-spawn-helper` 伴随文件Linux 直接使用 `forkpty`。固定版本的 `node-pty` 补丁只在该伴随文件存在时解析它,普通 Node 运行仍保留上游查找方式。显式的 `DSH_NODE_PTY_SPAWN_HELPER` 覆盖仍予保留,供当前提供非伴随 helper 的外部消费方使用。macOS 可执行文件与运行时 wheel 包的构建器会检查 thin Mach-O 文件头;若 helper 缺失、架构不匹配或不可执行,构建会在发布前失败。
## 考虑过的替代方案
@@ -30,4 +30,4 @@
## 后果
Profile 可以通过配置 persona 和描述复现外部 Agent而底层包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端shell 退出、超时或取消会丢失状态。编辑器把安全与变更策略委托给挂载的文件系统栈。runtime wheel 的使用者仍不需要安装 Node,但 wheel 现在包含主可执行文件及其私有原生 helper,而不是单个物理文件
Profile 可以通过配置 persona 和描述复现外部 Agent而底层包保持通用。持久 Bash 需要拥有它的 Agent 与真实 PTY 后端shell 退出、超时或取消会丢失状态。编辑器把安全与变更策略委托给挂载的文件系统栈。运行时 wheel 包的消费方仍无需安装 NodeLinux wheel 包包含一个可执行文件macOS wheel 包还包含其私有原生 helper。

View File

@@ -40,7 +40,7 @@ allowBuilds:
# JSONL durability calls MoveFileExW with write-through publication on Windows.
koffi: true
# The Python runtime deploy includes the reviewed workspace postinstall that
# places node-pty's spawn helper beside the compiled PTY backend.
# restores the executable bit on node-pty's macOS spawn helper.
'@deepseek-ai/dsh-pty-local@file:packages/pty/pty-local': true
# The Landlock launcher family is our own sibling-repo release, consumed

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write python/sdk-runtime/README.md
README.md: 0869b4a9dce0e261b168f21c90a80faf81ea4a64
README.zh.md: 3f2342c4c66e24b8213c78d4e7530c3360497022
README.md: 29ffc1dcc3ec3b273ccaee64734739f3c4f34b9c
README.zh.md: d79c87090867c60e09c50016bad4797170b34400

View File

@@ -8,7 +8,7 @@ Runtime carrier package for the Python SDK (dist `deepseek-harness-runtime-bin`,
Two carriers coexist under `src/deepseek_harness_runtime/runtime/`, both injected by the repo's `scripts/build-exe-for-python-sdk.ts` build and both gitignored:
- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg-<platform>-<arch>` plus its native `-spawn-helper` sibling (platform: `linux`/`macos`; arch: `x64`/`arm64`). The helper is required by `node-pty`; both files are built as one runtime product, and ELF or thin Mach-O headers must match the target. No Node installation is needed on the target machine. This is the only carrier that ships in wheel distributions; this package does not publish sdists.
- **exe (production)** — a single-file Node executable `dsh-jsonrpc-agent-pkg-<platform>-<arch>` (platform: `linux`/`macos`; arch: `x64`/`arm64`). macOS builds also ship the native `-spawn-helper` sibling that `node-pty` uses there, and its thin Mach-O header must match the target. No Node installation is needed on the target machine. This is the only carrier that ships in wheel distributions; this package does not publish sdists.
- **node (dev-only)** — the full deploy closure under `runtime/node/` (`package.json` + `node_modules/`), executed as `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` on a system Node >= 22.19. It is the current checkout's source build, meant for repo-local development and verification only; it is never selected automatically and is excluded from distributions.
Both carriers hold the same content, defined once: the [package.json](package.json) at this package's root is the deploy root of the single-exe pipeline — a pure dependency manifest (no code of its own) whose dependency closure IS both the plugin set compiled into the exe and the tree materialized into `runtime/node/`. Adding a plugin to the distribution means adding one dependency line there and rebuilding.

View File

@@ -8,7 +8,7 @@ Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`
两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 git 忽略:
- **exe生产**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 及其原生 `-spawn-helper` 伴随文件platform`linux`/`macos`arch`x64`/`arm64`)。`node-pty` 需要该 helper两者作为一个运行时产物构建且 ELF 或 thin Mach-O 文件头必须与目标匹配。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。
- **exe生产**——单文件 Node 可执行程序 `dsh-jsonrpc-agent-pkg-<platform>-<arch>`platform`linux`/`macos`arch`x64`/`arm64`)。macOS 构建还会随附 `node-pty` 在该平台使用的原生 `-spawn-helper` 伴随文件,其 thin Mach-O 文件头必须与目标匹配。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。
- **`node`(仅限开发)**——`runtime/node/` 下的完整部署闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。
两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的部署根目录——一份零代码的纯依赖 manifest其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。

View File

@@ -17,26 +17,18 @@ _SPAWN_HELPER_SUFFIX = "-spawn-helper"
def _spawn_helper_binary_target(header: bytes) -> str | None:
if (
len(header) >= 20
and header[:4] == b"\x7fELF"
and header[4] == 2
and header[5] == 1
):
machine = int.from_bytes(header[18:20], "little")
if machine == 62:
return "linux-x64"
if machine == 183:
return "linux-arm64"
if len(header) >= 8 and header[:4] == b"\xcf\xfa\xed\xfe":
if int.from_bytes(header[4:8], "little") == 0x0100000C:
cpu_type = int.from_bytes(header[4:8], "little")
if cpu_type == 0x01000007:
return "macos-x64"
if cpu_type == 0x0100000C:
return "macos-arm64"
return None
def _validate_spawn_helper(path: Path, expected_target: str) -> None:
with path.open("rb") as helper:
actual_target = _spawn_helper_binary_target(helper.read(20))
actual_target = _spawn_helper_binary_target(helper.read(8))
if actual_target != expected_target:
raise RuntimeError(
f"runtime spawn helper binary mismatch: expected {expected_target}, "
@@ -84,15 +76,18 @@ class RuntimeBuildHook(BuildHookInterface):
f"runtime wheel {platform_tag} must contain only {expected_executable}; found {found}"
)
expected_helper = f"{expected_executable}{_SPAWN_HELPER_SUFFIX}"
if [path.name for path in helpers] != [expected_helper]:
expected_helpers = [expected_helper] if expected_target.startswith("macos-") else []
if [path.name for path in helpers] != expected_helpers:
expected = ", ".join(expected_helpers) or "none"
found = ", ".join(path.name for path in helpers) or "none"
raise RuntimeError(
f"runtime wheel {platform_tag} must contain only {expected_helper}; found {found}"
f"runtime wheel {platform_tag} helper payload mismatch: expected {expected}; found {found}"
)
for executable in [executables[0], helpers[0]]:
for executable in [executables[0], *helpers]:
if executable.stat().st_mode & stat.S_IXUSR == 0:
raise RuntimeError(f"runtime executable is not executable: {executable}")
_validate_spawn_helper(helpers[0], expected_target)
if helpers:
_validate_spawn_helper(helpers[0], expected_target)
build_data["pure_python"] = False
build_data["infer_tag"] = False

View File

@@ -17,14 +17,10 @@ build_python_release = SimpleNamespace(**runpy.run_path(str(SCRIPT)))
def helper_header(target: str) -> bytes:
header = bytearray(20)
if target.startswith("linux-"):
header[:6] = b"\x7fELF\x02\x01"
machine = 62 if target == "linux-x64" else 183
header[18:20] = machine.to_bytes(2, "little")
else:
header[:4] = b"\xcf\xfa\xed\xfe"
header[4:8] = (0x0100000C).to_bytes(4, "little")
header = bytearray(8)
header[:4] = b"\xcf\xfa\xed\xfe"
cpu_type = 0x01000007 if target == "macos-x64" else 0x0100000C
header[4:8] = cpu_type.to_bytes(4, "little")
return bytes(header)
@@ -76,7 +72,7 @@ def test_stage_runtime_copies_executable_and_spawn_helper(tmp_path: Path) -> Non
def test_stage_runtime_rejects_missing_spawn_helper(tmp_path: Path) -> None:
executable = tmp_path / "dsh-jsonrpc-agent-pkg-linux-x64"
executable = tmp_path / "dsh-jsonrpc-agent-pkg-macos-arm64"
executable.write_bytes(b"runtime")
executable.chmod(0o755)
@@ -89,20 +85,36 @@ def test_stage_runtime_rejects_missing_spawn_helper(tmp_path: Path) -> None:
)
@pytest.mark.parametrize("target", ["linux-x64", "linux-arm64", "macos-arm64"])
@pytest.mark.parametrize("target", ["linux-x64", "linux-arm64"])
def test_stage_runtime_copies_linux_executable_without_spawn_helper(
tmp_path: Path, target: str
) -> None:
executable = tmp_path / f"dsh-jsonrpc-agent-pkg-{target}"
executable.write_bytes(b"runtime")
executable.chmod(0o755)
destination = tmp_path / "staging"
build_python_release.stage_runtime(destination, "1.2.3", executable, executable.name)
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
runtime_files = [path.name for path in runtime_dir.glob("dsh-jsonrpc-agent-pkg-*")]
assert runtime_files == [executable.name]
@pytest.mark.parametrize("target", ["macos-x64", "macos-arm64"])
def test_spawn_helper_binary_target(target: str) -> None:
assert build_python_release.spawn_helper_binary_target(helper_header(target)) == target
def test_stage_runtime_rejects_mismatched_spawn_helper(tmp_path: Path) -> None:
executable = tmp_path / "dsh-jsonrpc-agent-pkg-linux-x64"
executable = tmp_path / "dsh-jsonrpc-agent-pkg-macos-arm64"
executable.write_bytes(b"runtime")
executable.chmod(0o755)
spawn_helper = Path(f"{executable}-spawn-helper")
spawn_helper.write_bytes(helper_header("linux-arm64"))
spawn_helper.write_bytes(helper_header("macos-x64"))
spawn_helper.chmod(0o755)
with pytest.raises(ValueError, match="expected linux-x64, found linux-arm64"):
with pytest.raises(ValueError, match="expected macos-arm64, found macos-x64"):
build_python_release.stage_runtime(
tmp_path / "staging",
"1.2.3",

View File

@@ -8,8 +8,8 @@
import { spawn } from 'node:child_process'
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'
import { chmod, copyFile, readFile, rm, writeFile } from 'node:fs/promises'
import { basename, join, resolve, sep } from 'node:path'
import { chmod, copyFile, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
import { basename, dirname, join, resolve, sep } from 'node:path'
import { parseArgs } from 'node:util'
const root = resolve(import.meta.dirname, '..')
@@ -55,19 +55,11 @@ type Arch = (typeof ARCHES)[number]
interface RuntimeProduct {
executable: string
spawnHelper: string
spawnHelper?: string
}
function spawnHelperBinaryTarget(path: string): string | undefined {
const header = readFileSync(path).subarray(0, 20)
if (header.length >= 20
&& header.subarray(0, 4).equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))
&& header[4] === 2
&& header[5] === 1) {
const machine = header.readUInt16LE(18)
if (machine === 62) return 'linux-x64'
if (machine === 183) return 'linux-arm64'
}
const header = readFileSync(path).subarray(0, 8)
if (header.length >= 8 && header.readUInt32LE(0) === 0xfeedfacf) {
const cpuType = header.readUInt32LE(4)
if (cpuType === 0x01000007) return 'macos-x64'
@@ -76,6 +68,10 @@ function spawnHelperBinaryTarget(path: string): string | undefined {
return undefined
}
function runtimeProductFiles(product: RuntimeProduct): string[] {
return [product.executable, ...(product.spawnHelper === undefined ? [] : [product.spawnHelper])]
}
function isPlatform(value: string): value is Platform {
return (PLATFORMS as readonly string[]).includes(value)
}
@@ -317,7 +313,7 @@ class SingleExeBuild {
*/
async pack(target: Target): Promise<RuntimeProduct> {
const product = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
const spawnHelper = `${product}${SPAWN_HELPER_SUFFIX}`
await this.prepareNativePty(target)
if (!this.cli.dryRun) mkdirSync(this.outDir, { recursive: true })
await this.run(`pkg ${target.spec}`, pnpmBin(), [
'dlx',
@@ -332,6 +328,8 @@ class SingleExeBuild {
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}.`)
}
if (target.platform !== 'macos') return { executable: product }
const spawnHelper = `${product}${SPAWN_HELPER_SUFFIX}`
if (this.cli.dryRun) {
console.log(`build-exe-for-python-sdk: [dry-run] copy target node-pty spawn-helper to ${spawnHelper}`)
} else {
@@ -342,6 +340,38 @@ class SingleExeBuild {
return { executable: product, spawnHelper }
}
/**
* Put the target node-pty addon in the staged closure. Linux npm installs
* build it from source, but legacy deploy omits that side-effect directory.
* @param target - the pkg target whose native addon is being staged.
*/
private async prepareNativePty(target: Target): Promise<void> {
const stagedRoot = join(this.staging, 'node_modules', 'node-pty')
const stagedBuild = join(stagedRoot, 'build')
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${stagedBuild}`)
else await rm(stagedBuild, { recursive: true, force: true })
const nativePlatform = target.platform === 'macos' ? 'darwin' : 'linux'
const prebuilt = join(stagedRoot, 'prebuilds', `${nativePlatform}-${target.arch}`, 'pty.node')
const source = join(root, 'packages', 'pty', 'pty-local', 'node_modules', 'node-pty', 'build', 'Release', 'pty.node')
const destination = join(stagedBuild, 'Release', 'pty.node')
if (this.cli.dryRun) {
if (target.platform === 'linux') console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
return
}
if (existsSync(prebuilt)) return
const host = Target.host()
if (target.platform !== host.platform || target.arch !== host.arch || !existsSync(source)) {
throw new Error(
`build-exe-for-python-sdk: node-pty native addon for ${target.platform}-${target.arch} is missing; `
+ `checked ${prebuilt}, ${source}. Build the Linux runtime on its target architecture.`,
)
}
await mkdir(dirname(destination), { recursive: true })
await copyFile(source, destination)
}
/**
* Resolve the node-pty helper that matches a pkg target.
* @param target - the pkg target whose helper must be shipped.
@@ -349,14 +379,12 @@ class SingleExeBuild {
*/
private resolveSpawnHelper(target: Target): string {
const nodePtyRoot = join(this.staging, 'node_modules', 'node-pty')
const nativePlatform = target.platform === 'macos' ? 'darwin' : 'linux'
const candidates = [
join(nodePtyRoot, 'prebuilds', `${nativePlatform}-${target.arch}`, 'spawn-helper'),
join(nodePtyRoot, 'prebuilds', `darwin-${target.arch}`, 'spawn-helper'),
]
const hostPlatform = process.platform === 'darwin' ? 'macos' : process.platform
const hostArch = process.arch === 'x64' || process.arch === 'arm64' ? process.arch : undefined
if (target.platform === hostPlatform && target.arch === hostArch) {
candidates.push(join(nodePtyRoot, 'build', 'Release', 'spawn-helper'))
const host = Target.host()
if (target.platform === host.platform && target.arch === host.arch) {
candidates.push(join(root, 'packages', 'pty', 'pty-local', 'node_modules', 'node-pty', 'build', 'Release', 'spawn-helper'))
}
const helper = candidates.find(candidate => existsSync(candidate))
if (helper === undefined) {
@@ -387,11 +415,10 @@ class SingleExeBuild {
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.executable}`)
console.log(` ${product.spawnHelper}`)
for (const path of runtimeProductFiles(product)) console.log(` ${path}`)
continue
}
for (const path of [product.executable, product.spawnHelper]) {
for (const path of runtimeProductFiles(product)) {
const megabytes = statSync(path).size / (1024 * 1024)
console.log(` ${path} (${megabytes.toFixed(1)} MB)`)
}
@@ -407,7 +434,7 @@ class SingleExeBuild {
const destDir = resolve(root, PYTHON_RUNTIME_DIR)
if (this.cli.dryRun) {
for (const product of products) {
for (const path of [product.executable, product.spawnHelper]) {
for (const path of runtimeProductFiles(product)) {
console.log(`build-exe-for-python-sdk: [dry-run] cp ${path} ${join(destDir, basename(path))}`)
}
}
@@ -415,7 +442,7 @@ class SingleExeBuild {
}
mkdirSync(destDir, { recursive: true })
for (const product of products) {
for (const path of [product.executable, product.spawnHelper]) {
for (const path of runtimeProductFiles(product)) {
const destination = join(destDir, basename(path))
await copyFile(path, destination)
await chmod(destination, statSync(path).mode & 0o777)

View File

@@ -27,26 +27,18 @@ EXECUTABLE_TARGETS = {value[1]: key for key, value in PLATFORMS.items()}
def spawn_helper_binary_target(header: bytes) -> str | None:
if (
len(header) >= 20
and header[:4] == b"\x7fELF"
and header[4] == 2
and header[5] == 1
):
machine = int.from_bytes(header[18:20], "little")
if machine == 62:
return "linux-x64"
if machine == 183:
return "linux-arm64"
if len(header) >= 8 and header[:4] == b"\xcf\xfa\xed\xfe":
if int.from_bytes(header[4:8], "little") == 0x0100000C:
cpu_type = int.from_bytes(header[4:8], "little")
if cpu_type == 0x01000007:
return "macos-x64"
if cpu_type == 0x0100000C:
return "macos-arm64"
return None
def validate_spawn_helper(path: Path, expected_target: str) -> None:
with path.open("rb") as helper:
actual_target = spawn_helper_binary_target(helper.read(20))
actual_target = spawn_helper_binary_target(helper.read(8))
if actual_target != expected_target:
raise ValueError(
f"runtime spawn helper binary mismatch: expected {expected_target}, "
@@ -166,12 +158,14 @@ def stage_runtime(destination: Path, version: str, executable: Path, executable_
raise FileNotFoundError(f"runtime executable does not exist: {executable}")
if executable.stat().st_mode & stat.S_IXUSR == 0:
raise PermissionError(f"runtime executable is not executable: {executable}")
expected_target = EXECUTABLE_TARGETS[executable_name]
spawn_helper = Path(f"{executable}{SPAWN_HELPER_SUFFIX}")
if not spawn_helper.is_file():
raise FileNotFoundError(f"runtime spawn helper does not exist: {spawn_helper}")
if spawn_helper.stat().st_mode & stat.S_IXUSR == 0:
raise PermissionError(f"runtime spawn helper is not executable: {spawn_helper}")
validate_spawn_helper(spawn_helper, EXECUTABLE_TARGETS[executable_name])
if expected_target.startswith("macos-"):
if not spawn_helper.is_file():
raise FileNotFoundError(f"runtime spawn helper does not exist: {spawn_helper}")
if spawn_helper.stat().st_mode & stat.S_IXUSR == 0:
raise PermissionError(f"runtime spawn helper is not executable: {spawn_helper}")
validate_spawn_helper(spawn_helper, expected_target)
copy_package(ROOT / "python" / "sdk-runtime", destination)
rewrite_version(destination / "pyproject.toml", version)
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
@@ -179,9 +173,10 @@ def stage_runtime(destination: Path, version: str, executable: Path, executable_
destination_executable = runtime_dir / executable_name
shutil.copyfile(executable, destination_executable)
destination_executable.chmod(executable.stat().st_mode & 0o777)
destination_helper = runtime_dir / f"{executable_name}{SPAWN_HELPER_SUFFIX}"
shutil.copyfile(spawn_helper, destination_helper)
destination_helper.chmod(spawn_helper.stat().st_mode & 0o777)
if expected_target.startswith("macos-"):
destination_helper = runtime_dir / f"{executable_name}{SPAWN_HELPER_SUFFIX}"
shutil.copyfile(spawn_helper, destination_helper)
destination_helper.chmod(spawn_helper.stat().st_mode & 0o777)
def verify_wheel(
@@ -209,20 +204,27 @@ def verify_wheel(
assert platform is not None
if len(executables) != 1 or not executables[0].endswith(f"/runtime/{platform[1]}"):
raise RuntimeError(f"{wheel} must contain exactly {platform[1]}, found {executables}")
expected_target = EXECUTABLE_TARGETS[platform[1]]
expected_helper = f"{platform[1]}{SPAWN_HELPER_SUFFIX}"
if len(helpers) != 1 or not helpers[0].endswith(f"/runtime/{expected_helper}"):
raise RuntimeError(f"{wheel} must contain exactly {expected_helper}, found {helpers}")
for executable in [executables[0], helpers[0]]:
expected_helpers = [expected_helper] if expected_target.startswith("macos-") else []
found_helpers = [Path(helper).name for helper in helpers]
if found_helpers != expected_helpers:
expected = ", ".join(expected_helpers) or "none"
found = ", ".join(found_helpers) or "none"
raise RuntimeError(
f"{wheel} runtime helper payload mismatch: expected {expected}; found {found}"
)
for executable in [executables[0], *helpers]:
mode = archive.getinfo(executable).external_attr >> 16
if mode & stat.S_IXUSR == 0:
raise RuntimeError(f"{wheel} runtime executable lost its executable bit: {executable}")
actual_target = spawn_helper_binary_target(archive.read(helpers[0])[:20])
expected_target = EXECUTABLE_TARGETS[platform[1]]
if actual_target != expected_target:
raise RuntimeError(
f"{wheel} spawn helper binary mismatch: expected {expected_target}, "
f"found {actual_target or 'unsupported format or architecture'}"
)
if helpers:
actual_target = spawn_helper_binary_target(archive.read(helpers[0])[:8])
if actual_target != expected_target:
raise RuntimeError(
f"{wheel} spawn helper binary mismatch: expected {expected_target}, "
f"found {actual_target or 'unsupported format or architecture'}"
)
elif runtime_files:
raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}")
if package == "sdk":