__init__.py 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. """Locate and execute the bundled dsh CLI shipped with the Python SDK runtime.
  2. Two runtime carriers coexist under ``runtime/``, both injected by the repo's
  3. ``scripts/build-exe-for-python-sdk.ts`` build (neither is checked into git):
  4. - **exe (production)**: single-file Node executables named
  5. ``deepseek-harness-sdk-runtime-<platform>-<arch>`` for Linux/macOS and an
  6. ``.exe`` counterpart for Windows. Each has a sibling ripgrep executable;
  7. macOS also uses a sibling ``-spawn-helper``. The target machine needs no
  8. Node installation.
  9. - **node (dev-only)**: the full deploy closure under ``runtime/node/``
  10. (``package.json`` + ``node_modules/``), executed as ``node
  11. runtime/node/node_modules/@deepseek-ai/dsh/lib/bin.js`` on a
  12. system Node >= 22.19. It is the current checkout's source build, never
  13. selected automatically, and excluded from wheel/sdist distributions.
  14. Both carriers execute the same dsh command grammar. The Python SDK selects the
  15. ``sdk`` profile and requires an explicit Harness home; the installed ``dsh``
  16. console command requires ``DSH_HOME`` for the same reason.
  17. """
  18. from __future__ import annotations
  19. import json
  20. import os
  21. import platform
  22. import shutil
  23. import subprocess
  24. import sys
  25. from pathlib import Path
  26. PACKAGE_METADATA_FILENAME = "deepseek-harness-runtime.json"
  27. RUNTIME_MODE_ENV_VAR = "DSH_RUNTIME_MODE"
  28. _PLATFORM_TAGS = {"linux": "linux", "darwin": "macos", "win32": "win"}
  29. _ARCH_TAGS = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm64"}
  30. _EXE_ACQUISITION_HINT = (
  31. "Two ways to get the executable: run `scripts/build-exe-for-python-sdk.ts` (via tsx) in a "
  32. "deepseek-harness checkout, or install the matching `deepseek-harness-runtime-bin` platform "
  33. "wheel retained by the `build-exe-for-python-sdk` CI workflow. For local development "
  34. "against a repo source build, explicitly select the dev-only node carrier with "
  35. f"{RUNTIME_MODE_ENV_VAR}=node (or resolve_bundled_launch_args('node'))."
  36. )
  37. def bundled_package_dir() -> Path:
  38. """Root directory of the installed runtime package data (the directory of this module)."""
  39. root = Path(__file__).resolve().parent
  40. metadata = root / PACKAGE_METADATA_FILENAME
  41. if not metadata.is_file():
  42. raise FileNotFoundError(f"deepseek-harness-runtime-bin is missing {metadata}")
  43. return root
  44. def bundled_runtime_path() -> Path:
  45. """Absolute path of the bundled single-file runtime executable for the current platform.
  46. Raises FileNotFoundError when the platform is unsupported, the executable
  47. has not been placed into this package, the ripgrep or Office sidecar is
  48. missing, or the required macOS spawn helper is missing; the message names
  49. the acquisition routes (acquisition strategy is deliberately separate from
  50. this lookup interface, so an on-demand download can replace it without
  51. touching callers).
  52. """
  53. tag = _current_platform_tag()
  54. extension = ".exe" if tag.startswith("win-") else ""
  55. path = bundled_package_dir() / "runtime" / f"deepseek-harness-sdk-runtime-{tag}{extension}"
  56. if not path.is_file():
  57. raise FileNotFoundError(
  58. f"deepseek-harness-runtime-bin is missing the runtime executable at {path}. "
  59. + _EXE_ACQUISITION_HINT
  60. )
  61. ripgrep = (
  62. path.with_name(f"{path.stem}-rg.exe")
  63. if tag.startswith("win-")
  64. else Path(f"{path}-rg")
  65. )
  66. if not ripgrep.is_file():
  67. raise FileNotFoundError(
  68. f"deepseek-harness-runtime-bin is missing the ripgrep sidecar at {ripgrep}. "
  69. + _EXE_ACQUISITION_HINT
  70. )
  71. if tag.startswith("macos-"):
  72. helper = Path(f"{path}-spawn-helper")
  73. if not helper.is_file():
  74. raise FileNotFoundError(
  75. f"deepseek-harness-runtime-bin is missing the node-pty spawn helper at {helper}. "
  76. + _EXE_ACQUISITION_HINT
  77. )
  78. office = path.with_name(f"{path.name.removesuffix('.exe')}-office")
  79. adapter = office / "node_modules/@deepseek-ai/libreoffice-kit/package.json"
  80. if not adapter.is_file():
  81. raise FileNotFoundError(
  82. f"deepseek-harness-runtime-bin is missing the Office sidecar at {office}. "
  83. + _EXE_ACQUISITION_HINT
  84. )
  85. native = tag.replace("win-", "win32-").replace("macos-", "darwin-")
  86. declared = json.loads(adapter.read_text(encoding="utf-8")).get("optionalDependencies", {})
  87. engine = native if f"@deepseek-ai/libreoffice-kit-{native}" in declared else "wasm"
  88. if not (office / "node_modules" / f"@deepseek-ai/libreoffice-kit-{engine}/prebuilds.json").is_file():
  89. raise FileNotFoundError(
  90. f"deepseek-harness-runtime-bin is missing the Office sidecar engine {engine} at {office}. "
  91. + _EXE_ACQUISITION_HINT
  92. )
  93. return path
  94. def resolve_bundled_launch_args(mode: str | None = None) -> tuple[str, ...]:
  95. """The argv tuple that launches the bundled runtime.
  96. Mode selection: the explicit ``mode`` argument wins, then the
  97. ``DSH_RUNTIME_MODE`` environment variable (``exe`` | ``node``), then
  98. automatic resolution. Automatic resolution finds the production exe ONLY —
  99. the dev-only node carrier must be selected explicitly so a production
  100. deployment can never silently ride on a source build. Returns
  101. ``(exe_path,)`` in exe mode and ``(node_path, bin_js_path)`` in node mode;
  102. raises FileNotFoundError when the selected carrier is unavailable and
  103. ValueError for an unknown mode value.
  104. """
  105. selected = mode if mode is not None else os.environ.get(RUNTIME_MODE_ENV_VAR)
  106. if selected is None or selected == "exe":
  107. return (str(bundled_runtime_path()),)
  108. if selected == "node":
  109. return _node_launch_args()
  110. raise ValueError(
  111. f"unsupported DeepSeek Harness runtime mode {selected!r}: expected 'exe' or 'node' "
  112. f"(explicit argument or ${RUNTIME_MODE_ENV_VAR})"
  113. )
  114. def _current_platform_tag() -> str:
  115. plat = _PLATFORM_TAGS.get(sys.platform)
  116. arch = _ARCH_TAGS.get(platform.machine().lower())
  117. if (
  118. plat is None
  119. or arch is None
  120. or (plat == "win" and arch != "x64")
  121. ):
  122. raise FileNotFoundError(
  123. "no bundled DeepSeek Harness SDK runtime exists for this platform "
  124. f"(sys.platform={sys.platform!r}, machine={platform.machine()!r}); supported: "
  125. "Linux x64/arm64, macOS x64/arm64, and Windows x64. " + _EXE_ACQUISITION_HINT
  126. )
  127. return f"{plat}-{arch}"
  128. def _node_launch_args() -> tuple[str, str]:
  129. node_root = bundled_package_dir() / "runtime" / "node"
  130. bin_js = (
  131. node_root
  132. / "node_modules"
  133. / "@deepseek-ai"
  134. / "dsh"
  135. / "lib"
  136. / "bin.js"
  137. )
  138. if not bin_js.is_file():
  139. raise FileNotFoundError(
  140. f"the dev-only node runtime closure is missing at {node_root} "
  141. f"(no {bin_js}); run `scripts/build-exe-for-python-sdk.ts` in a deepseek-harness "
  142. "checkout, which builds and copies the deploy closure here. The node carrier "
  143. "is for repo-local development only — production uses the single-file exe."
  144. )
  145. node = shutil.which("node")
  146. if node is None:
  147. raise FileNotFoundError(
  148. "the node runtime mode needs a system `node` (>=22.19) on PATH; "
  149. "install Node.js or use the exe mode"
  150. )
  151. return (node, str(bin_js))
  152. def main() -> None:
  153. """Launch the CLI with explicit DSH_HOME; wait on Windows, replace the process on POSIX."""
  154. if not os.environ.get("DSH_HOME", "").strip():
  155. print(
  156. "dsh: the Python runtime command requires an explicit DSH_HOME; "
  157. "it never uses ~/.dsh implicitly",
  158. file=sys.stderr,
  159. )
  160. raise SystemExit(2)
  161. argv = (*resolve_bundled_launch_args(), *sys.argv[1:])
  162. if sys.platform == "win32":
  163. # Windows CRT exec does not replace the process; wait and preserve the runtime status.
  164. raise SystemExit(subprocess.run(argv, env=os.environ).returncode)
  165. os.execvpe(argv[0], argv, os.environ)
  166. __all__ = [
  167. "PACKAGE_METADATA_FILENAME",
  168. "RUNTIME_MODE_ENV_VAR",
  169. "bundled_package_dir",
  170. "bundled_runtime_path",
  171. "main",
  172. "resolve_bundled_launch_args",
  173. ]