diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml index cba3cd1c5c..c0d17b2a70 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml @@ -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/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md -2026-07-10-single-file-executable-sdk-runtime-distribution.md: ab09691b48684b8ebae269df4d585b499423e671 -2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d986105b087357bd8301251dca14736a5b427bbb +2026-07-10-single-file-executable-sdk-runtime-distribution.md: 3f8f11c4b9d8ece425ed737bdfdf962315412216 +2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: de0d011b9e7a2a52748ac273d5f0bbb7dce27894 diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md index ab09691b48..3f8f11c4b9 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md @@ -46,13 +46,13 @@ The deploy root includes `@deepseek-ai/dsh-mcp-client` as an explicitly supporte [`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-python-runtime-closure 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/` → restore direct workspace packages omitted by legacy deploy and reject any remaining manifest gap → replace staged dependency symlinks with their target bytes, remove package-manager `.bin` links, and fail if any symlink remains → inject pkg configuration whose bin is `node_modules/@deepseek-ai/dsh/lib/bin.js` and whose assets cover dynamic profile, bundle, frontend, preset, native-library, and configuration reads → stage the target `node-pty` addon → invoke `pkg --sea` once per target → write `deepseek-harness-sdk-runtime--` under `dist-exe/` and copy it into the runtime directory. Linux CI rebuilds `pty.node` inside the matching manylinux 2.28 container because legacy deploy omits that install side effect. Every target copies its native `@vscode/ripgrep` binary beside the executable as the required `-rg` sidecar; pkg runtimes select that sidecar through `process.pkg`, while ordinary Node execution uses `@vscode/ripgrep` directly. macOS uses its target prebuild and also emits the required `-spawn-helper`. All four deploy flags are grounded in measurement: `--legacy` is the mandatory path with inject-workspace-packages off; hoisted gives pkg a stable single-instance layout that the explicit materialization pass makes symlink-free; disabling automatic peer installation prevents undeclared peers from expanding the closure; link-workspace-packages selects direct workspace dependencies. [`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) overrides the transitive `@deepseek-ai/cosmokit` and `@deepseek-ai/schemastery` semver requests to the pinned vendor sources so legacy deploy never resolves those unpublished names from a registry. -CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all four targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS verifies that the executable's deployment target fits the wheel tag. A full four-target run retains five artifacts, each containing one release file: the platform-independent SDK wheel and four 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 `python-v` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and four native runtime wheels, then a single serialized job checks and publishes all five to the project PyPI registry. The [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the fourth target and the explicit exclusion of Windows arm64. +CI: [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml) is called for all five targets by the [installed-wheel Python runtime pull-request validation](../testing/2026-08-23-installed-python-wheel-black-box-ci.md) and the [public publication workflow](../process/2026-08-11-python-publication-workflow.md); `workflow_dispatch` can still select a subset. Native builds run on linux-x64 / linux-arm64 (`ubuntu-24.04-arm`) / macos-arm64 / macos-x64 (`macos-15-intel`) / win-x64 (`windows-2025`), with `~/.pkg-cache` cached where applicable, and pkg handles macOS ad-hoc signing. Each leg installs the release-shaped SDK and runtime wheels into a clean venv outside the checkout, proves their package and executable provenance, then drives the complete keyless scenario set through the public SDK and direct NDJSON JSON-RPC. Trusted pull requests additionally run a real DeepSeek two-turn tool smoke on every target; fork and Dependabot heads receive no key. Linux inspects the executable and native addon's GLIBC requirements and runs an additional manylinux 2.28 smoke, while macOS checks the runtime, ripgrep, and PTY helper architectures and verifies that all three deployment targets fit the wheel tag. A full five-target run retains six artifacts, each containing one release file: the platform-independent SDK wheel and five 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 `python-v` tag pipelines whose version matches the root `package.json`, builds one SDK wheel and five native runtime wheels, then a single serialized job checks and publishes all six to the project PyPI registry. The [Windows x64 runtime decision](2026-08-23-python-sdk-windows-x64-runtime.md) owns the Windows target and the explicit exclusion of Windows arm64. ### Python SDK distribution: two carriers, exe for production, node for development The Python SDK lives at [`python/`](../../../../python/README.md): `python/sdk` is the client and `python/sdk-runtime` is the runtime carrier package. The runtime package's data directory holds the build-injected platform executable with its required `-rg` sidecar and optional macOS helper, plus the build-injected `runtime/node/` closure tree for repository development. `resolve_bundled_launch_args()` selects the executable by default; explicit `DSH_RUNTIME_MODE=node` runs `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. The node carrier never enters wheel distributions, and neither carrier uses a checked-in complete `cordis.yml`. -[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v` 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. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, the conservative `py3-none-macosx_14_0_arm64` tag for the Node 24 executable's macOS 13.5 deployment target, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. +[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) reads the authoritative `X.Y.Z` or prerelease version from the repository root `package.json`, converts prereleases to their PEP 440 spelling, and stages both packages at that wheel version, with `deepseek-harness-sdk` depending exactly on the matching `deepseek-harness-runtime-bin`. An optional `python-v` 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. Staging also carries the repository license into both wheels and the third-party notices into the bundled runtime wheel. The SDK is a `py3-none-any` wheel; each wheel-only runtime package contains one exe and its architecture-matched ripgrep sidecar, and the macOS wheel also contains its architecture-matched spawn helper. Runtime wheels use `py3-none-manylinux_2_28_x86_64`, `py3-none-manylinux_2_28_aarch64`, the conservative `py3-none-macosx_14_0_arm64` or `py3-none-macosx_14_0_x86_64` tag for the validated macOS payload deployment targets, or `py3-none-win_amd64`; the Hatch hook rejects sdists, universal tags, mixed-platform payloads, missing or extra sidecars, and unsupported platforms. The Python client launches the packaged `dsh` command with the selected profile (`sdk` by default), ordered patch files, and an explicit Harness home. The profile owns JSON-RPC serving and application composition; missing homes, profiles, bundles, patches, and server rows fail without an external complete-config fallback. diff --git a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md index d986105b08..de0d011b9e 100644 --- a/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md +++ b/.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md @@ -46,13 +46,13 @@ exe 的 VFS 内是**构建产物形态的真实包树**(各包的 `lib/` + 真 [`scripts/build-exe-for-python-sdk.ts`](../../../../scripts/build-exe-for-python-sdk.ts):运行时闭包校验 → `pnpm run build` →(清空后)`pnpm --filter dsh-python-runtime-closure 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/` → 恢复 legacy deploy 遗漏的直接工作区包,并拒绝剩余的 manifest 缺口 → 将暂存依赖中的符号链接替换为目标文件内容,删除包管理器的 `.bin` 链接,并在仍有任何符号链接时失败 → 注入 pkg 配置,其中 bin 为 `node_modules/@deepseek-ai/dsh/lib/bin.js`,assets 覆盖动态读取的 profile、bundle、前端、preset、原生库与配置文件 → 暂存目标平台的 `node-pty` addon → 每个构建目标调用一次 `pkg --sea` → 将 `deepseek-harness-sdk-runtime--` 写入 `dist-exe/` 并拷回运行时目录。Linux CI 会在匹配的 manylinux 2.28 容器中重新构建 `pty.node`,因为 legacy deploy 会遗漏这一安装副作用。每个目标都会把对应的原生 `@vscode/ripgrep` 二进制复制到可执行文件旁,作为必需的 `-rg` 伴随文件;pkg 运行时通过 `process.pkg` 选择该伴随文件,普通 Node 执行则直接使用 `@vscode/ripgrep`。macOS 使用对应目标的预构建产物,并额外生成所需的 `-spawn-helper`。四个部署标志都有实测依据:未启用 `inject-workspace-packages` 时必须使用 `--legacy`;`hoisted` 为 pkg 提供稳定的单实例布局,再由显式物化步骤消除符号链接;关闭对等依赖自动安装可防止未声明的对等依赖扩大闭包;`link-workspace-packages` 选择直接工作区依赖。[`pnpm-workspace.yaml`](../../../../pnpm-workspace.yaml) 将传递的 `@deepseek-ai/cosmokit` 与 `@deepseek-ai/schemastery` semver 请求覆盖到固定的 vendor 源码,使 legacy deploy 不会从注册表解析这些未发布名称。 -CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)都会调用它构建全部四个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64 与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则验证可执行文件的部署目标符合 wheel 包标签。完整构建四个目标时保留 5 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 4 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 4 个原生运行时 wheel 包,再由单个串行任务校验并将这 5 个文件发布到项目的 PyPI 注册表。[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责第四个目标及对 Windows arm64 的明确排除。 +CI 使用 [`.github/workflows/build-exe-for-python-sdk.yml`](../../../../.github/workflows/build-exe-for-python-sdk.yml):[安装后 wheel Python 运行时拉取请求验证](../testing/2026-08-23-installed-python-wheel-black-box-ci.zh.md)与[公开发布工作流](../process/2026-08-11-python-publication-workflow.zh.md)都会调用它构建全部五个目标;`workflow_dispatch` 仍可选择部分目标。linux-x64、linux-arm64(`ubuntu-24.04-arm`)、macos-arm64、macos-x64(`macos-15-intel`)与 win-x64(`windows-2025`)分别进行原生构建,并在适用平台缓存 `~/.pkg-cache`;macOS 的 ad-hoc 签名由 pkg 处理。每个平台都把发布形态的 SDK wheel 包与运行时 wheel 包安装到 checkout 外的干净 venv,证明包与可执行文件来源,再通过公开 SDK 与直接 NDJSON JSON-RPC 运行完整 keyless 场景。可信拉取请求还会在每个目标上运行真实 DeepSeek 双轮工具冒烟测试;fork 与 Dependabot head 不会获得密钥。Linux 会检查可执行文件和原生 addon 各自的 GLIBC 依赖,并额外运行 manylinux 2.28 冒烟测试;macOS 则检查 runtime、ripgrep 与 PTY helper 的架构,并验证三个载荷的部署目标都符合 wheel 包标签。完整构建五个目标时保留 6 个产物,每个产物只含一个发布文件:平台无关的 SDK wheel 包与 5 个原生运行时 wheel 包;手动选择部分目标时保留 SDK wheel 与所选运行时 wheel。裸 exe 与源码包只作为测试中间输入。[`.gitlab-ci.yml`](../../../../.gitlab-ci.yml) 只接受版本与根目录 `package.json` 匹配的 `python-v` 标签流水线,构建一个 SDK wheel 包和 5 个原生运行时 wheel 包,再由单个串行任务校验并将这 6 个文件发布到项目的 PyPI 注册表。[Windows x64 运行时决策](2026-08-23-python-sdk-windows-x64-runtime.zh.md)负责 Windows 目标及对 Windows arm64 的明确排除。 ### Python SDK 分发:双载体,exe 用于生产,`node` 用于开发 Python SDK 位于 [`python/`](../../../../python/README.zh.md):`python/sdk` 是客户端,`python/sdk-runtime` 是运行时载体包。运行时包的数据目录包含构建注入的平台可执行文件及其必需的 `-rg` 伴随文件和可选的 macOS helper,以及供仓库开发使用的构建注入 `runtime/node/` 闭包树。`resolve_bundled_launch_args()` 默认选择可执行文件;显式设置 `DSH_RUNTIME_MODE=node` 会在系统 Node 22.19 或更高版本上运行 `runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。node 载体从不进入 wheel 分发,两种载体都不使用检入的完整 `cordis.yml`。 -[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe 及其架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、针对 Node 24 可执行文件 macOS 13.5 部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 标签,或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。 +[`scripts/build-python-release.py`](../../../../scripts/build-python-release.py) 从仓库根目录的 `package.json` 读取权威的 `X.Y.Z` 或预发布版本,把预发布版本转换为 PEP 440 写法,并以该 wheel 包版本暂存两个包,让 `deepseek-harness-sdk` 精确依赖匹配版本的 `deepseek-harness-runtime-bin`。可选的 `python-v` 发布标签只是一项一致性断言,与仓库版本不同时会被拒绝;源码 `pyproject.toml` 中的开发占位版本从不决定发布版本。暂存过程还会把仓库许可证放入两个 wheel 包,并把第三方声明放入内置运行时 wheel 包。SDK 是 `py3-none-any` wheel 包;每个只提供 wheel 包的运行时包都包含一个 exe 及其架构匹配的 ripgrep 伴随文件,macOS wheel 包还包含与其架构匹配的 spawn helper。运行时 wheel 包使用 `py3-none-manylinux_2_28_x86_64`、`py3-none-manylinux_2_28_aarch64`、针对已验证 macOS 载荷部署目标而保守选择的 `py3-none-macosx_14_0_arm64` 或 `py3-none-macosx_14_0_x86_64` 标签,或 `py3-none-win_amd64`;Hatch 钩子拒绝 sdist、通用标签、混合平台载荷、伴随文件缺失或多余,以及不支持的平台。 Python 客户端使用所选 profile(默认 `sdk`)、有序 patch 文件和显式 Harness home 启动打包后的 `dsh` 命令。Profile 负责 JSON-RPC 服务和应用组合;缺失 home、profile、bundle、patch 或 server 配置项都会失败,不存在外部完整配置回退。 diff --git a/.github/workflows/build-exe-for-python-sdk.yml b/.github/workflows/build-exe-for-python-sdk.yml index 8f500caa86..c00241b0e0 100644 --- a/.github/workflows/build-exe-for-python-sdk.yml +++ b/.github/workflows/build-exe-for-python-sdk.yml @@ -2,7 +2,7 @@ name: Build single-exe # Native builds for the release targets; see # .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md. -# A full target run retains one SDK wheel and four runtime wheels; subset +# A full target run retains one SDK wheel and five runtime wheels; subset # dispatch retains the SDK wheel and selected runtime wheels. Bare executables # and source closures are test inputs. Run manually or call it from the Python # release workflow. There is no `pull_request` trigger: a label trigger would @@ -12,7 +12,7 @@ on: workflow_call: inputs: targets: - description: Comma-separated pkg targets to build; empty builds all four. + description: Comma-separated pkg targets to build; empty builds all five. type: string required: false default: '' @@ -36,7 +36,7 @@ on: description: >- Comma-separated pkg targets to build. Any subset of: node24-linux-x64, node24-linux-arm64, node24-macos-arm64, - node24-win-x64. Empty builds all four. + node24-macos-x64, node24-win-x64. Empty builds all five. type: string required: false default: '' @@ -89,7 +89,7 @@ jobs: id: plan env: # Blank dispatch inputs build all targets. - TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64' }} + TARGETS: ${{ inputs.targets || 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64' }} run: | set -euo pipefail matrix='[]' @@ -98,14 +98,15 @@ jobs: t="$(echo "$raw" | xargs)" # trim surrounding whitespace [ -z "$t" ] && continue # Native-only: hosted arm64 Linux uses ubuntu-24.04-arm, while - # macos-latest is Apple Silicon. + # macos-latest is Apple Silicon; macos-15-intel is native x64. case "$t" in node24-linux-x64) runner=ubuntu-latest ;; node24-linux-arm64) runner=ubuntu-24.04-arm ;; node24-macos-arm64) runner=macos-latest ;; + node24-macos-x64) runner=macos-15-intel ;; node24-win-x64) runner=windows-2025 ;; *) - echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64, node24-win-x64." + echo "::error::Unknown target '$t'. Supported: node24-linux-x64, node24-linux-arm64, node24-macos-arm64, node24-macos-x64, node24-win-x64." exit 1 ;; esac @@ -251,6 +252,7 @@ jobs: linux-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl ;; linux-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl ;; macos-arm64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl ;; + macos-x64) wheel=deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_x86_64.whl ;; *) echo "::error::Unsupported runtime platform $platform"; exit 1 ;; esac [ -x "$exe" ] || { echo "::error::$exe missing or not executable"; exit 1; } @@ -432,13 +434,23 @@ jobs: exit 1 } - - name: Check macOS deployment target + - name: Check macOS payload architecture and deployment target if: runner.os == 'macOS' env: EXE: ${{ steps.runtime-posix.outputs.exe }} - run: >- - python3 scripts/check-macos-deployment-target.py - "$EXE" "$EXE-spawn-helper" + PLATFORM: ${{ steps.runtime-posix.outputs.platform }} + run: | + set -euo pipefail + case "$PLATFORM" in + macos-arm64) macho_arch=arm64 ;; + macos-x64) macho_arch=x86_64 ;; + *) echo "::error::Unsupported macOS platform $PLATFORM"; exit 1 ;; + esac + for payload in "$EXE" "$EXE-rg" "$EXE-spawn-helper"; do + lipo "$payload" -verify_arch "$macho_arch" + done + python3 scripts/check-macos-deployment-target.py \ + --platform "$PLATFORM" "$EXE" "$EXE-rg" "$EXE-spawn-helper" - name: Run wheel in a manylinux 2.28 container if: runner.os == 'Linux' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32c81a257b..7797fa9f7b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -328,7 +328,7 @@ jobs: name: python runtime / release-shaped matrix uses: ./.github/workflows/build-exe-for-python-sdk.yml with: - targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64 + targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64 ci: true secrets: DEEPSEEK_API_KEY_EXTERNAL: ${{ secrets.DEEPSEEK_API_KEY_EXTERNAL }} diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index 18524523b9..f3d9ab44c3 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -24,10 +24,10 @@ concurrency: jobs: build: - name: Build five wheels + name: Build six wheels uses: ./.github/workflows/build-exe-for-python-sdk.yml with: - targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64 + targets: node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64 release: true python-compat: @@ -149,6 +149,7 @@ jobs: actual="$(mktemp)" printf '%s\n' \ "deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_arm64.whl" \ + "deepseek_harness_runtime_bin-$VERSION-py3-none-macosx_14_0_x86_64.whl" \ "deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_aarch64.whl" \ "deepseek_harness_runtime_bin-$VERSION-py3-none-manylinux_2_28_x86_64.whl" \ "deepseek_harness_runtime_bin-$VERSION-py3-none-win_amd64.whl" \ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 008200560c..f970626f79 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -60,8 +60,16 @@ sdk-wheel: docker run --rm -v "$PWD:/work" -w /work "$image" bash -euxo pipefail -c "/opt/python/cp310-cp310/bin/python -m venv /tmp/dsh-sdk && /tmp/dsh-sdk/bin/python -m pip install --find-links /work/release/$PLATFORM --find-links /work/release/sdk deepseek-harness-sdk==$DSH_WHEEL_VERSION && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-default && /tmp/dsh-sdk/bin/python /work/scripts/smoke-python-runtime.py --scenario sdk-mcp" fi - | - if [ "$PLATFORM" = macos-arm64 ]; then - python3 scripts/check-macos-deployment-target.py "$EXE" "$EXE-spawn-helper" + if [ "${PLATFORM#macos-}" != "$PLATFORM" ]; then + case "$PLATFORM" in + macos-arm64) macho_arch=arm64 ;; + macos-x64) macho_arch=x86_64 ;; + *) echo "Unsupported macOS platform $PLATFORM"; exit 1 ;; + esac + for payload in "$EXE" "$EXE-rg" "$EXE-spawn-helper"; do + lipo "$payload" -verify_arch "$macho_arch" + done + python3 scripts/check-macos-deployment-target.py --platform "$PLATFORM" "$EXE" "$EXE-rg" "$EXE-spawn-helper" fi artifacts: paths: [release/$PLATFORM/*.whl] @@ -97,6 +105,16 @@ runtime-macos-arm64: - job: sdk-wheel artifacts: true +runtime-macos-x64: + extends: .runtime-wheel + tags: [macos-x64] + variables: + PKG_TARGET: node24-macos-x64 + PLATFORM: macos-x64 + needs: + - job: sdk-wheel + artifacts: true + runtime-windows-x64: stage: build tags: [windows-x64] @@ -147,6 +165,8 @@ publish-python: artifacts: true - job: runtime-macos-arm64 artifacts: true + - job: runtime-macos-x64 + artifacts: true - job: runtime-windows-x64 artifacts: true before_script: @@ -157,11 +177,12 @@ publish-python: - test "$CI_COMMIT_TAG" = "python-v$DSH_VERSION" || { echo "Tag $CI_COMMIT_TAG does not match package.json version $DSH_VERSION"; exit 1; } - python -m pip install twine==6.2.0 script: - - test "$(find release -name '*.whl' | wc -l | tr -d ' ')" = 5 + - test "$(find release -name '*.whl' | wc -l | tr -d ' ')" = 6 - test -f "release/sdk/deepseek_harness_sdk-${DSH_WHEEL_VERSION}-py3-none-any.whl" - test -f "release/linux-x64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-manylinux_2_28_x86_64.whl" - test -f "release/linux-arm64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-manylinux_2_28_aarch64.whl" - test -f "release/macos-arm64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-macosx_14_0_arm64.whl" + - test -f "release/macos-x64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-macosx_14_0_x86_64.whl" - test -f "release/win-x64/deepseek_harness_runtime_bin-${DSH_WHEEL_VERSION}-py3-none-win_amd64.whl" - python -m twine check release/*/*.whl - export TWINE_USERNAME=gitlab-ci-token diff --git a/python/development.i18n.yaml b/python/development.i18n.yaml index db08b00204..f1ada26c49 100644 --- a/python/development.i18n.yaml +++ b/python/development.i18n.yaml @@ -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/development.md -development.md: 72acaec2b3ec2142fc176d940f1a2854e44ebad8 -development.zh.md: 8bd17ad7e604b1cdc4456053bcec95c9e8e503cb +development.md: aa0144d7eaa66711d0f08316d4445da060918ca8 +development.zh.md: a35f6fc8de1bdbd282fd8999a1440fde0c400b34 diff --git a/python/development.md b/python/development.md index 72acaec2b3..aa0144d7ea 100644 --- a/python/development.md +++ b/python/development.md @@ -13,7 +13,7 @@ pnpm install pnpm exec tsx scripts/build-exe-for-python-sdk.ts ``` -Use `--skip-build` when the required `lib/` artifacts already exist, or `--targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64` to select platforms. Build each target on its native architecture. Products land in `dist-exe/` and the script syncs the selected carriers into `python/sdk-runtime/`. Windows emits `.exe` and `-rg.exe`; macOS also syncs the matching spawn helper required by `node-pty`. +Use `--skip-build` when the required `lib/` artifacts already exist, or `--targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64` to select platforms. Build each target on its native architecture. Products land in `dist-exe/` and the script syncs the selected carriers into `python/sdk-runtime/`. Windows emits `.exe` and `-rg.exe`; macOS also syncs the matching spawn helper required by `node-pty`. ## Validate the SDK @@ -79,11 +79,11 @@ pip install \ "dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl" ``` -The runtime distribution is wheel-only. The release pipeline publishes four platform wheels with the pure SDK wheel: Linux x64, Linux arm64, macOS 14 or newer on arm64, and Windows x64 (`win_amd64`). A `python-v` tag is accepted only when it matches the repository version; prerelease repository versions such as `0.0.1-rc.1` use their normalized PEP 440 spelling, such as `0.0.1rc1`, inside wheel filenames and metadata. +The runtime distribution is wheel-only. The release pipeline publishes five platform wheels with the pure SDK wheel: Linux x64, Linux arm64, macOS 14 or newer on arm64 and x64, and Windows x64 (`win_amd64`). A `python-v` tag is accepted only when it matches the repository version; prerelease repository versions such as `0.0.1-rc.1` use their normalized PEP 440 spelling, such as `0.0.1rc1`, inside wheel filenames and metadata. ## Validate a release candidate -Manually run the GitHub `Release (Python)` workflow with `publish=false` to build all five wheels, install the Linux release set on Python 3.10 and 3.14, check exact filenames and metadata, enforce PyPI's default per-file size limit, and retain one aggregate artifact with SHA-256 hashes. The run has no registry credentials; a dry run cannot enter either publication job. +Manually run the GitHub `Release (Python)` workflow with `publish=false` to build all six wheels, install the Linux release set on Python 3.10 and 3.14, check exact filenames and metadata, enforce PyPI's default per-file size limit, and retain one aggregate artifact with SHA-256 hashes. The run has no registry credentials; a dry run cannot enter either publication job. Public publication runs from the private automation repository. Package metadata points to the separate read-only public source mirror, which does not run release Actions. The private repository defines the repository variable `PYPI_PUBLISHER_REPOSITORY` as its own `owner/name` and keeps `PUBLIC_PYPI_RELEASE_ENABLED=false` except during an intentional release. diff --git a/python/development.zh.md b/python/development.zh.md index 8bd17ad7e6..a35f6fc8de 100644 --- a/python/development.zh.md +++ b/python/development.zh.md @@ -13,7 +13,7 @@ pnpm install pnpm exec tsx scripts/build-exe-for-python-sdk.ts ``` -所需 `lib/` 产物已存在时使用 `--skip-build`;如需选择平台,请使用 `--targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64`。每个目标都应在其原生架构上构建。产物写入 `dist-exe/`,脚本会将所选载体同步到 `python/sdk-runtime/`。Windows 会生成 `.exe` 与 `-rg.exe`;macOS 构建还会同步 `node-pty` 所需的配套 spawn 辅助程序。 +所需 `lib/` 产物已存在时使用 `--skip-build`;如需选择平台,请使用 `--targets=node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64`。每个目标都应在其原生架构上构建。产物写入 `dist-exe/`,脚本会将所选载体同步到 `python/sdk-runtime/`。Windows 会生成 `.exe` 与 `-rg.exe`;macOS 构建还会同步 `node-pty` 所需的配套 spawn 辅助程序。 ## 验证 SDK @@ -79,11 +79,11 @@ pip install \ "dist-python/deepseek_harness_runtime_bin-$version-py3-none-macosx_14_0_arm64.whl" ``` -运行时分发包仅提供 wheel 包。发布流水线会连同纯 SDK wheel 包一起发布四个平台 wheel 包:Linux x64、Linux arm64、macOS 14 或更高版本的 arm64,以及 Windows x64(`win_amd64`)。只有与仓库版本匹配时,才接受 `python-v` 标签;`0.0.1-rc.1` 之类的仓库预发布版本在 wheel 包文件名和元数据中使用规范化的 PEP 440 写法,例如 `0.0.1rc1`。 +运行时分发包仅提供 wheel 包。发布流水线会连同纯 SDK wheel 包一起发布五个平台 wheel 包:Linux x64、Linux arm64、macOS 14 或更高版本的 arm64 与 x64,以及 Windows x64(`win_amd64`)。只有与仓库版本匹配时,才接受 `python-v` 标签;`0.0.1-rc.1` 之类的仓库预发布版本在 wheel 包文件名和元数据中使用规范化的 PEP 440 写法,例如 `0.0.1rc1`。 ## 验证候选发行版 -手动运行 GitHub 的 `Release (Python)` 工作流并设置 `publish=false`,即可构建全部五个 wheel 包,在 Python 3.10 和 3.14 上安装 Linux 发行集合,检查精确文件名和元数据,执行 PyPI 默认单文件大小限制,并保留一份带 SHA-256 哈希的汇总产物。该运行没有注册表凭据,dry-run 运行无法进入任何发布作业。 +手动运行 GitHub 的 `Release (Python)` 工作流并设置 `publish=false`,即可构建全部六个 wheel 包,在 Python 3.10 和 3.14 上安装 Linux 发行集合,检查精确文件名和元数据,执行 PyPI 默认单文件大小限制,并保留一份带 SHA-256 哈希的汇总产物。该运行没有注册表凭据,dry-run 运行无法进入任何发布作业。 公开发布从私有自动化仓库运行。包元数据指向独立的只读公开源码镜像,该镜像不运行发布 Actions。私有仓库把仓库变量 `PYPI_PUBLISHER_REPOSITORY` 定义为自身的 `owner/name`,并且只在有意发布期间把 `PUBLIC_PYPI_RELEASE_ENABLED` 从 `false` 改为 `true`。 diff --git a/python/sdk-runtime/README.i18n.yaml b/python/sdk-runtime/README.i18n.yaml index 044ba3a728..410f250dd9 100644 --- a/python/sdk-runtime/README.i18n.yaml +++ b/python/sdk-runtime/README.i18n.yaml @@ -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: 28695259928a7edc6e6cf67e737f1012729df5a4 -README.zh.md: f23b253cfe47d9f1ae24568b51d9db810c7a4a9f +README.md: 050ae85d9b0a38b82a3c66a84c3d8f34e137be6c +README.zh.md: 7066d7224752294c25b58cfe8fb6a94524a2013d diff --git a/python/sdk-runtime/README.md b/python/sdk-runtime/README.md index 2869525992..050ae85d9b 100644 --- a/python/sdk-runtime/README.md +++ b/python/sdk-runtime/README.md @@ -8,7 +8,7 @@ Platform runtime wheel for the DeepSeek Harness Python SDK. It packages the norm The wheel installs a `dsh` console command and the `deepseek_harness_runtime` Python module. `dsh` forwards its arguments to the bundled executable and requires a non-empty `DSH_HOME`; it never falls back to `~/.dsh`. -Production executables are named `deepseek-harness-sdk-runtime--` under the module's `runtime/` directory; Windows uses the `.exe` suffix. Linux and macOS wheels include a target-native `-rg` sidecar, Windows includes `-rg.exe`, and macOS also includes `-spawn-helper` for `node-pty`. Published targets are Linux x64, Linux arm64, macOS arm64, and Windows x64. The wheel tag and payload must match exactly; no Windows arm64 wheel is published. +Production executables are named `deepseek-harness-sdk-runtime--` under the module's `runtime/` directory; Windows uses the `.exe` suffix. Linux and macOS wheels include a target-native `-rg` sidecar, Windows includes `-rg.exe`, and macOS also includes `-spawn-helper` for `node-pty`. Published targets are Linux x64, Linux arm64, macOS arm64, macOS x64, and Windows x64. The wheel tag and payload must match exactly; no Windows arm64 wheel is published. Repository builds also materialize a dev-only `runtime/node/` carrier. It runs `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js` on system Node 22.19 or newer. It is never selected automatically and is excluded from wheels and sdists. diff --git a/python/sdk-runtime/README.zh.md b/python/sdk-runtime/README.zh.md index f23b253cfe..7066d72247 100644 --- a/python/sdk-runtime/README.zh.md +++ b/python/sdk-runtime/README.zh.md @@ -8,7 +8,7 @@ DeepSeek Harness Python SDK 的平台运行时 wheel。它把普通 `dsh` CLI Wheel 会安装 `dsh` 控制台命令和 `deepseek_harness_runtime` Python 模块。`dsh` 将参数转发给内置可执行程序,并要求非空 `DSH_HOME`;它不会回退到 `~/.dsh`。 -生产可执行程序位于模块的 `runtime/` 目录,命名为 `deepseek-harness-sdk-runtime--`;Windows 使用 `.exe` 后缀。Linux 与 macOS wheel 包含目标平台原生的 `-rg` 伴随程序,Windows 包含 `-rg.exe`,macOS 还包含 `node-pty` 使用的 `-spawn-helper`。已发布目标是 Linux x64、Linux arm64、macOS arm64 与 Windows x64。Wheel tag 必须与载荷严格匹配;不发布 Windows arm64 wheel。 +生产可执行程序位于模块的 `runtime/` 目录,命名为 `deepseek-harness-sdk-runtime--`;Windows 使用 `.exe` 后缀。Linux 与 macOS wheel 包含目标平台原生的 `-rg` 伴随程序,Windows 包含 `-rg.exe`,macOS 还包含 `node-pty` 使用的 `-spawn-helper`。已发布目标是 Linux x64、Linux arm64、macOS arm64、macOS x64 与 Windows x64。Wheel tag 必须与载荷严格匹配;不发布 Windows arm64 wheel。 仓库构建还会物化仅限开发的 `runtime/node/` 载体。它在系统 Node 22.19 或更高版本上运行 `node runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`。系统不会自动选择它,而且 wheel 与 sdist 均不包含它。 diff --git a/python/sdk-runtime/platforms.json b/python/sdk-runtime/platforms.json index 9c0a1fec72..66105877bd 100644 --- a/python/sdk-runtime/platforms.json +++ b/python/sdk-runtime/platforms.json @@ -11,6 +11,10 @@ "tag": "macosx_14_0_arm64", "executable": "deepseek-harness-sdk-runtime-macos-arm64" }, + "macos-x64": { + "tag": "macosx_14_0_x86_64", + "executable": "deepseek-harness-sdk-runtime-macos-x64" + }, "win-x64": { "tag": "win_amd64", "executable": "deepseek-harness-sdk-runtime-win-x64.exe" diff --git a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py index 2081aa5070..1fc5377e10 100644 --- a/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py +++ b/python/sdk-runtime/src/deepseek_harness_runtime/__init__.py @@ -120,12 +120,11 @@ def _current_platform_tag() -> str: plat is None or arch is None or (plat == "win" and arch != "x64") - or (plat == "macos" and arch != "arm64") ): raise FileNotFoundError( "no bundled DeepSeek Harness SDK runtime exists for this platform " f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: " - "Linux x64/arm64, macOS arm64, and Windows x64. " + _EXE_ACQUISITION_HINT + "Linux x64/arm64, macOS x64/arm64, and Windows x64. " + _EXE_ACQUISITION_HINT ) return f"{plat}-{arch}" diff --git a/python/sdk/tests/test_macos_deployment_target.py b/python/sdk/tests/test_macos_deployment_target.py index 8e68089a6e..e3a74c9e2e 100644 --- a/python/sdk/tests/test_macos_deployment_target.py +++ b/python/sdk/tests/test_macos_deployment_target.py @@ -16,8 +16,15 @@ checker = SimpleNamespace(**runpy.run_path(str(SCRIPT))) def test_otool_parser_uses_the_newest_macho_slice() -> None: output = """ +Load command 8 + cmd LC_VERSION_MIN_MACOSX + cmdsize 16 + version 10.7 + sdk 11.1 +Load command 9 cmd LC_BUILD_VERSION minos 11.0 +Load command 10 cmd LC_BUILD_VERSION minos 13.5 """ @@ -26,12 +33,34 @@ def test_otool_parser_uses_the_newest_macho_slice() -> None: def test_otool_parser_requires_a_deployment_target() -> None: - with pytest.raises(ValueError, match="contains no LC_BUILD_VERSION"): + with pytest.raises(ValueError, match="contains no macOS deployment target"): checker.parse_otool_deployment_target("Load command 0\n") +def test_otool_parser_ignores_unrelated_version_fields() -> None: + output = """ +Load command 1 + cmd LC_ID_DYLIB + cmdsize 48 + current version 14.1.0 +compatibility version 1.0.0 + """ + + with pytest.raises(ValueError, match="contains no macOS deployment target"): + checker.parse_otool_deployment_target(output) + + def test_wheel_tag_rejects_a_newer_executable_target() -> None: checker.ensure_compatible(Path("runtime"), (13, 5), "macosx_14_0_arm64") + checker.ensure_compatible(Path("runtime-x64"), (10, 7), "macosx_14_0_x86_64") with pytest.raises(RuntimeError, match="requires macOS 14.1"): checker.ensure_compatible(Path("spawn-helper"), (14, 1), "macosx_14_0_arm64") + + +def test_wheel_tag_accepts_only_supported_macos_architectures() -> None: + assert checker.claimed_version("macosx_14_0_arm64") == (14, 0) + assert checker.claimed_version("macosx_14_0_x86_64") == (14, 0) + + with pytest.raises(ValueError, match="unsupported macOS wheel platform tag"): + checker.claimed_version("macosx_14_0_universal2") diff --git a/python/sdk/tests/test_release_version.py b/python/sdk/tests/test_release_version.py index 37d2a01734..9c87c5fe82 100644 --- a/python/sdk/tests/test_release_version.py +++ b/python/sdk/tests/test_release_version.py @@ -60,6 +60,8 @@ def test_pep440_version_spells_a_prerelease_the_python_way() -> None: def test_macos_wheel_tag_does_not_claim_unsupported_node_platforms() -> None: assert build_python_release.PLATFORMS["macos-arm64"][0] == "macosx_14_0_arm64" assert build_python_release.PLATFORMS["macos-arm64"][1] == "deepseek-harness-sdk-runtime-macos-arm64" + assert build_python_release.PLATFORMS["macos-x64"][0] == "macosx_14_0_x86_64" + assert build_python_release.PLATFORMS["macos-x64"][1] == "deepseek-harness-sdk-runtime-macos-x64" def test_windows_wheel_tag_and_payload_are_x64_only() -> None: @@ -95,7 +97,7 @@ def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path: @pytest.mark.parametrize( ("target", "with_helper"), - [("linux-x64", False), ("macos-arm64", True), ("win-x64.exe", False)], + [("linux-x64", False), ("macos-arm64", True), ("macos-x64", True), ("win-x64.exe", False)], ) def test_stage_runtime_copies_platform_payload( tmp_path: Path, target: str, with_helper: bool diff --git a/python/sdk/tests/test_runtime_resolution.py b/python/sdk/tests/test_runtime_resolution.py index 0f06deb28e..3241fd3ce1 100644 --- a/python/sdk/tests/test_runtime_resolution.py +++ b/python/sdk/tests/test_runtime_resolution.py @@ -79,12 +79,11 @@ def test_current_platform_supports_windows_x64_only(monkeypatch: pytest.MonkeyPa runtime._current_platform_tag() -def test_current_platform_rejects_macos_x64(monkeypatch: pytest.MonkeyPatch) -> None: +def test_current_platform_supports_macos_x64(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(runtime.sys, "platform", "darwin") monkeypatch.setattr(runtime.platform, "machine", lambda: "x86_64") - with pytest.raises(FileNotFoundError, match="macOS arm64"): - runtime._current_platform_tag() + assert runtime._current_platform_tag() == "macos-x64" def test_runtime_requires_ripgrep_sidecar( diff --git a/scripts/build-exe-for-python-sdk.spec.ts b/scripts/build-exe-for-python-sdk.spec.ts index 06c3f7fce3..4933c59810 100644 --- a/scripts/build-exe-for-python-sdk.spec.ts +++ b/scripts/build-exe-for-python-sdk.spec.ts @@ -59,6 +59,19 @@ describe('Python runtime executable builder CLI', () => { expect(result.stdout).not.toMatch(/pnpm\.cmd/i) }) + it('accepts the macOS x64 pkg target', () => { + const result = run( + { npm_execpath: 'C:\\tools\\pnpm.cjs' }, + '--skip-build', + '--dry-run', + '--targets=node24-macos-x64', + ) + + expect(result.status).toBe(0) + expect(result.stdout).toContain('exec pkg') + expect(result.stdout).toContain('--sea --targets node24-macos-x64') + }) + it('rejects a Windows arm64 product before any build step', () => { const result = run( { npm_execpath: 'C:\\tools\\pnpm.cjs' }, diff --git a/scripts/build-exe-for-python-sdk.ts b/scripts/build-exe-for-python-sdk.ts index 9a2ecaf932..c9759418d7 100644 --- a/scripts/build-exe-for-python-sdk.ts +++ b/scripts/build-exe-for-python-sdk.ts @@ -207,7 +207,7 @@ class BuildCli { return [ 'Usage: pnpm exec tsx scripts/build-exe-for-python-sdk.ts [flags]', '', - ' --targets= pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64.', + ' --targets= pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64.', ' 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.', diff --git a/scripts/check-macos-deployment-target.py b/scripts/check-macos-deployment-target.py index 633a3d7ced..7e3ba70fc7 100644 --- a/scripts/check-macos-deployment-target.py +++ b/scripts/check-macos-deployment-target.py @@ -12,7 +12,11 @@ from pathlib import Path ROOT = Path(__file__).resolve().parents[1] RELEASE = runpy.run_path(str(ROOT / "scripts" / "build-python-release.py")) -MACOS_PLATFORM_TAG = RELEASE["PLATFORMS"]["macos-arm64"][0] +MACOS_PLATFORMS = { + name: details[0] + for name, details in RELEASE["PLATFORMS"].items() + if name.startswith("macos-") +} def parse_version(value: str) -> tuple[int, ...]: @@ -24,7 +28,7 @@ def parse_version(value: str) -> tuple[int, ...]: def claimed_version(platform_tag: str) -> tuple[int, ...]: """Return the minimum macOS version encoded by a wheel platform tag.""" - match = re.fullmatch(r"macosx_(\d+)_(\d+)_arm64", platform_tag) + match = re.fullmatch(r"macosx_(\d+)_(\d+)_(?:arm64|x86_64)", platform_tag) if match is None: raise ValueError(f"unsupported macOS wheel platform tag: {platform_tag!r}") return int(match.group(1)), int(match.group(2)) @@ -32,12 +36,22 @@ def claimed_version(platform_tag: str) -> tuple[int, ...]: def parse_otool_deployment_target(output: str) -> tuple[int, ...]: """Return the newest deployment target from one or more Mach-O slices.""" - versions = [ - parse_version(match.group(1)) - for match in re.finditer(r"^\s*minos\s+(\d+(?:\.\d+)*)\s*$", output, re.MULTILINE) - ] + versions: list[tuple[int, ...]] = [] + command: str | None = None + for line in output.splitlines(): + stripped = line.strip() + if re.fullmatch(r"Load command \d+", stripped): + command = None + elif stripped == "cmd LC_BUILD_VERSION": + command = "build" + elif stripped == "cmd LC_VERSION_MIN_MACOSX": + command = "minimum" + elif command == "build" and (match := re.fullmatch(r"minos\s+(\d+(?:\.\d+)*)", stripped)): + versions.append(parse_version(match.group(1))) + elif command == "minimum" and (match := re.fullmatch(r"version\s+(\d+(?:\.\d+)*)", stripped)): + versions.append(parse_version(match.group(1))) if not versions: - raise ValueError("otool output contains no LC_BUILD_VERSION deployment target") + raise ValueError("otool output contains no macOS deployment target load command") return max(versions) @@ -73,7 +87,7 @@ def ensure_compatible( def validate_deployment_targets( - executables: list[Path], platform_tag: str = MACOS_PLATFORM_TAG + executables: list[Path], platform_tag: str ) -> list[tuple[Path, tuple[int, ...]]]: """Validate every executable and return its measured deployment target.""" measured = [(executable, deployment_target(executable)) for executable in executables] @@ -84,11 +98,13 @@ def validate_deployment_targets( def main() -> None: parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--platform", choices=tuple(MACOS_PLATFORMS), required=True) parser.add_argument("executables", type=Path, nargs="+") args = parser.parse_args() - for executable, version in validate_deployment_targets(args.executables): + platform_tag = MACOS_PLATFORMS[args.platform] + for executable, version in validate_deployment_targets(args.executables, platform_tag): rendered = ".".join(str(part) for part in version) - print(f"{executable}: macOS {rendered} <= {MACOS_PLATFORM_TAG}") + print(f"{executable}: macOS {rendered} <= {platform_tag}") if __name__ == "__main__": diff --git a/scripts/ci-workflow.spec.ts b/scripts/ci-workflow.spec.ts index 728f243dbb..c3b1a2ccf5 100644 --- a/scripts/ci-workflow.spec.ts +++ b/scripts/ci-workflow.spec.ts @@ -370,7 +370,7 @@ describe('CI workflow', () => { name: 'python runtime / release-shaped matrix', uses: './.github/workflows/build-exe-for-python-sdk.yml', with: { - targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64', + targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64', ci: true, }, secrets: { @@ -462,7 +462,7 @@ describe('Python release workflows', () => { expect(build).toMatchObject({ uses: './.github/workflows/build-exe-for-python-sdk.yml', with: { - targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64', + targets: 'node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-macos-x64,node24-win-x64', release: true, }, }) @@ -531,7 +531,7 @@ describe('Python release workflows', () => { const buildSteps: unknown[] = build.steps const manylinuxAddon = buildSteps.find(step => isRecord(step) && step.name === 'Rebuild Linux node-pty against manylinux 2.28') - const macosCheck = buildSteps.find(step => isRecord(step) && step.name === 'Check macOS deployment target') + const macosCheck = buildSteps.find(step => isRecord(step) && step.name === 'Check macOS payload architecture and deployment target') const manylinuxSmoke = buildSteps.find(step => isRecord(step) && step.name === 'Run wheel in a manylinux 2.28 container') const cleanVenvPosix = buildSteps.find(step => isRecord(step) && step.name === 'Install local SDK and runtime wheels into a clean venv (POSIX)') const cleanVenvWindows = buildSteps.find(step => isRecord(step) && step.name === 'Install local SDK and runtime wheels into a clean venv (Windows)') @@ -541,7 +541,8 @@ describe('Python release workflows', () => { const realApiPreflightWindows = buildSteps.find(step => isRecord(step) && step.name === 'Preflight installed-wheel real API test (Windows)') const installedRealApiPosix = buildSteps.find(step => isRecord(step) && step.name === 'Run installed-wheel real API black-box test (POSIX)') const installedRealApiWindows = buildSteps.find(step => isRecord(step) && step.name === 'Run installed-wheel real API black-box test (Windows)') - if (!isRecord(cleanVenvPosix) || !isRecord(cleanVenvWindows) + if (!isRecord(macosCheck) || typeof macosCheck.run !== 'string' + || !isRecord(cleanVenvPosix) || !isRecord(cleanVenvWindows) || !isRecord(installedKeylessPosix) || !isRecord(installedKeylessWindows) || !isRecord(realApiPreflightPosix) || !isRecord(realApiPreflightWindows) || !isRecord(installedRealApiPosix) || !isRecord(installedRealApiWindows)) { @@ -564,6 +565,9 @@ describe('Python release workflows', () => { expect(JSON.stringify(plan.steps)).toContain('pep440_version') const workflowJson = JSON.stringify(workflow) expect(workflowJson).toContain('macosx_14_0_arm64') + expect(workflowJson).toContain('macosx_14_0_x86_64') + expect(workflowJson).toContain('node24-macos-x64') + expect(workflowJson).toContain('macos-15-intel') expect(workflowJson).toContain('win_amd64') expect(workflowJson).toContain('node24-win-x64') expect(workflowJson).toContain('windows-2025') @@ -583,8 +587,10 @@ describe('Python release workflows', () => { expect(JSON.stringify(manylinuxAddon)).toContain('node-pty-glibc-versions.txt') expect(JSON.stringify(manylinuxAddon)).toContain('le 2.28') expect(macosCheck).toMatchObject({ if: "runner.os == 'macOS'" }) - expect(JSON.stringify(macosCheck)).toContain('scripts/check-macos-deployment-target.py') - expect(JSON.stringify(macosCheck)).toContain('$EXE-spawn-helper') + expect(macosCheck.run).toContain('scripts/check-macos-deployment-target.py') + expect(macosCheck.run).toContain('lipo "$payload" -verify_arch') + expect(macosCheck.run).toContain('$EXE-rg') + expect(macosCheck.run).toContain('$EXE-spawn-helper') expect(JSON.stringify(installedKeylessPosix)).toContain('--scenario all') expect(JSON.stringify(installedKeylessPosix)).toContain('env -u PYTHONPATH') expect(JSON.stringify(installedKeylessWindows)).toContain('--scenario all --installed-wheel') @@ -620,14 +626,29 @@ describe('Python release workflows', () => { } const runtimeScript: unknown[] = runtimeWheel.script const macosCheck = runtimeScript.find( - step => typeof step === 'string' && step.includes('PLATFORM" = macos-arm64'), + step => typeof step === 'string' && step.includes('${PLATFORM#macos-}'), ) if (typeof macosCheck !== 'string') { throw new TypeError('GitLab CI must check the macOS deployment target') } expect(macosCheck).toContain('scripts/check-macos-deployment-target.py') - expect(macosCheck).toContain('"$EXE" "$EXE-spawn-helper"') + expect(macosCheck).toContain('lipo "$payload" -verify_arch') + expect(macosCheck).toContain('"$EXE" "$EXE-rg" "$EXE-spawn-helper"') + }) + + it('builds the macOS x64 wheel on the matching GitLab runner', () => { + const workflow = loadWorkflow('.gitlab-ci.yml') + const macosX64 = workflow['runtime-macos-x64'] + const publish = workflow['publish-python'] + if (!isRecord(macosX64) || !isRecord(publish) || !Array.isArray(publish.needs)) { + throw new TypeError('GitLab CI must define the macOS x64 runtime and publication jobs') + } + + expect(macosX64.tags).toEqual(['macos-x64']) + expect(macosX64.variables).toMatchObject({ PKG_TARGET: 'node24-macos-x64', PLATFORM: 'macos-x64' }) + expect(publish.needs).toContainEqual({ job: 'runtime-macos-x64', artifacts: true }) + expect(JSON.stringify(publish.script)).toContain('macosx_14_0_x86_64.whl') }) it('builds and black-box tests the Windows x64 wheel in GitLab', () => { diff --git a/scripts/verify-runtime-closure.spec.ts b/scripts/verify-runtime-closure.spec.ts index 90dbf20799..6d1e8e3edb 100644 --- a/scripts/verify-runtime-closure.spec.ts +++ b/scripts/verify-runtime-closure.spec.ts @@ -21,6 +21,7 @@ const platforms = { 'linux-x64': { tag: 'manylinux_2_28_x86_64', executable: 'runtime-linux-x64' }, 'linux-arm64': { tag: 'manylinux_2_28_aarch64', executable: 'runtime-linux-arm64' }, 'macos-arm64': { tag: 'macosx_14_0_arm64', executable: 'runtime-macos-arm64' }, + 'macos-x64': { tag: 'macosx_14_0_x86_64', executable: 'runtime-macos-x64' }, 'win-x64': { tag: 'win_amd64', executable: 'runtime-win-x64.exe' }, } @@ -64,7 +65,7 @@ describe('verifyRuntimeClosure', () => { expect(result.presetCount).toBe(1) expect(result.failures).toEqual([ 'standard preset -> @scope/linux (linux-arm64, linux-x64)', - 'standard preset -> @scope/macos (macos-arm64)', + 'standard preset -> @scope/macos (macos-arm64, macos-x64)', 'standard preset -> @scope/windows (win-x64)', ]) }) @@ -83,7 +84,7 @@ describe('verifyRuntimeClosure', () => { const result = await verifyRuntimeClosure(root) expect(result.failures).toEqual([ - 'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64, win-x64)', + 'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64, macos-x64, win-x64)', ]) }) @@ -117,7 +118,7 @@ describe('verifyRuntimeClosure', () => { const result = await verifyRuntimeClosure(root) expect(result.failures).toEqual([ - 'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64, win-x64)', + 'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64, macos-x64, win-x64)', ]) })