Finish bootstrap uv before PM replaces its store entry. Keep failure receipts stdlib-only and align the cryptography requirement and override with the locked version. Let bundle builders declare launch paths and update ownership. Remove payload discovery, Store probing, and the unused develop command. Derive Nix Python from the PM lock and share its provenance stamp. Document setup, activation, optional dependencies, and distribution ownership. Targeted Windows tests, relocated runtime launches, Electron bundling, and bilingual docs builds pass. Native Nix and signed-package acceptance remain CI gates.
146 lines
7.0 KiB
Python
146 lines
7.0 KiB
Python
"""Shared bundle operations use real filesystem and entrypoint contracts."""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
|
|
import pytest
|
|
|
|
from scripts.bundles.payload import plant_surfaces, posix_launcher, project_entries, relativize_links, snapshot, write_manifest, stage_launchers
|
|
from scripts.bundles.desktop import release_version
|
|
|
|
|
|
def test_snapshot_and_manifest_are_shared_by_both_layouts(tmp_path):
|
|
source = tmp_path / "source"
|
|
source.mkdir()
|
|
subprocess.run(["git", "init", str(source)], check=True, capture_output=True)
|
|
project = '[project]\nname="fixture"\nversion="1.2.3"\n[project.scripts]\ncustom="entry:run"\n'
|
|
(source / "pyproject.toml").write_text(project, encoding="utf-8")
|
|
subprocess.run(["git", "add", "."], cwd=source, check=True)
|
|
subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit", "-m", "fixture"], cwd=source, check=True, capture_output=True)
|
|
(source / "untracked").write_text("must not ship", encoding="utf-8")
|
|
for repo_name, target in [("app", "linux-arm64-bionic"), ("hermes-agent", "win32-arm64")]:
|
|
root = tmp_path / repo_name
|
|
root.mkdir()
|
|
snapshot(source, "HEAD", root / repo_name)
|
|
manifest = write_manifest(root, target=target, repo=repo_name)
|
|
assert project_entries(root / manifest["repo"]) == {"custom": "entry:run"}
|
|
assert not (root / repo_name / "untracked").exists()
|
|
assert not (root / repo_name / ".git").exists()
|
|
assert json.loads((root / "manifest.json").read_text(encoding="utf-8-sig")) == manifest
|
|
assert release_version(source, "v1.2.3") == "1.2.3"
|
|
with pytest.raises(ValueError):
|
|
release_version(source, "v1.2.4")
|
|
|
|
|
|
def test_surfaces_require_complete_outputs_and_replace_stale_files(tmp_path):
|
|
source, repo = tmp_path / "build", tmp_path / "payload"
|
|
tui = source / "ui-tui/dist"
|
|
web = source / "hermes_cli/web_dist"
|
|
tui.mkdir(parents=True)
|
|
web.mkdir(parents=True)
|
|
(tui / "entry.js").write_text("built tui", encoding="utf-8")
|
|
(web / "index.html").write_text("built web", encoding="utf-8")
|
|
plant_surfaces(repo, source)
|
|
(repo / "hermes_cli/web_dist/stale").write_text("old", encoding="utf-8")
|
|
plant_surfaces(repo, source)
|
|
assert not (repo / "hermes_cli/web_dist/stale").exists()
|
|
assert (repo / "hermes_cli/tui_dist/entry.js").read_text(encoding="utf-8-sig") == "built tui"
|
|
(web / "index.html").unlink()
|
|
with pytest.raises(FileNotFoundError):
|
|
plant_surfaces(repo, source)
|
|
|
|
|
|
def test_wrapper_rejects_unresolved_template_fields(tmp_path, monkeypatch):
|
|
from scripts.bundles import payload
|
|
|
|
template = tmp_path / "launcher_wrapper.py"
|
|
template.write_text('entry = "__HERMES_ENTRY_MODULE__"\nmissing = "__HERMES_NEW__"\n', encoding="utf-8")
|
|
monkeypatch.setattr(payload, "__file__", str(tmp_path / "payload.py"))
|
|
with pytest.raises(ValueError, match="__HERMES_NEW__"):
|
|
payload.render_wrapper("entry:run", "../app", "../venv/Lib/site-packages")
|
|
|
|
|
|
def test_launcher_stage_reads_declared_entries_and_drops_stale_names(tmp_path, monkeypatch):
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from pm.lock import Facts
|
|
from pm.store import current_target
|
|
|
|
root = tmp_path / "payload"
|
|
repo, tools = root / "app", root / "tools"
|
|
repo.mkdir(parents=True)
|
|
tools.mkdir()
|
|
interpreter = tools / "python/python.exe"
|
|
interpreter.parent.mkdir()
|
|
interpreter.write_bytes(b"fixture interpreter")
|
|
(repo / "pyproject.toml").write_text('[project.scripts]\ncustom="entry:run"\n', encoding="utf-8")
|
|
Facts(tools / "facts.json").record("python", "3.11.16", "python", {}, tools)
|
|
manifest = write_manifest(root, target=current_target(), repo="app")
|
|
site = "venv/Lib/site-packages" if current_target().startswith("win32") else "venv/lib/python3.11/site-packages"
|
|
(root / site).mkdir(parents=True)
|
|
(root / "bin").mkdir()
|
|
(root / "bin/removed-entry").write_text("old", encoding="utf-8")
|
|
monkeypatch.setattr("pm.registry.get_package", lambda _: SimpleNamespace(binary=lambda *args: interpreter))
|
|
calls = []
|
|
|
|
def mint(argv, *, env, check):
|
|
calls.append(json.loads(env["HERMES_MINT_SPECS"]))
|
|
compile(Path(env["HERMES_MINT_WRAPPER"]).read_text(encoding="utf-8-sig"), "wrapper", "exec")
|
|
(root / "bin/custom.exe").write_bytes(b"mint fixture")
|
|
|
|
assert stage_launchers(root, manifest, run=mint) == ["custom"]
|
|
assert not (root / "bin/removed-entry").exists()
|
|
if current_target().startswith("win32"):
|
|
assert calls == [[{"name": "custom", "module": "entry", "func": "run"}]]
|
|
else:
|
|
assert (root / "bin/custom").is_file()
|
|
published = json.loads((root / "manifest.json").read_text(encoding="utf-8-sig"))
|
|
assert published["launchers"] == ["custom"]
|
|
assert published["runtime"] == {
|
|
"repoDir": "app", "toolsDir": "tools", "storePython": "tools/python/python.exe",
|
|
"sitePackages": site,
|
|
"commands": {"custom": "bin/custom.exe" if current_target().startswith("win32") else "bin/custom"},
|
|
}
|
|
|
|
|
|
@pytest.mark.platforms("posix")
|
|
def test_relocation_preserves_sibling_and_framework_links(tmp_path):
|
|
root = tmp_path / "payload"
|
|
store = root / "tools/python/bin"
|
|
venv = root / "venv/bin"
|
|
store.mkdir(parents=True)
|
|
venv.mkdir(parents=True)
|
|
(store / "python3").write_text("interpreter", encoding="utf-8")
|
|
(venv / "python").symlink_to("/builder/tools/python/bin/python3")
|
|
(venv / "python3").symlink_to("python")
|
|
framework = root / "tools/framework"
|
|
framework.symlink_to("python/bin/python3")
|
|
assert relativize_links(root) == 1
|
|
assert (venv / "python3").read_text(encoding="utf-8-sig") == "interpreter"
|
|
assert os.readlink(venv / "python3") == "python"
|
|
assert os.readlink(framework) == "python/bin/python3"
|
|
assert relativize_links(root) == 0
|
|
(venv / "bad").symlink_to("/usr/bin/python")
|
|
with pytest.raises(ValueError, match="escapes payload"):
|
|
relativize_links(root)
|
|
|
|
|
|
@pytest.mark.platforms("posix")
|
|
@pytest.mark.parametrize("target", ["linux-x64", "linux-arm64-bionic"])
|
|
def test_both_launchers_keep_active_home_and_call_declared_function(tmp_path, target):
|
|
root = tmp_path / "payload with spaces"
|
|
(root / "bin").mkdir(parents=True)
|
|
(root / "app").mkdir()
|
|
(root / "python").symlink_to(sys.executable)
|
|
(root / "app/entry.py").write_text("import json,os,sys\ndef run():\n print(json.dumps([os.environ['HERMES_HOME'],sys.argv[1:]])); return 7\n", encoding="utf-8")
|
|
launcher = root / "bin/custom"
|
|
launcher.write_text(posix_launcher("custom", "entry:run", python="python", repo="app", site="deps", target=target), encoding="utf-8")
|
|
result = subprocess.run(["sh", str(launcher), "two words", "$(nope)", ""], cwd=tmp_path,
|
|
env={**os.environ, "HERMES_HOME": str(tmp_path / "custom/profiles/memory")}, capture_output=True, text=True)
|
|
assert result.returncode == 7, result.stderr
|
|
assert json.loads(result.stdout) == [str(tmp_path / "custom/profiles/memory"), ["two words", "$(nope)", ""]]
|