mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
feat(python-runtime): package the Windows x64 dsh executable
Add node24-win-x64 as the only supported Windows runtime target and publish it as a py3-none-win_amd64 wheel containing the conventional dsh and ripgrep .exe payload names. Keep Windows ARM64 rejected explicitly so Python cannot claim a carrier that CI and release automation do not build. Teach the pkg builder to require a native x64 Windows host, validate both node-pty ConPTY addons, copy @vscode's win32 ripgrep executable, and recognize pkg's .exe output. Extend runtime resolution, wheel staging, payload validation, and the preset closure check so the Windows-specific PowerShell plugins and sidecars fail loud when omitted. The sidecar resolver now maps a packaged main.exe to main-rg.exe; focused TypeScript and Python tests cover that name, the win_amd64 manifest, x64-only host selection, complete wheel payload, ConPTY inventory, and platform-conditioned plugin closure.
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { existsSync } from 'node:fs'
|
import { existsSync } from 'node:fs'
|
||||||
import { isAbsolute, relative, sep } from 'node:path'
|
import { isAbsolute, join, parse, relative, sep } from 'node:path'
|
||||||
import type { Context } from '@deepseek-ai/cordis'
|
import type { Context } from '@deepseek-ai/cordis'
|
||||||
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
||||||
import { ItemRetainer, TextRetainer } from '@deepseek-ai/dsh-output-retention'
|
import { ItemRetainer, TextRetainer } from '@deepseek-ai/dsh-output-retention'
|
||||||
@@ -170,7 +170,10 @@ let rgPathPromise: Promise<string> | undefined
|
|||||||
*/
|
*/
|
||||||
export function resolveRgPath(): Promise<string> {
|
export function resolveRgPath(): Promise<string> {
|
||||||
rgPathPromise ??= Promise.resolve().then(async () => {
|
rgPathPromise ??= Promise.resolve().then(async () => {
|
||||||
const executableSidecar = `${process.execPath}-rg`
|
const executable = parse(process.execPath)
|
||||||
|
const executableSidecar = process.platform === 'win32'
|
||||||
|
? join(executable.dir, `${executable.name}-rg.exe`)
|
||||||
|
: `${process.execPath}-rg`
|
||||||
if ('pkg' in process && existsSync(executableSidecar)) return executableSidecar
|
if ('pkg' in process && existsSync(executableSidecar)) return executableSidecar
|
||||||
return (await import('@vscode/ripgrep')).rgPath
|
return (await import('@vscode/ripgrep')).rgPath
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import { join, parse } from 'node:path'
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
const { dependencyRgPath, existsSync } = vi.hoisted(() => ({
|
const { dependencyRgPath, existsSync } = vi.hoisted(() => ({
|
||||||
dependencyRgPath: '/node_modules/@vscode/ripgrep/bin/rg',
|
dependencyRgPath: '/node_modules/@vscode/ripgrep/bin/rg',
|
||||||
existsSync: vi.fn(),
|
existsSync: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
const originalPlatform = process.platform
|
||||||
|
const originalExecPath = process.execPath
|
||||||
|
|
||||||
vi.mock('node:fs', async (importOriginal) => {
|
vi.mock('node:fs', async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import('node:fs')>()
|
const actual = await importOriginal<typeof import('node:fs')>()
|
||||||
@@ -16,17 +19,35 @@ beforeEach(() => {
|
|||||||
vi.resetModules()
|
vi.resetModules()
|
||||||
existsSync.mockReset()
|
existsSync.mockReset()
|
||||||
Reflect.deleteProperty(process, 'pkg')
|
Reflect.deleteProperty(process, 'pkg')
|
||||||
|
Reflect.defineProperty(process, 'platform', { configurable: true, enumerable: true, value: originalPlatform })
|
||||||
|
process.execPath = originalExecPath
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
Reflect.deleteProperty(process, 'pkg')
|
Reflect.deleteProperty(process, 'pkg')
|
||||||
|
Reflect.defineProperty(process, 'platform', { configurable: true, enumerable: true, value: originalPlatform })
|
||||||
|
process.execPath = originalExecPath
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('ripgrep resolution', () => {
|
describe('ripgrep resolution', () => {
|
||||||
it('uses the native sidecar beside the current executable', async () => {
|
it('uses the native sidecar beside the current executable', async () => {
|
||||||
Reflect.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
Reflect.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||||
|
Reflect.defineProperty(process, 'platform', { configurable: true, enumerable: true, value: 'linux' })
|
||||||
|
process.execPath = '/runtime/dsh'
|
||||||
existsSync.mockReturnValue(true)
|
existsSync.mockReturnValue(true)
|
||||||
const sidecar = `${process.execPath}-rg`
|
const sidecar = '/runtime/dsh-rg'
|
||||||
|
const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
|
||||||
|
|
||||||
|
await expect(resolveRgPath()).resolves.toBe(sidecar)
|
||||||
|
expect(existsSync).toHaveBeenCalledWith(sidecar)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses a conventional executable name for the Windows ripgrep sidecar', async () => {
|
||||||
|
Reflect.defineProperty(process, 'pkg', { configurable: true, value: {} })
|
||||||
|
Reflect.defineProperty(process, 'platform', { configurable: true, enumerable: true, value: 'win32' })
|
||||||
|
process.execPath = 'C:\\runtime\\deepseek-harness-sdk-runtime-win-x64.exe'
|
||||||
|
existsSync.mockReturnValue(true)
|
||||||
|
const sidecar = 'C:\\runtime\\deepseek-harness-sdk-runtime-win-x64-rg.exe'
|
||||||
const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
|
const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
|
||||||
|
|
||||||
await expect(resolveRgPath()).resolves.toBe(sidecar)
|
await expect(resolveRgPath()).resolves.toBe(sidecar)
|
||||||
@@ -47,6 +68,10 @@ describe('ripgrep resolution', () => {
|
|||||||
const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
|
const { resolveRgPath } = await import('@deepseek-ai/dsh-tool-fs-search')
|
||||||
|
|
||||||
await expect(resolveRgPath()).resolves.toBe(dependencyRgPath)
|
await expect(resolveRgPath()).resolves.toBe(dependencyRgPath)
|
||||||
expect(existsSync).toHaveBeenCalledWith(`${process.execPath}-rg`)
|
const executable = parse(process.execPath)
|
||||||
|
const sidecar = process.platform === 'win32'
|
||||||
|
? join(executable.dir, `${executable.name}-rg.exe`)
|
||||||
|
: `${process.execPath}-rg`
|
||||||
|
expect(existsSync).toHaveBeenCalledWith(sidecar)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Generated
+6
@@ -9972,6 +9972,12 @@ importers:
|
|||||||
'@deepseek-ai/dsh-tool-jobs':
|
'@deepseek-ai/dsh-tool-jobs':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/jobs/tool-jobs
|
version: link:../../packages/jobs/tool-jobs
|
||||||
|
'@deepseek-ai/dsh-tool-pwsh':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/shell/tool-pwsh
|
||||||
|
'@deepseek-ai/dsh-tool-pwsh-persistent':
|
||||||
|
specifier: workspace:^
|
||||||
|
version: link:../../packages/shell/tool-pwsh-persistent
|
||||||
'@deepseek-ai/dsh-tool-ralph':
|
'@deepseek-ai/dsh-tool-ralph':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
version: link:../../packages/workflow/tool-ralph
|
version: link:../../packages/workflow/tool-ralph
|
||||||
|
|||||||
@@ -39,7 +39,15 @@ def _host_platform_tag() -> str:
|
|||||||
machine = platform.machine().lower()
|
machine = platform.machine().lower()
|
||||||
arch = "arm64" if machine in {"arm64", "aarch64"} else "x64" if machine in {"x86_64", "amd64"} else machine
|
arch = "arm64" if machine in {"arm64", "aarch64"} else "x64" if machine in {"x86_64", "amd64"} else machine
|
||||||
system = platform.system().lower()
|
system = platform.system().lower()
|
||||||
key = f"macos-{arch}" if system == "darwin" else f"linux-{arch}" if system == "linux" else system
|
key = (
|
||||||
|
f"macos-{arch}"
|
||||||
|
if system == "darwin"
|
||||||
|
else f"linux-{arch}"
|
||||||
|
if system == "linux"
|
||||||
|
else f"win-{arch}"
|
||||||
|
if system == "windows"
|
||||||
|
else system
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
return _PLATFORMS[key][0]
|
return _PLATFORMS[key][0]
|
||||||
except KeyError as exc:
|
except KeyError as exc:
|
||||||
@@ -69,16 +77,21 @@ class RuntimeBuildHook(BuildHookInterface):
|
|||||||
runtime_files = sorted(
|
runtime_files = sorted(
|
||||||
runtime_dir.glob("deepseek-harness-sdk-runtime-*") if runtime_dir.is_dir() else []
|
runtime_dir.glob("deepseek-harness-sdk-runtime-*") if runtime_dir.is_dir() else []
|
||||||
)
|
)
|
||||||
expected_files = [expected_executable, f"{expected_executable}-rg"]
|
expected_files = (
|
||||||
|
[expected_executable, f"{expected_executable.removesuffix('.exe')}-rg.exe"]
|
||||||
|
if expected_executable.endswith(".exe")
|
||||||
|
else [expected_executable, f"{expected_executable}-rg"]
|
||||||
|
)
|
||||||
if "-macos-" in expected_executable:
|
if "-macos-" in expected_executable:
|
||||||
expected_files.append(f"{expected_executable}-spawn-helper")
|
expected_files.append(f"{expected_executable}-spawn-helper")
|
||||||
|
expected_files.sort()
|
||||||
found_files = [path.name for path in runtime_files]
|
found_files = [path.name for path in runtime_files]
|
||||||
if found_files != expected_files:
|
if found_files != expected_files:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"runtime wheel {platform_tag} payload must be {expected_files}; found {found_files}"
|
f"runtime wheel {platform_tag} payload must be {expected_files}; found {found_files}"
|
||||||
)
|
)
|
||||||
for executable in runtime_files:
|
for executable in runtime_files:
|
||||||
if executable.stat().st_mode & stat.S_IXUSR == 0:
|
if platform_tag != "win_amd64" and executable.stat().st_mode & stat.S_IXUSR == 0:
|
||||||
raise RuntimeError(f"runtime executable is not executable: {executable}")
|
raise RuntimeError(f"runtime executable is not executable: {executable}")
|
||||||
build_data["pure_python"] = False
|
build_data["pure_python"] = False
|
||||||
build_data["infer_tag"] = False
|
build_data["infer_tag"] = False
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
"@deepseek-ai/dsh-plan-mode": "workspace:^",
|
||||||
"@deepseek-ai/dsh-persona": "workspace:^",
|
"@deepseek-ai/dsh-persona": "workspace:^",
|
||||||
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
"@deepseek-ai/dsh-pwsh-local": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-tool-pwsh-persistent": "workspace:^",
|
||||||
"@deepseek-ai/dsh-terminal": "workspace:^",
|
"@deepseek-ai/dsh-terminal": "workspace:^",
|
||||||
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
|
"@deepseek-ai/dsh-terminal-bash": "workspace:^",
|
||||||
"@deepseek-ai/dsh-repeat-tool-reminder": "workspace:^",
|
"@deepseek-ai/dsh-repeat-tool-reminder": "workspace:^",
|
||||||
@@ -103,6 +104,7 @@
|
|||||||
"@deepseek-ai/dsh-tool-fs": "workspace:^",
|
"@deepseek-ai/dsh-tool-fs": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-fs-search": "workspace:^",
|
"@deepseek-ai/dsh-tool-fs-search": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-goal": "workspace:^",
|
"@deepseek-ai/dsh-tool-goal": "workspace:^",
|
||||||
|
"@deepseek-ai/dsh-tool-pwsh": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
|
"@deepseek-ai/dsh-tool-ralph": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-skill": "workspace:^",
|
"@deepseek-ai/dsh-tool-skill": "workspace:^",
|
||||||
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
|
"@deepseek-ai/dsh-tool-str-replace-editor": "workspace:^",
|
||||||
|
|||||||
@@ -10,5 +10,9 @@
|
|||||||
"macos-arm64": {
|
"macos-arm64": {
|
||||||
"tag": "macosx_14_0_arm64",
|
"tag": "macosx_14_0_arm64",
|
||||||
"executable": "deepseek-harness-sdk-runtime-macos-arm64"
|
"executable": "deepseek-harness-sdk-runtime-macos-arm64"
|
||||||
|
},
|
||||||
|
"win-x64": {
|
||||||
|
"tag": "win_amd64",
|
||||||
|
"executable": "deepseek-harness-sdk-runtime-win-x64.exe"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ Two runtime carriers coexist under ``runtime/``, both injected by the repo's
|
|||||||
``scripts/build-exe-for-python-sdk.ts`` build (neither is checked into git):
|
``scripts/build-exe-for-python-sdk.ts`` build (neither is checked into git):
|
||||||
|
|
||||||
- **exe (production)**: single-file Node executables named
|
- **exe (production)**: single-file Node executables named
|
||||||
``deepseek-harness-sdk-runtime-<platform>-<arch>`` (platform in {linux, macos}, arch in
|
``deepseek-harness-sdk-runtime-<platform>-<arch>`` for Linux/macOS and an
|
||||||
{x64, arm64}) with a sibling ``-rg`` executable; macOS also uses a sibling
|
``.exe`` counterpart for Windows. Each has a sibling ripgrep executable;
|
||||||
``-spawn-helper``. The target machine needs no Node installation.
|
macOS also uses a sibling ``-spawn-helper``. The target machine needs no
|
||||||
|
Node installation.
|
||||||
- **node (dev-only)**: the full deploy closure under ``runtime/node/``
|
- **node (dev-only)**: the full deploy closure under ``runtime/node/``
|
||||||
(``package.json`` + ``node_modules/``), executed as ``node
|
(``package.json`` + ``node_modules/``), executed as ``node
|
||||||
runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`` on a
|
runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`` on a
|
||||||
@@ -30,7 +31,7 @@ PACKAGE_METADATA_FILENAME = "deepseek-harness-runtime.json"
|
|||||||
|
|
||||||
RUNTIME_MODE_ENV_VAR = "DSH_RUNTIME_MODE"
|
RUNTIME_MODE_ENV_VAR = "DSH_RUNTIME_MODE"
|
||||||
|
|
||||||
_PLATFORM_TAGS = {"linux": "linux", "darwin": "macos"}
|
_PLATFORM_TAGS = {"linux": "linux", "darwin": "macos", "win32": "win"}
|
||||||
_ARCH_TAGS = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm64"}
|
_ARCH_TAGS = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm64"}
|
||||||
|
|
||||||
_EXE_ACQUISITION_HINT = (
|
_EXE_ACQUISITION_HINT = (
|
||||||
@@ -62,13 +63,18 @@ def bundled_runtime_path() -> Path:
|
|||||||
touching callers).
|
touching callers).
|
||||||
"""
|
"""
|
||||||
tag = _current_platform_tag()
|
tag = _current_platform_tag()
|
||||||
path = bundled_package_dir() / "runtime" / f"deepseek-harness-sdk-runtime-{tag}"
|
extension = ".exe" if tag.startswith("win-") else ""
|
||||||
|
path = bundled_package_dir() / "runtime" / f"deepseek-harness-sdk-runtime-{tag}{extension}"
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. "
|
f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. "
|
||||||
+ _EXE_ACQUISITION_HINT
|
+ _EXE_ACQUISITION_HINT
|
||||||
)
|
)
|
||||||
ripgrep = Path(f"{path}-rg")
|
ripgrep = (
|
||||||
|
path.with_name(f"{path.stem}-rg.exe")
|
||||||
|
if tag.startswith("win-")
|
||||||
|
else Path(f"{path}-rg")
|
||||||
|
)
|
||||||
if not ripgrep.is_file():
|
if not ripgrep.is_file():
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
f"deepseek-harness-runtime-bin is missing the ripgrep sidecar at {ripgrep}. "
|
f"deepseek-harness-runtime-bin is missing the ripgrep sidecar at {ripgrep}. "
|
||||||
@@ -110,11 +116,11 @@ def resolve_bundled_launch_args(mode: str | None = None) -> tuple[str, ...]:
|
|||||||
def _current_platform_tag() -> str:
|
def _current_platform_tag() -> str:
|
||||||
plat = _PLATFORM_TAGS.get(sys.platform)
|
plat = _PLATFORM_TAGS.get(sys.platform)
|
||||||
arch = _ARCH_TAGS.get(platform.machine().lower())
|
arch = _ARCH_TAGS.get(platform.machine().lower())
|
||||||
if plat is None or arch is None:
|
if plat is None or arch is None or (plat == "win" and arch != "x64"):
|
||||||
raise FileNotFoundError(
|
raise FileNotFoundError(
|
||||||
"no bundled DeepSeek Harness SDK runtime exists for this platform "
|
"no bundled DeepSeek Harness SDK runtime exists for this platform "
|
||||||
f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: "
|
f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: "
|
||||||
"linux/macos on x64/arm64. " + _EXE_ACQUISITION_HINT
|
"Linux x64/arm64, macOS arm64, and Windows x64. " + _EXE_ACQUISITION_HINT
|
||||||
)
|
)
|
||||||
return f"{plat}-{arch}"
|
return f"{plat}-{arch}"
|
||||||
|
|
||||||
|
|||||||
@@ -93,11 +93,14 @@ class HarnessClient:
|
|||||||
self._start_stderr_thread()
|
self._start_stderr_thread()
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
|
"""Close the runtime after a bounded opportunity to flush durable state."""
|
||||||
proc = self._proc
|
proc = self._proc
|
||||||
if proc is None:
|
if proc is None:
|
||||||
return
|
return
|
||||||
|
shutdown_completed = False
|
||||||
try:
|
try:
|
||||||
self.request("shutdown", None, response_model=_ShutdownResponse, timeout_seconds=self.config.shutdown_timeout_seconds)
|
self.request("shutdown", None, response_model=_ShutdownResponse, timeout_seconds=self.config.shutdown_timeout_seconds)
|
||||||
|
shutdown_completed = True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._stderr_lines.append(f"shutdown request failed: {exc}")
|
self._stderr_lines.append(f"shutdown request failed: {exc}")
|
||||||
if proc.stdin:
|
if proc.stdin:
|
||||||
@@ -105,16 +108,22 @@ class HarnessClient:
|
|||||||
proc.stdin.close()
|
proc.stdin.close()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self._stderr_lines.append(f"stdin close failed: {exc}")
|
self._stderr_lines.append(f"stdin close failed: {exc}")
|
||||||
|
if shutdown_completed:
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=self.config.shutdown_timeout_seconds)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
pass
|
||||||
if proc.poll() is None:
|
if proc.poll() is None:
|
||||||
try:
|
try:
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
except ProcessLookupError:
|
except ProcessLookupError:
|
||||||
pass
|
pass
|
||||||
try:
|
if proc.poll() is None:
|
||||||
proc.wait(timeout=self.config.shutdown_timeout_seconds)
|
try:
|
||||||
except subprocess.TimeoutExpired:
|
proc.wait(timeout=self.config.shutdown_timeout_seconds)
|
||||||
proc.kill()
|
except subprocess.TimeoutExpired:
|
||||||
proc.wait()
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
self._proc = None
|
self._proc = None
|
||||||
self._fail_waiters(self._runtime_closed_error("DeepSeek Harness runtime closed"))
|
self._fail_waiters(self._runtime_closed_error("DeepSeek Harness runtime closed"))
|
||||||
if self._reader_thread and self._reader_thread.is_alive():
|
if self._reader_thread and self._reader_thread.is_alive():
|
||||||
|
|||||||
@@ -783,6 +783,43 @@ for line in sys.stdin:
|
|||||||
assert client._proc is None
|
assert client._proc is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_client_close_allows_eof_quiescence_after_shutdown_response(tmp_path: Path) -> None:
|
||||||
|
script = tmp_path / "fake_runtime.py"
|
||||||
|
marker = tmp_path / "quiesced.txt"
|
||||||
|
script.write_text(
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
for line in sys.stdin:
|
||||||
|
msg = json.loads(line)
|
||||||
|
if msg.get("method") == "initialize":
|
||||||
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {"serverInfo": {"name": "fake-dsh"}}}), flush=True)
|
||||||
|
elif msg.get("method") == "shutdown":
|
||||||
|
print(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": {}}), flush=True)
|
||||||
|
|
||||||
|
time.sleep(0.05)
|
||||||
|
Path(os.environ["QUIESCED_MARKER"]).write_text("quiesced")
|
||||||
|
""".strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
client = HarnessClient(
|
||||||
|
HarnessConfig(
|
||||||
|
_launch_args=(sys.executable, str(script)),
|
||||||
|
env={"QUIESCED_MARKER": str(marker)},
|
||||||
|
shutdown_timeout_seconds=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
client.start()
|
||||||
|
client.initialize(provider="deepseek-official", cwd="/workspace", model="dsagent")
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
assert marker.read_text() == "quiesced"
|
||||||
|
|
||||||
|
|
||||||
def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
|
def test_initialize_failure_reaps_started_runtime(tmp_path: Path) -> None:
|
||||||
script = tmp_path / "rejecting_runtime.py"
|
script = tmp_path / "rejecting_runtime.py"
|
||||||
script.write_text(
|
script.write_text(
|
||||||
|
|||||||
@@ -62,6 +62,14 @@ def test_macos_wheel_tag_does_not_claim_unsupported_node_platforms() -> None:
|
|||||||
assert build_python_release.PLATFORMS["macos-arm64"][1] == "deepseek-harness-sdk-runtime-macos-arm64"
|
assert build_python_release.PLATFORMS["macos-arm64"][1] == "deepseek-harness-sdk-runtime-macos-arm64"
|
||||||
|
|
||||||
|
|
||||||
|
def test_windows_wheel_tag_and_payload_are_x64_only() -> None:
|
||||||
|
assert build_python_release.PLATFORMS["win-x64"] == (
|
||||||
|
"win_amd64",
|
||||||
|
"deepseek-harness-sdk-runtime-win-x64.exe",
|
||||||
|
)
|
||||||
|
assert not any(name.startswith("win-") and name != "win-x64" for name in build_python_release.PLATFORMS)
|
||||||
|
|
||||||
|
|
||||||
def test_platform_manifest_rejects_incomplete_entries(tmp_path: Path) -> None:
|
def test_platform_manifest_rejects_incomplete_entries(tmp_path: Path) -> None:
|
||||||
manifest = tmp_path / "platforms.json"
|
manifest = tmp_path / "platforms.json"
|
||||||
manifest.write_text('{"macos-arm64":{"tag":"macosx_14_0_arm64"}}\n')
|
manifest.write_text('{"macos-arm64":{"tag":"macosx_14_0_arm64"}}\n')
|
||||||
@@ -85,7 +93,10 @@ def test_stage_sdk_keeps_distribution_module_and_runtime_pin_distinct(tmp_path:
|
|||||||
assert (destination / "src" / "deepseek_harness" / "__init__.py").is_file()
|
assert (destination / "src" / "deepseek_harness" / "__init__.py").is_file()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(("target", "with_helper"), [("linux-x64", False), ("macos-arm64", True)])
|
@pytest.mark.parametrize(
|
||||||
|
("target", "with_helper"),
|
||||||
|
[("linux-x64", False), ("macos-arm64", True), ("win-x64.exe", False)],
|
||||||
|
)
|
||||||
def test_stage_runtime_copies_platform_payload(
|
def test_stage_runtime_copies_platform_payload(
|
||||||
tmp_path: Path, target: str, with_helper: bool
|
tmp_path: Path, target: str, with_helper: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -93,7 +104,11 @@ def test_stage_runtime_copies_platform_payload(
|
|||||||
executable.write_bytes(b"runtime")
|
executable.write_bytes(b"runtime")
|
||||||
executable.chmod(0o755)
|
executable.chmod(0o755)
|
||||||
expected = {executable.name: b"runtime"}
|
expected = {executable.name: b"runtime"}
|
||||||
ripgrep = Path(f"{executable}-rg")
|
ripgrep = (
|
||||||
|
executable.with_name(f"{executable.stem}-rg.exe")
|
||||||
|
if executable.suffix == ".exe"
|
||||||
|
else Path(f"{executable}-rg")
|
||||||
|
)
|
||||||
ripgrep.write_bytes(b"ripgrep")
|
ripgrep.write_bytes(b"ripgrep")
|
||||||
ripgrep.chmod(0o755)
|
ripgrep.chmod(0o755)
|
||||||
expected[ripgrep.name] = b"ripgrep"
|
expected[ripgrep.name] = b"ripgrep"
|
||||||
|
|||||||
@@ -55,6 +55,30 @@ def test_runtime_requires_spawn_helper_only_on_macos(
|
|||||||
assert runtime.bundled_runtime_path() == linux
|
assert runtime.bundled_runtime_path() == linux
|
||||||
|
|
||||||
|
|
||||||
|
def test_windows_runtime_uses_exe_payload_and_exe_sidecar(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
runtime_dir = tmp_path / "runtime"
|
||||||
|
runtime_dir.mkdir()
|
||||||
|
executable = runtime_dir / "deepseek-harness-sdk-runtime-win-x64.exe"
|
||||||
|
executable.touch()
|
||||||
|
(runtime_dir / "deepseek-harness-sdk-runtime-win-x64-rg.exe").touch()
|
||||||
|
monkeypatch.setattr(runtime, "bundled_package_dir", lambda: tmp_path)
|
||||||
|
monkeypatch.setattr(runtime, "_current_platform_tag", lambda: "win-x64")
|
||||||
|
|
||||||
|
assert runtime.bundled_runtime_path() == executable
|
||||||
|
|
||||||
|
|
||||||
|
def test_current_platform_supports_windows_x64_only(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.setattr(runtime.sys, "platform", "win32")
|
||||||
|
monkeypatch.setattr(runtime.platform, "machine", lambda: "AMD64")
|
||||||
|
assert runtime._current_platform_tag() == "win-x64"
|
||||||
|
|
||||||
|
monkeypatch.setattr(runtime.platform, "machine", lambda: "ARM64")
|
||||||
|
with pytest.raises(FileNotFoundError, match="Windows x64"):
|
||||||
|
runtime._current_platform_tag()
|
||||||
|
|
||||||
|
|
||||||
def test_runtime_requires_ripgrep_sidecar(
|
def test_runtime_requires_ripgrep_sidecar(
|
||||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
import { dirname, join } from 'node:path'
|
import { dirname, join } from 'node:path'
|
||||||
import { afterEach, describe, expect, it } from 'vitest'
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
import { resolveLinuxNodePtyAddon } from './build-exe-for-python-sdk-native-pty.ts'
|
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
|
||||||
|
|
||||||
const roots: string[] = []
|
const roots: string[] = []
|
||||||
|
|
||||||
@@ -35,6 +35,24 @@ describe('resolveLinuxNodePtyAddon', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('resolveWindowsNodePtyAddons', () => {
|
||||||
|
it('requires both ConPTY addons from the x64 prebuild', () => {
|
||||||
|
const root = temporaryPackage()
|
||||||
|
const conpty = createAddon(root, 'prebuilds', 'win32-x64', 'conpty.node')
|
||||||
|
const consoleList = createAddon(root, 'prebuilds', 'win32-x64', 'conpty_console_list.node')
|
||||||
|
|
||||||
|
expect(resolveWindowsNodePtyAddons(root, 'x64')).toEqual([conpty, consoleList])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('names every missing Windows addon', () => {
|
||||||
|
const root = temporaryPackage()
|
||||||
|
|
||||||
|
expect(() => resolveWindowsNodePtyAddons(root, 'x64')).toThrow(
|
||||||
|
`Windows node-pty addons are missing: ${join(root, 'prebuilds', 'win32-x64', 'conpty.node')}, ${join(root, 'prebuilds', 'win32-x64', 'conpty_console_list.node')}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
function temporaryPackage(): string {
|
function temporaryPackage(): string {
|
||||||
const root = mkdtempSync(join(tmpdir(), 'dsh-node-pty-addon-'))
|
const root = mkdtempSync(join(tmpdir(), 'dsh-node-pty-addon-'))
|
||||||
roots.push(root)
|
roots.push(root)
|
||||||
|
|||||||
@@ -21,3 +21,25 @@ export function resolveLinuxNodePtyAddon(
|
|||||||
`build-exe-for-python-sdk: node-pty addon is absent from both ${built} and ${prebuilt}.`,
|
`build-exe-for-python-sdk: node-pty addon is absent from both ${built} and ${prebuilt}.`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Require both node-pty addons used by the Windows ConPTY backend.
|
||||||
|
* @param packageDirectory - staged node-pty package directory.
|
||||||
|
* @param arch - Windows target architecture.
|
||||||
|
* @returns the existing addon paths in load order.
|
||||||
|
*/
|
||||||
|
export function resolveWindowsNodePtyAddons(
|
||||||
|
packageDirectory: string,
|
||||||
|
arch: 'x64',
|
||||||
|
): string[] {
|
||||||
|
const directory = join(packageDirectory, 'prebuilds', `win32-${arch}`)
|
||||||
|
const addons = [
|
||||||
|
join(directory, 'conpty.node'),
|
||||||
|
join(directory, 'conpty_console_list.node'),
|
||||||
|
]
|
||||||
|
const missing = addons.filter(path => !existsSync(path))
|
||||||
|
if (missing.length > 0) {
|
||||||
|
throw new Error(`build-exe-for-python-sdk: Windows node-pty addons are missing: ${missing.join(', ')}.`)
|
||||||
|
}
|
||||||
|
return addons
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { spawnSync } from 'node:child_process'
|
||||||
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||||
|
import { tmpdir } from 'node:os'
|
||||||
|
import { dirname, join, resolve } from 'node:path'
|
||||||
|
import { afterEach, describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
|
const root = resolve(import.meta.dirname, '..')
|
||||||
|
const script = resolve(root, 'scripts/build-exe-for-python-sdk.ts')
|
||||||
|
const temporaryDirectories: string[] = []
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
for (const directory of temporaryDirectories.splice(0)) {
|
||||||
|
rmSync(directory, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function run(env: NodeJS.ProcessEnv, ...args: string[]) {
|
||||||
|
return spawnSync(process.execPath, ['--import', 'tsx/esm', script, ...args], {
|
||||||
|
cwd: root,
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: isolatedPnpmEnvironment(env),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Python runtime executable builder CLI', () => {
|
||||||
|
it('runs pnpm through its JavaScript entrypoint without a command shell', () => {
|
||||||
|
const result = run(
|
||||||
|
{ npm_execpath: 'C:\\tools\\pnpm.cjs' },
|
||||||
|
'--skip-build',
|
||||||
|
'--dry-run',
|
||||||
|
'--targets=node24-macos-arm64',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.status).toBe(0)
|
||||||
|
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs run verify-runtime-closure`)
|
||||||
|
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs --filter dsh-python-runtime-closure deploy`)
|
||||||
|
expect(result.stdout).toContain(`${process.execPath} C:\\tools\\pnpm.cjs dlx @yao-pkg/pkg@6.21.0`)
|
||||||
|
expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resolves the pnpm package behind a Windows command shim', () => {
|
||||||
|
const setup = mkdtempSync(join(tmpdir(), 'dsh-pnpm-home-'))
|
||||||
|
temporaryDirectories.push(setup)
|
||||||
|
const home = join(setup, 'node_modules', '.bin')
|
||||||
|
const entrypoint = join(setup, 'node_modules', 'pnpm', 'bin', 'pnpm.mjs')
|
||||||
|
mkdirSync(home, { recursive: true })
|
||||||
|
mkdirSync(dirname(entrypoint), { recursive: true })
|
||||||
|
writeFileSync(entrypoint, '')
|
||||||
|
|
||||||
|
const result = run(
|
||||||
|
{ npm_execpath: 'C:\\tools\\pnpm.cmd', PNPM_HOME: home },
|
||||||
|
'--skip-build',
|
||||||
|
'--dry-run',
|
||||||
|
'--targets=node24-macos-arm64',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.status).toBe(0)
|
||||||
|
expect(result.stdout).toContain(`${process.execPath} ${entrypoint} run verify-runtime-closure`)
|
||||||
|
expect(result.stdout).not.toMatch(/pnpm\.cmd/i)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a Windows arm64 product before any build step', () => {
|
||||||
|
const result = run(
|
||||||
|
{ npm_execpath: 'C:\\tools\\pnpm.cjs' },
|
||||||
|
'--skip-build',
|
||||||
|
'--dry-run',
|
||||||
|
'--targets=node24-win-arm64',
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.status).not.toBe(0)
|
||||||
|
expect(result.stderr).toContain('Windows supports x64 only')
|
||||||
|
expect(result.stdout).toBe('')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function isolatedPnpmEnvironment(overrides: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
|
const environment = Object.fromEntries(
|
||||||
|
Object.entries(process.env).filter(([key]) => !['npm_execpath', 'pnpm_home'].includes(key.toLowerCase())),
|
||||||
|
)
|
||||||
|
return { ...environment, ...overrides }
|
||||||
|
}
|
||||||
@@ -9,9 +9,9 @@
|
|||||||
import { spawn } from 'node:child_process'
|
import { spawn } from 'node:child_process'
|
||||||
import { existsSync, statSync } from 'node:fs'
|
import { existsSync, statSync } from 'node:fs'
|
||||||
import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
|
import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
|
||||||
import { basename, dirname, join, resolve, sep } from 'node:path'
|
import { basename, dirname, extname, join, resolve, sep } from 'node:path'
|
||||||
import { parseArgs } from 'node:util'
|
import { parseArgs } from 'node:util'
|
||||||
import { resolveLinuxNodePtyAddon } from './build-exe-for-python-sdk-native-pty.ts'
|
import { resolveLinuxNodePtyAddon, resolveWindowsNodePtyAddons } from './build-exe-for-python-sdk-native-pty.ts'
|
||||||
|
|
||||||
const root = resolve(import.meta.dirname, '..')
|
const root = resolve(import.meta.dirname, '..')
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ const ASSET_GLOBS = [
|
|||||||
'node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*',
|
'node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*',
|
||||||
]
|
]
|
||||||
|
|
||||||
const PLATFORMS = ['linux', 'macos'] as const
|
const PLATFORMS = ['linux', 'macos', 'win'] as const
|
||||||
const ARCHES = ['x64', 'arm64'] as const
|
const ARCHES = ['x64', 'arm64'] as const
|
||||||
type Platform = (typeof PLATFORMS)[number]
|
type Platform = (typeof PLATFORMS)[number]
|
||||||
type Arch = (typeof ARCHES)[number]
|
type Arch = (typeof ARCHES)[number]
|
||||||
@@ -83,10 +83,7 @@ class Target {
|
|||||||
private constructor(
|
private constructor(
|
||||||
/** pkg Node range (`node<major>`). */
|
/** pkg Node range (`node<major>`). */
|
||||||
readonly nodeRange: string,
|
readonly nodeRange: string,
|
||||||
/**
|
/** pkg platform tag. */
|
||||||
* pkg platform tag. Windows is a documented non-goal
|
|
||||||
* (.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md).
|
|
||||||
*/
|
|
||||||
readonly platform: Platform,
|
readonly platform: Platform,
|
||||||
/** pkg CPU tag. */
|
/** pkg CPU tag. */
|
||||||
readonly arch: Arch,
|
readonly arch: Arch,
|
||||||
@@ -117,6 +114,9 @@ class Target {
|
|||||||
if (!isArch(arch)) {
|
if (!isArch(arch)) {
|
||||||
throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: arch must be one of ${ARCHES.join(', ')}, got ${JSON.stringify(arch)}.`)
|
throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: arch must be one of ${ARCHES.join(', ')}, got ${JSON.stringify(arch)}.`)
|
||||||
}
|
}
|
||||||
|
if (platform === 'win' && arch !== 'x64') {
|
||||||
|
throw new Error(`build-exe-for-python-sdk: target ${JSON.stringify(spec)}: Windows supports x64 only.`)
|
||||||
|
}
|
||||||
return new Target(nodeRange, platform, arch)
|
return new Target(nodeRange, platform, arch)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +125,13 @@ class Target {
|
|||||||
* @returns the host target; throws on an unsupported host platform or arch.
|
* @returns the host target; throws on an unsupported host platform or arch.
|
||||||
*/
|
*/
|
||||||
static host(): Target {
|
static host(): Target {
|
||||||
const platform = process.platform === 'darwin' ? 'macos' : process.platform === 'linux' ? 'linux' : undefined
|
const platform = process.platform === 'darwin'
|
||||||
|
? 'macos'
|
||||||
|
: process.platform === 'linux'
|
||||||
|
? 'linux'
|
||||||
|
: process.platform === 'win32'
|
||||||
|
? 'win'
|
||||||
|
: undefined
|
||||||
if (platform === undefined) {
|
if (platform === undefined) {
|
||||||
throw new Error(`build-exe-for-python-sdk: unsupported host platform ${process.platform}; pass --targets explicitly.`)
|
throw new Error(`build-exe-for-python-sdk: unsupported host platform ${process.platform}; pass --targets explicitly.`)
|
||||||
}
|
}
|
||||||
@@ -133,6 +139,9 @@ class Target {
|
|||||||
if (arch === undefined) {
|
if (arch === undefined) {
|
||||||
throw new Error(`build-exe-for-python-sdk: unsupported host arch ${process.arch}; pass --targets explicitly.`)
|
throw new Error(`build-exe-for-python-sdk: unsupported host arch ${process.arch}; pass --targets explicitly.`)
|
||||||
}
|
}
|
||||||
|
if (platform === 'win' && arch !== 'x64') {
|
||||||
|
throw new Error('build-exe-for-python-sdk: Windows supports x64 only; use an x64 Node process.')
|
||||||
|
}
|
||||||
return new Target(DEFAULT_NODE_RANGE, platform, arch)
|
return new Target(DEFAULT_NODE_RANGE, platform, arch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +209,7 @@ class BuildCli {
|
|||||||
return [
|
return [
|
||||||
'Usage: pnpm exec tsx scripts/build-exe-for-python-sdk.ts [flags]',
|
'Usage: pnpm exec tsx scripts/build-exe-for-python-sdk.ts [flags]',
|
||||||
'',
|
'',
|
||||||
' --targets=<t1,t2,...> pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64.',
|
' --targets=<t1,t2,...> pkg targets, e.g. node24-linux-x64,node24-linux-arm64,node24-macos-arm64,node24-win-x64.',
|
||||||
' Default: the host platform only (on node24).',
|
' Default: the host platform only (on node24).',
|
||||||
' --skip-build skip `pnpm run build` (lib/ artifacts must already exist).',
|
' --skip-build skip `pnpm run build` (lib/ artifacts must already exist).',
|
||||||
' --dry-run print every command and config patch without executing.',
|
' --dry-run print every command and config patch without executing.',
|
||||||
@@ -212,8 +221,27 @@ class BuildCli {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function pnpmBin(): string {
|
function pnpmInvocation(args: string[]): [command: string, args: string[]] {
|
||||||
return process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'
|
const entrypoint = process.env.npm_execpath?.trim()
|
||||||
|
if (entrypoint !== undefined && entrypoint !== '') {
|
||||||
|
const extension = extname(entrypoint).toLowerCase()
|
||||||
|
if (extension === '.js' || extension === '.cjs' || extension === '.mjs') {
|
||||||
|
return [process.execPath, [entrypoint, ...args]]
|
||||||
|
}
|
||||||
|
if (extension !== '.cmd') return [entrypoint, args]
|
||||||
|
}
|
||||||
|
const home = process.env.PNPM_HOME?.trim()
|
||||||
|
if (home !== undefined && home !== '') {
|
||||||
|
const packageBin = resolve(home, '..', 'pnpm', 'bin')
|
||||||
|
for (const filename of ['pnpm.mjs', 'pnpm.cjs']) {
|
||||||
|
const candidate = resolve(packageBin, filename)
|
||||||
|
if (existsSync(candidate)) return [process.execPath, [candidate, ...args]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
throw new Error('build-exe-for-python-sdk: pnpm must expose a JavaScript entrypoint through npm_execpath or PNPM_HOME on Windows.')
|
||||||
|
}
|
||||||
|
return ['pnpm', args]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -241,7 +269,7 @@ class SingleExeBuild {
|
|||||||
|
|
||||||
/** Verify the closure before compiling or packaging. */
|
/** Verify the closure before compiling or packaging. */
|
||||||
async verifyClosure(): Promise<void> {
|
async verifyClosure(): Promise<void> {
|
||||||
await this.run('runtime dependency closure', pnpmBin(), ['run', 'verify-runtime-closure'])
|
await this.runPnpm('runtime dependency closure', ['run', 'verify-runtime-closure'])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build all package artifacts unless `--skip-build` was passed. */
|
/** Build all package artifacts unless `--skip-build` was passed. */
|
||||||
@@ -250,7 +278,7 @@ class SingleExeBuild {
|
|||||||
console.log('build-exe-for-python-sdk: skipping pnpm run build (--skip-build)')
|
console.log('build-exe-for-python-sdk: skipping pnpm run build (--skip-build)')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await this.run('build', pnpmBin(), ['run', 'build'])
|
await this.runPnpm('build', ['run', 'build'])
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear and deploy the runtime closure into the node carrier. */
|
/** Clear and deploy the runtime closure into the node carrier. */
|
||||||
@@ -260,7 +288,7 @@ class SingleExeBuild {
|
|||||||
}
|
}
|
||||||
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${this.staging}`)
|
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${this.staging}`)
|
||||||
else await rm(this.staging, { recursive: true, force: true })
|
else await rm(this.staging, { recursive: true, force: true })
|
||||||
await this.run('deploy', pnpmBin(), [
|
await this.runPnpm('deploy', [
|
||||||
'--filter',
|
'--filter',
|
||||||
DEPLOY_ROOT_PACKAGE,
|
DEPLOY_ROOT_PACKAGE,
|
||||||
'deploy',
|
'deploy',
|
||||||
@@ -393,10 +421,11 @@ class SingleExeBuild {
|
|||||||
* @returns the executable and ripgrep sidecar paths, plus the macOS spawn helper path when required.
|
* @returns the executable and ripgrep sidecar paths, plus the macOS spawn helper path when required.
|
||||||
*/
|
*/
|
||||||
async pack(target: Target): Promise<string[]> {
|
async pack(target: Target): Promise<string[]> {
|
||||||
const product = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
|
const productBase = join(this.outDir, `${OUTPUT_BASENAME}-${target.platform}-${target.arch}`)
|
||||||
|
const product = target.platform === 'win' ? `${productBase}.exe` : productBase
|
||||||
await this.prepareNativePty(target)
|
await this.prepareNativePty(target)
|
||||||
if (!this.cli.dryRun) await mkdir(this.outDir, { recursive: true })
|
if (!this.cli.dryRun) await mkdir(this.outDir, { recursive: true })
|
||||||
await this.run(`pkg ${target.spec}`, pnpmBin(), [
|
await this.runPnpm(`pkg ${target.spec}`, [
|
||||||
'dlx',
|
'dlx',
|
||||||
PKG_SPEC,
|
PKG_SPEC,
|
||||||
this.staging,
|
this.staging,
|
||||||
@@ -424,16 +453,19 @@ class SingleExeBuild {
|
|||||||
|
|
||||||
/** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */
|
/** Copy the target ripgrep binary beside the executable so Node can spawn it outside pkg's virtual filesystem. */
|
||||||
private async copyRipgrepSidecar(target: Target, product: string): Promise<string> {
|
private async copyRipgrepSidecar(target: Target, product: string): Promise<string> {
|
||||||
const platform = target.platform === 'macos' ? 'darwin' : target.platform
|
const platform = target.platform === 'macos' ? 'darwin' : target.platform === 'win' ? 'win32' : target.platform
|
||||||
|
const executable = target.platform === 'win' ? 'rg.exe' : 'rg'
|
||||||
const source = join(
|
const source = join(
|
||||||
this.staging,
|
this.staging,
|
||||||
'node_modules',
|
'node_modules',
|
||||||
'@vscode',
|
'@vscode',
|
||||||
`ripgrep-${platform}-${target.arch}`,
|
`ripgrep-${platform}-${target.arch}`,
|
||||||
'bin',
|
'bin',
|
||||||
'rg',
|
executable,
|
||||||
)
|
)
|
||||||
const destination = `${product}-rg`
|
const destination = target.platform === 'win'
|
||||||
|
? `${product.slice(0, -'.exe'.length)}-rg.exe`
|
||||||
|
: `${product}-rg`
|
||||||
if (this.cli.dryRun) {
|
if (this.cli.dryRun) {
|
||||||
console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
|
console.log(`build-exe-for-python-sdk: [dry-run] cp ${source} ${destination}`)
|
||||||
return destination
|
return destination
|
||||||
@@ -455,7 +487,6 @@ class SingleExeBuild {
|
|||||||
const stagedBuild = join(this.staging, 'node_modules', 'node-pty', 'build')
|
const stagedBuild = join(this.staging, 'node_modules', 'node-pty', 'build')
|
||||||
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${stagedBuild}`)
|
if (this.cli.dryRun) console.log(`build-exe-for-python-sdk: [dry-run] rm -rf ${stagedBuild}`)
|
||||||
else await rm(stagedBuild, { recursive: true, force: true })
|
else await rm(stagedBuild, { recursive: true, force: true })
|
||||||
if (target.platform !== 'linux') return
|
|
||||||
const packageDirectory = join(
|
const packageDirectory = join(
|
||||||
root,
|
root,
|
||||||
'packages',
|
'packages',
|
||||||
@@ -464,6 +495,21 @@ class SingleExeBuild {
|
|||||||
'node_modules',
|
'node_modules',
|
||||||
'node-pty',
|
'node-pty',
|
||||||
)
|
)
|
||||||
|
if (target.platform === 'win') {
|
||||||
|
if (target.arch !== 'x64') {
|
||||||
|
throw new Error('build-exe-for-python-sdk: Windows supports x64 only.')
|
||||||
|
}
|
||||||
|
const host = Target.host()
|
||||||
|
if (target.platform !== host.platform || target.arch !== host.arch) {
|
||||||
|
throw new Error(
|
||||||
|
'build-exe-for-python-sdk: build the Windows runtime under x64 Node on its target host; '
|
||||||
|
+ `target ${target.platform}-${target.arch} does not match host ${host.platform}-${host.arch}.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
resolveWindowsNodePtyAddons(join(this.staging, 'node_modules', 'node-pty'), target.arch)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (target.platform !== 'linux') return
|
||||||
const destination = join(stagedBuild, 'Release', 'pty.node')
|
const destination = join(stagedBuild, 'Release', 'pty.node')
|
||||||
const source = resolveLinuxNodePtyAddon(packageDirectory, target.arch)
|
const source = resolveLinuxNodePtyAddon(packageDirectory, target.arch)
|
||||||
if (this.cli.dryRun) {
|
if (this.cli.dryRun) {
|
||||||
@@ -553,6 +599,12 @@ class SingleExeBuild {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Run pnpm through its JavaScript entrypoint when the caller supplies one. */
|
||||||
|
private async runPnpm(label: string, args: string[]): Promise<void> {
|
||||||
|
const [command, invocationArgs] = pnpmInvocation(args)
|
||||||
|
await this.run(label, command, invocationArgs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main(): Promise<void> {
|
async function main(): Promise<void> {
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ def load_platforms(path: Path = PLATFORM_MANIFEST) -> dict[str, tuple[str, str]]
|
|||||||
PLATFORMS = load_platforms()
|
PLATFORMS = load_platforms()
|
||||||
|
|
||||||
|
|
||||||
def runtime_suffixes(executable_name: str) -> tuple[str, ...]:
|
def runtime_filenames(executable_name: str) -> tuple[str, ...]:
|
||||||
suffixes = ("", "-rg")
|
"""Return the exact platform payload names for one runtime executable."""
|
||||||
return (*suffixes, "-spawn-helper") if "-macos-" in executable_name else suffixes
|
if executable_name.endswith(".exe"):
|
||||||
|
return (executable_name, f"{executable_name.removesuffix('.exe')}-rg.exe")
|
||||||
|
names = (executable_name, f"{executable_name}-rg")
|
||||||
|
return (*names, f"{executable_name}-spawn-helper") if "-macos-" in executable_name else names
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
@@ -210,8 +213,9 @@ def stage_runtime(destination: Path, version: str, executable: Path, executable_
|
|||||||
rewrite_version(destination / "pyproject.toml", version)
|
rewrite_version(destination / "pyproject.toml", version)
|
||||||
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
|
runtime_dir = destination / "src" / "deepseek_harness_runtime" / "runtime"
|
||||||
runtime_dir.mkdir(parents=True, exist_ok=True)
|
runtime_dir.mkdir(parents=True, exist_ok=True)
|
||||||
for suffix in runtime_suffixes(executable_name):
|
source_directory = executable.parent
|
||||||
shutil.copy2(Path(f"{executable}{suffix}"), runtime_dir / f"{executable_name}{suffix}")
|
for filename in runtime_filenames(executable_name):
|
||||||
|
shutil.copy2(source_directory / filename, runtime_dir / filename)
|
||||||
|
|
||||||
|
|
||||||
def verify_wheel(
|
def verify_wheel(
|
||||||
@@ -250,13 +254,13 @@ def verify_wheel(
|
|||||||
]
|
]
|
||||||
if package == "runtime":
|
if package == "runtime":
|
||||||
assert platform is not None
|
assert platform is not None
|
||||||
expected_files = [f"{platform[1]}{suffix}" for suffix in runtime_suffixes(platform[1])]
|
expected_files = sorted(runtime_filenames(platform[1]))
|
||||||
found_files = sorted(Path(name).name for name in runtime_files)
|
found_files = sorted(Path(name).name for name in runtime_files)
|
||||||
if found_files != expected_files:
|
if found_files != expected_files:
|
||||||
raise RuntimeError(f"{wheel} runtime payload must be {expected_files}, found {found_files}")
|
raise RuntimeError(f"{wheel} runtime payload must be {expected_files}, found {found_files}")
|
||||||
for runtime_file in runtime_files:
|
for runtime_file in runtime_files:
|
||||||
mode = archive.getinfo(runtime_file).external_attr >> 16
|
mode = archive.getinfo(runtime_file).external_attr >> 16
|
||||||
if mode & stat.S_IXUSR == 0:
|
if platform[0] != "win_amd64" and mode & stat.S_IXUSR == 0:
|
||||||
raise RuntimeError(f"{wheel} runtime executable lost its executable bit: {runtime_file}")
|
raise RuntimeError(f"{wheel} runtime executable lost its executable bit: {runtime_file}")
|
||||||
elif runtime_files:
|
elif runtime_files:
|
||||||
raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}")
|
raise RuntimeError(f"SDK wheel unexpectedly contains runtime executables: {runtime_files}")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const platforms = {
|
|||||||
'linux-x64': { tag: 'manylinux_2_28_x86_64', executable: 'runtime-linux-x64' },
|
'linux-x64': { tag: 'manylinux_2_28_x86_64', executable: 'runtime-linux-x64' },
|
||||||
'linux-arm64': { tag: 'manylinux_2_28_aarch64', executable: 'runtime-linux-arm64' },
|
'linux-arm64': { tag: 'manylinux_2_28_aarch64', executable: 'runtime-linux-arm64' },
|
||||||
'macos-arm64': { tag: 'macosx_14_0_arm64', executable: 'runtime-macos-arm64' },
|
'macos-arm64': { tag: 'macosx_14_0_arm64', executable: 'runtime-macos-arm64' },
|
||||||
|
'win-x64': { tag: 'win_amd64', executable: 'runtime-win-x64.exe' },
|
||||||
}
|
}
|
||||||
|
|
||||||
function workspace(root: string, name: string, manifest: Record<string, unknown>): void {
|
function workspace(root: string, name: string, manifest: Record<string, unknown>): void {
|
||||||
@@ -35,7 +36,7 @@ afterEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('verifyRuntimeClosure', () => {
|
describe('verifyRuntimeClosure', () => {
|
||||||
it('requires only plugins active for a Linux or macOS target', async () => {
|
it('requires only plugins active for each published target', async () => {
|
||||||
const root = fixture({
|
const root = fixture({
|
||||||
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/shared': 'workspace:^' } },
|
'python/sdk-runtime/package.json': { name: 'runtime', dependencies: { '@scope/shared': 'workspace:^' } },
|
||||||
'python/sdk-runtime/platforms.json': platforms,
|
'python/sdk-runtime/platforms.json': platforms,
|
||||||
@@ -52,6 +53,9 @@ describe('verifyRuntimeClosure', () => {
|
|||||||
- id: macos
|
- id: macos
|
||||||
name: '@scope/macos'
|
name: '@scope/macos'
|
||||||
disabled: !!js process.platform !== 'darwin'
|
disabled: !!js process.platform !== 'darwin'
|
||||||
|
- id: windows
|
||||||
|
name: '@scope/windows'
|
||||||
|
disabled: !!js process.platform !== 'win32'
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,6 +65,7 @@ describe('verifyRuntimeClosure', () => {
|
|||||||
expect(result.failures).toEqual([
|
expect(result.failures).toEqual([
|
||||||
'standard preset -> @scope/linux (linux-arm64, linux-x64)',
|
'standard preset -> @scope/linux (linux-arm64, linux-x64)',
|
||||||
'standard preset -> @scope/macos (macos-arm64)',
|
'standard preset -> @scope/macos (macos-arm64)',
|
||||||
|
'standard preset -> @scope/windows (win-x64)',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -78,7 +83,7 @@ describe('verifyRuntimeClosure', () => {
|
|||||||
const result = await verifyRuntimeClosure(root)
|
const result = await verifyRuntimeClosure(root)
|
||||||
|
|
||||||
expect(result.failures).toEqual([
|
expect(result.failures).toEqual([
|
||||||
'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64)',
|
'standard preset -> @scope/conditional (linux-arm64, linux-x64, macos-arm64, win-x64)',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -112,7 +117,7 @@ describe('verifyRuntimeClosure', () => {
|
|||||||
const result = await verifyRuntimeClosure(root)
|
const result = await verifyRuntimeClosure(root)
|
||||||
|
|
||||||
expect(result.failures).toEqual([
|
expect(result.failures).toEqual([
|
||||||
'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64)',
|
'standard preset -> @scope/plugin [runtime dependency is "1.2.3"; expected workspace:] (linux-arm64, linux-x64, macos-arm64, win-x64)',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ function disabledOnPlatform(value: unknown, processPlatform: string): boolean {
|
|||||||
function processPlatformForTarget(target: string): string {
|
function processPlatformForTarget(target: string): string {
|
||||||
if (target.startsWith('linux-')) return 'linux'
|
if (target.startsWith('linux-')) return 'linux'
|
||||||
if (target.startsWith('macos-')) return 'darwin'
|
if (target.startsWith('macos-')) return 'darwin'
|
||||||
|
if (target.startsWith('win-')) return 'win32'
|
||||||
throw new Error(`verify-runtime-closure: unsupported runtime target ${JSON.stringify(target)}`)
|
throw new Error(`verify-runtime-closure: unsupported runtime target ${JSON.stringify(target)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user