Build TUI, web, desktop UI and runnable agent products from explicit prepared inputs. Keep dependency preparation separate from distribution packaging, with PM and native builds sharing uv environment construction. Docker copies compiled frontend products instead of build dependencies. Nix retains uv2nix environments and consumes shared assembly through store references. Native desktop and Termux use the same launcher and frontend contracts. Preserve the independent PM runtime and source imports from arbitrary working directories. Keep failed frontend builds from replacing the previous product, reject source/output overlap, and bound dependency-process output draining. Include hermes_wisdom in the Nix wheel: real CLI smoke tests exposed its missing package declaration on the base revision too. Verified focused Python and JavaScript suites, Docker build/runtime checks, Nix desktop and CLI/ACP checks, standalone TUI and packaged Electron PTY, and real full-Chromium interaction. Native signed installers, Android device installation and the full repository suite remain CI verification.
253 lines
11 KiB
Python
253 lines
11 KiB
Python
"""PM-owned uv commands use the installed interpreter, never host discovery."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
from pm.lock import Facts, Lockfile
|
|
from pm.package import InstallError
|
|
from pm.packages import Python, Uv
|
|
from pm.store import current_target
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def isolated_machine_home(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
def installed_uv(tmp_path, monkeypatch):
|
|
import pm.paths as paths
|
|
import pm.registry as registry
|
|
|
|
uv = shutil.which("uv")
|
|
assert uv, "the interpreter selection contract requires real uv"
|
|
store = tmp_path / "store"
|
|
lock = Lockfile(tmp_path / "lock.json")
|
|
target = current_target()
|
|
digest = "1" * 64
|
|
entry = store / "uv"
|
|
entry.mkdir(parents=True)
|
|
binary = entry / ("uv.exe" if os.name == "nt" else "uv")
|
|
shutil.copy2(uv, binary)
|
|
uvx = Path(uv).with_name("uvx" + binary.suffix)
|
|
assert uvx.is_file(), "the real uv distribution must include uvx"
|
|
shutil.copy2(uvx, entry / uvx.name)
|
|
lock.set_pin("uv", "test", {target: {"url": "https://test.invalid/uv", "sha256": digest}})
|
|
lock.set_pin("python", "test", {target: {"url": "https://test.invalid/python", "sha256": digest}})
|
|
lock.save()
|
|
facts = Facts(store / "facts.json")
|
|
facts.record("uv", "test", entry.name, {}, store, target=target, artifacts=[digest])
|
|
monkeypatch.setattr(paths, "lockfile_path", lambda: lock.path)
|
|
monkeypatch.setattr(paths, "store_root", lambda: store)
|
|
monkeypatch.setattr(paths, "writable_store_root", lambda: store)
|
|
monkeypatch.setitem(registry._packages, "uv", Uv())
|
|
return tmp_path, binary, facts, target, digest
|
|
|
|
|
|
def test_all_uv_commands_keep_the_pm_interpreter(installed_uv, monkeypatch):
|
|
import pm.registry as registry
|
|
|
|
root, uv, facts, target, digest = installed_uv
|
|
selected = root / "store" / "selected-python"
|
|
clean = {key: value for key, value in os.environ.items() if not key.startswith("UV_")}
|
|
clean.update({"UV_NO_CONFIG": "1", "UV_OFFLINE": "1", "UV_PYTHON_DOWNLOADS": "never"})
|
|
subprocess.run([str(uv), "venv", "--python", sys.executable, str(selected)],
|
|
cwd=root, env=clean, check=True, capture_output=True, timeout=60)
|
|
python = selected / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
|
|
|
|
class FixturePython(Python):
|
|
binary_rel = {"win32": "Scripts/python.exe", "posix": "bin/python"}
|
|
|
|
monkeypatch.setitem(registry._packages, "python", FixturePython())
|
|
facts.record("python", "test", selected.name, {}, selected.parent,
|
|
target=target, artifacts=[digest])
|
|
monkeypatch.setenv("UV_PYTHON", str(root / "ambient-python"))
|
|
monkeypatch.setenv("UV_PROJECT_ENVIRONMENT", str(root / "ambient-venv"))
|
|
before = dict(os.environ)
|
|
managed_uv, env = importlib.import_module("pm.ensure").uv(realize=False)
|
|
assert managed_uv == str(uv)
|
|
assert env.get("UV_PYTHON") == str(python)
|
|
assert "UV_PROJECT_ENVIRONMENT" not in env
|
|
assert dict(os.environ) == before
|
|
uvx, uvx_env = importlib.import_module("pm.ensure").uv("uvx", realize=False)
|
|
assert Path(uvx) == uv.with_name("uvx" + uv.suffix)
|
|
assert uvx_env["UV_PYTHON"] == str(python)
|
|
subprocess.run([uvx, "--version"], cwd=root, env=uvx_env, check=True, capture_output=True, timeout=30)
|
|
|
|
project = root / "project"
|
|
project.mkdir()
|
|
(project / "pyproject.toml").write_text(
|
|
'[project]\nname="pm-python-proof"\nversion="1"\nrequires-python=">=3.11"\n'
|
|
'[tool.uv]\npackage=false\n', encoding="utf-8",
|
|
)
|
|
(project / ".python-version").write_text(str(root / "host-only-python"), encoding="utf-8")
|
|
env.pop("UV_NO_CONFIG")
|
|
env.update({"UV_OFFLINE": "1", "UV_PYTHON_DOWNLOADS": "never"})
|
|
environment = root / "candidate"
|
|
env["VIRTUAL_ENV"] = str(environment)
|
|
for args in (["venv", str(environment)], ["lock"], ["sync", "--frozen", "--active"],
|
|
["run", "--active", "--no-sync", "python", "-c", "import sys; print(sys.prefix)"]):
|
|
result = subprocess.run([managed_uv, *args], cwd=project, env=env,
|
|
capture_output=True, text=True, check=True, timeout=60)
|
|
assert Path(result.stdout.strip()).resolve() == environment.resolve()
|
|
|
|
incomplete = root / "store" / "uv-without-uvx"
|
|
incomplete.mkdir()
|
|
shutil.copy2(uv, incomplete / uv.name)
|
|
facts.record("uv", "test", incomplete.name, {}, incomplete.parent,
|
|
target=target, artifacts=[digest])
|
|
ensure = importlib.import_module("pm.ensure")
|
|
assert ensure.uv("uvx", realize=False)[0] is None
|
|
with pytest.raises(InstallError, match="binary is missing"):
|
|
ensure.uv("uvx")
|
|
|
|
|
|
def test_uv_refuses_discovery_when_pm_python_is_missing(installed_uv, monkeypatch):
|
|
root, _, facts, target, digest = installed_uv
|
|
ensure = importlib.import_module("pm.ensure")
|
|
monkeypatch.setattr(ensure, "lazy_installs_allowed", lambda: False)
|
|
monkeypatch.setenv("UV_PYTHON", str(root / "ambient-python"))
|
|
managed_uv, env = ensure.uv(realize=False)
|
|
assert managed_uv is None
|
|
assert "UV_PYTHON" not in env
|
|
assert not (root / "store" / "python-test").exists()
|
|
with pytest.raises(InstallError, match="python"):
|
|
ensure.uv()
|
|
|
|
entry = root / "store" / "missing-binary"
|
|
entry.mkdir()
|
|
facts.record("python", "test", entry.name, {}, entry.parent,
|
|
target=target, artifacts=[digest])
|
|
recorded = facts.path.read_bytes()
|
|
assert ensure.uv(realize=False)[0] is None
|
|
with pytest.raises(InstallError, match="lazy installs are disabled: python"):
|
|
ensure.uv()
|
|
assert facts.path.read_bytes() == recorded
|
|
assert not list(entry.iterdir())
|
|
|
|
|
|
@pytest.mark.platforms("windows")
|
|
def test_bundled_uv_uses_a_verified_writable_python_without_changing_runtime(installed_uv, monkeypatch):
|
|
import pm.paths as paths
|
|
import pm.registry as registry
|
|
from pm.store import Store, tree_digest
|
|
|
|
root, uv_binary, facts, target, digest = installed_uv
|
|
ensure = importlib.import_module("pm.ensure")
|
|
shipped = uv_binary.parent.parent
|
|
writable = root / "writable-tools"
|
|
monkeypatch.setattr(paths, "writable_store_root", lambda: writable)
|
|
(shipped.parent / "manifest.json").write_text("{}", encoding="utf-8")
|
|
|
|
class FixturePython(Python):
|
|
def verify(self, entry, target):
|
|
return "" if self.binary(entry, target).read_bytes() == b"pinned interpreter" else "damaged Python"
|
|
|
|
class FixtureUv(Uv):
|
|
emulated_arch_targets = {target}
|
|
|
|
monkeypatch.setitem(registry._packages, "uv", FixtureUv())
|
|
facts.record("uv", "test", uv_binary.parent.name, {}, shipped,
|
|
target=target, artifacts=[digest], digest=tree_digest(uv_binary.parent))
|
|
python = FixturePython()
|
|
monkeypatch.setitem(registry._packages, "python", python)
|
|
entry = shipped / python.store_entry("test", target)
|
|
entry.mkdir()
|
|
binary = entry / "python.exe"
|
|
binary.write_bytes(b"pinned interpreter")
|
|
(entry / "python.dll").write_bytes(b"pinned runtime")
|
|
facts.record("python", "test", entry.name, python.env(entry, target), shipped,
|
|
target=target, artifacts=[digest], digest=tree_digest(entry))
|
|
before = facts.path.read_bytes()
|
|
shipped_digest = tree_digest(entry)
|
|
monkeypatch.setattr(ensure, "lazy_installs_allowed", lambda: False)
|
|
|
|
def no_download(*args, **kwargs):
|
|
raise AssertionError("the verified Python must be copied without downloading")
|
|
|
|
monkeypatch.setattr(Store, "fetch_many", no_download)
|
|
assert ensure.uv(realize=False)[0] is None
|
|
assert not writable.exists()
|
|
with pytest.raises(InstallError, match="lazy installs are disabled"):
|
|
ensure.uv()
|
|
assert not writable.exists()
|
|
|
|
resolved_uv, env = ensure.uv(explicit=True)
|
|
copied = writable / entry.name
|
|
assert resolved_uv == str(uv_binary)
|
|
assert env["UV_PYTHON"] == str(copied / "python.exe")
|
|
assert tree_digest(copied) == shipped_digest
|
|
copied_fact = Facts(writable / "facts.json").get("python")
|
|
assert copied_fact["digest"] == shipped_digest
|
|
assert copied_fact["artifacts"] == [digest]
|
|
assert copied_fact["target"] == target
|
|
assert ensure.installed_package("python").binary == binary
|
|
assert str(copied) not in ensure.env_for("python")["PATH"]
|
|
|
|
def no_copy(*args, **kwargs):
|
|
raise AssertionError("a matching copy must be reused")
|
|
|
|
with monkeypatch.context() as reuse:
|
|
reuse.setattr(shutil, "copytree", no_copy)
|
|
assert ensure.uv(realize=False)[1]["UV_PYTHON"] == env["UV_PYTHON"]
|
|
assert ensure.uv()[1]["UV_PYTHON"] == env["UV_PYTHON"]
|
|
|
|
assert facts.path.read_bytes() == before
|
|
assert tree_digest(entry) == shipped_digest
|
|
|
|
|
|
@pytest.mark.platforms("windows")
|
|
@pytest.mark.parametrize("damage", ["source", "copy", "publication"])
|
|
def test_copy_failure_preserves_previous_python(installed_uv, monkeypatch, damage):
|
|
import pm.registry as registry
|
|
from pm.store import Store, tree_digest
|
|
|
|
root, _, facts, target, digest = installed_uv
|
|
ensure = importlib.import_module("pm.ensure")
|
|
shipped = facts.path.parent
|
|
writable = root / "writable-tools"
|
|
writable.mkdir()
|
|
python = Python()
|
|
monkeypatch.setattr(python, "verify", lambda *_: "")
|
|
monkeypatch.setitem(registry._packages, "python", python)
|
|
entry_name = python.store_entry("test", target)
|
|
source = shipped / entry_name
|
|
source.mkdir()
|
|
(source / "python.exe").write_bytes(b"new interpreter")
|
|
facts.record("python", "test", entry_name, {}, shipped,
|
|
target=target, artifacts=[digest], digest=tree_digest(source))
|
|
previous = writable / entry_name
|
|
previous.mkdir()
|
|
(previous / "python.exe").write_bytes(b"previous interpreter")
|
|
previous_facts = Facts(writable / "facts.json")
|
|
previous_facts.record("python", "previous", entry_name, {}, writable,
|
|
target=target, artifacts=[digest], digest=tree_digest(previous))
|
|
before = previous_facts.path.read_bytes()
|
|
copytree = shutil.copytree
|
|
|
|
if damage == "source":
|
|
(source / "python.exe").write_bytes(b"damaged source")
|
|
elif damage == "copy":
|
|
def damaged_copy(src, dest, **kwargs):
|
|
copytree(src, dest, **kwargs)
|
|
(dest / "python.exe").write_bytes(b"damaged copy")
|
|
monkeypatch.setattr(shutil, "copytree", damaged_copy)
|
|
else:
|
|
def failed_publication(*args, **kwargs):
|
|
raise OSError("publication refused")
|
|
monkeypatch.setattr(Store, "publish", failed_publication)
|
|
|
|
with pytest.raises(InstallError, match="verification|copied bytes|publication refused"):
|
|
ensure._install(python, ensure._lockfile(), previous_facts, Store(writable), target,
|
|
copy_from=(facts, Store(shipped)))
|
|
assert (previous / "python.exe").read_bytes() == b"previous interpreter"
|
|
assert previous_facts.path.read_bytes() == before
|