From f847a62d8cc483460bdf0bd21e7468196a2ce137 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Fri, 13 Nov 2026 11:59:54 +0000 Subject: [PATCH] Isolate host Python imports in the PHP CI setup Run both selectors with -I -S -B and invoke the TLS control directly to keep discovery from adding workspace imports. Cover working-directory, PYTHONPATH, site customization and TLS-directory marker injections. Refs #342. --- scripts/ci/setup-php-container.sh | 5 +- tests/ci/test_php_container_launcher.py | 92 +++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/scripts/ci/setup-php-container.sh b/scripts/ci/setup-php-container.sh index 515d5fe..2a1dee7 100755 --- a/scripts/ci/setup-php-container.sh +++ b/scripts/ci/setup-php-container.sh @@ -3,7 +3,7 @@ set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" minor="${1:?Usage: setup-php-container.sh 8.4|8.5}" -image="$(python3 - "$root/scripts/ci/php-container-images.json" "$minor" <<'PY' +image="$(python3 -I -S -B - "$root/scripts/ci/php-container-images.json" "$minor" <<'PY' import json,re,sys images=json.load(open(sys.argv[1])) image=images[sys.argv[2]] @@ -35,8 +35,9 @@ state="${RUNNER_TEMP:?RUNNER_TEMP is required}/sample-app-php-ci" mkdir -p "$state/bin" "$state/tmp" "$state/composer-home/cache" ln -sf "$root/scripts/ci/run-php-container.sh" "$state/bin/php" ln -sf "$root/scripts/ci/run-php-container.sh" "$state/bin/composer" +# Direct isolated execution keeps discovery from adding the checkout to sys.path. SAMPLE_APP_PHP_CI_TEST_IMAGE="$runtime_id" SAMPLE_APP_PHP_CI_ROOT="$root" SAMPLE_APP_PHP_CI_STATE="$state" \ - python3 -m unittest discover -s "$root/tests/ci" -p test_php_container_tls.py -v + python3 -I -S -B "$root/tests/ci/test_php_container_tls.py" -v { echo "SAMPLE_APP_PHP_CI_IMAGE=$runtime_id" echo "SAMPLE_APP_PHP_CI_ROOT=$root" diff --git a/tests/ci/test_php_container_launcher.py b/tests/ci/test_php_container_launcher.py index 09a41bd..e0625ee 100644 --- a/tests/ci/test_php_container_launcher.py +++ b/tests/ci/test_php_container_launcher.py @@ -1,7 +1,10 @@ import json import os import pathlib +import shlex +import shutil import subprocess +import sys import tempfile import unittest @@ -84,5 +87,94 @@ def test_missing_runtime_identity_refuses_before_docker(self): self.assertIn("PHP CI repository root is required", result.stderr) +class PhpContainerSetupImportTests(unittest.TestCase): + def check_import_isolation(self, location, module="json"): + with tempfile.TemporaryDirectory() as directory: + state = pathlib.Path(directory) + source = state / "source" + for relative in ( + "scripts/ci/setup-php-container.sh", + "scripts/ci/run-php-container.sh", + "scripts/ci/php-container-images.json", + "tests/ci/test_php_container_tls.py", + ): + destination = source / relative + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(ROOT / relative, destination) + guard = source / ".central-apt-source/docker/apt/require-fixed-bookworm-apt.sh" + guard.parent.mkdir(parents=True) + guard.touch() + working_directory = state / "working-directory" + import_directory = state / "imports" + working_directory.mkdir() + import_directory.mkdir() + home = state / "home" + home.mkdir() + marker = state / "import-executed" + planted = { + "working-directory": working_directory, + "PYTHONPATH": import_directory, + "TLS-script-directory": source / "tests/ci", + }[location] + (planted / f"{module}.py").write_text( + f"open({str(marker)!r}, 'w').write('marker-only import\\n')\n" + "raise SystemExit(42)\n" + ) + binary_directory = state / "bin" + binary_directory.mkdir() + # Native-command stubs keep this an import-only control: no build, + # container, certificate generation, network request or trust claim. + commands = { + "git": "printf '%s\\n' 18783be6bef737cc778e4c7282a8797e39daa61a\n", + "sha256sum": "cat >/dev/null\n", + "docker": ( + "case \"$1\" in\n" + " build|run) exit 0 ;;\n" + f" image) printf '%s\\n' sha256:{'f' * 64} ;;\n" + " *) exit 91 ;;\nesac\n" + ), + "python3": ( + # Import the real TLS control, but skip its runtime test. + "unset SAMPLE_APP_PHP_CI_TEST_IMAGE\n" + f"exec {shlex.quote(sys.executable)} \"$@\"\n" + ), + } + for name, body in commands.items(): + command = binary_directory / name + command.write_text("#!/bin/sh\nset -eu\n" + body) + command.chmod(0o700) + environment = { + "PATH": str(binary_directory) + os.pathsep + os.defpath, + "HOME": str(home), + "TMPDIR": str(state), + "RUNNER_TEMP": str(state), + "GITHUB_ENV": str(state / "github-env"), + "GITHUB_PATH": str(state / "github-path"), + } + if location == "PYTHONPATH": + environment["PYTHONPATH"] = str(import_directory) + result = subprocess.run( + ["bash", str(source / "scripts/ci/setup-php-container.sh"), "8.4"], + cwd=working_directory, env=environment, text=True, capture_output=True, timeout=15, + ) + self.assertFalse(marker.exists(), f"Executed {location}/{module}.py") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("skipped=1", result.stderr, "The real TLS test module must be imported") + self.assertIn("SAMPLE_APP_PHP_CI_IMAGE=sha256:", (state / "github-env").read_text()) + self.assertEqual(list(source.rglob("__pycache__")), []) + + def test_selector_ignores_working_directory_imports(self): + self.check_import_isolation("working-directory") + + def test_selector_ignores_pythonpath_imports(self): + self.check_import_isolation("PYTHONPATH") + + def test_startup_ignores_pythonpath_site_customization(self): + self.check_import_isolation("PYTHONPATH", "sitecustomize") + + def test_tls_control_does_not_add_its_script_directory_to_imports(self): + self.check_import_isolation("TLS-script-directory") + + if __name__ == "__main__": unittest.main()