mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(build): validate packaged spawn helpers
This commit is contained in:
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# 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
|
# 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: 26949e7435bcbf05132662c308f33f322920c7eb
|
2026-07-29-persistent-bash-str-replace-editor.md: b1be9cc40e11b07b722877e666de0e0328636a05
|
||||||
2026-07-29-persistent-bash-str-replace-editor.zh.md: 6d5eadbfa68a59157e8f4bc148d03144bc665cb2
|
2026-07-29-persistent-bash-str-replace-editor.zh.md: 0bf100514e971dfa759f44dcca15a7ad6a2fdd8a
|
||||||
|
|||||||
@@ -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.
|
`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 (or when `DSH_NODE_PTY_SPAWN_HELPER` explicitly selects one), preserving upstream lookup in ordinary Node runs; the executable and runtime-wheel builders 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`, 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.
|
||||||
|
|
||||||
## Alternatives considered
|
## Alternatives considered
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
`dsh-system-prompt` 接受 `includeHarnessIdentity: false`;`dsh-agent-spine-demo` 会转发该设置,并接受 `toolBash: false`。因此部署可以拥有精确 persona,并替换 spine 的原生 Bash,而不会重复注册提示词或工具。既有默认值不变。
|
`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` 补丁只在该伴随文件存在时解析它(也可由 `DSH_NODE_PTY_SPAWN_HELPER` 显式指定),普通 Node 运行仍保留上游查找方式;若 helper 缺失、架构不匹配或不可执行,可执行文件与 runtime wheel 构建会在发布前失败。
|
两个插件都进入 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 缺失、架构不匹配或不可执行,构建会在发布前失败。
|
||||||
|
|
||||||
## 考虑过的替代方案
|
## 考虑过的替代方案
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ diff --git a/lib/unixTerminal.js b/lib/unixTerminal.js
|
|||||||
index 1ec12f796a822c78fba9ad7f6448c3987e325c23..5cd6b7d635f4752be5a6c5ff9cf9edf988cf94c5 100644
|
index 1ec12f796a822c78fba9ad7f6448c3987e325c23..5cd6b7d635f4752be5a6c5ff9cf9edf988cf94c5 100644
|
||||||
--- a/lib/unixTerminal.js
|
--- a/lib/unixTerminal.js
|
||||||
+++ b/lib/unixTerminal.js
|
+++ b/lib/unixTerminal.js
|
||||||
@@ -26,10 +26,22 @@ var terminal_1 = require("./terminal");
|
@@ -26,10 +26,23 @@ var terminal_1 = require("./terminal");
|
||||||
var utils_1 = require("./utils");
|
var utils_1 = require("./utils");
|
||||||
var native = utils_1.loadNativeModule('pty');
|
var native = utils_1.loadNativeModule('pty');
|
||||||
var pty = native.module;
|
var pty = native.module;
|
||||||
@@ -10,6 +10,7 @@ index 1ec12f796a822c78fba9ad7f6448c3987e325c23..5cd6b7d635f4752be5a6c5ff9cf9edf9
|
|||||||
-helperPath = path.resolve(__dirname, helperPath);
|
-helperPath = path.resolve(__dirname, helperPath);
|
||||||
-helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
-helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
||||||
-helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
-helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
||||||
|
+// A current external embedded-runtime consumer supplies a non-sibling helper.
|
||||||
+var helperPath = process.env.DSH_NODE_PTY_SPAWN_HELPER;
|
+var helperPath = process.env.DSH_NODE_PTY_SPAWN_HELPER;
|
||||||
+if (helperPath) {
|
+if (helperPath) {
|
||||||
+ helperPath = path.resolve(helperPath);
|
+ helperPath = path.resolve(helperPath);
|
||||||
@@ -33,7 +34,7 @@ diff --git a/src/unixTerminal.ts b/src/unixTerminal.ts
|
|||||||
index 98733dc0cd752b554bd94e45904ca341ad141bba..fa234291206617ae5a6d8605abf9771220392d17 100644
|
index 98733dc0cd752b554bd94e45904ca341ad141bba..fa234291206617ae5a6d8605abf9771220392d17 100644
|
||||||
--- a/src/unixTerminal.ts
|
--- a/src/unixTerminal.ts
|
||||||
+++ b/src/unixTerminal.ts
|
+++ b/src/unixTerminal.ts
|
||||||
@@ -14,10 +14,20 @@ import { assign, loadNativeModule } from './utils';
|
@@ -14,10 +14,21 @@ import { assign, loadNativeModule } from './utils';
|
||||||
|
|
||||||
const native = loadNativeModule('pty');
|
const native = loadNativeModule('pty');
|
||||||
const pty: IUnixNative = native.module;
|
const pty: IUnixNative = native.module;
|
||||||
@@ -41,6 +42,7 @@ index 98733dc0cd752b554bd94e45904ca341ad141bba..fa234291206617ae5a6d8605abf97712
|
|||||||
-helperPath = path.resolve(__dirname, helperPath);
|
-helperPath = path.resolve(__dirname, helperPath);
|
||||||
-helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
-helperPath = helperPath.replace('app.asar', 'app.asar.unpacked');
|
||||||
-helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
-helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked');
|
||||||
|
+// A current external embedded-runtime consumer supplies a non-sibling helper.
|
||||||
+let helperPath = process.env.DSH_NODE_PTY_SPAWN_HELPER;
|
+let helperPath = process.env.DSH_NODE_PTY_SPAWN_HELPER;
|
||||||
+if (helperPath) {
|
+if (helperPath) {
|
||||||
+ helperPath = path.resolve(helperPath);
|
+ helperPath = path.resolve(helperPath);
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -6,7 +6,7 @@ settings:
|
|||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
'@earendil-works/pi-tui@0.80.7': 6c30c5386c0159131e1361023cddf31377f5728962524841964373312c1ed946
|
'@earendil-works/pi-tui@0.80.7': 6c30c5386c0159131e1361023cddf31377f5728962524841964373312c1ed946
|
||||||
node-pty@1.1.0: 4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15
|
node-pty@1.1.0: 7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
@@ -656,7 +656,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
node-pty:
|
node-pty:
|
||||||
specifier: 1.1.0
|
specifier: 1.1.0
|
||||||
version: 1.1.0(patch_hash=4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15)
|
version: 1.1.0(patch_hash=7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6)
|
||||||
|
|
||||||
packages/acp/acp:
|
packages/acp/acp:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3402,7 +3402,7 @@ importers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
node-pty:
|
node-pty:
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.0(patch_hash=4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15)
|
version: 1.1.0(patch_hash=7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6)
|
||||||
schemastery:
|
schemastery:
|
||||||
specifier: ^3.18.0
|
specifier: ^3.18.0
|
||||||
version: 3.18.0
|
version: 3.18.0
|
||||||
@@ -15287,7 +15287,7 @@ snapshots:
|
|||||||
fetch-blob: 3.2.0
|
fetch-blob: 3.2.0
|
||||||
formdata-polyfill: 4.0.10
|
formdata-polyfill: 4.0.10
|
||||||
|
|
||||||
node-pty@1.1.0(patch_hash=4a1568bc9ef77084629054d0736430818818155abcd0dce581ef8c782e974c15):
|
node-pty@1.1.0(patch_hash=7a0c04f1f49d798a9ffe2f7f414c01064a44ca2489772d0c3e1235ab336755e6):
|
||||||
dependencies:
|
dependencies:
|
||||||
node-addon-api: 7.1.1
|
node-addon-api: 7.1.1
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@
|
|||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# 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:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write python/sdk-runtime/README.md
|
# pnpm run verify-translation-pairing --write python/sdk-runtime/README.md
|
||||||
README.md: ee3791eddf26b526316d4f3952793a03cc48841e
|
README.md: 0869b4a9dce0e261b168f21c90a80faf81ea4a64
|
||||||
README.zh.md: 59d40ee56688cb377902ff126b7fa77606c7ad8b
|
README.zh.md: 3f2342c4c66e24b8213c78d4e7530c3360497022
|
||||||
|
|||||||
@@ -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:
|
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 and validated as one runtime product. 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>` 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.
|
||||||
- **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.
|
- **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.
|
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.
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Python SDK 的运行时载体包(分发名 `deepseek-harness-runtime-bin`,
|
|||||||
|
|
||||||
两种载体并存于 `src/deepseek_harness_runtime/runtime/` 之下,均由仓库的 `scripts/build-exe-for-python-sdk.ts` 构建注入,且均被 git 忽略:
|
两种载体并存于 `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;构建与校验会把两者视作同一个运行时产物。目标机器无需安装 Node。这是唯一随 wheel 包分发的载体;本包不发布 sdist。
|
- **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。
|
||||||
- **`node`(仅限开发)**——`runtime/node/` 下的完整部署闭包(`package.json` + `node_modules/`),在系统 Node >= 22.19 上以 `node runtime/node/node_modules/@deepseek-ai/dsh-jsonrpc-demo/lib/bin.js` 执行。它是当前检出的源码构建,仅用于仓库本地的开发与验证;不会被自动选中,也不进入分发物。
|
- **`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/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。
|
两种载体承载相同的内容,且只定义一次:本包根目录的 [package.json](package.json) 是 single-exe 流水线的部署根目录——一份零代码的纯依赖 manifest,其依赖闭包既是编译进 exe 的插件集,也是物化到 `runtime/node/` 的文件树。往分发物里加插件,就是在那里加一行依赖再重新构建。
|
||||||
|
|||||||
@@ -16,6 +16,34 @@ _PLATFORMS = {
|
|||||||
_SPAWN_HELPER_SUFFIX = "-spawn-helper"
|
_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:
|
||||||
|
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))
|
||||||
|
if actual_target != expected_target:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"runtime spawn helper binary mismatch: expected {expected_target}, "
|
||||||
|
f"found {actual_target or 'unsupported format or architecture'} at {path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _host_platform_tag() -> str:
|
def _host_platform_tag() -> str:
|
||||||
machine = platform.machine().lower()
|
machine = platform.machine().lower()
|
||||||
arch = "arm64" if machine in {"arm64", "aarch64"} else "x64" if machine in {"x86_64", "amd64"} else machine
|
arch = "arm64" if machine in {"arm64", "aarch64"} else "x64" if machine in {"x86_64", "amd64"} else machine
|
||||||
@@ -39,13 +67,13 @@ class RuntimeBuildHook(BuildHookInterface):
|
|||||||
)
|
)
|
||||||
|
|
||||||
platform_tag = os.environ.get("DSH_RUNTIME_PLATFORM_TAG") or _host_platform_tag()
|
platform_tag = os.environ.get("DSH_RUNTIME_PLATFORM_TAG") or _host_platform_tag()
|
||||||
matches = [value for value in _PLATFORMS.values() if value[0] == platform_tag]
|
matches = [(key, value) for key, value in _PLATFORMS.items() if value[0] == platform_tag]
|
||||||
if len(matches) != 1:
|
if len(matches) != 1:
|
||||||
supported = ", ".join(value[0] for value in _PLATFORMS.values())
|
supported = ", ".join(value[0] for value in _PLATFORMS.values())
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"unsupported DSH_RUNTIME_PLATFORM_TAG {platform_tag!r}; expected one of {supported}"
|
f"unsupported DSH_RUNTIME_PLATFORM_TAG {platform_tag!r}; expected one of {supported}"
|
||||||
)
|
)
|
||||||
expected_executable = matches[0][1]
|
expected_target, (_, expected_executable) = matches[0]
|
||||||
runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime"
|
runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime"
|
||||||
runtime_files = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else [])
|
runtime_files = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else [])
|
||||||
executables = [path for path in runtime_files if not path.name.endswith(_SPAWN_HELPER_SUFFIX)]
|
executables = [path for path in runtime_files if not path.name.endswith(_SPAWN_HELPER_SUFFIX)]
|
||||||
@@ -64,6 +92,7 @@ class RuntimeBuildHook(BuildHookInterface):
|
|||||||
for executable in [executables[0], helpers[0]]:
|
for executable in [executables[0], helpers[0]]:
|
||||||
if executable.stat().st_mode & stat.S_IXUSR == 0:
|
if executable.stat().st_mode & stat.S_IXUSR == 0:
|
||||||
raise RuntimeError(f"runtime executable is not executable: {executable}")
|
raise RuntimeError(f"runtime executable is not executable: {executable}")
|
||||||
|
_validate_spawn_helper(helpers[0], expected_target)
|
||||||
|
|
||||||
build_data["pure_python"] = False
|
build_data["pure_python"] = False
|
||||||
build_data["infer_tag"] = False
|
build_data["infer_tag"] = False
|
||||||
|
|||||||
@@ -16,6 +16,18 @@ SCRIPT = ROOT / "scripts" / "build-python-release.py"
|
|||||||
build_python_release = SimpleNamespace(**runpy.run_path(str(SCRIPT)))
|
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")
|
||||||
|
return bytes(header)
|
||||||
|
|
||||||
|
|
||||||
def test_repository_version_matches_root_package_json() -> None:
|
def test_repository_version_matches_root_package_json() -> None:
|
||||||
expected = json.loads((ROOT / "package.json").read_text())["version"]
|
expected = json.loads((ROOT / "package.json").read_text())["version"]
|
||||||
|
|
||||||
@@ -45,7 +57,7 @@ def test_stage_runtime_copies_executable_and_spawn_helper(tmp_path: Path) -> Non
|
|||||||
executable.write_bytes(b"runtime")
|
executable.write_bytes(b"runtime")
|
||||||
executable.chmod(0o755)
|
executable.chmod(0o755)
|
||||||
spawn_helper = Path(f"{executable}-spawn-helper")
|
spawn_helper = Path(f"{executable}-spawn-helper")
|
||||||
spawn_helper.write_bytes(b"helper")
|
spawn_helper.write_bytes(helper_header("macos-arm64"))
|
||||||
spawn_helper.chmod(0o751)
|
spawn_helper.chmod(0o751)
|
||||||
destination = tmp_path / "staging"
|
destination = tmp_path / "staging"
|
||||||
|
|
||||||
@@ -59,7 +71,7 @@ def test_stage_runtime_copies_executable_and_spawn_helper(tmp_path: Path) -> Non
|
|||||||
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
|
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
|
||||||
assert (runtime_dir / executable.name).read_bytes() == b"runtime"
|
assert (runtime_dir / executable.name).read_bytes() == b"runtime"
|
||||||
copied_helper = runtime_dir / spawn_helper.name
|
copied_helper = runtime_dir / spawn_helper.name
|
||||||
assert copied_helper.read_bytes() == b"helper"
|
assert copied_helper.read_bytes() == helper_header("macos-arm64")
|
||||||
assert copied_helper.stat().st_mode & stat.S_IXUSR
|
assert copied_helper.stat().st_mode & stat.S_IXUSR
|
||||||
|
|
||||||
|
|
||||||
@@ -75,3 +87,42 @@ def test_stage_runtime_rejects_missing_spawn_helper(tmp_path: Path) -> None:
|
|||||||
executable,
|
executable,
|
||||||
executable.name,
|
executable.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("target", ["linux-x64", "linux-arm64", "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.write_bytes(b"runtime")
|
||||||
|
executable.chmod(0o755)
|
||||||
|
spawn_helper = Path(f"{executable}-spawn-helper")
|
||||||
|
spawn_helper.write_bytes(helper_header("linux-arm64"))
|
||||||
|
spawn_helper.chmod(0o755)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="expected linux-x64, found linux-arm64"):
|
||||||
|
build_python_release.stage_runtime(
|
||||||
|
tmp_path / "staging",
|
||||||
|
"1.2.3",
|
||||||
|
executable,
|
||||||
|
executable.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stage_runtime_rejects_non_binary_spawn_helper(tmp_path: Path) -> None:
|
||||||
|
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(b"helper")
|
||||||
|
spawn_helper.chmod(0o755)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="unsupported format or architecture"):
|
||||||
|
build_python_release.stage_runtime(
|
||||||
|
tmp_path / "staging",
|
||||||
|
"1.2.3",
|
||||||
|
executable,
|
||||||
|
executable.name,
|
||||||
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
import { existsSync, mkdirSync, statSync } from 'node:fs'
|
import { existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'
|
||||||
import { chmod, copyFile, readFile, rm, writeFile } from 'node:fs/promises'
|
import { chmod, copyFile, readFile, rm, writeFile } from 'node:fs/promises'
|
||||||
import { basename, join, resolve, sep } from 'node:path'
|
import { basename, join, resolve, sep } from 'node:path'
|
||||||
import { parseArgs } from 'node:util'
|
import { parseArgs } from 'node:util'
|
||||||
@@ -58,6 +58,24 @@ interface RuntimeProduct {
|
|||||||
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'
|
||||||
|
}
|
||||||
|
if (header.length >= 8 && header.readUInt32LE(0) === 0xfeedfacf) {
|
||||||
|
const cpuType = header.readUInt32LE(4)
|
||||||
|
if (cpuType === 0x01000007) return 'macos-x64'
|
||||||
|
if (cpuType === 0x0100000c) return 'macos-arm64'
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
function isPlatform(value: string): value is Platform {
|
function isPlatform(value: string): value is Platform {
|
||||||
return (PLATFORMS as readonly string[]).includes(value)
|
return (PLATFORMS as readonly string[]).includes(value)
|
||||||
}
|
}
|
||||||
@@ -347,7 +365,17 @@ class SingleExeBuild {
|
|||||||
+ `checked ${candidates.join(', ')}. Build each runtime on its target platform and architecture.`,
|
+ `checked ${candidates.join(', ')}. Build each runtime on its target platform and architecture.`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (statSync(helper).mode & 0o111) return helper
|
if (statSync(helper).mode & 0o111) {
|
||||||
|
const expected = `${target.platform}-${target.arch}`
|
||||||
|
const actual = spawnHelperBinaryTarget(helper)
|
||||||
|
if (actual !== expected) {
|
||||||
|
throw new Error(
|
||||||
|
`build-exe-for-python-sdk: node-pty spawn-helper binary mismatch: expected ${expected}, `
|
||||||
|
+ `found ${actual ?? 'unsupported format or architecture'} at ${helper}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return helper
|
||||||
|
}
|
||||||
throw new Error(`build-exe-for-python-sdk: node-pty spawn-helper is not executable: ${helper}`)
|
throw new Error(`build-exe-for-python-sdk: node-pty spawn-helper is not executable: ${helper}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,35 @@ PLATFORMS = {
|
|||||||
"macos-arm64": ("macosx_11_0_arm64", "dsh-jsonrpc-agent-pkg-macos-arm64"),
|
"macos-arm64": ("macosx_11_0_arm64", "dsh-jsonrpc-agent-pkg-macos-arm64"),
|
||||||
}
|
}
|
||||||
SPAWN_HELPER_SUFFIX = "-spawn-helper"
|
SPAWN_HELPER_SUFFIX = "-spawn-helper"
|
||||||
|
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:
|
||||||
|
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))
|
||||||
|
if actual_target != expected_target:
|
||||||
|
raise ValueError(
|
||||||
|
f"runtime spawn helper binary mismatch: expected {expected_target}, "
|
||||||
|
f"found {actual_target or 'unsupported format or architecture'} at {path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -142,6 +171,7 @@ def stage_runtime(destination: Path, version: str, executable: Path, executable_
|
|||||||
raise FileNotFoundError(f"runtime spawn helper does not exist: {spawn_helper}")
|
raise FileNotFoundError(f"runtime spawn helper does not exist: {spawn_helper}")
|
||||||
if spawn_helper.stat().st_mode & stat.S_IXUSR == 0:
|
if spawn_helper.stat().st_mode & stat.S_IXUSR == 0:
|
||||||
raise PermissionError(f"runtime spawn helper is not executable: {spawn_helper}")
|
raise PermissionError(f"runtime spawn helper is not executable: {spawn_helper}")
|
||||||
|
validate_spawn_helper(spawn_helper, EXECUTABLE_TARGETS[executable_name])
|
||||||
copy_package(ROOT / "python" / "sdk-runtime", destination)
|
copy_package(ROOT / "python" / "sdk-runtime", destination)
|
||||||
rewrite_version(destination / "pyproject.toml", version)
|
rewrite_version(destination / "pyproject.toml", version)
|
||||||
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
|
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
|
||||||
@@ -186,6 +216,13 @@ def verify_wheel(
|
|||||||
mode = archive.getinfo(executable).external_attr >> 16
|
mode = archive.getinfo(executable).external_attr >> 16
|
||||||
if mode & stat.S_IXUSR == 0:
|
if mode & stat.S_IXUSR == 0:
|
||||||
raise RuntimeError(f"{wheel} runtime executable lost its executable bit: {executable}")
|
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'}"
|
||||||
|
)
|
||||||
elif runtime_files:
|
elif runtime_files:
|
||||||
raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}")
|
raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}")
|
||||||
if package == "sdk":
|
if package == "sdk":
|
||||||
|
|||||||
Reference in New Issue
Block a user