298 lines
15 KiB
Python
298 lines
15 KiB
Python
"""Desktop preparation keeps dependency acquisition out of live installs."""
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import os
|
|
from pathlib import Path
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
|
|
def test_bootstrap_environment_isolates_owned_paths_without_mutating_caller(tmp_path):
|
|
toolchain = importlib.import_module("scripts.bundles.desktop_toolchain")
|
|
source, work, cache = (tmp_path / name for name in ("source", "work", "cache"))
|
|
original_home = tmp_path / "live-user"
|
|
inherited = {
|
|
"HOME": str(original_home), "USERPROFILE": str(original_home),
|
|
"LOCALAPPDATA": str(original_home / "AppData/Local"),
|
|
"APPDATA": str(original_home / "AppData/Roaming"),
|
|
"HERMES_HOME": str(original_home / "profile"),
|
|
"HERMES_RUNTIME_DIR": str(original_home / "tools"),
|
|
"HERMES_INSTALL_ROOT": str(original_home / "installed"),
|
|
"HERMES_PAYLOAD_ROOT": str(original_home / "payload"),
|
|
"HERMES_PAYLOAD_TAG": "v1.2.3", "HERMES_BUILD_COMMIT": "a" * 40,
|
|
"HERMES_SITE": str(original_home / "site"),
|
|
"HERMES_PYTHON_SRC_ROOT": str(original_home / "repo"),
|
|
"HERMES_PYTHON": str(original_home / "python"),
|
|
"HERMES_NODE": str(original_home / "node"),
|
|
"HERMES_PROFILE": "live", "HERMES_REAL_HOME": str(original_home),
|
|
"HERMES_BUNDLED_SKILLS": str(original_home / "skills"),
|
|
"HERMES_OPTIONAL_MCPS": str(original_home / "mcps"),
|
|
"VIRTUAL_ENV": str(original_home / "venv"),
|
|
"PYTHONHOME": str(original_home / "python-home"),
|
|
"PYTHONPATH": str(original_home / "site"),
|
|
"UV_CACHE_DIR": str(original_home / "uv-cache"),
|
|
"npm_config_cache": str(original_home / "npm-cache"),
|
|
"npm_execpath": str(original_home / "foreign-npm.js"),
|
|
"NPM_CONFIG_USERCONFIG": str(original_home / "credentials.npmrc"),
|
|
"CARGO_HOME": str(original_home / "custom-cargo"),
|
|
"RUSTUP_HOME": str(original_home / "custom-rustup"),
|
|
"PATH": os.defpath, "SIGNING_TOKEN": "inherited-not-serialized",
|
|
}
|
|
before = inherited.copy()
|
|
process_before = dict(os.environ)
|
|
environment = toolchain.bootstrap_environment(source, work, cache, inherited)
|
|
assert inherited == before
|
|
assert dict(os.environ) == process_before
|
|
assert environment["CARGO_HOME"] == inherited["CARGO_HOME"]
|
|
assert environment["RUSTUP_HOME"] == inherited["RUSTUP_HOME"]
|
|
for key in ("HOME", "USERPROFILE", "LOCALAPPDATA", "APPDATA", "HERMES_HOME",
|
|
"XDG_CONFIG_HOME", "XDG_CACHE_HOME"):
|
|
assert Path(environment[key]).is_relative_to(work)
|
|
assert Path(environment["HERMES_RUNTIME_DIR"]) == cache / "tools"
|
|
assert Path(environment["UV_CACHE_DIR"]) == cache / "python/runtime"
|
|
assert Path(environment["npm_config_cache"]) == cache / "npm"
|
|
assert "npm_execpath" not in environment
|
|
assert "NPM_CONFIG_USERCONFIG" not in environment
|
|
assert environment["npm_config_userconfig"] == os.devnull
|
|
assert "npm_config_globalconfig" not in environment
|
|
assert environment["HERMES_PYTHON_SRC_ROOT"] == str(source)
|
|
for key in ("HERMES_INSTALL_ROOT", "HERMES_PAYLOAD_ROOT", "HERMES_PAYLOAD_TAG",
|
|
"HERMES_BUILD_COMMIT", "HERMES_SITE", "HERMES_PROFILE", "HERMES_REAL_HOME",
|
|
"HERMES_PYTHON", "HERMES_NODE", "HERMES_BUNDLED_SKILLS", "HERMES_OPTIONAL_MCPS",
|
|
"VIRTUAL_ENV", "PYTHONHOME", "PYTHONPATH"):
|
|
assert key not in environment
|
|
assert environment["SIGNING_TOKEN"] == inherited["SIGNING_TOKEN"]
|
|
assert environment["PATH"] == inherited["PATH"]
|
|
assert not work.exists() # Forming the child environment is pure.
|
|
|
|
defaults = toolchain.bootstrap_environment(source, work, cache, {"HOME": str(original_home),
|
|
"USERPROFILE": str(original_home)})
|
|
assert defaults["CARGO_HOME"] == str(original_home / ".cargo")
|
|
assert defaults["RUSTUP_HOME"] == str(original_home / ".rustup")
|
|
|
|
mixed = toolchain.bootstrap_environment(source, work, cache, {
|
|
"Home": str(original_home), "UserProfile": str(original_home), "LocalAppData": "live-local",
|
|
"Hermes_Home": "live-profile", "Hermes_Runtime_Dir": "live-store", "Cargo_Home": "custom-cargo",
|
|
"Rustup_Home": "custom-rustup", "NPM_CONFIG_CACHE": "live-npm", "Path": os.defpath,
|
|
})
|
|
assert mixed["CARGO_HOME"] == "custom-cargo" and mixed["RUSTUP_HOME"] == "custom-rustup"
|
|
assert len([key for key in mixed if key.upper() == "HOME"]) == 1
|
|
assert len([key for key in mixed if key.upper() == "HERMES_RUNTIME_DIR"]) == 1
|
|
|
|
|
|
@pytest.mark.parametrize("exit_code", [0, 7])
|
|
@pytest.mark.platforms("linux", "macos", "windows")
|
|
def test_run_preparation_bootstraps_before_worker_in_isolated_child(tmp_path, monkeypatch, exit_code):
|
|
from scripts.bundles import desktop_toolchain
|
|
|
|
source, work, cache = (tmp_path / name for name in ("source space", "work", "cache"))
|
|
(source / "pm").mkdir(parents=True)
|
|
(source / "pm/__init__.py").write_text("", encoding="utf-8")
|
|
# A bootstrap double runs in a REAL child: imports must already be isolated,
|
|
# not temporarily redirected in the parent's process around the PM call.
|
|
(source / "pm/runtime.py").write_text(
|
|
"import os, sys\n"
|
|
"from pathlib import Path\n"
|
|
"assert Path(os.environ['HOME']).name == 'home'\n"
|
|
"assert 'HERMES_INSTALL_ROOT' not in os.environ\n"
|
|
"assert sys.flags.isolated and sys.flags.no_site\n"
|
|
"def runtime_command(script, args, *, cache):\n"
|
|
" assert cache == Path(os.environ['UV_CACHE_DIR'])\n"
|
|
" return [sys.executable, '-I', '-B', str(script), *args]\n",
|
|
encoding="utf-8",
|
|
)
|
|
worker = source / "scripts/bundles/desktop_prepare.py"
|
|
worker.parent.mkdir(parents=True)
|
|
worker.write_text(
|
|
"import json, os, sys\n"
|
|
"from pathlib import Path\n"
|
|
"assert sys.argv[1] == '--request' and sys.argv[3:] == ['--worker']\n"
|
|
"import ssl\n"
|
|
"assert ssl.SSLContext.__module__.startswith('truststore')\n"
|
|
"request = json.loads(Path(sys.argv[2]).read_text())\n"
|
|
"assert os.environ['HERMES_RUNTIME_DIR'] == request['tools']\n"
|
|
"Path(request['receipt']).write_text(str(Path.cwd()))\n"
|
|
"sys.exit(request['exit_code'])\n", encoding="utf-8",
|
|
)
|
|
request_file = tmp_path / "request space.json"
|
|
receipt = tmp_path / "worker-ran"
|
|
request_file.write_text(json.dumps({"receipt": str(receipt), "tools": str(cache / "tools"),
|
|
"exit_code": exit_code}), encoding="utf-8")
|
|
monkeypatch.setenv("HERMES_INSTALL_ROOT", str(tmp_path / "live-install"))
|
|
before = dict(os.environ)
|
|
assert desktop_toolchain.run_preparation(source, work, cache, request_file) == exit_code
|
|
assert dict(os.environ) == before
|
|
assert receipt.read_text(encoding="utf-8-sig") == str(source)
|
|
|
|
|
|
def test_bootstrap_real_pm_resolves_only_build_owned_state(tmp_path):
|
|
from scripts.bundles.desktop_toolchain import bootstrap_environment
|
|
|
|
source = Path(__file__).resolve().parents[2]
|
|
work, cache = tmp_path / "work", tmp_path / "cache"
|
|
env = bootstrap_environment(source, work, cache, os.environ)
|
|
probe = (
|
|
"import json, sys; from pathlib import Path; sys.path.insert(0, sys.argv[1]); "
|
|
"from pm import paths; from pm.packages import uv_cache_dir; "
|
|
"from pm.environments import install_state_dir; "
|
|
"print(json.dumps([str(paths.store_root()), str(paths.partials_root()), "
|
|
"str(uv_cache_dir()), str(install_state_dir(Path(sys.argv[1])))]))"
|
|
)
|
|
result = subprocess.run([sys.executable, "-I", "-S", "-B", "-c", probe, str(source)],
|
|
env=env, capture_output=True, text=True, check=True)
|
|
store, partials, uv_default, state = map(Path, json.loads(result.stdout))
|
|
assert store == cache / "tools"
|
|
assert all(path.is_relative_to(work) for path in (partials, uv_default, state))
|
|
|
|
|
|
@pytest.mark.parametrize("target", ["darwin-arm64", "darwin-x64", "linux-x64", "win32-arm64"])
|
|
def test_packaging_preserves_keychain_home_without_retargeting_build_state(tmp_path, target):
|
|
from scripts.bundles.desktop_inputs import packaging_environment
|
|
from scripts.bundles.desktop_toolchain import bootstrap_environment
|
|
|
|
source, work, cache = (tmp_path / name for name in ("source", "work", "cache"))
|
|
login_home = str(tmp_path / "login")
|
|
inherited = {"HOME": login_home, "CSC_KEYCHAIN": "explicit.keychain"}
|
|
isolated = bootstrap_environment(source, work, cache, inherited)
|
|
before = isolated.copy()
|
|
for caller in (inherited, {**inherited, "HOME": str(work / "launcher-home"),
|
|
"HERMES_REAL_HOME": login_home}, {}):
|
|
packaged = packaging_environment(isolated, caller, target)
|
|
expected_home = (caller.get("HERMES_REAL_HOME") or caller.get("HOME") or str(Path.home())
|
|
if target.startswith("darwin-") else isolated["HOME"])
|
|
assert packaged == {**isolated, "HOME": expected_home}
|
|
assert isolated == before
|
|
|
|
|
|
@pytest.mark.platforms("linux", "macos", "windows")
|
|
def test_prepare_tools_uses_pm_native_pins_and_separate_cache(tmp_path, monkeypatch):
|
|
import pm
|
|
from scripts.bundles import desktop_toolchain
|
|
from pm import native_build
|
|
|
|
source, work, cache = (tmp_path / name for name in ("source", "work", "cache"))
|
|
env = desktop_toolchain.bootstrap_environment(source, work, cache, os.environ)
|
|
monkeypatch.setenv("HERMES_RUNTIME_DIR", env["HERMES_RUNTIME_DIR"])
|
|
acquired, native_calls = [], []
|
|
bins = {name: cache / "tools" / name / "bin" / name for name in ("python", "node")}
|
|
for binary in bins.values():
|
|
binary.parent.mkdir(parents=True)
|
|
binary.touch()
|
|
|
|
def ensure(name, *, explicit):
|
|
assert explicit
|
|
acquired.append(name)
|
|
|
|
def native(**kwargs):
|
|
native_calls.append(kwargs)
|
|
return {**kwargs["env"], "OPENSSL_DIR": str(cache / "native/openssl")}
|
|
|
|
monkeypatch.setattr(pm, "ensure", ensure)
|
|
monkeypatch.setattr(pm, "installed_package", lambda name: SimpleNamespace(binary=bins[name]))
|
|
monkeypatch.setattr(pm, "env_for", lambda *names, base_env: {**base_env, "PATH": "pm-tools"})
|
|
monkeypatch.setattr(native_build, "prepare_windows_environment", native)
|
|
monkeypatch.setattr(desktop_toolchain, "native_cache_path", lambda cache, env: cache / "python/runtime/native-identity",
|
|
raising=False)
|
|
before = dict(os.environ)
|
|
python, node, prepared = desktop_toolchain.prepare_tools(source, work, cache, env)
|
|
assert acquired == ["uv", "npm"]
|
|
assert python == bins["python"] and node == bins["node"]
|
|
assert prepared["HERMES_PYTHON"] == str(python)
|
|
assert prepared["HERMES_NODE"] == str(node)
|
|
assert prepared["PATH"] == "pm-tools"
|
|
assert Path(prepared["UV_CACHE_DIR"]) == cache / "python/runtime/native-identity"
|
|
assert dict(os.environ) == before
|
|
if pm.current_target() == "win32-arm64":
|
|
assert len(native_calls) == 1
|
|
assert native_calls[0]["state"].is_relative_to(cache / "native")
|
|
assert prepared["OPENSSL_DIR"] == str(cache / "native/openssl")
|
|
else:
|
|
assert native_calls == []
|
|
|
|
|
|
@pytest.mark.platforms("linux", "macos", "windows")
|
|
def test_native_cache_identity_tracks_compilers_sdk_and_openssl(tmp_path, monkeypatch):
|
|
from scripts.bundles import desktop_toolchain
|
|
|
|
openssl = tmp_path / "openssl"
|
|
(openssl / "include/openssl").mkdir(parents=True)
|
|
(openssl / "include/openssl/opensslv.h").write_bytes(b"version one")
|
|
(openssl / "lib").mkdir()
|
|
library = openssl / "lib/libcrypto.lib"
|
|
library.write_bytes(b"first build")
|
|
(openssl / "lib/libssl.lib").write_bytes(b"ssl build")
|
|
env = {"PATH": os.defpath, "OPENSSL_DIR": str(openssl),
|
|
"WindowsSDKVersion": "10.0.1", "VCToolsVersion": "14.1"}
|
|
version = ["compiler one"]
|
|
|
|
def probe(command, **kwargs):
|
|
assert kwargs["env"] == env
|
|
return SimpleNamespace(returncode=0, stdout=version[0], stderr="")
|
|
|
|
# Let stdlib cache its real-host probe before substituting compiler commands.
|
|
desktop_toolchain.platform.platform()
|
|
monkeypatch.setattr(desktop_toolchain.subprocess, "run", probe)
|
|
cache = tmp_path / "cache"
|
|
first = desktop_toolchain.native_cache_path(cache, env)
|
|
assert first.is_relative_to(cache / "python/runtime")
|
|
assert desktop_toolchain.native_cache_path(cache, env) == first
|
|
moved_cache = tmp_path / "relocated-cache"
|
|
moved_openssl = moved_cache / "native/openssl"
|
|
import shutil
|
|
shutil.copytree(openssl, cache / "native/openssl")
|
|
shutil.copytree(openssl, moved_openssl)
|
|
env["OPENSSL_DIR"] = str(cache / "native/openssl")
|
|
portable = desktop_toolchain.native_cache_path(cache, env)
|
|
env["OPENSSL_DIR"] = str(moved_openssl)
|
|
assert desktop_toolchain.native_cache_path(moved_cache, env).name == portable.name
|
|
env["OPENSSL_DIR"] = str(openssl)
|
|
version[0] = "compiler two"
|
|
second = desktop_toolchain.native_cache_path(cache, env)
|
|
assert second != first
|
|
library.write_bytes(b"changed build")
|
|
third = desktop_toolchain.native_cache_path(cache, env)
|
|
assert third != second
|
|
env["WindowsSDKVersion"] = "10.0.2"
|
|
assert desktop_toolchain.native_cache_path(cache, env) != third
|
|
monkeypatch.setattr(desktop_toolchain.subprocess, "run", lambda *a, **k: (_ for _ in ()).throw(FileNotFoundError()))
|
|
assert desktop_toolchain.native_cache_path(cache, env) != desktop_toolchain.native_cache_path(cache, env)
|
|
|
|
|
|
@pytest.mark.parametrize("complete", [False, True])
|
|
def test_native_cache_leaves_room_for_sdist_compiler_outputs(tmp_path, monkeypatch, complete):
|
|
from pathlib import PureWindowsPath
|
|
from scripts.bundles import desktop_toolchain
|
|
|
|
# Replay the Windows CI layout as path data, without faking the host OS.
|
|
# uv builds in its cached sdist; /Fo is relative to that working directory.
|
|
cache = tmp_path / "cache"
|
|
desktop_toolchain.platform.platform()
|
|
|
|
def probe(*args, **kwargs):
|
|
if not complete:
|
|
raise FileNotFoundError()
|
|
return SimpleNamespace(returncode=0, stdout="compiler identity", stderr="")
|
|
|
|
openssl = cache / "native/openssl"
|
|
for name in ("include/openssl/opensslv.h", "lib/libcrypto.lib"):
|
|
path = openssl / name
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(b"native input")
|
|
monkeypatch.setattr(desktop_toolchain.subprocess, "run", probe)
|
|
selected = desktop_toolchain.native_cache_path(cache, {
|
|
"WindowsSDKVersion": "10.0.1", "VCToolsVersion": "14.1", "OPENSSL_DIR": str(openssl),
|
|
})
|
|
assert selected.is_relative_to(cache / "python/runtime")
|
|
runner_cache = PureWindowsPath("D:/a/hermes-agent/hermes-agent/.cache/desktop-inputs")
|
|
sdist = PureWindowsPath("sdists-v9/pypi/pilk/0.2.4/5b4cbVXt0GPuGQrp/src")
|
|
for architecture in ("win-amd64", "win-arm64"):
|
|
output = (runner_cache.joinpath(*selected.relative_to(cache).parts) / sdist
|
|
/ f"build/temp.{architecture}-cpython-314/Release/src/SKP_SILK_SRC"
|
|
/ "SKP_Silk_NLSF_VQ_rate_distortion_FIX.obj")
|
|
assert len(str(output)) < 260, str(output)
|