diff --git a/.github/workflows/termux-verify.yml b/.github/workflows/termux-verify.yml index f1e38f540c..c314f46e34 100644 --- a/.github/workflows/termux-verify.yml +++ b/.github/workflows/termux-verify.yml @@ -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 diff --git a/pm/store.py b/pm/store.py index b335c2d178..bb99585929 100644 --- a/pm/store.py +++ b/pm/store.py @@ -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: diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh index bb6a84d6ef..4e938e530c 100755 --- a/scripts/run_tests.sh +++ b/scripts/run_tests.sh @@ -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" "$@" diff --git a/scripts/termux/probe_python_linkage.py b/scripts/termux/probe_python_linkage.py index 1f3492d498..fa47b18e14 100644 --- a/scripts/termux/probe_python_linkage.py +++ b/scripts/termux/probe_python_linkage.py @@ -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" diff --git a/scripts/termux/stage_runtime_libs.py b/scripts/termux/stage_runtime_libs.py index e0acd536a5..abbd62d518 100644 --- a/scripts/termux/stage_runtime_libs.py +++ b/scripts/termux/stage_runtime_libs.py @@ -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) diff --git a/tests/test_run_tests_shell.py b/tests/test_run_tests_shell.py new file mode 100644 index 0000000000..684f4a90a8 --- /dev/null +++ b/tests/test_run_tests_shell.py @@ -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