Stage npm, ffmpeg and its bionic runtime libraries, and static ARM ripgrep. Bind caches to actual build inputs. Generate entrypoints from the project manifest and verify real CLI/TUI startup and media conversion offline. Keep versions unchanged. Windows service work remains out of scope.
220 lines
7.9 KiB
Python
220 lines
7.9 KiB
Python
"""Behavior contracts for pm's bionic target + DebPackage + stage_only.
|
|
|
|
The lock VALUES are bumped by review; these tests pin the relationships:
|
|
the linux-arm64-bionic rows exist and agree with their suppliers' shapes,
|
|
DebPackage extraction is hardened, and cross-target staging never touches
|
|
this host's installed facts.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import tarfile
|
|
import io
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _pm():
|
|
import sys
|
|
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
import pm
|
|
|
|
return pm
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def lock():
|
|
return json.loads((REPO_ROOT / "pm" / "lock.json").read_text(encoding="utf-8"))
|
|
|
|
|
|
def test_all_targets_includes_bionic():
|
|
from pm.store import ALL_TARGETS
|
|
|
|
assert "linux-arm64-bionic" in ALL_TARGETS
|
|
assert ALL_TARGETS.count("linux-arm64-bionic") == 1
|
|
|
|
|
|
def _assert_pinned_bionic_row(lock, pkg, url_suffix_re):
|
|
"""The bionic rows are DELIBERATE explicit pins: the version axis follows
|
|
main (the desktop artifacts), while the termux/TUR suppliers rotate or
|
|
lag, so the bionic row pins whatever the supplier actually ships today.
|
|
The stage path consumes the row's url+sha256 directly. The contract:
|
|
the row exists, is https, matches the supplier's filename shape, and
|
|
fetch_url AGREES with the pinned row for this target."""
|
|
import re as _re
|
|
|
|
row = lock["packages"][pkg]["artifacts"].get("linux-arm64-bionic")
|
|
assert row, f"{pkg} has no linux-arm64-bionic artifact"
|
|
assert row["url"].startswith("https://"), f"{pkg} bionic row not https"
|
|
m = _re.search(url_suffix_re, row["url"])
|
|
assert m, f"{pkg} bionic url shape: {row['url']}"
|
|
assert _re.fullmatch(r"[0-9a-f]{64}", row["sha256"])
|
|
from pm.registry import get_package
|
|
|
|
cls = get_package(pkg)
|
|
# fetch_url must reproduce the pinned row exactly for this target
|
|
# (the pinned version is recovered from the URL itself).
|
|
assert cls.fetch_url(m.group("ver"), "linux-arm64-bionic") == row["url"]
|
|
|
|
|
|
def test_python_bionic_row_matches_supplier(lock):
|
|
"""The python bionic row is an explicit pin of the TUR .deb; the row and
|
|
Python.fetch_url(bionic arm) must agree on the same pinned artifact."""
|
|
_assert_pinned_bionic_row(lock, "python", r"python3\.11_(?P<ver>[0-9.]+)_aarch64\.deb$")
|
|
def test_node_bionic_row_matches_supplier(lock):
|
|
"""The node bionic row is an explicit pin of the termux-main nodejs .deb;
|
|
the row and Nodejs.fetch_url(bionic arm) must agree."""
|
|
_assert_pinned_bionic_row(lock, "node", r"nodejs_(?P<ver>[0-9.]+)-1_aarch64\.deb$")
|
|
def test_termux_docker_row_pins_digest(lock):
|
|
row = lock["packages"]["termux-docker"]["artifacts"]["linux-arm64-bionic"]
|
|
version = lock["packages"]["termux-docker"]["version"]
|
|
assert version.startswith("sha256:")
|
|
assert len(version) == 7 + 64
|
|
assert row["url"] == f"docker://termux/termux-docker@{version}"
|
|
|
|
|
|
def test_bionic_fetch_urls_resolve():
|
|
from pm.registry import get_package
|
|
|
|
py = get_package("python")
|
|
version = "3.11.15+20260807"
|
|
assert py.fetch_url(version, "linux-arm64-bionic").endswith(
|
|
"python3.11_3.11.15_aarch64.deb"
|
|
)
|
|
nd = get_package("node")
|
|
assert nd.fetch_url("26.4.0", "linux-arm64-bionic").endswith(
|
|
"nodejs_26.4.0-1_aarch64.deb"
|
|
)
|
|
|
|
|
|
def test_uv_bionic_row_matches_supplier(lock):
|
|
"""The uv bionic row is an explicit pin of the termux-main pool .deb;
|
|
the row and Uv.fetch_url(bionic arm) must agree."""
|
|
_assert_pinned_bionic_row(lock, "uv", r"/u/uv/uv_(?P<ver>[0-9.]+)_aarch64\.deb$")
|
|
def _build_fake_deb(path: Path, control: dict[str, str], files: dict[str, bytes]) -> None:
|
|
def ar_member(name: str, data: bytes) -> bytes:
|
|
hdr = (
|
|
name.ljust(16).encode()
|
|
+ b"0".ljust(12)
|
|
+ b"0".ljust(6)
|
|
+ b"0".ljust(6)
|
|
+ b"100644".ljust(8)
|
|
+ str(len(data)).encode().ljust(10)
|
|
+ b"`\n"
|
|
)
|
|
pad = b"\n" if len(data) % 2 else b""
|
|
return hdr + data + pad
|
|
|
|
ctrl_buf = io.BytesIO()
|
|
with tarfile.open(fileobj=ctrl_buf, mode="w:gz") as tf:
|
|
body = "".join(f"{k}: {v}\n" for k, v in control.items()).encode()
|
|
info = tarfile.TarInfo("control")
|
|
info.size = len(body)
|
|
tf.addfile(info, io.BytesIO(body))
|
|
data_buf = io.BytesIO()
|
|
with tarfile.open(fileobj=data_buf, mode="w:gz") as tf:
|
|
for name, content in files.items():
|
|
info = tarfile.TarInfo(name)
|
|
info.size = len(content)
|
|
tf.addfile(info, io.BytesIO(content))
|
|
path.write_bytes(
|
|
b"!<arch>\n"
|
|
+ ar_member("debian-binary", b"2.0\n")
|
|
+ ar_member("control.tar.gz", ctrl_buf.getvalue())
|
|
+ ar_member("data.tar.gz", data_buf.getvalue())
|
|
)
|
|
|
|
|
|
def test_debpackage_unpack_hardened(tmp_path: Path):
|
|
"""DebPackage.unpack extracts data members and refuses traversal."""
|
|
from pm.package import DebPackage
|
|
|
|
class _P(DebPackage):
|
|
name = "test-deb"
|
|
|
|
deb = tmp_path / "test.deb"
|
|
_build_fake_deb(
|
|
deb,
|
|
{"Package": "test-deb", "Version": "1.0"},
|
|
{"data/data/com.termux/files/usr/bin/tool": b"\x7fELF"},
|
|
)
|
|
staged = tmp_path / "staged"
|
|
staged.mkdir()
|
|
_P().unpack(deb, staged, "linux-arm64-bionic")
|
|
assert (staged / "data/data/com.termux/files/usr/bin/tool").read_bytes() == b"\x7fELF"
|
|
|
|
# traversal member must be refused
|
|
evil = tmp_path / "evil.deb"
|
|
_build_fake_deb(
|
|
evil, {"Package": "evil", "Version": "1.0"}, {"../escape": b"x"}
|
|
)
|
|
with pytest.raises(Exception):
|
|
_P().unpack(evil, tmp_path / "staged2", "linux-arm64-bionic")
|
|
|
|
|
|
def test_python_bionic_verify_is_file_evidence(tmp_path: Path):
|
|
"""bionic verify never executes the staged binary; presence is the
|
|
contract (the digest already proved the bytes)."""
|
|
from pm.registry import get_package
|
|
|
|
py = get_package("python")
|
|
bin_rel = Path("data/data/com.termux/files/usr/bin/python3.11")
|
|
entry = tmp_path / "entry"
|
|
(entry / bin_rel).parent.mkdir(parents=True)
|
|
(entry / bin_rel).write_bytes(b"bionic-elf-bytes")
|
|
assert py.verify(entry, "linux-arm64-bionic") == ""
|
|
empty = tmp_path / "empty"
|
|
empty.mkdir()
|
|
assert "missing" in py.verify(empty, "linux-arm64-bionic")
|
|
|
|
|
|
def test_bionic_binary_and_env_contract(tmp_path: Path):
|
|
"""On bionic, _BionicDebArm.binary() must return the staged deb's main
|
|
binary path (file evidence, no exec), so the base Package.env contract
|
|
exposes the tool through PATH like every other pm package."""
|
|
from pm.registry import get_package
|
|
|
|
for name in ("uv", "python", "node"):
|
|
pkg = get_package(name)
|
|
entry = tmp_path / name
|
|
main = entry / pkg.prefix_rel / pkg.main_rel("linux-arm64-bionic")
|
|
main.parent.mkdir(parents=True)
|
|
main.write_bytes(b"bionic-elf")
|
|
|
|
binary = pkg.binary(entry, "linux-arm64-bionic")
|
|
assert binary == main, f"{name}.binary() on bionic: {binary}"
|
|
|
|
env = pkg.env(entry, "linux-arm64-bionic")
|
|
assert env.get("PATH") == [str(main.parent)], (
|
|
f"{name}.env() on bionic does not follow the Package.env PATH contract"
|
|
)
|
|
|
|
|
|
def test_stage_only_does_not_record_host_facts(tmp_path, monkeypatch):
|
|
"""stage_only publishes the entry but must not touch this machine's
|
|
installed facts -- the fact slot belongs to the HOST target."""
|
|
pm = _pm()
|
|
from pm.ensure import stage_only
|
|
from pm.lock import Facts
|
|
from pm.paths import facts_path
|
|
|
|
def snapshot() -> dict:
|
|
path = facts_path()
|
|
if not path.is_file():
|
|
return {}
|
|
return Facts(path)._packages
|
|
|
|
before = snapshot()
|
|
entry = stage_only("termux-docker", "linux-arm64-bionic")
|
|
after = snapshot()
|
|
assert before == after
|
|
# termux-docker is a pin_only package: stage_only returns the would-be
|
|
# entry path (store root + entry name) without staging bytes.
|
|
assert "termux-docker" in str(entry)
|