fix(termux): classify bionic from its interpreter and run the Linux tests

The POSIX runner had a blank line after exec env, so it printed the environment and never ran pytest. Keep the command attached and prove failure propagation. The actual payload records ANDROID_API_LEVEL in sysconfig; use that instead of looking for text in a guessed libc file. Rebuild unproven runtime-library extracts from verified archives.
This commit is contained in:
ethernet
2026-09-06 14:12:12 -04:00
parent 9d7921cd6e
commit 7f6d63bcbb
6 changed files with 46 additions and 26 deletions

View File

@@ -44,7 +44,8 @@ jobs:
tests/test_termux_python_linkage.py tests/test_termux_retag_wheel.py \
tests/test_termux_wheelhouse_cache.py tests/test_termux_runtime_libs.py \
tests/test_termux_launchers.py tests/test_pm_bionic.py \
tests/pm/test_stage_only.py tests/pm/test_deb_safety.py tests/test_stage_apt_repo.py
tests/pm/test_stage_only.py tests/pm/test_deb_safety.py tests/test_stage_apt_repo.py \
tests/test_run_tests_shell.py
native-linkage:
name: Reproduce and verify bionic Python linkage

View File

@@ -76,17 +76,12 @@ def _is_bionic_libc() -> bool:
"""
if sys.platform == "android":
return True
# Termux python reports sys.platform == "linux"; ask the C runtime.
try:
with open("/system/lib/libc.so", "rb") as f:
return b"Bionic" in f.read(4096)
except OSError:
pass
try:
with open("/lib/libc.so", "rb") as f:
return b"Bionic" in f.read(4096)
except OSError:
return False
# Termux's pre-3.13 interpreter reports Linux but records its Android
# build target in sysconfig. This also works in a container without
# the phone's /system mount or a recognizable string in libc's header.
import sysconfig
return bool(sysconfig.get_config_var("ANDROID_API_LEVEL"))
def current_target() -> str:

View File

@@ -213,5 +213,4 @@ fi
echo "▶ posix: per-file parallel suite via run_tests_parallel.py"
echo " (TZ=UTC LANG=C.UTF-8 PYTHONHASHSEED=0; clean env)"
exec env -i "${HERMETIC_ENV[@]}" \
"$PYTHON" "$SCRIPT_DIR/run_tests_parallel.py" "$@"

View File

@@ -41,6 +41,13 @@ def main() -> int:
ap.add_argument("--output", type=Path, required=True)
ap.add_argument("--built-wheel", type=Path)
args = ap.parse_args()
sys.path.insert(0, str(args.repo))
from pm.store import current_target
target = current_target()
print("PAYLOAD_TARGET", target, "ANDROID_API_LEVEL", sysconfig.get_config_var("ANDROID_API_LEVEL"), flush=True)
if target != "linux-arm64-bionic":
raise RuntimeError(f"payload interpreter misclassified: {target}")
prefix = Path("/data/data/com.termux/files/usr")
python = args.payload / "python" / prefix.relative_to("/") / "bin/python3.11"
uv = args.payload / "uv" / prefix.relative_to("/") / "bin/uv"

View File

@@ -112,22 +112,12 @@ def _ensure_extracted(work: Path, name: str, row: dict) -> Path:
scratch = work / "dl"
scratch.mkdir(parents=True, exist_ok=True)
archive = scratch / f"{name}.deb"
marker = extract / ".deb-sha256"
archive_ok = False
if archive.exists():
try:
archive_ok = _sha256_file(archive) == row["sha256"]
except OSError:
archive_ok = False
extraction_ok = (
any(extract.glob(f"{PREFIX_REL}/lib/*.so*"))
and marker.is_file()
and marker.read_text(encoding="utf-8").strip() == row["sha256"]
)
if extraction_ok and archive_ok:
return extract
if not archive_ok:
Download([Source(row["url"], archive, row["sha256"])],
partials_dir=scratch).run()
@@ -135,7 +125,6 @@ def _ensure_extracted(work: Path, name: str, row: dict) -> Path:
shutil.rmtree(extract)
extract.mkdir(parents=True, exist_ok=True)
_LibDeb(name).unpack(archive, extract, "linux-arm64-bionic")
marker.write_text(row["sha256"], encoding="utf-8")
return extract
@@ -156,8 +145,8 @@ def stage(payload: Path, table: dict) -> Path:
f"{len(list(out.glob('*.so*')))} .so* -> {out}")
return out
# Miss: rebuild the merged dir COMPLETELY from scratch. Nothing from
# a previous partial or corrupted state survives.
# A cache miss rebuilds from verified archives, never scratch extracts.
manifest_path.unlink(missing_ok=True)
if out.exists():
shutil.rmtree(out)
out.mkdir(parents=True, exist_ok=True)

View File

@@ -0,0 +1,29 @@
"""The canonical shell must run tests and propagate a failing test's status."""
from pathlib import Path
import os
import subprocess
import sys
import pytest
@pytest.mark.platforms("posix")
def test_shell_runner_executes_tests_and_propagates_failure(tmp_path):
case = tmp_path / "test_runner_canary.py"
marker = tmp_path / "executed"
case.write_text(
"from pathlib import Path\n"
"def test_canary():\n"
f" Path({str(marker)!r}).write_text('executed')\n"
" assert False, 'runner failure propagation canary'\n",
encoding="utf-8",
)
root = Path(__file__).resolve().parents[1]
result = subprocess.run(
["bash", str(root / "scripts/run_tests.sh"), "-j", "1", str(case)],
cwd=tmp_path, capture_output=True, text=True, timeout=180,
env={**os.environ, "HERMES_PYTHON": sys.executable, "HERMES_TEST_FILE_RETRIES": "0"},
)
assert marker.read_text(encoding="utf-8") == "executed", result.stdout + result.stderr
assert result.returncode != 0, result.stdout + result.stderr
assert "runner failure propagation canary" in result.stdout + result.stderr