python: close runtime packaging and platform wheels
This commit is contained in:
@@ -45,6 +45,7 @@
|
|||||||
"verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts",
|
"verify-translation-pairing": "tsx scripts/verify-translation-pairing.ts",
|
||||||
"verify-doc-budgets": "tsx scripts/verify-doc-budgets.ts",
|
"verify-doc-budgets": "tsx scripts/verify-doc-budgets.ts",
|
||||||
"verify-node-next-types": "tsx scripts/verify-node-next-types.ts",
|
"verify-node-next-types": "tsx scripts/verify-node-next-types.ts",
|
||||||
|
"verify-runtime-closure": "tsx scripts/verify-runtime-closure.ts",
|
||||||
"gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts",
|
"gen-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts",
|
||||||
"gen-rfc-index": "tsx scripts/gen-rfc-index.ts",
|
"gen-rfc-index": "tsx scripts/gen-rfc-index.ts",
|
||||||
"verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check",
|
"verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
|
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
|
||||||
"constraints": "tsx scripts/check-workspace-constraints.ts",
|
"constraints": "tsx scripts/check-workspace-constraints.ts",
|
||||||
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-dispatch && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets",
|
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-cordis-api && pnpm run verify-export-jsdoc && pnpm run verify-tool-catalog && pnpm run verify-config-catalog && pnpm run verify-persistence-catalog && pnpm run verify-doc-graphs && pnpm run verify-scoped-dispatch && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-mermaid && pnpm run verify-rfc-classification && pnpm run verify-rfc-format && pnpm run verify-type-equiv && pnpm run verify-translation-pairing && pnpm run verify-doc-budgets",
|
||||||
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types",
|
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types && pnpm run verify-runtime-closure",
|
||||||
"demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml",
|
"demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml",
|
||||||
"demo:repl": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml",
|
"demo:repl": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml",
|
||||||
"demo:code-mode": "node scripts/demo-code-mode.mjs",
|
"demo:code-mode": "node scripts/demo-code-mode.mjs",
|
||||||
|
|||||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -1706,6 +1706,12 @@ importers:
|
|||||||
'@deepseek-ai/dsh-repeat-tool-guard':
|
'@deepseek-ai/dsh-repeat-tool-guard':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/guard/repeat-tool-guard
|
version: link:../../packages/guard/repeat-tool-guard
|
||||||
|
'@deepseek-ai/dsh-sandbox':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/sandbox/sandbox
|
||||||
|
'@deepseek-ai/dsh-scope':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/core/scope
|
||||||
'@deepseek-ai/dsh-session':
|
'@deepseek-ai/dsh-session':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/core/session
|
version: link:../../packages/core/session
|
||||||
@@ -1718,6 +1724,12 @@ importers:
|
|||||||
'@deepseek-ai/dsh-session-persistence-sqlite':
|
'@deepseek-ai/dsh-session-persistence-sqlite':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/session-persistence/session-persistence-sqlite
|
version: link:../../packages/session-persistence/session-persistence-sqlite
|
||||||
|
'@deepseek-ai/dsh-skill':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/skill/skill
|
||||||
|
'@deepseek-ai/dsh-skill-local':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/skill/skill-local
|
||||||
'@deepseek-ai/dsh-subagent':
|
'@deepseek-ai/dsh-subagent':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/subagent/subagent
|
version: link:../../packages/subagent/subagent
|
||||||
@@ -1757,6 +1769,9 @@ importers:
|
|||||||
'@deepseek-ai/dsh-tool-fs':
|
'@deepseek-ai/dsh-tool-fs':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/fs/tool-fs
|
version: link:../../packages/fs/tool-fs
|
||||||
|
'@deepseek-ai/dsh-tool-skill':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/skill/tool-skill
|
||||||
'@deepseek-ai/dsh-tool-subagent':
|
'@deepseek-ai/dsh-tool-subagent':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/subagent/tool-subagent
|
version: link:../../packages/subagent/tool-subagent
|
||||||
@@ -1772,6 +1787,9 @@ importers:
|
|||||||
'@deepseek-ai/dsh-tools':
|
'@deepseek-ai/dsh-tools':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/core/tools
|
version: link:../../packages/core/tools
|
||||||
|
'@deepseek-ai/dsh-user-approval':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/ui/user-approval
|
||||||
'@deepseek-ai/dsh-user-interaction':
|
'@deepseek-ai/dsh-user-interaction':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/ui/user-interaction
|
version: link:../../packages/ui/user-interaction
|
||||||
|
|||||||
60
python/sdk-runtime/hatch_build.py
Normal file
60
python/sdk-runtime/hatch_build.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import stat
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
||||||
|
|
||||||
|
|
||||||
|
_PLATFORMS = {
|
||||||
|
"linux-x64": ("manylinux_2_28_x86_64", "dsh-jsonrpc-agent-pkg-linux-x64"),
|
||||||
|
"linux-arm64": ("manylinux_2_28_aarch64", "dsh-jsonrpc-agent-pkg-linux-arm64"),
|
||||||
|
"macos-arm64": ("macosx_11_0_arm64", "dsh-jsonrpc-agent-pkg-macos-arm64"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _host_platform_tag() -> str:
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
arch = "arm64" if machine in {"arm64", "aarch64"} else "x64" if machine in {"x86_64", "amd64"} else machine
|
||||||
|
system = platform.system().lower()
|
||||||
|
key = f"macos-{arch}" if system == "darwin" else f"linux-{arch}" if system == "linux" else system
|
||||||
|
try:
|
||||||
|
return _PLATFORMS[key][0]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise RuntimeError(f"unsupported deepseek-harness-runtime-bin build platform: {key}") from exc
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeBuildHook(BuildHookInterface):
|
||||||
|
"""Assign the native wheel tag and reject incomplete or mixed-platform payloads."""
|
||||||
|
|
||||||
|
def initialize(self, version: str, build_data: dict[str, object]) -> None:
|
||||||
|
if version == "editable":
|
||||||
|
return
|
||||||
|
if self.target_name == "sdist":
|
||||||
|
raise RuntimeError(
|
||||||
|
"deepseek-harness-runtime-bin is wheel-only; build and publish platform wheels only."
|
||||||
|
)
|
||||||
|
|
||||||
|
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]
|
||||||
|
if len(matches) != 1:
|
||||||
|
supported = ", ".join(value[0] for value in _PLATFORMS.values())
|
||||||
|
raise RuntimeError(
|
||||||
|
f"unsupported DSH_RUNTIME_PLATFORM_TAG {platform_tag!r}; expected one of {supported}"
|
||||||
|
)
|
||||||
|
expected_executable = matches[0][1]
|
||||||
|
runtime_dir = Path(self.root) / "src" / "deepseek_harness_runtime" / "runtime"
|
||||||
|
executables = sorted(runtime_dir.glob("dsh-jsonrpc-agent-pkg-*") if runtime_dir.is_dir() else [])
|
||||||
|
if [path.name for path in executables] != [expected_executable]:
|
||||||
|
found = ", ".join(path.name for path in executables) or "none"
|
||||||
|
raise RuntimeError(
|
||||||
|
f"runtime wheel {platform_tag} must contain only {expected_executable}; found {found}"
|
||||||
|
)
|
||||||
|
if executables[0].stat().st_mode & stat.S_IXUSR == 0:
|
||||||
|
raise RuntimeError(f"runtime executable is not executable: {executables[0]}")
|
||||||
|
|
||||||
|
build_data["pure_python"] = False
|
||||||
|
build_data["infer_tag"] = False
|
||||||
|
build_data["tag"] = f"py3-none-{platform_tag}"
|
||||||
@@ -33,10 +33,14 @@
|
|||||||
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
|
"@deepseek-ai/dsh-llm-deepseek": "workspace:^",
|
||||||
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
|
"@deepseek-ai/dsh-llm-pi-ai": "workspace:^",
|
||||||
"@deepseek-ai/dsh-repeat-tool-guard": "workspace:^",
|
"@deepseek-ai/dsh-repeat-tool-guard": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-sandbox": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-scope": "workspace:^",
|
||||||
"@deepseek-ai/dsh-session": "workspace:^",
|
"@deepseek-ai/dsh-session": "workspace:^",
|
||||||
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
"@deepseek-ai/dsh-session-persistence": "workspace:^",
|
||||||
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
"@deepseek-ai/dsh-session-persistence-jsonl": "workspace:^",
|
||||||
"@deepseek-ai/dsh-session-persistence-sqlite": "workspace:^",
|
"@deepseek-ai/dsh-session-persistence-sqlite": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-skill": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-skill-local": "workspace:^",
|
||||||
"@deepseek-ai/dsh-subagent": "workspace:^",
|
"@deepseek-ai/dsh-subagent": "workspace:^",
|
||||||
"@deepseek-ai/dsh-subagent-acp": "workspace:^",
|
"@deepseek-ai/dsh-subagent-acp": "workspace:^",
|
||||||
"@deepseek-ai/dsh-subagent-fork": "workspace:^",
|
"@deepseek-ai/dsh-subagent-fork": "workspace:^",
|
||||||
@@ -50,11 +54,13 @@
|
|||||||
"@deepseek-ai/dsh-tool-bash": "workspace:^",
|
"@deepseek-ai/dsh-tool-bash": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
|
"@deepseek-ai/dsh-tool-cordis": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-fs": "workspace:^",
|
"@deepseek-ai/dsh-tool-fs": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-tool-skill": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
|
"@deepseek-ai/dsh-tool-subagent": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-todo": "workspace:^",
|
"@deepseek-ai/dsh-tool-todo": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-web": "workspace:^",
|
"@deepseek-ai/dsh-tool-web": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
|
"@deepseek-ai/dsh-tool-workflow": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tools": "workspace:^",
|
"@deepseek-ai/dsh-tools": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-user-approval": "workspace:^",
|
||||||
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
"@deepseek-ai/dsh-user-interaction": "workspace:^",
|
||||||
"@deepseek-ai/dsh-web": "workspace:^",
|
"@deepseek-ai/dsh-web": "workspace:^",
|
||||||
"@deepseek-ai/dsh-web-fetch-local": "workspace:^",
|
"@deepseek-ai/dsh-web-fetch-local": "workspace:^",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deepseek-harness-runtime-bin"
|
name = "deepseek-harness-runtime-bin"
|
||||||
version = "0.0.0-dev"
|
version = "0.0.0.dev0"
|
||||||
description = "Pinned DeepSeek Harness runtime for the Python SDK"
|
description = "Pinned DeepSeek Harness runtime for the Python SDK"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
@@ -19,3 +19,7 @@ exclude = ["src/deepseek_harness_runtime/runtime/node"]
|
|||||||
|
|
||||||
[tool.hatch.build.targets.wheel]
|
[tool.hatch.build.targets.wheel]
|
||||||
packages = ["src/deepseek_harness_runtime"]
|
packages = ["src/deepseek_harness_runtime"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel.hooks.custom]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.sdist.hooks.custom]
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deepseek-harness"
|
name = "deepseek-harness"
|
||||||
version = "0.0.0-dev"
|
version = "0.0.0.dev0"
|
||||||
description = "Python SDK for DeepSeek Harness"
|
description = "Python SDK for DeepSeek Harness"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
license = { text = "BSD-3-Clause" }
|
license = { text = "BSD-3-Clause" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pydantic>=2.12",
|
"pydantic>=2.12",
|
||||||
"deepseek-harness-runtime-bin==0.0.0-dev",
|
"deepseek-harness-runtime-bin==0.0.0.dev0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ const OUT_DIR = 'dist-exe'
|
|||||||
const PYTHON_RUNTIME_DIR = 'python/sdk-runtime/src/deepseek_harness_runtime/runtime'
|
const PYTHON_RUNTIME_DIR = 'python/sdk-runtime/src/deepseek_harness_runtime/runtime'
|
||||||
/** Subdir of {@link PYTHON_RUNTIME_DIR} carrying the staged closure for node-mode execution. */
|
/** Subdir of {@link PYTHON_RUNTIME_DIR} carrying the staged closure for node-mode execution. */
|
||||||
const PYTHON_NODE_SUBDIR = 'node'
|
const PYTHON_NODE_SUBDIR = 'node'
|
||||||
|
/** Deploy-root documentation is not runtime input and violates the generated-directory i18n exclusion if retained. */
|
||||||
|
const DEPLOY_ONLY_DOCS = ['README.md', 'README.zh.md', 'README.i18n.yaml']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whole-tree asset globs. The cordis Loader dynamic-imports bare package names
|
* Whole-tree asset globs. The cordis Loader dynamic-imports bare package names
|
||||||
@@ -295,6 +297,11 @@ class SingleExeBuild {
|
|||||||
|
|
||||||
constructor(private readonly cli: BuildCli) {}
|
constructor(private readonly cli: BuildCli) {}
|
||||||
|
|
||||||
|
/** Gate the manifest before spending time compiling or packaging it. */
|
||||||
|
async verifyClosure(): Promise<void> {
|
||||||
|
await this.run('runtime dependency closure', pnpmBin(), ['run', 'verify-runtime-closure'])
|
||||||
|
}
|
||||||
|
|
||||||
/** Step 1: `pnpm run build` — all packages emit `lib/` (skipped via --skip-build). */
|
/** Step 1: `pnpm run build` — all packages emit `lib/` (skipped via --skip-build). */
|
||||||
async build(): Promise<void> {
|
async build(): Promise<void> {
|
||||||
if (this.cli.skipBuild) {
|
if (this.cli.skipBuild) {
|
||||||
@@ -322,6 +329,11 @@ class SingleExeBuild {
|
|||||||
'--config.link-workspace-packages=true',
|
'--config.link-workspace-packages=true',
|
||||||
this.staging,
|
this.staging,
|
||||||
])
|
])
|
||||||
|
if (this.cli.dryRun) {
|
||||||
|
for (const name of DEPLOY_ONLY_DOCS) console.log(`build-exe-for-python-sdk: [dry-run] rm -f ${join(this.staging, name)}`)
|
||||||
|
} else {
|
||||||
|
await Promise.all(DEPLOY_ONLY_DOCS.map(name => rm(join(this.staging, name), { force: true })))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Step 3: patch the staged package.json with the bin entry + pkg asset globs. */
|
/** Step 3: patch the staged package.json with the bin entry + pkg asset globs. */
|
||||||
@@ -445,6 +457,7 @@ async function main(): Promise<void> {
|
|||||||
const pipeline = new SingleExeBuild(cli)
|
const pipeline = new SingleExeBuild(cli)
|
||||||
console.log(`build-exe-for-python-sdk: targets: ${cli.targets.map(target => target.spec).join(', ')}`)
|
console.log(`build-exe-for-python-sdk: targets: ${cli.targets.map(target => target.spec).join(', ')}`)
|
||||||
console.log(`build-exe-for-python-sdk: staging: ${pipeline.staging}`)
|
console.log(`build-exe-for-python-sdk: staging: ${pipeline.staging}`)
|
||||||
|
await pipeline.verifyClosure()
|
||||||
await pipeline.build()
|
await pipeline.build()
|
||||||
await pipeline.deployStaging()
|
await pipeline.deployStaging()
|
||||||
await pipeline.injectPkgConfig()
|
await pipeline.injectPkgConfig()
|
||||||
|
|||||||
160
scripts/build-python-release.py
Normal file
160
scripts/build-python-release.py
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Stage and build one Python release wheel from a stable ``python-vX.Y.Z`` tag."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import email
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
PLATFORMS = {
|
||||||
|
"linux-x64": ("manylinux_2_28_x86_64", "dsh-jsonrpc-agent-pkg-linux-x64"),
|
||||||
|
"linux-arm64": ("manylinux_2_28_aarch64", "dsh-jsonrpc-agent-pkg-linux-arm64"),
|
||||||
|
"macos-arm64": ("macosx_11_0_arm64", "dsh-jsonrpc-agent-pkg-macos-arm64"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--package", choices=("sdk", "runtime"), required=True)
|
||||||
|
parser.add_argument("--tag", required=True)
|
||||||
|
parser.add_argument("--output-dir", type=Path, required=True)
|
||||||
|
parser.add_argument("--platform", choices=tuple(PLATFORMS))
|
||||||
|
parser.add_argument("--runtime-exe", type=Path)
|
||||||
|
args = parser.parse_args()
|
||||||
|
version = version_from_tag(args.tag)
|
||||||
|
if args.package == "runtime" and (args.platform is None or args.runtime_exe is None):
|
||||||
|
parser.error("runtime builds require --platform and --runtime-exe")
|
||||||
|
if args.package == "sdk" and (args.platform is not None or args.runtime_exe is not None):
|
||||||
|
parser.error("SDK builds do not accept --platform or --runtime-exe")
|
||||||
|
|
||||||
|
output_dir = args.output_dir.resolve()
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
with tempfile.TemporaryDirectory(prefix="dsh-python-release-") as temporary:
|
||||||
|
staging = Path(temporary) / args.package
|
||||||
|
if args.package == "sdk":
|
||||||
|
stage_sdk(staging, version)
|
||||||
|
environment = None
|
||||||
|
expected = output_dir / f"deepseek_harness-{version}-py3-none-any.whl"
|
||||||
|
else:
|
||||||
|
platform_tag, executable_name = PLATFORMS[args.platform]
|
||||||
|
stage_runtime(staging, version, args.runtime_exe.resolve(), executable_name)
|
||||||
|
environment = {"DSH_RUNTIME_PLATFORM_TAG": platform_tag}
|
||||||
|
expected = output_dir / f"deepseek_harness_runtime_bin-{version}-py3-none-{platform_tag}.whl"
|
||||||
|
command = ["uv", "build", "--wheel", "--out-dir", str(output_dir), str(staging)]
|
||||||
|
subprocess.run(command, cwd=ROOT, env=None if environment is None else {**os.environ, **environment}, check=True)
|
||||||
|
if not expected.is_file():
|
||||||
|
raise RuntimeError(f"build did not produce expected wheel: {expected}")
|
||||||
|
verify_wheel(expected, args.package, version, None if args.platform is None else PLATFORMS[args.platform])
|
||||||
|
print(expected)
|
||||||
|
|
||||||
|
|
||||||
|
def version_from_tag(tag: str) -> str:
|
||||||
|
match = re.fullmatch(r"python-v(\d+\.\d+\.\d+)", tag)
|
||||||
|
if match is None:
|
||||||
|
raise ValueError(f"release tag must match python-vX.Y.Z, got {tag!r}")
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_package(source: Path, destination: Path) -> None:
|
||||||
|
shutil.copytree(
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
ignore=shutil.ignore_patterns(
|
||||||
|
".venv",
|
||||||
|
".pytest_cache",
|
||||||
|
"__pycache__",
|
||||||
|
"*.pyc",
|
||||||
|
"dist",
|
||||||
|
"node_modules",
|
||||||
|
"dsh-jsonrpc-agent-pkg-*",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def rewrite_version(pyproject: Path, version: str) -> None:
|
||||||
|
text, count = re.subn(
|
||||||
|
r'^version = "[^"]+"$',
|
||||||
|
f'version = "{version}"',
|
||||||
|
pyproject.read_text(),
|
||||||
|
count=1,
|
||||||
|
flags=re.MULTILINE,
|
||||||
|
)
|
||||||
|
if count != 1:
|
||||||
|
raise RuntimeError(f"could not rewrite version in {pyproject}")
|
||||||
|
pyproject.write_text(text)
|
||||||
|
|
||||||
|
|
||||||
|
def stage_sdk(destination: Path, version: str) -> None:
|
||||||
|
copy_package(ROOT / "python" / "sdk", destination)
|
||||||
|
pyproject = destination / "pyproject.toml"
|
||||||
|
rewrite_version(pyproject, version)
|
||||||
|
text, count = re.subn(
|
||||||
|
r'"deepseek-harness-runtime-bin==[^"]+"',
|
||||||
|
f'"deepseek-harness-runtime-bin=={version}"',
|
||||||
|
pyproject.read_text(),
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
|
if count != 1:
|
||||||
|
raise RuntimeError("SDK must contain exactly one runtime dependency pin")
|
||||||
|
pyproject.write_text(text)
|
||||||
|
|
||||||
|
|
||||||
|
def stage_runtime(destination: Path, version: str, executable: Path, executable_name: str) -> None:
|
||||||
|
if not executable.is_file():
|
||||||
|
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}")
|
||||||
|
copy_package(ROOT / "python" / "sdk-runtime", destination)
|
||||||
|
rewrite_version(destination / "pyproject.toml", version)
|
||||||
|
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
|
||||||
|
runtime_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
destination_executable = runtime_dir / executable_name
|
||||||
|
shutil.copyfile(executable, destination_executable)
|
||||||
|
destination_executable.chmod(executable.stat().st_mode & 0o777)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_wheel(
|
||||||
|
wheel: Path,
|
||||||
|
package: str,
|
||||||
|
version: str,
|
||||||
|
platform: tuple[str, str] | None,
|
||||||
|
) -> None:
|
||||||
|
expected_tag = "py3-none-any" if platform is None else f"py3-none-{platform[0]}"
|
||||||
|
with zipfile.ZipFile(wheel) as archive:
|
||||||
|
wheel_metadata_path = next(name for name in archive.namelist() if name.endswith(".dist-info/WHEEL"))
|
||||||
|
metadata_path = next(name for name in archive.namelist() if name.endswith(".dist-info/METADATA"))
|
||||||
|
wheel_metadata = email.message_from_bytes(archive.read(wheel_metadata_path))
|
||||||
|
metadata = email.message_from_bytes(archive.read(metadata_path))
|
||||||
|
if wheel_metadata.get_all("Tag") != [expected_tag]:
|
||||||
|
raise RuntimeError(f"{wheel} has wrong WHEEL tags: {wheel_metadata.get_all('Tag')}")
|
||||||
|
if metadata.get("Version") != version:
|
||||||
|
raise RuntimeError(f"{wheel} has version {metadata.get('Version')}, expected {version}")
|
||||||
|
executables = [name for name in archive.namelist() if "/runtime/dsh-jsonrpc-agent-pkg-" in name]
|
||||||
|
if package == "runtime":
|
||||||
|
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}")
|
||||||
|
mode = archive.getinfo(executables[0]).external_attr >> 16
|
||||||
|
if mode & stat.S_IXUSR == 0:
|
||||||
|
raise RuntimeError(f"{wheel} runtime executable lost its executable bit")
|
||||||
|
elif executables:
|
||||||
|
raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {executables}")
|
||||||
|
if package == "sdk":
|
||||||
|
requirements = metadata.get_all("Requires-Dist") or []
|
||||||
|
expected_requirement = f"deepseek-harness-runtime-bin=={version}"
|
||||||
|
if expected_requirement not in requirements:
|
||||||
|
raise RuntimeError(f"{wheel} does not pin {expected_requirement}; found {requirements}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -151,6 +151,7 @@ function gatesForMode(selected: Mode): Gate[] {
|
|||||||
]
|
]
|
||||||
case 'pre-push':
|
case 'pre-push':
|
||||||
return [
|
return [
|
||||||
|
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||||
pnpmScript('test', 'test'),
|
pnpmScript('test', 'test'),
|
||||||
pnpmScript('snapshot', 'test:snapshot'),
|
pnpmScript('snapshot', 'test:snapshot'),
|
||||||
pnpmScript('build', 'build'),
|
pnpmScript('build', 'build'),
|
||||||
@@ -163,6 +164,7 @@ function gatesForMode(selected: Mode): Gate[] {
|
|||||||
|
|
||||||
function ciPrimaryGates(): Gate[] {
|
function ciPrimaryGates(): Gate[] {
|
||||||
return [
|
return [
|
||||||
|
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||||
pnpmScript('constraints', 'constraints'),
|
pnpmScript('constraints', 'constraints'),
|
||||||
pnpmScript('typecheck', 'typecheck'),
|
pnpmScript('typecheck', 'typecheck'),
|
||||||
lintGate(),
|
lintGate(),
|
||||||
@@ -184,6 +186,7 @@ function ciPrimaryGates(): Gate[] {
|
|||||||
|
|
||||||
function ciStaticGates(): Gate[] {
|
function ciStaticGates(): Gate[] {
|
||||||
return [
|
return [
|
||||||
|
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||||
pnpmScript('constraints', 'constraints'),
|
pnpmScript('constraints', 'constraints'),
|
||||||
demoSmokeGate(),
|
demoSmokeGate(),
|
||||||
...docSyncLeafGates(),
|
...docSyncLeafGates(),
|
||||||
|
|||||||
116
scripts/verify-runtime-closure.ts
Normal file
116
scripts/verify-runtime-closure.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Verify that the Python single-exe deploy manifest explicitly supplies every
|
||||||
|
* required workspace peer of every workspace package in its dependency graph.
|
||||||
|
*
|
||||||
|
* `pnpm deploy --config.auto-install-peers=false` cannot repair an incomplete
|
||||||
|
* runtime root. Keeping the peer at the root also prevents a successful build
|
||||||
|
* from producing an executable that fails only when Cordis loads the plugin.
|
||||||
|
*/
|
||||||
|
import { readFile, readdir } from 'node:fs/promises'
|
||||||
|
import { join, resolve } from 'node:path'
|
||||||
|
import { parseArgs } from 'node:util'
|
||||||
|
|
||||||
|
interface PackageManifest {
|
||||||
|
name?: string
|
||||||
|
dependencies?: Record<string, string>
|
||||||
|
optionalDependencies?: Record<string, string>
|
||||||
|
peerDependencies?: Record<string, string>
|
||||||
|
peerDependenciesMeta?: Record<string, { optional?: boolean }>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorkspacePackage {
|
||||||
|
path: string
|
||||||
|
manifest: PackageManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = resolve(import.meta.dirname, '..')
|
||||||
|
const { values } = parseArgs({
|
||||||
|
args: process.argv.slice(2),
|
||||||
|
options: { manifest: { type: 'string' } },
|
||||||
|
})
|
||||||
|
const runtimeManifestPath = resolve(root, values.manifest ?? 'python/sdk-runtime/package.json')
|
||||||
|
const runtimeManifest = await loadManifest(runtimeManifestPath)
|
||||||
|
const runtimeName = runtimeManifest.name ?? 'python/sdk-runtime'
|
||||||
|
const workspace = await loadWorkspacePackages()
|
||||||
|
const runtimeDependencies = runtimeManifest.dependencies ?? {}
|
||||||
|
const parents = new Map<string, string | undefined>()
|
||||||
|
const queue: string[] = []
|
||||||
|
|
||||||
|
for (const dependency of Object.keys(runtimeDependencies).sort()) {
|
||||||
|
if (!workspace.has(dependency)) continue
|
||||||
|
parents.set(dependency, undefined)
|
||||||
|
queue.push(dependency)
|
||||||
|
}
|
||||||
|
|
||||||
|
const failures: string[] = []
|
||||||
|
for (let index = 0; index < queue.length; index += 1) {
|
||||||
|
const packageName = queue[index]
|
||||||
|
if (packageName === undefined) continue
|
||||||
|
const current = workspace.get(packageName)
|
||||||
|
if (current === undefined) continue
|
||||||
|
const peers = current.manifest.peerDependencies ?? {}
|
||||||
|
const peerMeta = current.manifest.peerDependenciesMeta ?? {}
|
||||||
|
for (const peer of Object.keys(peers).sort()) {
|
||||||
|
if (!workspace.has(peer) || peerMeta[peer]?.optional === true) continue
|
||||||
|
if (runtimeDependencies[peer]?.startsWith('workspace:') === true) continue
|
||||||
|
failures.push(`${formatChain(runtimeName, packageName, parents)} -> ${peer}`)
|
||||||
|
}
|
||||||
|
const dependencies = {
|
||||||
|
...current.manifest.dependencies,
|
||||||
|
...current.manifest.optionalDependencies,
|
||||||
|
}
|
||||||
|
for (const dependency of Object.keys(dependencies).sort()) {
|
||||||
|
if (!workspace.has(dependency) || parents.has(dependency)) continue
|
||||||
|
parents.set(dependency, packageName)
|
||||||
|
queue.push(dependency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (failures.length > 0) {
|
||||||
|
console.error('verify-runtime-closure: required workspace peers are missing from python/sdk-runtime dependencies:')
|
||||||
|
for (const failure of failures) console.error(` ${failure}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`verify-runtime-closure: ${queue.length} workspace packages form a closed runtime dependency graph.`)
|
||||||
|
|
||||||
|
async function loadWorkspacePackages(): Promise<Map<string, WorkspacePackage>> {
|
||||||
|
const paths: string[] = []
|
||||||
|
for (const group of await childDirectories(join(root, 'packages'))) {
|
||||||
|
for (const packageDir of await childDirectories(join(root, 'packages', group))) {
|
||||||
|
paths.push(join(root, 'packages', group, packageDir, 'package.json'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const packageDir of await childDirectories(join(root, 'vendor'))) {
|
||||||
|
paths.push(join(root, 'vendor', packageDir, 'package.json'))
|
||||||
|
}
|
||||||
|
const result = new Map<string, WorkspacePackage>()
|
||||||
|
for (const path of paths) {
|
||||||
|
const manifest = await loadManifest(path)
|
||||||
|
if (manifest.name !== undefined) result.set(manifest.name, { path, manifest })
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
async function childDirectories(path: string): Promise<string[]> {
|
||||||
|
const entries = await readdir(path, { withFileTypes: true })
|
||||||
|
return entries.filter(entry => entry.isDirectory()).map(entry => entry.name).sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadManifest(path: string): Promise<PackageManifest> {
|
||||||
|
return JSON.parse(await readFile(path, 'utf8')) as PackageManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatChain(
|
||||||
|
runtimeName: string,
|
||||||
|
packageName: string,
|
||||||
|
parents: ReadonlyMap<string, string | undefined>,
|
||||||
|
): string {
|
||||||
|
const chain = [packageName]
|
||||||
|
let parent = parents.get(packageName)
|
||||||
|
while (parent !== undefined) {
|
||||||
|
chain.unshift(parent)
|
||||||
|
parent = parents.get(parent)
|
||||||
|
}
|
||||||
|
return [runtimeName, ...chain].join(' -> ')
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user