From f0b5ec615a844b85d03b8ddabd44b8a3505da86e Mon Sep 17 00:00:00 2001 From: PurHur Date: Thu, 21 May 2026 11:59:50 +0000 Subject: [PATCH] Add tiered local CI for fast iteration and bounded LLVM RAM (#436) Introduce ci-fast.sh for VM/compliance-only runs, split full ci-local.sh LLVM work into aot-lint, jit, and aot-link phases with an optional PHP_COMPILER_CI_RAM_GB ulimit, and wire make/phpc/docker entrypoints. Co-authored-by: Cursor --- .cursor/rules/local-ci-only.mdc | 9 +- Makefile | 14 ++- README.md | 10 +- bin/phpc.php | 11 +- docs/bootstrap-inventory.md | 77 ++++++------ lib/Doctor.php | 2 + script/ci-common.sh | 129 +++++++++++++++++++++ script/ci-fast.sh | 25 ++++ script/ci-local.sh | 116 ++++-------------- script/ci-resource-limits.sh | 23 ++++ script/docker-ci-local.sh | 15 ++- test/aot/AotTest.php | 1 + test/aot/BootstrapAotLintTest.php | 1 + test/aot/ExampleWebAotTest.php | 1 + test/aot/NestedSuperglobalsAotTest.php | 1 + test/aot/RuntimeSuperglobalRefreshTest.php | 1 + test/aot/StdlibWebBuiltinsTest.php | 1 + test/aot/WebAppNumberFormatTest.php | 1 + test/real/ServeAotTest.php | 1 + test/unit/CiScriptsTest.php | 40 +++++++ test/unit/ExamplesCompileTest.php | 7 ++ 21 files changed, 340 insertions(+), 146 deletions(-) create mode 100644 script/ci-common.sh create mode 100755 script/ci-fast.sh create mode 100755 script/ci-resource-limits.sh create mode 100644 test/unit/CiScriptsTest.php diff --git a/.cursor/rules/local-ci-only.mdc b/.cursor/rules/local-ci-only.mdc index a752e7b5560..dc59d775e5d 100644 --- a/.cursor/rules/local-ci-only.mdc +++ b/.cursor/rules/local-ci-only.mdc @@ -12,14 +12,17 @@ This project **does not use GitHub Actions for CI** (billing limits). Do **not** From repo root, after `composer install`: ```bash -./script/ci-local.sh +./script/ci-local.sh # full gate (JIT + AOT lint + link) +./script/ci-fast.sh # VM/compliance only while iterating (#436) ``` Or in Docker (preferred on harness hosts without host PHP/LLVM): ```bash make docker-build-22 # once -make test # or: ./script/docker-ci-local.sh +make test # full CI +make test-fast # fast CI (no LLVM compile) +# or: ./script/docker-ci-local.sh ``` Image: `php-compiler:22.04-dev` (`Docker/dev/ubuntu-22.04/Dockerfile`). @@ -41,4 +44,4 @@ If `-v "$(pwd):/compiler"` shows an empty tree, use: ## Before merge -Run the same suite you would expect from CI locally. Do not assume cloud workflows will run. +Run `./script/ci-local.sh` (full LLVM phases). Use `ci-fast.sh` only for iteration. Set `PHP_COMPILER_CI_RAM_GB` (default 100) if LLVM phases exhaust host RAM (#436). diff --git a/Makefile b/Makefile index 87021cedecf..7d0f4ce9d39 100755 --- a/Makefile +++ b/Makefile @@ -81,10 +81,17 @@ test-legacy-16: rebuild-changed docker run -v $(shell pwd):/compiler ircmaxell/php-compiler:16.04-dev php vendor/bin/phpunit # Run the full PHPUnit suite on the host PHP (no Docker). Requires composer install. -.PHONY: test-local +.PHONY: test-local test-fast test-docker-fast test-local: ./script/ci-local.sh +# Fast CI: VM/compliance only — no JIT/AOT compile (issue #436). +test-fast: + ./script/ci-fast.sh + +test-docker-fast: docker-build-22 + ./script/docker-ci-local.sh fast + # VM smoke: examples/001-SimpleWeb with ?name=Test .PHONY: web-smoke web-smoke: @@ -129,10 +136,9 @@ test-docker: docker-build-22 test-harness: ./script/docker-ci-local.sh $(ARGS) -# Quick PHPUnit in 22.04 dev image (after composer install on host or in container) +# Quick PHPUnit in 22.04 dev image (deprecated: prefer test-docker-fast / ci-fast.sh) .PHONY: test-docker-quick -test-docker-quick: - docker run --rm -v $(shell pwd):/compiler -w /compiler $(LOCAL_DEV_IMAGE) php vendor/bin/phpunit --exclude-group llvm +test-docker-quick: test-docker-fast .PHONY: bootstrap-inventory bootstrap-profile bootstrap-aot-lint bootstrap-inventory: diff --git a/README.md b/README.md index 9065e8dd9a8..791a8ff32f2 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CircleCI](https://circleci.com/gh/ircmaxell/php-compiler.svg?style=svg)](https://circleci.com/gh/ircmaxell/php-compiler) -**CI:** run `./script/ci-local.sh` on the host or `make test` in Docker (`php-compiler:22.04-dev`). GitHub Actions workflows are disabled (see [#394](https://github.com/PurHur/php-compiler/issues/394)). +**CI:** full gate `./script/ci-local.sh` or `make test`; fast iteration `./script/ci-fast.sh` or `make test-fast` ([#436](https://github.com/PurHur/php-compiler/issues/436)). Docker image: `php-compiler:22.04-dev`. GitHub Actions workflows are disabled (see [#394](https://github.com/PurHur/php-compiler/issues/394)). Ok, so this used to be a dead project. It required calling out to all sorts of hackery to generate PHP extensions, or PHP itself. @@ -18,7 +18,8 @@ On a modern Linux host with PHP 8.1+ (8.2 recommended): git clone https://github.com/PurHur/php-compiler.git cd php-compiler composer install -./phpc test # full PHPUnit suite (VM, compliance, JIT, AOT) +./phpc test --fast # VM/compliance only (no LLVM compile) +./phpc test # full suite (VM, JIT, AOT lint + link) mkdir my-app && ./phpc init my-app # phpc.json + public/index.php scaffold ./phpc run -r 'echo 1;' # VM mode (or: php bin/vm.php -r 'echo 1;') ./phpc run -q 'name=Dev' examples/001-SimpleWeb/example.php # web example without TCP @@ -72,13 +73,16 @@ composer install | `PHP_COMPILER_SKIP_SERVE_TESTS` | Skip `ServeTest` / `ServeAotTest` (use in sandboxes that cannot bind TCP) | | `PHP_COMPILER_RUN_SERVE_TESTS` | Force HTTP serve integration tests even when loopback bind probe fails | | `PHP_COMPILER_ALLOW_JIT_SKIP` | Do not fail `ci-local.sh` when LLVM is present but JIT compliance tests are 100% skipped (broken dev env only) | +| `PHP_COMPILER_CI_RAM_GB` | Virtual-memory cap (`ulimit -v`) during LLVM phases in `ci-local.sh` (default `100`; set `0` to disable) | -`script/ci-local.sh` sets LLVM paths automatically when `.llvm/libLLVM-9.so.1` exists. It probes `127.0.0.1` bind capability and runs `@group serve` tests when allowed. **Local and Docker CI** (`make test-docker`, `./script/docker-ci-local.sh`) should run those tests — only set `PHP_COMPILER_SKIP_SERVE_TESTS=1` when loopback bind is unavailable. +`script/ci-local.sh` sets LLVM paths automatically when `.llvm/libLLVM-9.so.1` exists. It runs LLVM work in phases (`aot-lint`, `jit`, `aot-link`) so compile subprocesses exit between stages ([#436](https://github.com/PurHur/php-compiler/issues/436)). Use `script/ci-fast.sh` (or `phpc test --fast`) while iterating — same VM/compliance gate without JIT/AOT. It probes `127.0.0.1` bind capability and runs `@group serve` tests when allowed. **Local and Docker CI** (`make test-docker`, `./script/docker-ci-local.sh`) should run those tests — only set `PHP_COMPILER_SKIP_SERVE_TESTS=1` when loopback bind is unavailable. ### Running tests on the host ```console +make test-fast # VM/compliance only (no LLVM) make test-local # same as ./script/ci-local.sh +./script/ci-fast.sh --filter VMTest ./script/ci-local.sh --filter VMTest make web-smoke make examples-web-smoke # HTTP serve + curl (skipped when loopback bind fails) diff --git a/bin/phpc.php b/bin/phpc.php index 0f128adf42e..086df949fea 100755 --- a/bin/phpc.php +++ b/bin/phpc.php @@ -16,7 +16,7 @@ * phpc lint --project [--json] * phpc lint --all [--json] * phpc init [--force] [target-dir] - * phpc test [-- phpunit/ci-local args...] + * phpc test [--fast] [-- phpunit/ci-local args...] * phpc doctor Probe PHP, LLVM, deps, loopback (issue #253) * phpc validate-manifest [dir] Validate phpc.json schema and paths (issue #263) */ @@ -43,7 +43,7 @@ phpc lint --project [--json] Entry + literal include/require chain phpc lint --all [--json] All .php under a tree (aggregated) phpc init [--force] [target-dir] Scaffold phpc.json + public/index.php - phpc test [args...] Run ./script/ci-local.sh + phpc test [--fast] [args...] Run ci-local.sh (full) or ci-fast.sh (no LLVM) phpc doctor Probe environment for full local CI phpc validate-manifest [dir] Validate phpc.json (default: cwd) @@ -94,7 +94,12 @@ exit(runProcess(array_merge($php, [$repoRoot.'/bin/init.php'], $args), $repoRoot)); case 'test': - $testScript = $repoRoot.'/script/ci-local.sh'; + $fast = false; + if ([] !== $args && in_array($args[0], ['--fast', 'fast'], true)) { + $fast = true; + array_shift($args); + } + $testScript = $repoRoot.'/script/'.($fast ? 'ci-fast.sh' : 'ci-local.sh'); if (!is_executable($testScript)) { fwrite(STDERR, "phpc test: {$testScript} is not executable\n"); exit(1); diff --git a/docs/bootstrap-inventory.md b/docs/bootstrap-inventory.md index 4e92106f1f1..d8148dcad33 100644 --- a/docs/bootstrap-inventory.md +++ b/docs/bootstrap-inventory.md @@ -10,7 +10,7 @@ Regenerate: `php script/bootstrap-inventory.php` |--------|------:| | PHP files on vm.php path | 289 | | Source constructs flagged (blockers) | 10 | -| Source constructs flagged (warnings) | 753 | +| Source constructs flagged (warnings) | 754 | ## Compiler CFG gaps (`lib/Compiler.php`) @@ -222,7 +222,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: | `lib/AOT/Linker.php` | 8 | 1 | | `lib/Block.php` | 0 | 4 | | `lib/Cli/PhpcInit.php` | 0 | 1 | -| `lib/Compiler.php` | 0 | 84 | +| `lib/Compiler.php` | 0 | 85 | | `lib/Doctor.php` | 0 | 1 | | `lib/Frame.php` | 0 | 1 | | `lib/Func.php` | 0 | 1 | @@ -1532,39 +1532,40 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new OpCode (line 650) - new Block (line 657) - new OpCode (line 668) -- new OpCode (line 676) -- new OpCode (line 684) -- new OpCode (line 693) -- new OpCode (line 699) -- new OpCode (line 707) -- new OpCode (line 710) -- new OpCode (line 716) -- new OpCode (line 736) -- new Block (line 755) -- new Block (line 759) -- new Operand\Literal (line 762) -- new OpCode (line 765) -- new OpCode (line 771) -- new Block (line 775) +- new OpCode (line 677) +- new OpCode (line 685) +- new OpCode (line 694) +- new OpCode (line 700) +- new OpCode (line 708) +- new OpCode (line 711) +- new OpCode (line 717) +- new OpCode (line 735) +- new OpCode (line 750) +- new Block (line 769) +- new Block (line 773) +- new Operand\Literal (line 776) - new OpCode (line 779) - new OpCode (line 785) -- new Block (line 869) -- new Block (line 872) -- new OpCode (line 875) -- new OpCode (line 881) +- new Block (line 789) +- new OpCode (line 793) +- new OpCode (line 799) +- new Block (line 883) +- new Block (line 886) +- new OpCode (line 889) - new OpCode (line 895) -- new Block (line 902) -- new OpCode (line 905) -- new OpCode (line 915) -- new Temporary (line 925) -- new Variable (line 935) -- new Operand\Temporary (line 937) -- new Variable (line 1030) -- new OpCode (line 1060) -- new OpCode (line 1066) -- new OpCode (line 1070) -- new OpCode (line 1075) -- 34 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler +- new OpCode (line 909) +- new Block (line 916) +- new OpCode (line 919) +- new OpCode (line 929) +- new Temporary (line 939) +- new Variable (line 949) +- new Operand\Temporary (line 951) +- new Variable (line 1044) +- new OpCode (line 1074) +- new OpCode (line 1080) +- new OpCode (line 1084) +- new OpCode (line 1089) +- 35 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler - 1 closure(s) ### `lib/Doctor.php` @@ -1851,8 +1852,8 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new Scope (line 120) - new Result (line 212) - new Variable (line 288) -- new Variable (line 501) -- new Variable (line 533) +- new Variable (line 512) +- new Variable (line 544) - 32 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/JIT/HashTableHelper.php` @@ -1938,7 +1939,7 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: **Warnings** (review for bootstrap subset): - new Variable (line 128) -- new VMVariable (line 152) +- new VMVariable (line 155) - 7 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/JIT/ValueEchoHelper.php` @@ -1962,9 +1963,9 @@ These `LogicException` messages indicate CFG ops or expressions not yet lowered: - new Variable (line 384) - new Variable (line 405) - new Variable (line 429) -- new Variable (line 447) -- new Variable (line 474) -- new Variable (line 495) +- new Variable (line 451) +- new Variable (line 486) +- new Variable (line 507) - 15 class method(s) — PHPCfg Op\Stmt\ClassMethod not lowered in Compiler ### `lib/Lint/IncrementDetector.php` diff --git a/lib/Doctor.php b/lib/Doctor.php index 1ef0530f30a..337b17f4ad4 100644 --- a/lib/Doctor.php +++ b/lib/Doctor.php @@ -45,12 +45,14 @@ public static function run(string $repoRoot): int if ($failed > 0) { fwrite(STDOUT, "\n".$failed.' required check(s) failed.'."\n"); fwrite(STDOUT, "Full local CI: ./script/ci-local.sh or ./script/docker-ci-local.sh (make test-docker)\n"); + fwrite(STDOUT, "Fast iteration (no LLVM): ./script/ci-fast.sh or phpc test --fast (make test-fast)\n"); return 1; } fwrite(STDOUT, "\nEnvironment ready for full local CI (VM + LLVM + serve when loopback bind OK).\n"); fwrite(STDOUT, "Run: phpc test or ./script/docker-ci-local.sh\n"); + fwrite(STDOUT, "Fast (VM only): phpc test --fast or ./script/ci-fast.sh\n"); return 0; } diff --git a/script/ci-common.sh b/script/ci-common.sh new file mode 100644 index 00000000000..31d01f9563e --- /dev/null +++ b/script/ci-common.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# Shared CI bootstrap for ci-fast.sh and ci-local.sh (issue #436). +set -euo pipefail + +_CI_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +_CI_REPO_ROOT="$(cd "$_CI_SCRIPT_DIR/.." && pwd)" + +# shellcheck source=php-env.sh +source "$_CI_SCRIPT_DIR/php-env.sh" +# shellcheck source=ci-resource-limits.sh +source "$_CI_SCRIPT_DIR/ci-resource-limits.sh" + +ci_repo_root() { + printf '%s\n' "$_CI_REPO_ROOT" +} + +ci_cd_repo() { + cd "$_CI_REPO_ROOT" +} + +ci_install_deps() { + local ext_dir="$PHP_COMPILER_EXT_DIR" + if command -v composer >/dev/null 2>&1 && composer --version >/dev/null 2>&1; then + COMPOSER=(composer) + elif [[ -f /tmp/composer.phar ]]; then + COMPOSER=("$PHP_BIN" -d "extension=$ext_dir/phar.so" -d "extension=$ext_dir/mbstring.so" /tmp/composer.phar) + else + python3 -c "import urllib.request; urllib.request.urlretrieve('https://getcomposer.org/download/latest-stable/composer.phar','/tmp/composer.phar')" + COMPOSER=("$PHP_BIN" -d "extension=$ext_dir/phar.so" -d "extension=$ext_dir/mbstring.so" /tmp/composer.phar) + fi + "${COMPOSER[@]}" install --no-interaction --ignore-platform-reqs 2>/dev/null || true + + chmod +x script/install-llvm9.sh script/apply-patches.sh 2>/dev/null || true + if [[ -z "${PHP_COMPILER_LLVM_PATH:-}" || ! -f "${PHP_COMPILER_LLVM_PATH}/libLLVM-9.so.1" ]]; then + if [[ -x script/install-llvm9.sh ]]; then + script/install-llvm9.sh || true + fi + fi + if [[ -x script/apply-patches.sh ]]; then + script/apply-patches.sh || true + fi +} + +ci_run_inventory_checks() { + "$PHP_BIN" "${PHP_OPTS[@]}" script/capability-matrix.php --check + "$PHP_BIN" "${PHP_OPTS[@]}" script/bootstrap-inventory.php --check + "$PHP_BIN" "${PHP_OPTS[@]}" script/bootstrap-profile.php --check +} + +ci_llvm_dir() { + LLVM_DIR="${PHP_COMPILER_LLVM_PATH:-$_CI_REPO_ROOT/.llvm}" + printf '%s\n' "$LLVM_DIR" +} + +ci_report_llvm_status() { + local llvm_dir + llvm_dir="$(ci_llvm_dir)" + if [[ -f "$llvm_dir/libLLVM-9.so.1" ]]; then + echo "LLVM 9 found at $llvm_dir: JIT compliance, AOT fixtures (simple_web_*, static_web), and ExampleWebAotTest will run." + else + echo "LLVM 9 missing: @group llvm tests (JIT, AOT, web AOT) are skipped. Run: script/install-llvm9.sh" + fi +} + +ci_can_bind_loopback() { + "$PHP_BIN" "${PHP_OPTS[@]}" script/can-bind-loopback.php +} + +ci_configure_serve_tests() { + if [[ -n "${PHP_COMPILER_SKIP_SERVE_TESTS:-}" ]]; then + echo "HTTP serve integration tests skipped (PHP_COMPILER_SKIP_SERVE_TESTS is set)." + return + fi + if [[ "${PHP_COMPILER_RUN_SERVE_TESTS:-}" == "1" ]]; then + echo "HTTP serve integration tests forced (PHP_COMPILER_RUN_SERVE_TESTS=1)." + return + fi + if ci_can_bind_loopback; then + echo "Loopback TCP bind OK: ServeTest and ServeAotTest will run." + return + fi + export PHP_COMPILER_SKIP_SERVE_TESTS=1 + echo "Cannot bind 127.0.0.1 — skipping @group serve tests." + echo " Set PHP_COMPILER_RUN_SERVE_TESTS=1 to force, or PHP_COMPILER_SKIP_SERVE_TESTS=1 to silence." +} + +ci_llvm_ready() { + local llvm_dir + llvm_dir="$(ci_llvm_dir)" + [[ -f "$llvm_dir/libLLVM-9.so.1" ]] +} + +ci_run_bootstrap_aot_lint() { + echo "Bootstrap AOT lint (issue #212 Phase B)..." + set +e + "$PHP_BIN" "${PHP_OPTS[@]}" script/bootstrap-aot-lint.php + local bootstrap_lint_code=$? + set -e + if [[ "$bootstrap_lint_code" -eq 0 ]]; then + : + elif [[ "$bootstrap_lint_code" -eq 2 ]]; then + echo "bootstrap-aot-lint skipped (LLVM 9 not available)." + else + exit 1 + fi +} + +ci_should_run_jit() { + if [[ -n "${PHP_COMPILER_FORCE_JIT_TESTS:-}" ]]; then + echo "JIT compliance forced (PHP_COMPILER_FORCE_JIT_TESTS=1)." + return 0 + fi + if "$PHP_BIN" "${PHP_OPTS[@]}" script/jit-runtime-probe.php; then + return 0 + fi + echo "JIT MCJIT probe failed (segfault or bad output); skipping @group jit." + echo " Re-run with PHP_COMPILER_FORCE_JIT_TESTS=1 after fixing bin/jit.php / LLVM 9." + return 1 +} + +ci_guard_jit_compliance() { + local junit_path="$1" + local llvm_dir="$2" + if [[ -n "${PHP_COMPILER_ALLOW_JIT_SKIP:-}" ]]; then + echo "JIT compliance guard skipped (PHP_COMPILER_ALLOW_JIT_SKIP is set)." + return 0 + fi + "$PHP_BIN" "${PHP_OPTS[@]}" script/check-jit-compliance-ran.php "$junit_path" "$llvm_dir" +} diff --git a/script/ci-fast.sh b/script/ci-fast.sh new file mode 100755 index 00000000000..4988bb5db2e --- /dev/null +++ b/script/ci-fast.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Fast local CI: VM/compliance + optional serve — no LLVM compile phases (issue #436). +# +# Use while iterating on compiler/VM changes. Before merge, run ./script/ci-local.sh +# (or: phpc test, make test) for the full JIT/AOT/link gate. +set -euo pipefail + +# shellcheck source=ci-common.sh +source "$(dirname "$0")/ci-common.sh" + +ci_cd_repo +ci_install_deps +ci_run_inventory_checks +ci_report_llvm_status +ci_configure_serve_tests + +echo "PHPUnit (fast): VM, compliance, real-world — excluding @group llvm..." +"$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --exclude-group llvm,serve "$@" + +if [[ -z "${PHP_COMPILER_SKIP_SERVE_TESTS:-}" ]]; then + echo "PHPUnit (fast): HTTP serve (bin/serve.php, no AOT compile)..." + "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --group serve --exclude-group llvm "$@" +fi + +echo "Fast CI finished. Full LLVM compile gate: ./script/ci-local.sh (issue #436)." diff --git a/script/ci-local.sh b/script/ci-local.sh index 52f2945d81f..9bfbf926164 100755 --- a/script/ci-local.sh +++ b/script/ci-local.sh @@ -1,67 +1,15 @@ #!/usr/bin/env bash # Local CI baseline: install deps and run the full PHPUnit suite (no Docker). set -euo pipefail -cd "$(dirname "$0")/.." -# shellcheck source=php-env.sh -source "$(dirname "$0")/php-env.sh" -EXT_DIR="$PHP_COMPILER_EXT_DIR" -if command -v composer >/dev/null 2>&1 && composer --version >/dev/null 2>&1; then - COMPOSER=(composer) -elif [[ -f /tmp/composer.phar ]]; then - COMPOSER=("$PHP_BIN" -d "extension=$EXT_DIR/phar.so" -d "extension=$EXT_DIR/mbstring.so" /tmp/composer.phar) -else - python3 -c "import urllib.request; urllib.request.urlretrieve('https://getcomposer.org/download/latest-stable/composer.phar','/tmp/composer.phar')" - COMPOSER=("$PHP_BIN" -d "extension=$EXT_DIR/phar.so" -d "extension=$EXT_DIR/mbstring.so" /tmp/composer.phar) -fi -"${COMPOSER[@]}" install --no-interaction --ignore-platform-reqs 2>/dev/null || true - -chmod +x script/install-llvm9.sh script/apply-patches.sh 2>/dev/null || true -if [[ -z "${PHP_COMPILER_LLVM_PATH:-}" || ! -f "${PHP_COMPILER_LLVM_PATH}/libLLVM-9.so.1" ]]; then - if [[ -x script/install-llvm9.sh ]]; then - script/install-llvm9.sh || true - fi -fi -if [[ -x script/apply-patches.sh ]]; then - script/apply-patches.sh || true -fi - -"$PHP_BIN" "${PHP_OPTS[@]}" script/capability-matrix.php --check -"$PHP_BIN" "${PHP_OPTS[@]}" script/bootstrap-inventory.php --check -"$PHP_BIN" "${PHP_OPTS[@]}" script/bootstrap-profile.php --check - -LLVM_DIR="${PHP_COMPILER_LLVM_PATH:-$(cd "$(dirname "$0")/.." && pwd)/.llvm}" -if [[ -f "$LLVM_DIR/libLLVM-9.so.1" ]]; then - echo "LLVM 9 found at $LLVM_DIR: JIT compliance, AOT fixtures (simple_web_*, static_web), and ExampleWebAotTest will run." -else - echo "LLVM 9 missing: @group llvm tests (JIT, AOT, web AOT) are skipped. Run: script/install-llvm9.sh" -fi - -# HTTP serve integration tests (ServeTest, ServeAotTest) need loopback TCP. -# Local/Docker CI should run serve tests unless loopback bind fails or PHP_COMPILER_SKIP_SERVE_TESTS is set. -can_bind_loopback() { - "$PHP_BIN" "${PHP_OPTS[@]}" script/can-bind-loopback.php -} +# shellcheck source=ci-common.sh +source "$(dirname "$0")/ci-common.sh" -configure_serve_tests() { - if [[ -n "${PHP_COMPILER_SKIP_SERVE_TESTS:-}" ]]; then - echo "HTTP serve integration tests skipped (PHP_COMPILER_SKIP_SERVE_TESTS is set)." - return - fi - if [[ "${PHP_COMPILER_RUN_SERVE_TESTS:-}" == "1" ]]; then - echo "HTTP serve integration tests forced (PHP_COMPILER_RUN_SERVE_TESTS=1)." - return - fi - if can_bind_loopback; then - echo "Loopback TCP bind OK: ServeTest and ServeAotTest will run." - return - fi - export PHP_COMPILER_SKIP_SERVE_TESTS=1 - echo "Cannot bind 127.0.0.1 — skipping @group serve tests." - echo " Set PHP_COMPILER_RUN_SERVE_TESTS=1 to force, or PHP_COMPILER_SKIP_SERVE_TESTS=1 to silence." -} - -configure_serve_tests +ci_cd_repo +ci_install_deps +ci_run_inventory_checks +ci_report_llvm_status +ci_configure_serve_tests echo "PHPUnit: VM, compliance (no LLVM), real-world (includes ExamplesCompileTest VM lint/smoke)..." "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --exclude-group llvm,serve "$@" @@ -71,41 +19,27 @@ if [[ -z "${PHP_COMPILER_SKIP_SERVE_TESTS:-}" ]]; then "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --group serve "$@" fi -if [[ -f "$LLVM_DIR/libLLVM-9.so.1" ]]; then - RUN_JIT=1 - if [[ -n "${PHP_COMPILER_FORCE_JIT_TESTS:-}" ]]; then - echo "JIT compliance forced (PHP_COMPILER_FORCE_JIT_TESTS=1)." - elif ! "$PHP_BIN" "${PHP_OPTS[@]}" script/jit-runtime-probe.php; then - RUN_JIT=0 - echo "JIT MCJIT probe failed (segfault or bad output); running @group aot only." - echo " Re-run with PHP_COMPILER_FORCE_JIT_TESTS=1 after fixing bin/jit.php / LLVM 9." - fi +if ci_llvm_ready; then + ci_apply_resource_limits + ci_run_bootstrap_aot_lint - echo "Bootstrap AOT lint (issue #212 Phase B)..." - set +e - "$PHP_BIN" "${PHP_OPTS[@]}" script/bootstrap-aot-lint.php - bootstrap_lint_code=$? - set -e - if [[ "$bootstrap_lint_code" -eq 0 ]]; then - : - elif [[ "$bootstrap_lint_code" -eq 2 ]]; then - echo "bootstrap-aot-lint skipped (LLVM 9 not available)." - else - exit 1 - fi + if ci_should_run_jit; then + echo "PHPUnit: AOT lint only (@group aot-lint)..." + "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --group aot-lint "$@" - if [[ "$RUN_JIT" -eq 1 ]]; then - echo "PHPUnit: JIT + AOT (@group llvm, excluding serve)..." - LLVM_JUNIT="$(mktemp "${TMPDIR:-/tmp}/llvm-junit.XXXXXX.xml")" - "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --group llvm --exclude-group serve --log-junit "$LLVM_JUNIT" "$@" - if [[ -n "${PHP_COMPILER_ALLOW_JIT_SKIP:-}" ]]; then - echo "JIT compliance guard skipped (PHP_COMPILER_ALLOW_JIT_SKIP is set)." - else - "$PHP_BIN" "${PHP_OPTS[@]}" script/check-jit-compliance-ran.php "$LLVM_JUNIT" "$LLVM_DIR" - fi + echo "PHPUnit: JIT compliance (@group jit)..." + LLVM_JUNIT="$(mktemp "${TMPDIR:-/tmp}/llvm-jit-junit.XXXXXX.xml")" + "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --group jit --log-junit "$LLVM_JUNIT" "$@" + ci_guard_jit_compliance "$LLVM_JUNIT" "$(ci_llvm_dir)" rm -f "$LLVM_JUNIT" + + echo "PHPUnit: AOT link + execute (@group aot-link, excluding serve)..." + "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --group aot-link --exclude-group serve "$@" else - echo "PHPUnit: AOT (@group aot — PHPT fixtures, web examples, compile lint)..." - "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --group aot "$@" + echo "PHPUnit: AOT lint (@group aot-lint)..." + "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --group aot-lint "$@" + + echo "PHPUnit: AOT link + execute (@group aot-link — PHPT fixtures, web examples)..." + "$PHP_BIN" "${PHP_OPTS[@]}" vendor/bin/phpunit --group aot-link --exclude-group serve "$@" fi fi diff --git a/script/ci-resource-limits.sh b/script/ci-resource-limits.sh new file mode 100755 index 00000000000..537af33cc1f --- /dev/null +++ b/script/ci-resource-limits.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Apply virtual-memory cap for LLVM compile phases (issue #436). +# Child processes (compile.php, jit.php) inherit the shell ulimit at fork time. +# +# PHP_COMPILER_CI_RAM_GB — soft cap in GiB (default 100; hosts with ~126 GiB RAM). +# Set to 0 to disable ulimit -v for this CI run. +ci_apply_resource_limits() { + local gb="${PHP_COMPILER_CI_RAM_GB:-100}" + if [[ "$gb" == "0" ]]; then + echo "CI resource limits disabled (PHP_COMPILER_CI_RAM_GB=0)." + return 0 + fi + if ! [[ "$gb" =~ ^[0-9]+$ ]] || [[ "$gb" -lt 1 ]]; then + echo "CI resource limits: invalid PHP_COMPILER_CI_RAM_GB=${gb}; skipping ulimit." + return 0 + fi + local kb=$((gb * 1024 * 1024)) + if ulimit -v "$kb" 2>/dev/null; then + echo "CI virtual memory cap: ${gb} GiB (ulimit -v ${kb} KB; issue #436)." + else + echo "CI resource limits: could not set ulimit -v (continuing without cap)." + fi +} diff --git a/script/docker-ci-local.sh b/script/docker-ci-local.sh index 4e8e0b5fd4b..9835ec7229d 100755 --- a/script/docker-ci-local.sh +++ b/script/docker-ci-local.sh @@ -1,20 +1,27 @@ #!/usr/bin/env bash -# Run script/ci-local.sh inside the PHP 8.2 dev container (issues #202, #73, #272). +# Run script/ci-local.sh (or ci-fast.sh) inside the PHP 8.2 dev container (issues #202, #73, #272). # Harness hosts where bind-mounts appear empty can pipe the repo via tar instead. # Preferred entrypoint on Runforge: make test-harness (same as this script). +# Usage: ./script/docker-ci-local.sh [fast] [phpunit args...] set -euo pipefail cd "$(dirname "$0")/.." IMAGE="${PHP_COMPILER_DEV_IMAGE:-php-compiler:22.04-dev}" +CI_SCRIPT=ci-local.sh +if [[ "${1:-}" == "fast" ]]; then + CI_SCRIPT=ci-fast.sh + shift +fi + if ! docker image inspect "$IMAGE" >/dev/null 2>&1; then echo "Building dev image $IMAGE (make docker-build-22)..." make docker-build-22 fi -if [[ -f vendor/bin/phpunit ]] && docker run --rm -v "$(pwd):/compiler" -w /compiler "$IMAGE" test -f script/ci-local.sh 2>/dev/null; then - exec docker run --rm -v "$(pwd):/compiler" -w /compiler "$IMAGE" bash script/ci-local.sh "$@" +if [[ -f vendor/bin/phpunit ]] && docker run --rm -v "$(pwd):/compiler" -w /compiler "$IMAGE" test -f "script/${CI_SCRIPT}" 2>/dev/null; then + exec docker run --rm -v "$(pwd):/compiler" -w /compiler "$IMAGE" bash "script/${CI_SCRIPT}" "$@" fi echo "Bind-mount has no vendor/; copying repo into container via tar..." quoted=$(printf '%q ' "$@") -tar -cf - --exclude='.git' --exclude='.llvm' . | docker run --rm -i -w /compiler "$IMAGE" bash -c "tar -xf - && ./script/ci-local.sh ${quoted}" +tar -cf - --exclude='.git' --exclude='.llvm' . | docker run --rm -i -w /compiler "$IMAGE" bash -c "tar -xf - && ./script/${CI_SCRIPT} ${quoted}" diff --git a/test/aot/AotTest.php b/test/aot/AotTest.php index 9a52a1cab13..d72a7c467b2 100644 --- a/test/aot/AotTest.php +++ b/test/aot/AotTest.php @@ -11,6 +11,7 @@ * * @group llvm * @group aot + * @group aot-link */ final class AotTest extends BaseTest { diff --git a/test/aot/BootstrapAotLintTest.php b/test/aot/BootstrapAotLintTest.php index b7180374a22..6c5725ecfd8 100644 --- a/test/aot/BootstrapAotLintTest.php +++ b/test/aot/BootstrapAotLintTest.php @@ -11,6 +11,7 @@ * * @group llvm * @group aot + * @group aot-lint */ final class BootstrapAotLintTest extends TestCase { diff --git a/test/aot/ExampleWebAotTest.php b/test/aot/ExampleWebAotTest.php index 46082fca46b..5524ba26587 100644 --- a/test/aot/ExampleWebAotTest.php +++ b/test/aot/ExampleWebAotTest.php @@ -11,6 +11,7 @@ * * @group llvm * @group aot + * @group aot-link */ final class ExampleWebAotTest extends TestCase { diff --git a/test/aot/NestedSuperglobalsAotTest.php b/test/aot/NestedSuperglobalsAotTest.php index d9d36840e75..80b2ad9c28e 100644 --- a/test/aot/NestedSuperglobalsAotTest.php +++ b/test/aot/NestedSuperglobalsAotTest.php @@ -14,6 +14,7 @@ * * @group llvm * @group aot + * @group aot-link */ final class NestedSuperglobalsAotTest extends TestCase { diff --git a/test/aot/RuntimeSuperglobalRefreshTest.php b/test/aot/RuntimeSuperglobalRefreshTest.php index a660bd54e62..9240f83c885 100644 --- a/test/aot/RuntimeSuperglobalRefreshTest.php +++ b/test/aot/RuntimeSuperglobalRefreshTest.php @@ -11,6 +11,7 @@ * * @group llvm * @group aot + * @group aot-link */ final class RuntimeSuperglobalRefreshTest extends TestCase { diff --git a/test/aot/StdlibWebBuiltinsTest.php b/test/aot/StdlibWebBuiltinsTest.php index 7a9ec482af3..1ee858b07c7 100644 --- a/test/aot/StdlibWebBuiltinsTest.php +++ b/test/aot/StdlibWebBuiltinsTest.php @@ -11,6 +11,7 @@ * * @group llvm * @group aot + * @group aot-link */ final class StdlibWebBuiltinsTest extends TestCase { diff --git a/test/aot/WebAppNumberFormatTest.php b/test/aot/WebAppNumberFormatTest.php index 918e2d69c7f..008912b1407 100644 --- a/test/aot/WebAppNumberFormatTest.php +++ b/test/aot/WebAppNumberFormatTest.php @@ -11,6 +11,7 @@ * * @group llvm * @group aot + * @group aot-link */ final class WebAppNumberFormatTest extends TestCase { diff --git a/test/real/ServeAotTest.php b/test/real/ServeAotTest.php index 62e22c448b4..0257772f6d0 100644 --- a/test/real/ServeAotTest.php +++ b/test/real/ServeAotTest.php @@ -11,6 +11,7 @@ * * @group llvm * @group serve + * @group aot-link */ final class ServeAotTest extends TestCase { diff --git a/test/unit/CiScriptsTest.php b/test/unit/CiScriptsTest.php new file mode 100644 index 00000000000..5ae2e1eb33d --- /dev/null +++ b/test/unit/CiScriptsTest.php @@ -0,0 +1,40 @@ +assertFileExists($fast); + $this->assertTrue(is_executable($fast)); + $body = (string) file_get_contents($fast); + $this->assertStringContainsString('--exclude-group llvm', $body); + $this->assertStringContainsString('ci-local.sh', $body); + } + + public function testCiLocalRunsPhasedLlvmGroups(): void + { + $local = dirname(__DIR__, 2).'/script/ci-local.sh'; + $body = (string) file_get_contents($local); + $this->assertStringContainsString('--group aot-lint', $body); + $this->assertStringContainsString('--group jit', $body); + $this->assertStringContainsString('--group aot-link', $body); + $this->assertStringContainsString('ci_apply_resource_limits', $body); + } + + public function testAotLinkGroupTaggedOnAotTest(): void + { + $source = (string) file_get_contents(dirname(__DIR__).'/aot/AotTest.php'); + $this->assertStringContainsString('@group aot-link', $source); + } +} diff --git a/test/unit/ExamplesCompileTest.php b/test/unit/ExamplesCompileTest.php index 22e07765039..aab10d67d26 100644 --- a/test/unit/ExamplesCompileTest.php +++ b/test/unit/ExamplesCompileTest.php @@ -158,6 +158,7 @@ public function testVmSmokePost001SimpleWeb(): void * * @group llvm * @group aot + * @group aot-lint */ public function testAotLint(string $examplePath): void { @@ -176,6 +177,7 @@ public function testAotLint(string $examplePath): void * * @group llvm * @group aot + * @group aot-link */ public function testAotExecuteSimpleWebDualQuery(): void { @@ -213,6 +215,7 @@ public function testAotExecuteSimpleWebDualQuery(): void * * @group llvm * @group aot + * @group aot-link */ public function testAotExecuteSimpleWebPost(): void { @@ -244,6 +247,7 @@ public function testAotExecuteSimpleWebPost(): void * * @group llvm * @group aot + * @group aot-link * * @see https://github.com/PurHur/php-compiler/issues/270 */ @@ -275,6 +279,7 @@ public function testAotExecuteSmoke004ApiJson(): void * * @group llvm * @group aot + * @group aot-link * * @see https://github.com/PurHur/php-compiler/issues/247 */ @@ -302,6 +307,7 @@ public function testAotExecuteSmoke002StaticWeb(): void * * @group llvm * @group aot + * @group aot-link */ public function testPhpcBuildProject002StaticWeb(): void { @@ -354,6 +360,7 @@ public function testPhpcBuildProject002StaticWeb(): void * * @group llvm * @group aot + * @group aot-link */ public function testPhpcBuildSmoke002StaticWeb(): void {