`import pm.ensure` bound the submodule onto the package, shadowing the facade's `pm.ensure()` function for every later caller in the process (photon's sidecar start hit `'module' object is not callable`). The module is pm.install now; the function keeps its name. The facade resolves through `__import__` rather than `importlib.import_module` so a test that patches import_module globally does not break attribute access on pm. tools/lazy_deps.py returns to the 16-line stop_for_relaunch shim the branch wrote (an origin/main merge had replaced it with main's 775-line implementation); the project-metadata tests follow. update_cmd re-exports the four old_updater_deps names the shim tests resolve through hermes_cli.update_cmd.
105 lines
5.0 KiB
Python
105 lines
5.0 KiB
Python
"""Read-only source-install acceptance, before a CLI launch can repair it.
|
|
|
|
The driver owns this file; probes import only the installed tree's passive
|
|
readers. No bootstrap, installer, build, PM worker, or application entry point.
|
|
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
def desktop_outputs(root: Path) -> list[Path]:
|
|
desktop = root / "apps/desktop"
|
|
return [path for pattern in (
|
|
"release/*/resources/app.asar.unpacked/dist",
|
|
"release/mac*/Hermes.app/Contents/Resources/app.asar.unpacked/dist",
|
|
) for path in desktop.glob(pattern)]
|
|
|
|
|
|
def verify_products(root: Path, desktop: str, node: Path | None = None) -> None:
|
|
app = root / "apps/desktop"
|
|
outputs = desktop_outputs(root)
|
|
has_desktop = (app / "dist/index.html").exists() or any((app / "release").glob("*"))
|
|
if desktop == "absent" and has_desktop:
|
|
raise RuntimeError("unexpected desktop output in a no-desktop scenario")
|
|
if desktop == "present":
|
|
if not outputs or not any((out / "index.html").is_file() for out in outputs):
|
|
raise RuntimeError("desktop packaged renderer is missing or incomplete")
|
|
executables = [path for pattern in (
|
|
"release/*/Hermes.exe", "release/*/Hermes", "release/*/hermes",
|
|
"release/mac*/Hermes.app/Contents/MacOS/Hermes",
|
|
) for path in app.glob(pattern) if path.is_file() and path.stat().st_size]
|
|
if not executables:
|
|
raise RuntimeError("desktop executable is missing or empty")
|
|
if node is None:
|
|
return # Historical builds have no compiler receipt contract.
|
|
products = [("tui", root / "ui-tui/dist"), ("web", root / "hermes_cli/web_dist")]
|
|
if desktop == "present":
|
|
products.extend(("desktop", out) for out in outputs)
|
|
for product, out in products:
|
|
result = subprocess.run(
|
|
[str(node), str(root / "scripts/build/freshness.mjs"),
|
|
"--source", str(root), "--product", product, "--out", str(out)],
|
|
cwd=root, capture_output=True, text=True, check=True, timeout=120,
|
|
)
|
|
if result.stdout.strip() != "true":
|
|
raise RuntimeError(f"{product} output is missing, stale, or damaged: {out}")
|
|
|
|
|
|
def probe_pm(root: Path, desktop: str, command: list[str]) -> None:
|
|
# Use the launcher's Python ABI, but deliberately do NOT execute its
|
|
# bootstrap: that would complete dependencies or recover markers for it.
|
|
sys.path.insert(0, str(root))
|
|
from hermes_cli._launchers import runtime_command
|
|
from pm.environments import selected_venv, site_packages
|
|
|
|
if command != runtime_command(root):
|
|
raise RuntimeError("published launcher belongs to another installation or Python")
|
|
if any((root / marker).exists() for marker in (".update-incomplete", ".lazy-refresh-incomplete")):
|
|
raise RuntimeError("source update left an incomplete marker; refusing launch-time recovery")
|
|
sys.path.insert(1, str(site_packages(selected_venv(root))))
|
|
from pm.install import installed_package, venv_is_current
|
|
|
|
if not venv_is_current(project_root=root):
|
|
raise RuntimeError("installed dependency generation is not current")
|
|
node = installed_package("node")
|
|
if node is None or node.binary is None:
|
|
raise RuntimeError("installed Node is missing or does not match the target pin")
|
|
verify_products(root, desktop, node.binary)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--root", type=Path, required=True)
|
|
parser.add_argument("--launcher", type=Path, required=True)
|
|
parser.add_argument("--desktop", choices=("absent", "present"), required=True)
|
|
parser.add_argument("--runtime-command", help=argparse.SUPPRESS)
|
|
args = parser.parse_args()
|
|
root = args.root.resolve()
|
|
if args.runtime_command:
|
|
probe_pm(root, args.desktop, json.loads(args.runtime_command))
|
|
elif (root / "pm/lock.json").is_file():
|
|
env = dict(os.environ, PYTHONDONTWRITEBYTECODE="1", HERMES_DISABLE_LAZY_INSTALLS="1")
|
|
result = subprocess.run([str(args.launcher), "--print-runtime-command"],
|
|
capture_output=True, text=True, check=True, env=env, timeout=30)
|
|
command = json.loads(result.stdout)
|
|
if not isinstance(command, list) or not command or not all(isinstance(s, str) for s in command):
|
|
raise RuntimeError("invalid published runtime command")
|
|
subprocess.run([command[0], "-I", "-B", str(Path(__file__).resolve()),
|
|
"--root", str(root), "--launcher", str(args.launcher),
|
|
"--desktop", args.desktop, "--runtime-command", json.dumps(command)],
|
|
env=env, check=True, timeout=300)
|
|
else:
|
|
verify_products(root, args.desktop)
|
|
print("Historical install: artifact presence only (no PM/compiler receipt capability)")
|
|
print(f"Read-only source verification passed: {root} (desktop={args.desktop})")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main() |