fix: download optional Python runtime resources explicitly
This commit is contained in:
1 parent
15c10e0e87
commit
89f35fe2e9
34 files changed
+1159
-259
No files matched your search
@@ -10,7 +10,7 @@ Two runtime carriers coexist under ``runtime/``, both injected by the repo's
|
||||
Node installation.
|
||||
- **node (dev-only)**: the full deploy closure under ``runtime/node/``
|
||||
(``package.json`` + ``node_modules/``), executed as ``node
|
||||
runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`` on a
|
||||
runtime/node/runtime-bootstrap.mjs`` on a
|
||||
system Node >= 22.19. It is the current checkout's source build, never
|
||||
selected automatically, and excluded from wheel/sdist distributions.
|
||||
|
||||
@@ -28,7 +28,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from ._resources import validate_resources
|
||||
from ._resources import validate_downloads, office_launch_args
|
||||
|
||||
PACKAGE_METADATA_FILENAME = "deepseek-harness-runtime.json"
|
||||
|
||||
@@ -59,10 +59,9 @@ def bundled_runtime_path() -> Path:
|
||||
"""Absolute path of the bundled single-file runtime executable for the current platform.
|
||||
|
||||
Raises FileNotFoundError when the platform is unsupported, the executable
|
||||
has not been placed into this package, the ripgrep or Office sidecar is
|
||||
missing, the authoring resources are incomplete, or the required macOS
|
||||
spawn helper is missing. Invalid authoring metadata or Python permissions
|
||||
raise ValueError. Missing executable/Office messages name
|
||||
has not been placed into this package, the ripgrep sidecar or download
|
||||
manifest is missing, or the required macOS spawn helper is missing.
|
||||
Invalid download metadata raises ValueError. Missing executable messages name
|
||||
the acquisition routes (acquisition strategy is deliberately separate from
|
||||
this lookup interface, so an on-demand download can replace it without
|
||||
touching callers).
|
||||
@@ -92,22 +91,7 @@ def bundled_runtime_path() -> Path:
|
||||
f"deepseek-harness-runtime-bin is missing the node-pty spawn helper at {helper}. "
|
||||
+ _EXE_ACQUISITION_HINT
|
||||
)
|
||||
office = path.with_name(f"{path.name.removesuffix('.exe')}-office")
|
||||
adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
|
||||
if not adapter.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"deepseek-harness-runtime-bin is missing the Office sidecar at {office}. "
|
||||
+ _EXE_ACQUISITION_HINT
|
||||
)
|
||||
native = tag.replace("win-", "win32-").replace("macos-", "darwin-")
|
||||
declared = json.loads(adapter.read_text(encoding="utf-8")).get("optionalDependencies", {})
|
||||
engine = native if f"@deepseek-ai/libreoffice-kit-{native}" in declared else "wasm"
|
||||
if not (office / "node_modules" / f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json").is_file():
|
||||
raise FileNotFoundError(
|
||||
f"deepseek-harness-runtime-bin is missing the Office sidecar engine {engine} at {office}. "
|
||||
+ _EXE_ACQUISITION_HINT
|
||||
)
|
||||
validate_resources(path.with_name(tag), tag)
|
||||
validate_downloads(path.with_name(tag), tag)
|
||||
return path
|
||||
|
||||
|
||||
@@ -152,14 +136,7 @@ def _current_platform_tag() -> str:
|
||||
|
||||
def _node_launch_args() -> tuple[str, str]:
|
||||
node_root = bundled_package_dir() / "runtime" / "node"
|
||||
bin_js = (
|
||||
node_root
|
||||
/ "node_modules"
|
||||
/ "@deepseek-ai"
|
||||
/ "dsh"
|
||||
/ "lib"
|
||||
/ "bin.js"
|
||||
)
|
||||
bin_js = node_root / "runtime-bootstrap.mjs"
|
||||
if not bin_js.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"the dev-only node runtime closure is missing at {node_root} "
|
||||
@@ -176,6 +153,43 @@ def _node_launch_args() -> tuple[str, str]:
|
||||
return (node, str(bin_js))
|
||||
|
||||
|
||||
def _download(flag: str) -> Path:
|
||||
result = subprocess.run((*resolve_bundled_launch_args(), flag), capture_output=True, text=True)
|
||||
if result.returncode:
|
||||
raise RuntimeError(result.stderr.strip() or f"resource download failed with exit code {result.returncode}")
|
||||
path = json.loads(result.stdout)
|
||||
if not isinstance(path, str) or not Path(path).is_absolute():
|
||||
raise ValueError("runtime returned an invalid resource path")
|
||||
return Path(path)
|
||||
|
||||
|
||||
def download_office() -> Path:
|
||||
"""Download the locked npm Office sidecar and standalone Node into the resource cache.
|
||||
|
||||
Return its absolute directory. Reuse completed downloads; failures leave no
|
||||
published partial installation. DSH_RESOURCE_CACHE selects the cache root.
|
||||
This explicit operation requires network access on a cache miss, not a
|
||||
Harness home, agent, or model. Other SDK operations never trigger downloads.
|
||||
"""
|
||||
return _download("--download-office")
|
||||
|
||||
|
||||
def download_primary_runtime() -> Path:
|
||||
"""Download locked CPython, libraries, Node, pnpm and skills into the resource cache.
|
||||
|
||||
Return the absolute primary-runtime directory. This is independent of the
|
||||
Office engine download and does not copy resources into a Harness home.
|
||||
Completed downloads are reused; DSH_RESOURCE_CACHE selects the cache root.
|
||||
"""
|
||||
return _download("--download-primary-runtime")
|
||||
|
||||
|
||||
def resolve_office_launch_args() -> tuple[str, str]:
|
||||
"""Locate the explicitly downloaded Office CLI and its standalone Node; never download."""
|
||||
tag = _current_platform_tag()
|
||||
return office_launch_args(bundled_runtime_path().with_name(tag), tag)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Launch the CLI with explicit DSH_HOME; wait on Windows, replace the process on POSIX."""
|
||||
if not os.environ.get("DSH_HOME", "").strip():
|
||||
@@ -197,6 +211,9 @@ __all__ = [
|
||||
"RUNTIME_MODE_ENV_VAR",
|
||||
"bundled_package_dir",
|
||||
"bundled_runtime_path",
|
||||
"download_office",
|
||||
"download_primary_runtime",
|
||||
"resolve_office_launch_args",
|
||||
"main",
|
||||
"resolve_bundled_launch_args",
|
||||
]
|
||||
@@ -3,6 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
from collections.abc import Sequence
|
||||
import re
|
||||
import stat
|
||||
import zipfile
|
||||
@@ -41,3 +45,71 @@ def validate_resources(root: Path | zipfile.Path, target: str) -> None:
|
||||
if isinstance(binary, zipfile.Path) else binary.stat().st_mode)
|
||||
if mode & stat.S_IXUSR == 0:
|
||||
raise ValueError(f"runtime interpreter lost its executable bit: {binary}")
|
||||
|
||||
|
||||
def validate_downloads(root: Path | zipfile.Path, target: str) -> dict:
|
||||
"""Validate the lightweight download manifest carried by one platform wheel."""
|
||||
path = root / "downloads.json"
|
||||
value = json.loads(path.read_text(encoding="utf-8"))
|
||||
if (not isinstance(value, dict) or value.get("target") != target.replace("macos-", "mac-")
|
||||
or not isinstance(value.get("identity"), str)
|
||||
or not re.fullmatch(r"[a-f0-9]{64}", value["identity"])
|
||||
or not isinstance(value.get("office"), list) or not value["office"]
|
||||
or not isinstance(value.get("pnpm"), dict)):
|
||||
raise ValueError(f"invalid runtime download manifest: {path}")
|
||||
for file in ["scripts/check_office.py", *(f"office-{kind}/SKILL.md" for kind in ("docx", "pptx", "xlsx"))]:
|
||||
if not (root / "office-skills" / file).is_file():
|
||||
raise FileNotFoundError(f"runtime Office skill is missing: {root / 'office-skills' / file}")
|
||||
return value
|
||||
|
||||
|
||||
def office_launch_args(root: Path, target: str) -> tuple[str, str]:
|
||||
"""Require an explicitly installed sidecar, including its independent Node interpreter."""
|
||||
metadata = validate_downloads(root, target)
|
||||
cache = Path(os.environ.get("DSH_RESOURCE_CACHE", str(Path.home() / ".cache/deepseek-harness/resources")))
|
||||
if not cache.is_absolute():
|
||||
raise ValueError("DSH_RESOURCE_CACHE must be an absolute directory")
|
||||
office = Path(os.environ.get("DSH_OFFICE_SIDECAR", str(cache / metadata["identity"] / "office")))
|
||||
if not office.is_absolute():
|
||||
raise ValueError("DSH_OFFICE_SIDECAR must be an absolute directory")
|
||||
node = office / "node/bin" / ("node.exe" if target == "win-x64" else "node")
|
||||
cli = office / "node_modules/@deepseek-ai/libreoffice-kit/lib/cli.js"
|
||||
for path in (node, cli):
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(f"Office resource is missing: {path}. Call deepseek_harness_runtime.download_office() first.")
|
||||
return str(node), str(cli)
|
||||
|
||||
|
||||
def run_office_process(arguments: Sequence[str], *, capture_output: bool) -> subprocess.CompletedProcess[bytes]:
|
||||
"""Run Office with captured or inherited streams; a second interrupt kills its process tree."""
|
||||
taskkill = os.path.join(os.environ.get("SystemRoot", ""), "System32", "taskkill.exe")
|
||||
if os.name == "nt" and not os.path.isabs(taskkill):
|
||||
raise ValueError("Office operations on Windows require an absolute SystemRoot environment value.")
|
||||
with subprocess.Popen(
|
||||
arguments,
|
||||
stdin=subprocess.DEVNULL if capture_output else None,
|
||||
stdout=subprocess.PIPE if capture_output else None,
|
||||
stderr=subprocess.PIPE if capture_output else None,
|
||||
start_new_session=os.name != "nt",
|
||||
) as process:
|
||||
try:
|
||||
stdout, stderr = process.communicate()
|
||||
except KeyboardInterrupt:
|
||||
# Windows console Ctrl+C already reaches children in the same console.
|
||||
if os.name != "nt":
|
||||
process.send_signal(signal.SIGINT)
|
||||
try:
|
||||
process.communicate()
|
||||
except KeyboardInterrupt:
|
||||
if os.name == "nt":
|
||||
subprocess.run([taskkill, "/PID", str(process.pid), "/T", "/F"],
|
||||
stdin=subprocess.DEVNULL, capture_output=True, check=False)
|
||||
else:
|
||||
try:
|
||||
os.killpg(process.pid, signal.SIGKILL)
|
||||
except ProcessLookupError as error:
|
||||
# The CLI and its children can finish before the second interrupt.
|
||||
pass
|
||||
process.communicate()
|
||||
raise
|
||||
return subprocess.CompletedProcess(arguments, process.returncode, stdout, stderr)
|
||||
Reference in new issue
Block a user