Introduce the pm store: a unified, hash-verified package store that
replaces lazy_deps and the old installer's ad-hoc tool downloads.
Store tools are provisioned on PATH (ffmpeg, node/npm via pinned uv),
with a resumable 8-way downloader, verify() returning failure reasons,
and adopt() made EPERM-safe. chromium ships in the payload for every
target. The 3600-line install.sh is replaced by a staged bootstrapper
(heavy deps are pm's job after this); setup-hermes.sh, Dockerfile and
nix pin tables are rewired onto the store. Old install-script tests,
lazy_deps/managed_uv/build_info, and the ps1/bash installer test
batteries are removed with the machinery they tested.
Rebuilt from ethie/pm onto upstream/main (ac6c8028e0) after the
utf-8-sig sweep. 16 hot files (main also churned them) hand-merged:
platform adapters, main.py, electron/main.ts, tui_gateway/server.py,
cua_backend, installer-tests workflow, install.sh (full rewrite),
setup-hermes.sh, plugins doc.
123 lines
3.7 KiB
Python
123 lines
3.7 KiB
Python
"""Extras: the runtime features of the python venv.
|
|
|
|
Feature names ARE pyproject extra names. This table maps each extra to the
|
|
import that proves it (the anchor), so availability is one find_spec — no
|
|
package-manager machinery on the hot path. sync_venv([extra]) makes an extra
|
|
true; pm owns HOW (uv sync inside the venv package).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
|
|
|
|
# extra name -> module that proves it is installed
|
|
ANCHORS: dict[str, str | tuple[str, ...]] = {
|
|
"anthropic": "anthropic",
|
|
"bedrock": "boto3",
|
|
"vertex": "google.auth",
|
|
"azure-identity": "azure.identity",
|
|
"exa": "exa_py",
|
|
"firecrawl": "firecrawl",
|
|
"parallel-web": "parallel",
|
|
"otlp": "opentelemetry.sdk",
|
|
"mistral": "mistralai",
|
|
"edge-tts": "edge_tts",
|
|
"tts-premium": "elevenlabs",
|
|
"voice": "faster_whisper",
|
|
"stt-whisper": "faster_whisper",
|
|
"audio-io": ("sounddevice", "numpy"),
|
|
"silk": "pilk",
|
|
"wake": "openwakeword",
|
|
"wake-openwakeword": "openwakeword",
|
|
"wake-sherpa": "sherpa_onnx",
|
|
"wake-porcupine": "pvporcupine",
|
|
"wake-tflite": "ai_edge_litert",
|
|
"fal": "fal_client",
|
|
"honcho": "honcho",
|
|
"hindsight": "hindsight",
|
|
"supermemory": "supermemory",
|
|
"mem0": "mem0",
|
|
"messaging": "telegram",
|
|
"telegram": "telegram",
|
|
"discord": "discord",
|
|
"slack": "slack_bolt",
|
|
"matrix": ("mautrix", "asyncpg", "aiosqlite", "markdown", "aiohttp_socks"),
|
|
"dingtalk": "dingtalk_stream",
|
|
"feishu": "lark_oapi",
|
|
"wecom": "defusedxml",
|
|
"teams": "microsoft.teams.apps",
|
|
"modal": "modal",
|
|
"daytona": "daytona",
|
|
"vercel": "vercel",
|
|
"google": "googleapiclient",
|
|
"google-chat": "google.cloud.pubsub_v1",
|
|
"youtube": "youtube_transcript_api",
|
|
"acp": "acp",
|
|
"web": "fastapi",
|
|
"doc-extract": "anydoc",
|
|
"computer-use": "mcp",
|
|
"trace-upload": "huggingface_hub",
|
|
}
|
|
|
|
|
|
def _anchors(extra: str) -> tuple[str, ...]:
|
|
got = ANCHORS.get(extra, extra.replace("-", "_"))
|
|
return got if isinstance(got, tuple) else (got,)
|
|
|
|
|
|
def _importable(anchor: str) -> bool:
|
|
"""An anchor already present in sys.modules counts even without a
|
|
findable spec — tests fake SDKs by inserting modules there, and the
|
|
caller's import right after this check resolves the same way."""
|
|
import sys
|
|
|
|
if anchor in sys.modules:
|
|
return True
|
|
try:
|
|
return importlib.util.find_spec(anchor) is not None
|
|
except (ImportError, ValueError):
|
|
return False
|
|
|
|
|
|
def available(extra: str) -> bool:
|
|
"""Fast, side-effect-free: are all of this extra's anchors importable?"""
|
|
return all(_importable(a) for a in _anchors(extra))
|
|
|
|
|
|
def ensure_import(extra: str) -> None:
|
|
"""Make an extra available: no-op when the anchor imports, otherwise
|
|
sync the venv with the extra enabled. Raises InstallError on failure."""
|
|
if available(extra):
|
|
return
|
|
from pm.ensure import sync_venv
|
|
|
|
sync_venv([extra])
|
|
|
|
|
|
def ensure_and_bind(extra, importer, target_globals) -> bool:
|
|
"""ensure_import + rebind module-level names after a mid-process install.
|
|
importer returns {name: value}; bound into target_globals on success."""
|
|
try:
|
|
ensure_import(extra)
|
|
except Exception as exc:
|
|
import logging
|
|
|
|
logging.getLogger(__name__).warning("extra %r unavailable: %s", extra, exc)
|
|
return False
|
|
try:
|
|
bindings = importer()
|
|
except ImportError as exc:
|
|
import logging
|
|
|
|
logging.getLogger(__name__).warning(
|
|
"import after installing %r failed: %s", extra, exc
|
|
)
|
|
return False
|
|
target_globals.update(bindings)
|
|
return True
|
|
|
|
|
|
def missing(extra: str) -> tuple[str, ...]:
|
|
return tuple(a for a in _anchors(extra) if not _importable(a))
|