Use a verified writable copy of pinned Python for uv builds. Keep the signed launchers and bundled Python as the MSIX runtime so plugin rebuilds do not remove package access. Identify Microsoft Store builds from the artifact stamp. Use StoreContext for update checks, downloads, and installation requests inside Hermes. Keep relaunch and recovery ownership across the update. Do not report unknown checks or no-op requests as successful installations. Open the build commit link in the system browser. Verification: 168 Python tests, 107 Electron tests, and 121 renderer tests passed. Typechecks, lint, lock validation, and desktop builds passed. A real packaged process completed plugin admission, import, and repair. Store-acquired download, installation, and relaunch remain unverified. This host has only the sideloaded MSIX. Its real Store API call returned an error, which the app reports as unknown. macOS updates are unchanged.
247 lines
11 KiB
Python
247 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
|
|
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
|