Files
hermes-agent/tests/scripts/test_termux_assembly_permissions.py
2026-10-10 15:20:13 +02:00

82 lines
3.3 KiB
Python

"""Exercise the build mount's real Linux UID permissions, not Android binaries."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import pytest
ROOT = Path(__file__).resolve().parents[2]
# Only the stdlib facts publisher runs here; the bionic tool bytes are never
# executed. A native Linux image keeps this kernel-permissions test off QEMU.
IMAGE = "python@sha256:ffb752e139c0a19692a43af8d8523b274222dd68eebad5d583b45c2201c6e30a"
@pytest.mark.platforms("linux")
@pytest.mark.parametrize("build_exit", [0, 23], ids=["success", "failed-build"])
def test_assembly_publishes_facts_as_termux_uid_and_returns_host_owned_output(tmp_path, build_exit):
if not shutil.which("docker"):
pytest.skip("Docker is required for the bind-mount ownership regression")
available = subprocess.run(
["docker", "info"], capture_output=True, timeout=15,
check=False,
)
if available.returncode:
pytest.skip("Docker daemon is unavailable")
image = subprocess.run(
["docker", "image", "inspect", IMAGE], capture_output=True, timeout=15,
check=False,
)
if image.returncode:
pytest.skip(f"Pre-pull the native Linux test image: docker pull {IMAGE}")
payload = tmp_path / "payload"
payload.mkdir()
for name in ("python", "uv"):
entry = payload / name
entry.mkdir()
(entry / "bytes").write_bytes(b"read-only staged tool bytes")
before = {path: (path.stat().st_uid, path.stat().st_gid, path.stat().st_mode)
for path in payload.rglob("*")}
result = subprocess.run(
["bash", "-c", r'''
set -euo pipefail
IMAGE="$1"; PAYLOAD_ABS="$2"; REPO="$3"
source "$REPO/scripts/termux/assembly_permissions.sh"
prepare_assembly
trap restore_assembly_owner EXIT
docker run --rm --user 1000:1000 --network none \
-v "$ASSEMBLY:/assembly" \
-v "$PAYLOAD_ABS/python:/assembly/tools/python:ro" \
-v "$PAYLOAD_ABS/uv:/assembly/tools/uv:ro" \
-v "$REPO:/app:ro" \
-e HERMES_HOME=/tmp/hermes \
"$IMAGE" sh -ec '
python -B /app/scripts/termux/build_environment.py prepare-tools \
--root /assembly --source-tools /assembly/tools
mkdir /assembly/venv /assembly/pm-runtime
umask 077
printf "private build output" > /assembly/pm-runtime/receipt
stat -c "%u:%g" /assembly/tools/facts.json > /assembly/writer
exit "$1"
' sh "$4"
''', "assembly-permissions-test", IMAGE, str(payload), str(ROOT), str(build_exit)],
cwd=ROOT, capture_output=True, text=True, timeout=90,
check=False,
)
assert result.returncode == build_exit, result.stdout + result.stderr
assembly, = payload.glob(".environments-*")
facts = json.loads((assembly / "tools/facts.json").read_text())
assert set(facts["packages"]) == {"python", "uv"}
assert (assembly / "writer").read_text().strip() == "1000:1000"
assert (assembly / "pm-runtime/receipt").read_text() == "private build output"
for path in [assembly, *assembly.rglob("*")]:
info = path.stat()
assert (info.st_uid, info.st_gid) == (os.getuid(), os.getgid()), path
assert not info.st_mode & 0o022, path
assert {path: (path.stat().st_uid, path.stat().st_gid, path.stat().st_mode)
for path in before} == before
shutil.rmtree(assembly)