Files
hermes-agent/tests/scripts/test_termux_assembly_permissions.py
ethernet febb908bd1 fix(build): preserve toolchain state and Termux assembly ownership
Reuse a matching ARM64 Visual Studio environment instead of growing its paths on each initialization. Preserve Rust and Cargo homes before isolating bundle state. Hand the private Termux assembly directory to its container user and restore host ownership afterward.

Verified targeted tests, real ARM64 SDK/OpenSSL execution, Rust compilation, and container facts publication. Full release packages were not rebuilt. The Windows x64 source-build timeout and the unchanged Termux linkage fixture failure remain unresolved.
2026-09-12 02:05:48 -04:00

79 lines
3.2 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,
)
if available.returncode:
pytest.skip("Docker daemon is unavailable")
image = subprocess.run(
["docker", "image", "inspect", IMAGE], capture_output=True, timeout=15,
)
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,
)
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)