diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 16ad76e06..0166bfa00 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -136,12 +136,12 @@ jobs: macos-x86_64) DOWN_CMD="composer update --no-dev --classmap-authoritative && ./bin/spc doctor --auto-fix && ./bin/spc download" BUILD_CMD="./bin/spc build" - RUNS_ON="macos-13" + RUNS_ON="macos-15-intel" ;; macos-aarch64) DOWN_CMD="composer update --no-dev --classmap-authoritative && ./bin/spc doctor --auto-fix && ./bin/spc download" BUILD_CMD="./bin/spc build" - RUNS_ON="macos-14" + RUNS_ON="macos-15" ;; esac DOWN_CMD="$DOWN_CMD --with-php=${{ inputs.php-version }} --for-extensions=${{ inputs.extensions }} --ignore-cache-sources=php-src" diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml index 84d3b518f..e93e91459 100644 --- a/.github/workflows/ext-matrix-tests.yml +++ b/.github/workflows/ext-matrix-tests.yml @@ -85,9 +85,9 @@ jobs: - "8.4" operating-system: - "ubuntu-latest" - #- "macos-13" + #- "macos-15-intel" #- "debian-arm64-self-hosted" - - "macos-14" + - "macos-15" steps: - name: "Checkout" @@ -99,11 +99,11 @@ jobs: OS="" if [ "${{ matrix.operating-system }}" = "ubuntu-latest" ]; then OS="linux-x86_64" - elif [ "${{ matrix.operating-system }}" = "macos-13" ]; then + elif [ "${{ matrix.operating-system }}" = "macos-15-intel" ]; then OS="macos-x86_64" elif [ "${{ matrix.operating-system }}" = "debian-arm64-self-hosted" ]; then OS="linux-aarch64" - elif [ "${{ matrix.operating-system }}" = "macos-14" ]; then + elif [ "${{ matrix.operating-system }}" = "macos-15" ]; then OS="macos-aarch64" fi echo "OS=$OS" >> $GITHUB_ENV diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index 61004bd34..aaeee1ffc 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -27,7 +27,7 @@ jobs: os: "ubuntu-latest" filename: "spc-linux-x86_64.tar.gz" - name: "macos-x86_64" - os: "macos-13" + os: "macos-15-intel" filename: "spc-macos-x86_64.tar.gz" - name: "linux-aarch64" os: "ubuntu-latest" @@ -147,11 +147,11 @@ jobs: - name: "linux-x86_64" os: "ubuntu-latest" - name: "macos-x86_64" - os: "macos-13" + os: "macos-15-intel" - name: "linux-aarch64" os: "ubuntu-24.04-arm" - name: "macos-aarch64" - os: "macos-latest" + os: "macos-15" - name: "windows-x64" os: "windows-latest" steps: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8accd62fb..132b33f00 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,13 +1,9 @@ name: Tests on: - push: - branches: - - main - paths: - - 'src/globals/test-extensions.php' pull_request: branches: [ "main" ] + types: [ opened, synchronize, reopened ] paths: - 'src/**' - 'config/**' @@ -179,7 +175,7 @@ jobs: key: php-dependencies-${{ matrix.os }} - name: "Install Dependencies" - run: composer update -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + run: composer update -vvv --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist --no-plugins - name: "Run Build Tests (doctor)" run: php src/globals/test-extensions.php doctor_cmd ${{ matrix.os }} ${{ matrix.php }} diff --git a/.gitignore b/.gitignore index 4bf41b559..0d2bd5540 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ docker/source/ # default package root directory /pkgroot/** +# Windows PHP SDK binary tools +/php-sdk-binary-tools/** + # default pack:lib and release directory /dist/** packlib_files.txt diff --git a/bin/spc-alpine-docker b/bin/spc-alpine-docker index 3a58547fe..2790a5c34 100755 --- a/bin/spc-alpine-docker +++ b/bin/spc-alpine-docker @@ -150,6 +150,7 @@ MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/source:/app/source" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/dist:/app/dist" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/downloads:/app/downloads" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot" +MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/log:/app/log" if [ -f "$(pwd)/craft.yml" ]; then MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/craft.yml:/app/craft.yml" fi @@ -161,6 +162,47 @@ if [ ! -z "$GITHUB_TOKEN" ]; then ENV_LIST="$ENV_LIST -e GITHUB_TOKEN=$GITHUB_TOKEN" fi +# Intercept and rewrite --with-frankenphp-app option, and mount host path to /app/app +FRANKENPHP_APP_PATH="" +NEW_ARGS=() +while [ $# -gt 0 ]; do + case "$1" in + --with-frankenphp-app=*) + FRANKENPHP_APP_PATH="${1#*=}" + NEW_ARGS+=("--with-frankenphp-app=/app/app") + shift + ;; + --with-frankenphp-app) + if [ -n "${2:-}" ]; then + FRANKENPHP_APP_PATH="$2" + NEW_ARGS+=("--with-frankenphp-app=/app/app") + shift 2 + else + NEW_ARGS+=("$1") + shift + fi + ;; + *) + NEW_ARGS+=("$1") + shift + ;; + esac +done + +# Normalize the path and add mount if provided +if [ -n "$FRANKENPHP_APP_PATH" ]; then + # expand ~ to $HOME + if [ "${FRANKENPHP_APP_PATH#~}" != "$FRANKENPHP_APP_PATH" ]; then + FRANKENPHP_APP_PATH="$HOME${FRANKENPHP_APP_PATH#~}" + fi + # make absolute if relative + case "$FRANKENPHP_APP_PATH" in + /*) ABS_APP_PATH="$FRANKENPHP_APP_PATH" ;; + *) ABS_APP_PATH="$(pwd)/$FRANKENPHP_APP_PATH" ;; + esac + MOUNT_LIST="$MOUNT_LIST -v $ABS_APP_PATH:/app/app" +fi + # Run docker # shellcheck disable=SC2068 # shellcheck disable=SC2086 @@ -182,5 +224,5 @@ if [ "$SPC_DOCKER_DEBUG" = "yes" ]; then set -ex $DOCKER_EXECUTABLE run $PLATFORM_ARG --rm $INTERACT $ENV_LIST $MOUNT_LIST cwcc-spc-$SPC_USE_ARCH-$SPC_DOCKER_VERSION /bin/bash else - $DOCKER_EXECUTABLE run $PLATFORM_ARG --rm $INTERACT $ENV_LIST $MOUNT_LIST cwcc-spc-$SPC_USE_ARCH-$SPC_DOCKER_VERSION bin/spc $@ + $DOCKER_EXECUTABLE run $PLATFORM_ARG --rm $INTERACT $ENV_LIST $MOUNT_LIST cwcc-spc-$SPC_USE_ARCH-$SPC_DOCKER_VERSION bin/spc "${NEW_ARGS[@]}" fi diff --git a/bin/spc-gnu-docker b/bin/spc-gnu-docker index 97c9a089d..68f85109f 100755 --- a/bin/spc-gnu-docker +++ b/bin/spc-gnu-docker @@ -158,6 +158,7 @@ MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/source:/app/source" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/dist:/app/dist" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/downloads:/app/downloads" MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/pkgroot:/app/pkgroot" +MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/log:/app/log" if [ -f "$(pwd)/craft.yml" ]; then MOUNT_LIST="$MOUNT_LIST -v ""$(pwd)""/craft.yml:/app/craft.yml" fi @@ -173,6 +174,47 @@ if [ ! -z "$GITHUB_TOKEN" ]; then ENV_LIST="$ENV_LIST -e GITHUB_TOKEN=$GITHUB_TOKEN" fi +# Intercept and rewrite --with-frankenphp-app option, and mount host path to /app/app +FRANKENPHP_APP_PATH="" +NEW_ARGS=() +while [ $# -gt 0 ]; do + case "$1" in + --with-frankenphp-app=*) + FRANKENPHP_APP_PATH="${1#*=}" + NEW_ARGS+=("--with-frankenphp-app=/app/app") + shift + ;; + --with-frankenphp-app) + if [ -n "${2:-}" ]; then + FRANKENPHP_APP_PATH="$2" + NEW_ARGS+=("--with-frankenphp-app=/app/app") + shift 2 + else + NEW_ARGS+=("$1") + shift + fi + ;; + *) + NEW_ARGS+=("$1") + shift + ;; + esac +done + +# Normalize the path and add mount if provided +if [ -n "$FRANKENPHP_APP_PATH" ]; then + # expand ~ to $HOME + if [ "${FRANKENPHP_APP_PATH#~}" != "$FRANKENPHP_APP_PATH" ]; then + FRANKENPHP_APP_PATH="$HOME${FRANKENPHP_APP_PATH#~}" + fi + # make absolute if relative + case "$FRANKENPHP_APP_PATH" in + /*) ABS_APP_PATH="$FRANKENPHP_APP_PATH" ;; + *) ABS_APP_PATH="$(pwd)/$FRANKENPHP_APP_PATH" ;; + esac + MOUNT_LIST="$MOUNT_LIST -v $ABS_APP_PATH:/app/app" +fi + # Run docker # shellcheck disable=SC2068 # shellcheck disable=SC2086 @@ -195,5 +237,5 @@ if [ "$SPC_DOCKER_DEBUG" = "yes" ]; then set -ex $DOCKER_EXECUTABLE run $PLATFORM_ARG --privileged --rm -it $INTERACT $ENV_LIST --env-file /tmp/spc-gnu-docker.env $MOUNT_LIST cwcc-spc-gnu-$SPC_USE_ARCH-$SPC_DOCKER_VERSION /bin/bash else - $DOCKER_EXECUTABLE run $PLATFORM_ARG --rm $INTERACT $ENV_LIST --env-file /tmp/spc-gnu-docker.env $MOUNT_LIST cwcc-spc-gnu-$SPC_USE_ARCH-$SPC_DOCKER_VERSION bin/spc $@ + $DOCKER_EXECUTABLE run $PLATFORM_ARG --rm $INTERACT $ENV_LIST --env-file /tmp/spc-gnu-docker.env $MOUNT_LIST cwcc-spc-gnu-$SPC_USE_ARCH-$SPC_DOCKER_VERSION bin/spc "${NEW_ARGS[@]}" fi diff --git a/composer.lock b/composer.lock index 069a340fe..e8f320c7c 100644 --- a/composer.lock +++ b/composer.lock @@ -8,7 +8,7 @@ "packages": [ { "name": "illuminate/collections", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", @@ -64,7 +64,7 @@ }, { "name": "illuminate/conditionable", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/conditionable.git", @@ -110,7 +110,7 @@ }, { "name": "illuminate/contracts", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", @@ -158,7 +158,7 @@ }, { "name": "illuminate/macroable", - "version": "v11.45.3", + "version": "v11.46.1", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -416,16 +416,16 @@ }, { "name": "symfony/console", - "version": "v7.3.3", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", - "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", "shasum": "" }, "require": { @@ -490,7 +490,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.3" + "source": "https://github.com/symfony/console/tree/v7.3.6" }, "funding": [ { @@ -510,7 +510,7 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2025-11-04T01:21:42+00:00" }, { "name": "symfony/deprecation-contracts", @@ -916,16 +916,16 @@ }, { "name": "symfony/process", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", - "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", + "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", + "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", "shasum": "" }, "require": { @@ -957,7 +957,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.3" + "source": "https://github.com/symfony/process/tree/v7.3.4" }, "funding": [ { @@ -977,20 +977,20 @@ "type": "tidelift" } ], - "time": "2025-08-18T09:42:54+00:00" + "time": "2025-09-11T10:12:26+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.0", + "version": "v3.6.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", - "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", "shasum": "" }, "require": { @@ -1044,7 +1044,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" }, "funding": [ { @@ -1055,25 +1055,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-25T09:37:31+00:00" + "time": "2025-07-15T11:30:57+00:00" }, { "name": "symfony/string", - "version": "v7.3.3", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", - "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -1088,7 +1092,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -1131,7 +1134,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.3" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -1151,20 +1154,20 @@ "type": "tidelift" } ], - "time": "2025-08-25T06:35:40+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.3", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" + "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", - "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", "shasum": "" }, "require": { @@ -1207,7 +1210,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.3" + "source": "https://github.com/symfony/yaml/tree/v7.3.5" }, "funding": [ { @@ -1227,20 +1230,20 @@ "type": "tidelift" } ], - "time": "2025-08-27T11:34:33+00:00" + "time": "2025-09-27T09:00:46+00:00" }, { "name": "zhamao/logger", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/zhamao-robot/zhamao-logger.git", - "reference": "2551b3e1dbbb1db81794d5c3a666c66804a9dca9" + "reference": "b28c0c26d59d58f153c764f224c21c5901213e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zhamao-robot/zhamao-logger/zipball/2551b3e1dbbb1db81794d5c3a666c66804a9dca9", - "reference": "2551b3e1dbbb1db81794d5c3a666c66804a9dca9", + "url": "https://api.github.com/repos/zhamao-robot/zhamao-logger/zipball/b28c0c26d59d58f153c764f224c21c5901213e25", + "reference": "b28c0c26d59d58f153c764f224c21c5901213e25", "shasum": "" }, "require": { @@ -1294,9 +1297,9 @@ "description": "Another Console Logger for CLI Applications", "support": { "issues": "https://github.com/zhamao-robot/zhamao-logger/issues", - "source": "https://github.com/zhamao-robot/zhamao-logger/tree/1.1.3" + "source": "https://github.com/zhamao-robot/zhamao-logger/tree/1.1.4" }, - "time": "2025-08-02T13:44:27+00:00" + "time": "2025-11-08T15:38:01+00:00" } ], "packages-dev": [ @@ -2861,16 +2864,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.87.1", + "version": "v3.89.2", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "2f5170365e2a422d0c5421f9c8818b2c078105f6" + "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/2f5170365e2a422d0c5421f9c8818b2c078105f6", - "reference": "2f5170365e2a422d0c5421f9c8818b2c078105f6", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7569658f91e475ec93b99bd5964b059ad1336dcf", + "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf", "shasum": "" }, "require": { @@ -2885,7 +2888,6 @@ "php": "^7.4 || ^8.0", "react/child-process": "^0.6.6", "react/event-loop": "^1.5", - "react/promise": "^3.3", "react/socket": "^1.16", "react/stream": "^1.4", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", @@ -2897,20 +2899,20 @@ "symfony/polyfill-mbstring": "^1.33", "symfony/polyfill-php80": "^1.33", "symfony/polyfill-php81": "^1.33", + "symfony/polyfill-php84": "^1.33", "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2", "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0" }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.7", - "infection/infection": "^0.29.14", + "infection/infection": "^0.31.0", "justinrainbow/json-schema": "^6.5", "keradus/cli-executor": "^2.2", "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.8", + "php-coveralls/php-coveralls": "^2.9", "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", - "symfony/polyfill-php84": "^1.33", "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2", "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2" }, @@ -2953,7 +2955,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.87.1" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.2" }, "funding": [ { @@ -2961,20 +2963,20 @@ "type": "github" } ], - "time": "2025-09-02T15:27:36+00:00" + "time": "2025-11-06T21:12:50+00:00" }, { "name": "humbug/box", - "version": "4.6.6", + "version": "4.6.10", "source": { "type": "git", "url": "https://github.com/box-project/box.git", - "reference": "09646041cb2e0963ab6ca109e1b366337617a0f2" + "reference": "6dc6a1314d63e9d75c8195c996e1081e68514c36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/box-project/box/zipball/09646041cb2e0963ab6ca109e1b366337617a0f2", - "reference": "09646041cb2e0963ab6ca109e1b366337617a0f2", + "url": "https://api.github.com/repos/box-project/box/zipball/6dc6a1314d63e9d75c8195c996e1081e68514c36", + "reference": "6dc6a1314d63e9d75c8195c996e1081e68514c36", "shasum": "" }, "require": { @@ -2988,13 +2990,13 @@ "fidry/console": "^0.6.0", "fidry/filesystem": "^1.2.1", "humbug/php-scoper": "^0.18.14", - "justinrainbow/json-schema": "^5.2.12", + "justinrainbow/json-schema": "^6.2.0", "nikic/iter": "^2.2", "php": "^8.2", "phpdocumentor/reflection-docblock": "^5.4", "phpdocumentor/type-resolver": "^1.7", "psr/log": "^3.0", - "sebastian/diff": "^5.0", + "sebastian/diff": "^5.0 || ^6.0 || ^7.0", "seld/jsonlint": "^1.10.2", "seld/phar-utils": "^1.2", "symfony/finder": "^6.4.0 || ^7.0.0", @@ -3003,7 +3005,10 @@ "symfony/process": "^6.4.0 || ^7.0.0", "symfony/var-dumper": "^6.4.0 || ^7.0.0", "thecodingmachine/safe": "^2.5 || ^3.0", - "webmozart/assert": "^1.11" + "webmozart/assert": "^1.12" + }, + "conflict": { + "marc-mabe/php-enum": "<4.4" }, "replace": { "symfony/polyfill-php80": "*", @@ -3070,22 +3075,22 @@ ], "support": { "issues": "https://github.com/box-project/box/issues", - "source": "https://github.com/box-project/box/tree/4.6.6" + "source": "https://github.com/box-project/box/tree/4.6.10" }, - "time": "2025-03-02T18:20:45+00:00" + "time": "2025-10-31T18:38:02+00:00" }, { "name": "humbug/php-scoper", - "version": "0.18.17", + "version": "0.18.18", "source": { "type": "git", "url": "https://github.com/humbug/php-scoper.git", - "reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a" + "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/humbug/php-scoper/zipball/0a2556c7c23776a61cf22689e2f24298ba00e33a", - "reference": "0a2556c7c23776a61cf22689e2f24298ba00e33a", + "url": "https://api.github.com/repos/humbug/php-scoper/zipball/dd55d01a937602c9473cfbe0ecab9521cb9740aa", + "reference": "dd55d01a937602c9473cfbe0ecab9521cb9740aa", "shasum": "" }, "require": { @@ -3154,9 +3159,9 @@ "description": "Prefixes all PHP namespaces in a file or directory.", "support": { "issues": "https://github.com/humbug/php-scoper/issues", - "source": "https://github.com/humbug/php-scoper/tree/0.18.17" + "source": "https://github.com/humbug/php-scoper/tree/0.18.18" }, - "time": "2025-02-19T22:50:39+00:00" + "time": "2025-10-15T15:29:47+00:00" }, { "name": "jetbrains/phpstorm-stubs", @@ -3207,30 +3212,40 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.3.0", + "version": "6.6.1", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", - "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", + "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", "shasum": "" }, "require": { - "php": ">=7.1" + "ext-json": "*", + "marc-mabe/php-enum": "^4.0", + "php": "^7.2 || ^8.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", - "json-schema/json-schema-test-suite": "1.2.0", - "phpunit/phpunit": "^4.8.35" + "friendsofphp/php-cs-fixer": "3.3.0", + "json-schema/json-schema-test-suite": "^23.2", + "marc-mabe/php-enum-phpstan": "^2.0", + "phpspec/prophecy": "^1.19", + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^8.5" }, "bin": [ "bin/validate-json" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, "autoload": { "psr-4": { "JsonSchema\\": "src/JsonSchema/" @@ -3259,16 +3274,16 @@ } ], "description": "A library to validate a json schema.", - "homepage": "https://github.com/justinrainbow/json-schema", + "homepage": "https://github.com/jsonrainbow/json-schema", "keywords": [ "json", "schema" ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.1" }, - "time": "2024-07-06T21:00:26+00:00" + "time": "2025-11-07T18:30:29+00:00" }, { "name": "kelunik/certificate", @@ -3502,6 +3517,79 @@ ], "time": "2024-12-08T08:18:47+00:00" }, + { + "name": "marc-mabe/php-enum", + "version": "v4.7.2", + "source": { + "type": "git", + "url": "https://github.com/marc-mabe/php-enum.git", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/marc-mabe/php-enum/zipball/bb426fcdd65c60fb3638ef741e8782508fda7eef", + "reference": "bb426fcdd65c60fb3638ef741e8782508fda7eef", + "shasum": "" + }, + "require": { + "ext-reflection": "*", + "php": "^7.1 | ^8.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.16.10 || ^1.0.4", + "phpstan/phpstan": "^1.3.1", + "phpunit/phpunit": "^7.5.20 | ^8.5.22 | ^9.5.11", + "vimeo/psalm": "^4.17.0 | ^5.26.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.2-dev", + "dev-master": "4.7-dev" + } + }, + "autoload": { + "psr-4": { + "MabeEnum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Marc Bennewitz", + "email": "dev@mabe.berlin", + "homepage": "https://mabe.berlin/", + "role": "Lead" + } + ], + "description": "Simple and fast implementation of enumerations with native PHP", + "homepage": "https://github.com/marc-mabe/php-enum", + "keywords": [ + "enum", + "enum-map", + "enum-set", + "enumeration", + "enumerator", + "enummap", + "enumset", + "map", + "set", + "type", + "type-hint", + "typehint" + ], + "support": { + "issues": "https://github.com/marc-mabe/php-enum/issues", + "source": "https://github.com/marc-mabe/php-enum/tree/v4.7.2" + }, + "time": "2025-09-14T11:18:39+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.13.4", @@ -3616,16 +3704,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.1", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", - "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", "shasum": "" }, "require": { @@ -3668,9 +3756,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" }, - "time": "2025-08-13T20:13:15+00:00" + "time": "2025-10-21T19:32:17+00:00" }, { "name": "phar-io/composer-distributor", @@ -4228,16 +4316,11 @@ }, { "name": "phpstan/phpstan", - "version": "1.12.28", - "source": { - "type": "git", - "url": "https://github.com/phpstan/phpstan.git", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9" - }, + "version": "1.12.32", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", - "reference": "fcf8b71aeab4e1a1131d1783cef97b23a51b87a9", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/2770dcdf5078d0b0d53f94317e06affe88419aa8", + "reference": "2770dcdf5078d0b0d53f94317e06affe88419aa8", "shasum": "" }, "require": { @@ -4282,7 +4365,7 @@ "type": "github" } ], - "time": "2025-07-17T17:15:39+00:00" + "time": "2025-09-30T10:16:31+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4607,16 +4690,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.53", + "version": "10.5.58", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "32768472ebfb6969e6c7399f1c7b09009723f653" + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/32768472ebfb6969e6c7399f1c7b09009723f653", - "reference": "32768472ebfb6969e6c7399f1c7b09009723f653", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e24fb46da450d8e6a5788670513c1af1424f16ca", + "reference": "e24fb46da450d8e6a5788670513c1af1424f16ca", "shasum": "" }, "require": { @@ -4637,10 +4720,10 @@ "phpunit/php-timer": "^6.0.0", "sebastian/cli-parser": "^2.0.1", "sebastian/code-unit": "^2.0.0", - "sebastian/comparator": "^5.0.3", + "sebastian/comparator": "^5.0.4", "sebastian/diff": "^5.1.1", "sebastian/environment": "^6.1.0", - "sebastian/exporter": "^5.1.2", + "sebastian/exporter": "^5.1.4", "sebastian/global-state": "^6.0.2", "sebastian/object-enumerator": "^5.0.0", "sebastian/recursion-context": "^5.0.1", @@ -4688,7 +4771,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.53" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.58" }, "funding": [ { @@ -4712,7 +4795,7 @@ "type": "tidelift" } ], - "time": "2025-08-20T14:40:06+00:00" + "time": "2025-09-28T12:04:46+00:00" }, { "name": "psr/event-dispatcher", @@ -5640,16 +5723,16 @@ }, { "name": "sebastian/comparator", - "version": "5.0.3", + "version": "5.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", - "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e8e53097718d2b53cfb2aa859b06a41abf58c62e", + "reference": "e8e53097718d2b53cfb2aa859b06a41abf58c62e", "shasum": "" }, "require": { @@ -5705,15 +5788,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", "security": "https://github.com/sebastianbergmann/comparator/security/policy", - "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2024-10-18T14:56:07+00:00" + "time": "2025-09-07T05:25:07+00:00" }, { "name": "sebastian/complexity", @@ -5906,16 +6001,16 @@ }, { "name": "sebastian/exporter", - "version": "5.1.2", + "version": "5.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + "reference": "0735b90f4da94969541dac1da743446e276defa6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", - "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", "shasum": "" }, "require": { @@ -5924,7 +6019,7 @@ "sebastian/recursion-context": "^5.0" }, "require-dev": { - "phpunit/phpunit": "^10.0" + "phpunit/phpunit": "^10.5" }, "type": "library", "extra": { @@ -5972,15 +6067,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", "security": "https://github.com/sebastianbergmann/exporter/security/policy", - "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T07:17:12+00:00" + "time": "2025-09-24T06:09:11+00:00" }, { "name": "sebastian/global-state", @@ -6673,16 +6780,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.2", + "version": "v7.3.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", - "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", + "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", "shasum": "" }, "require": { @@ -6719,7 +6826,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.2" + "source": "https://github.com/symfony/filesystem/tree/v7.3.6" }, "funding": [ { @@ -6739,20 +6846,20 @@ "type": "tidelift" } ], - "time": "2025-07-07T08:17:47+00:00" + "time": "2025-11-05T09:52:27+00:00" }, { "name": "symfony/finder", - "version": "v7.3.2", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + "reference": "9f696d2f1e340484b4683f7853b273abff94421f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", - "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f", "shasum": "" }, "require": { @@ -6787,7 +6894,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.2" + "source": "https://github.com/symfony/finder/tree/v7.3.5" }, "funding": [ { @@ -6807,7 +6914,7 @@ "type": "tidelift" } ], - "time": "2025-07-15T13:41:35+00:00" + "time": "2025-10-15T18:45:57+00:00" }, { "name": "symfony/options-resolver", @@ -7108,16 +7215,16 @@ }, { "name": "symfony/var-dumper", - "version": "v7.3.3", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f" + "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", - "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", "shasum": "" }, "require": { @@ -7171,7 +7278,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.3" + "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" }, "funding": [ { @@ -7191,7 +7298,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-09-27T09:00:46+00:00" }, { "name": "thecodingmachine/safe", @@ -7334,16 +7441,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.2.3", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", - "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb", "shasum": "" }, "require": { @@ -7372,7 +7479,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + "source": "https://github.com/theseer/tokenizer/tree/1.3.0" }, "funding": [ { @@ -7380,32 +7487,32 @@ "type": "github" } ], - "time": "2024-03-03T12:36:25+00:00" + "time": "2025-11-13T13:44:09+00:00" }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", "shasum": "" }, "require": { "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", "php": "^7.2 || ^8.0" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { @@ -7436,9 +7543,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2025-10-29T15:56:20+00:00" } ], "aliases": [], diff --git a/config/env.ini b/config/env.ini index 4b9f294ad..7448cc373 100644 --- a/config/env.ini +++ b/config/env.ini @@ -1,39 +1,41 @@ -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ; static-php-cli (spc) env configuration ; ; This file is used to set default env vars for static-php-cli build. ; As dynamic build process, some of these vars can be overwritten by CLI options. ; And you can also overwrite these vars by setting them in your shell environment. +; The value should be changed only if you know what you are doing. Otherwise, please leave them as default. ; ; We need to use some pre-defined internal env vars, like `BUILD_ROOT_PATH`, `DOWNLOAD_PATH`, etc. -; Please note that these vars cannot be defined in this file, they are only be defined before static-php-cli running. +; Please note that these vars cannot be defined in this file, they should only be defined before static-php-cli running. ; -; Here's a list of env vars, these value cannot be changed anywhere: +; Here's a list of env vars, these variables will be defined if not defined: ; -; SPC_VERSION: the version of static-php-cli. -; WORKING_DIR: the working directory of the build process. (default: `$(pwd)`) -; ROOT_DIR: the root directory of static-php-cli. (default: `/path/to/static-php-cli`, when running in phar or micro mode: `phar://path/to/spc.phar`) ; BUILD_ROOT_PATH: the root path of the build process. (default: `$(pwd)/buildroot`) ; BUILD_INCLUDE_PATH: the path of the include files. (default: `$BUILD_ROOT_PATH/include`) ; BUILD_LIB_PATH: the path of the lib files. (default: `$BUILD_ROOT_PATH/lib`) ; BUILD_BIN_PATH: the path of the bin files. (default: `$BUILD_ROOT_PATH/bin`) -; PKG_ROOT_PATH: the root path of the package files. (default: `$(pwd)/pkgroot`) +; BUILD_MODULES_PATH: the path of the php modules (shared extensions) files. (default: `$BUILD_ROOT_PATH/modules`) +; PKG_ROOT_PATH: the root path of the package files. (default: `$(pwd)/pkgroot/$GNU_ARCH-{darwin|linux|windows}`) ; SOURCE_PATH: the path of the source files. (default: `$(pwd)/source`) ; DOWNLOAD_PATH: the path of the download files. (default: `$(pwd)/downloads`) -; CPU_COUNT: the count of the CPU cores. (default: `$(nproc)`) -; SPC_ARCH: the arch of the current system, for some libraries needed `--host=XXX` args. (default: `$(uname -m)`, e.g. `x86_64`, `aarch64`, `arm64`) -; GNU_ARCH: the GNU arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `aarch64`) -; MAC_ARCH: the MAC arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `arm64`) +; PATH: (*nix only) static-php-cli will add `$BUILD_BIN_PATH` to PATH. +; PKG_CONFIG_PATH: (*nix only) static-php-cli will set `$BUILD_LIB_PATH/pkgconfig` to PKG_CONFIG_PATH. ; -; * These vars are only be defined in Unix (macOS, Linux, FreeBSD)Builder and cannot be changed anywhere: -; PATH: static-php-cli will add `$BUILD_BIN_PATH` to PATH. -; PKG_CONFIG: static-php-cli will set `$BUILD_BIN_PATH/pkg-config` to PKG_CONFIG. -; PKG_CONFIG_PATH: static-php-cli will set `$BUILD_LIB_PATH/pkgconfig` to PKG_CONFIG_PATH. +; Here's a list of env vars, these variables is defined in SPC and cannot be changed anywhere: ; -; * These vars are only be defined in LinuxBuilder and cannot be changed anywhere: -; SPC_LINUX_DEFAULT_CC: the default compiler for linux. (For alpine linux: `gcc`, default: `$GNU_ARCH-linux-musl-gcc`) -; SPC_LINUX_DEFAULT_CXX: the default c++ compiler for linux. (For alpine linux: `g++`, default: `$GNU_ARCH-linux-musl-g++`) -; SPC_LINUX_DEFAULT_AR: the default archiver for linux. (For alpine linux: `ar`, default: `$GNU_ARCH-linux-musl-ar`) +; SPC_VERSION: the version of static-php-cli. +; WORKING_DIR: the working directory of the build process. (default: `$(pwd)`) +; ROOT_DIR: the root directory of static-php-cli. (default: `/path/to/static-php-cli`, when running in phar or micro mode: `phar://path/to/spc.phar`) +; CPU_COUNT: the count of the CPU cores. (default: `$(nproc)`) +; SPC_ARCH: the arch of the current system, for some libraries needed `--host=XXX` args. (default: `$(uname -m)`, e.g. `x86_64`, `aarch64`, `arm64`) +; GNU_ARCH: the GNU arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `aarch64`) +; MAC_ARCH: the MAC arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `arm64`) +; PKG_CONFIG: (*nix only) static-php-cli will set `$BUILD_BIN_PATH/pkg-config` to PKG_CONFIG. +; SPC_LINUX_DEFAULT_CC: (linux only) the default compiler for linux. (For alpine linux: `gcc`, default: `$GNU_ARCH-linux-musl-gcc`) +; SPC_LINUX_DEFAULT_CXX: (linux only) the default c++ compiler for linux. (For alpine linux: `g++`, default: `$GNU_ARCH-linux-musl-g++`) +; SPC_LINUX_DEFAULT_AR: (linux only) the default archiver for linux. (For alpine linux: `ar`, default: `$GNU_ARCH-linux-musl-ar`) +; SPC_EXTRA_PHP_VARS: (linux only) the extra vars for building php, used in `configure` and `make` command. [global] ; Build concurrency for make -jN, default is CPU_COUNT, this value are used in every libs. @@ -43,7 +45,7 @@ SPC_SKIP_PHP_VERSION_CHECK="no" ; Ignore some check item for bin/spc doctor command, comma separated (e.g. SPC_SKIP_DOCTOR_CHECK_ITEMS="if homebrew has installed") SPC_SKIP_DOCTOR_CHECK_ITEMS="" ; extra modules that xcaddy will include in the FrankenPHP build -SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/frankenphp/caddy --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli" +SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli" ; The display message for php version output (PHP >= 8.4 available) PHP_BUILD_PROVIDER="static-php-cli ${SPC_VERSION}" @@ -64,7 +66,7 @@ PHP_SDK_PATH="${WORKING_DIR}\php-sdk-binary-tools" ; upx executable path UPX_EXEC="${PKG_ROOT_PATH}\bin\upx.exe" ; phpmicro patches, for more info, see: https://github.com/easysoft/phpmicro/tree/master/patches -SPC_MICRO_PATCHES=static_extensions_win32,cli_checks,disable_huge_page,vcruntime140,win32,zend_stream,cli_static +SPC_MICRO_PATCHES=static_extensions_win32,cli_checks,disable_huge_page,vcruntime140,win32,zend_stream,cli_static,win32_api [linux] ; Linux can use different build toolchains. @@ -104,15 +106,11 @@ SPC_MICRO_PATCHES=cli_checks,disable_huge_page SPC_CMD_PREFIX_PHP_BUILDCONF="./buildconf --force" ; configure command SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --disable-shared --enable-static --disable-all --disable-phpdbg --with-pic" -; make command -SPC_CMD_PREFIX_PHP_MAKE="make -j${SPC_CONCURRENCY}" ; *** default build vars for building php *** ; embed type for php, static (libphp.a) or shared (libphp.so) SPC_CMD_VAR_PHP_EMBED_TYPE="static" -; CFLAGS for configuring php -SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -fPIE" -; EXTRA_CFLAGS for `make` php +; EXTRA_CFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_C_FLAGS}" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" @@ -138,16 +136,12 @@ SPC_MICRO_PATCHES=cli_checks,macos_iconv SPC_CMD_PREFIX_PHP_BUILDCONF="./buildconf --force" ; configure command SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable-shared=no --enable-static=yes --disable-all --disable-phpdbg" -; make command -SPC_CMD_PREFIX_PHP_MAKE="make -j${SPC_CONCURRENCY}" ; *** default build vars for building php *** ; embed type for php, static (libphp.a) or shared (libphp.dylib) SPC_CMD_VAR_PHP_EMBED_TYPE="static" -; CFLAGS for configuring php -SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -Werror=unknown-warning-option" -; EXTRA_CFLAGS for `make` php -SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie ${SPC_DEFAULT_C_FLAGS}" +; EXTRA_CFLAGS for `configure` and `make` php +SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_C_FLAGS}" [freebsd] ; compiler environments diff --git a/config/ext.json b/config/ext.json index 188aea581..e8f69339e 100644 --- a/config/ext.json +++ b/config/ext.json @@ -341,6 +341,7 @@ "ext-depends": [ "xml" ], + "build-with-php": true, "target": [ "static" ] @@ -357,6 +358,18 @@ "liblz4" ] }, + "maxminddb": { + "support": { + "BSD": "wip", + "Windows": "wip" + }, + "type": "external", + "source": "ext-maxminddb", + "arg-type": "with", + "lib-depends": [ + "libmaxminddb" + ] + }, "mbregex": { "type": "builtin", "arg-type": "custom", @@ -453,7 +466,7 @@ "type": "external", "source": "msgpack", "arg-type-unix": "with", - "arg-type-win": "enable", + "arg-type-windows": "enable", "ext-depends": [ "session" ] @@ -461,6 +474,7 @@ "mysqli": { "type": "builtin", "arg-type": "with", + "build-with-php": true, "ext-depends": [ "mysqlnd" ] @@ -468,6 +482,7 @@ "mysqlnd": { "type": "builtin", "arg-type-windows": "with", + "build-with-php": true, "lib-depends": [ "zlib" ] @@ -687,7 +702,7 @@ "type": "builtin", "arg-type": "with-path", "lib-depends": [ - "readline" + "libedit" ], "target": [ "static" @@ -753,17 +768,27 @@ "apcu" ] }, + "snmp": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "type": "builtin", + "arg-type-unix": "with", + "arg-type-windows": "with", + "lib-depends": [ + "net-snmp" + ] + }, "soap": { "support": { "BSD": "wip" }, "type": "builtin", "arg-type": "custom", - "lib-depends": [ - "libxml2" - ], - "ext-depends-windows": [ - "xml" + "ext-depends": [ + "libxml", + "session" ] }, "sockets": { @@ -799,6 +824,7 @@ "type": "builtin", "arg-type": "with-path", "arg-type-windows": "with", + "build-with-php": true, "lib-depends": [ "sqlite" ] @@ -887,36 +913,38 @@ "mysqli" ] }, - "swoole-hook-pgsql": { + "swoole-hook-odbc": { "support": { "Windows": "no", - "BSD": "wip", - "Darwin": "partial" + "BSD": "wip" }, "notes": true, "type": "addon", "arg-type": "none", "ext-depends": [ - "pgsql", "pdo", "swoole" + ], + "lib-depends": [ + "unixodbc" ] }, - "swoole-hook-sqlite": { + "swoole-hook-pgsql": { "support": { "Windows": "no", - "BSD": "wip" + "BSD": "wip", + "Darwin": "partial" }, "notes": true, "type": "addon", "arg-type": "none", "ext-depends": [ - "sqlite3", + "pgsql", "pdo", "swoole" ] }, - "swoole-hook-odbc": { + "swoole-hook-sqlite": { "support": { "Windows": "no", "BSD": "wip" @@ -925,11 +953,9 @@ "type": "addon", "arg-type": "none", "ext-depends": [ + "sqlite3", "pdo", "swoole" - ], - "lib-depends": [ - "unixodbc" ] }, "swow": { @@ -986,6 +1012,14 @@ "type": "builtin", "build-with-php": true }, + "trader": { + "support": { + "BSD": "wip", + "Windows": "wip" + }, + "type": "external", + "source": "ext-trader" + }, "uuid": { "support": { "Windows": "wip", @@ -1154,8 +1188,9 @@ "support": { "BSD": "wip" }, - "type": "builtin", - "arg-type": "with-path", + "type": "external", + "source": "ext-zip", + "arg-type": "custom", "arg-type-windows": "enable", "lib-depends-unix": [ "libzip" @@ -1178,6 +1213,7 @@ "lib-depends": [ "zlib" ], + "build-with-php": true, "target": [ "static" ] diff --git a/config/lib.json b/config/lib.json index 37faca3c4..47f3c7b89 100644 --- a/config/lib.json +++ b/config/lib.json @@ -7,12 +7,14 @@ "source": "php-src", "lib-depends": [ "lib-base", - "micro" + "micro", + "frankenphp" ], "lib-depends-macos": [ "lib-base", "micro", - "libxml2" + "libxml2", + "frankenphp" ], "lib-suggests-linux": [ "libacl", @@ -24,6 +26,10 @@ "watcher" ] }, + "frankenphp": { + "source": "frankenphp", + "type": "target" + }, "micro": { "type": "target", "source": "micro" @@ -98,7 +104,9 @@ "ngtcp2", "zstd", "libcares", - "ldap" + "ldap", + "idn2", + "krb5" ], "lib-suggests-windows": [ "brotli", @@ -202,7 +210,7 @@ "openssl", "libcares" ], - "provide-pre-built": true, + "cpp-library": true, "frameworks": [ "CoreFoundation" ] @@ -228,8 +236,27 @@ "unicode" ] }, + "idn2": { + "source": "libidn2", + "pkg-configs": [ + "libidn2" + ], + "headers": [ + "idn2.h" + ], + "lib-suggests-unix": [ + "libiconv", + "gettext", + "libunistring" + ], + "lib-depends-macos": [ + "libiconv", + "gettext" + ] + }, "imagemagick": { "source": "imagemagick", + "cpp-library": true, "pkg-configs": [ "Magick++-7.Q16HDRI", "MagickCore-7.Q16HDRI", @@ -274,6 +301,26 @@ "jbig_ar.h" ] }, + "krb5": { + "source": "krb5", + "pkg-configs": [ + "krb5-gssapi" + ], + "headers": [ + "krb5.h", + "gssapi/gssapi.h" + ], + "lib-depends": [ + "openssl" + ], + "lib-suggests": [ + "ldap", + "libedit" + ], + "frameworks": [ + "Kerberos" + ] + }, "ldap": { "source": "ldap", "pkg-configs": [ @@ -343,6 +390,15 @@ ], "cpp-library": true }, + "libedit": { + "source": "libedit", + "static-libs-unix": [ + "libedit.a" + ], + "lib-depends": [ + "ncurses" + ] + }, "libevent": { "source": "libevent", "static-libs-unix": [ @@ -450,6 +506,16 @@ "liblz4.a" ] }, + "libmaxminddb": { + "source": "libmaxminddb", + "static-libs-unix": [ + "libmaxminddb.a" + ], + "headers": [ + "maxminddb.h", + "maxminddb_config.h" + ] + }, "libmemcached": { "source": "libmemcached", "cpp-library": true, @@ -496,13 +562,16 @@ }, "librdkafka": { "source": "librdkafka", - "static-libs-unix": [ - "librdkafka.a", - "librdkafka++.a", - "librdkafka-static.a" + "pkg-configs": [ + "rdkafka++-static", + "rdkafka-static" ], "cpp-library": true, "lib-suggests": [ + "curl", + "liblz4", + "openssl", + "zlib", "zstd" ] }, @@ -549,6 +618,31 @@ "zstd" ] }, + "libunistring": { + "source": "libunistring", + "static-libs-unix": [ + "libunistring.a" + ], + "headers": [ + "unistr.h", + "unistring/" + ] + }, + "liburing": { + "source": "liburing", + "pkg-configs": [ + "liburing", + "liburing-ffi" + ], + "static-libs-linux": [ + "liburing.a", + "liburing-ffi.a" + ], + "headers-linux": [ + "liburing/", + "liburing.h" + ] + }, "libuuid": { "source": "libuuid", "static-libs-unix": [ @@ -582,8 +676,8 @@ }, "libxml2": { "source": "libxml2", - "static-libs-unix": [ - "libxml2.a" + "pkg-configs": [ + "libxml-2.0" ], "static-libs-windows": [ "libxml2s.lib", @@ -672,6 +766,17 @@ "libncurses.a" ] }, + "net-snmp": { + "source": "net-snmp", + "pkg-configs": [ + "netsnmp", + "netsnmp-agent" + ], + "lib-depends": [ + "openssl", + "zlib" + ] + }, "nghttp2": { "source": "nghttp2", "static-libs-unix": [ @@ -770,7 +875,7 @@ "libxml2", "openssl", "zlib", - "readline" + "libedit" ], "lib-suggests": [ "icu", @@ -822,6 +927,7 @@ }, "snappy": { "source": "snappy", + "cpp-library": true, "static-libs-unix": [ "libsnappy.a" ], @@ -867,6 +973,7 @@ }, "watcher": { "source": "watcher", + "cpp-library": true, "static-libs-unix": [ "libwatcher-c.a" ], @@ -927,20 +1034,5 @@ "zstd.h", "zstd_errors.h" ] - }, - "liburing": { - "source": "liburing", - "pkg-configs": [ - "liburing", - "liburing-ffi" - ], - "static-libs-linux": [ - "liburing.a", - "liburing-ffi.a" - ], - "headers-linux": [ - "liburing/", - "liburing.h" - ] } } diff --git a/config/pkg.json b/config/pkg.json index 00e835956..d3b4fb909 100644 --- a/config/pkg.json +++ b/config/pkg.json @@ -23,8 +23,8 @@ "type": "url", "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", "extract-files": { - "nasm-2.16.01/nasm.exe": "{php_sdk_path}/bin/nasm.exe", - "nasm-2.16.01/ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" + "nasm.exe": "{php_sdk_path}/bin/nasm.exe", + "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" } }, "pkg-config-aarch64-linux": { @@ -84,7 +84,7 @@ "repo": "upx/upx", "match": "upx.+-win64\\.zip", "extract-files": { - "upx-*-win64/upx.exe": "{pkg_root_path}/bin/upx.exe" + "upx.exe": "{pkg_root_path}/bin/upx.exe" } }, "zig-aarch64-linux": { diff --git a/config/source.json b/config/source.json index 4a0e0fdb5..06aa5c6f5 100644 --- a/config/source.json +++ b/config/source.json @@ -47,7 +47,7 @@ "provide-pre-built": true, "license": { "type": "file", - "path": "doc/COPYING" + "path": "doc/COPYING.LGPL" } }, "brotli": { @@ -184,6 +184,15 @@ ] } }, + "ext-maxminddb": { + "type": "url", + "url": "https://pecl.php.net/get/maxminddb", + "filename": "ext-maxminddb.tgz", + "license": { + "type": "file", + "path": "LICENSE" + } + }, "ext-memcache": { "type": "url", "url": "https://pecl.php.net/get/memcache", @@ -233,6 +242,16 @@ "path": "LICENSE" } }, + "ext-trader": { + "type": "url", + "url": "https://pecl.php.net/get/trader", + "path": "php-src/ext/trader", + "filename": "trader.tgz", + "license": { + "type": "file", + "path": "LICENSE" + } + }, "ext-uuid": { "type": "url", "url": "https://pecl.php.net/get/uuid", @@ -263,6 +282,15 @@ "path": "LICENSE" } }, + "ext-zip": { + "type": "url", + "url": "https://pecl.php.net/get/zip", + "filename": "ext-zip.tgz", + "license": { + "type": "file", + "path": "LICENSE" + } + }, "ext-zstd": { "type": "git", "path": "php-src/ext/zstd", @@ -282,10 +310,19 @@ "path": "LICENSE.MIT" } }, + "frankenphp": { + "type": "ghtar", + "repo": "php/frankenphp", + "prefer-stable": true, + "license": { + "type": "file", + "path": "LICENSE" + } + }, "freetype": { - "type": "git", - "rev": "VER-2-13-2", - "url": "https://github.com/freetype/freetype", + "type": "ghtagtar", + "repo": "freetype/freetype", + "match": "VER-2-\\d+-\\d+", "license": { "type": "file", "path": "LICENSE.TXT" @@ -297,16 +334,17 @@ "regex": "/href=\"(?gettext-(?[^\"]+)\\.tar\\.xz)\"/", "license": { "type": "file", - "path": "COPYING" + "path": "gettext-runtime/intl/COPYING.LIB" } }, "gmp": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/gmp/gmp-6.3.0.tar.xz", + "type": "filelist", + "url": "https://gmplib.org/download/gmp/", + "regex": "/href=\"(?gmp-(?[^\"]+)\\.tar\\.xz)\"/", "provide-pre-built": true, "alt": { - "type": "ghtagtar", - "repo": "alisw/GMP" + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/gmp/gmp-6.3.0.tar.xz" }, "license": { "type": "text", @@ -324,7 +362,7 @@ }, "grpc": { "type": "git", - "rev": "v1.68.x", + "rev": "v1.75.x", "url": "https://github.com/grpc/grpc.git", "provide-pre-built": true, "license": { @@ -401,6 +439,16 @@ "path": "COPYING" } }, + "krb5": { + "type": "ghtagtar", + "repo": "krb5/krb5", + "match": "krb5.+-final", + "prefer-stable": true, + "license": { + "type": "file", + "path": "NOTICE" + } + }, "ldap": { "type": "filelist", "url": "https://www.openldap.org/software/download/OpenLDAP/openldap-release/", @@ -430,7 +478,7 @@ "provide-pre-built": true, "license": { "type": "file", - "path": "doc/COPYING" + "path": "doc/COPYING.LGPL" } }, "libaom": { @@ -489,6 +537,16 @@ "path": "COPYING" } }, + "libedit": { + "type": "filelist", + "url": "https://thrysoee.dk/editline/", + "regex": "/href=\"(?libedit-(?[^\"]+)\\.tar\\.gz)\"/", + "provide-pre-built": true, + "license": { + "type": "file", + "path": "COPYING" + } + }, "libevent": { "type": "ghrel", "repo": "libevent/libevent", @@ -537,7 +595,7 @@ "provide-pre-built": true, "license": { "type": "file", - "path": "COPYING" + "path": "COPYING.LIB" } }, "libiconv-win": { @@ -549,6 +607,15 @@ "path": "source/COPYING" } }, + "libidn2": { + "type": "filelist", + "url": "https://ftp.gnu.org/gnu/libidn/", + "regex": "/href=\"(?libidn2-(?[^\"]+)\\.tar\\.gz)\"/", + "license": { + "type": "file", + "path": "COPYING.LESSERv3" + } + }, "libjpeg": { "type": "ghtar", "repo": "libjpeg-turbo/libjpeg-turbo", @@ -583,6 +650,16 @@ "path": "LICENSE" } }, + "libmaxminddb": { + "type": "ghrel", + "repo": "maxmind/libmaxminddb", + "match": "libmaxminddb-.+\\.tar\\.gz", + "prefer-stable": true, + "license": { + "type": "file", + "path": "LICENSE" + } + }, "libmemcached": { "type": "ghtagtar", "repo": "awesomized/libmemcached", @@ -650,10 +727,30 @@ "path": "LICENSE.md" } }, + "libunistring": { + "type": "filelist", + "url": "https://ftp.gnu.org/gnu/libunistring/", + "regex": "/href=\"(?libunistring-(?[^\"]+)\\.tar\\.gz)\"/", + "provide-pre-built": true, + "license": { + "type": "file", + "path": "COPYING.LIB" + } + }, + "liburing": { + "type": "ghtar", + "repo": "axboe/liburing", + "prefer-stable": true, + "license": { + "type": "file", + "path": "COPYING" + } + }, "libuuid": { "type": "git", "url": "https://github.com/static-php/libuuid.git", "rev": "master", + "provide-pre-built": true, "license": { "type": "file", "path": "COPYING" @@ -733,7 +830,7 @@ "micro": { "type": "git", "path": "php-src/sapi/micro", - "rev": "php-85-win", + "rev": "master", "url": "https://github.com/static-php/phpmicro", "license": { "type": "file", @@ -781,6 +878,14 @@ "path": "COPYING" } }, + "net-snmp": { + "type": "ghtagtar", + "repo": "net-snmp/net-snmp", + "license": { + "type": "file", + "path": "COPYING" + } + }, "nghttp2": { "type": "ghrel", "repo": "nghttp2/nghttp2", @@ -880,7 +985,7 @@ "postgresql": { "type": "ghtagtar", "repo": "postgres/postgres", - "match": "REL_16_\\d+", + "match": "REL_18_\\d+", "license": { "type": "file", "path": "COPYRIGHT" @@ -971,7 +1076,6 @@ }, "snappy": { "type": "git", - "repo": "google/snappy", "rev": "main", "url": "https://github.com/google/snappy", "license": { @@ -980,9 +1084,8 @@ } }, "spx": { - "type": "git", - "rev": "master", - "url": "https://github.com/static-php/php-spx.git", + "type": "pie", + "repo": "noisebynorthwest/php-spx", "path": "php-src/ext/spx", "license": { "type": "file", @@ -1136,14 +1239,5 @@ "type": "file", "path": "LICENSE" } - }, - "liburing": { - "type": "ghtar", - "repo": "axboe/liburing", - "prefer-stable": true, - "license": { - "type": "file", - "path": "COPYING" - } } } diff --git a/docs/.vitepress/components/CliGenerator.vue b/docs/.vitepress/components/CliGenerator.vue index 430dec241..378f6eaed 100644 --- a/docs/.vitepress/components/CliGenerator.vue +++ b/docs/.vitepress/components/CliGenerator.vue @@ -744,10 +744,26 @@ h2 { } select { - border-radius: 4px; + border-radius: 8px; border: 1px solid var(--vp-c-divider); - padding: 0 4px; + padding: 8px 12px; width: 300px; + background-color: var(--vp-c-bg-soft); + color: var(--vp-c-text-1); + font-size: 14px; + transition: all 0.2s ease; + cursor: pointer; + outline: none; +} + +select:hover { + border-color: var(--vp-c-brand-1); + background-color: var(--vp-c-bg); +} + +select:focus { + border-color: var(--vp-c-brand-1); + box-shadow: 0 0 0 3px var(--vp-c-brand-soft); } .my-btn { @@ -781,17 +797,160 @@ select { .textarea { border: 1px solid var(--vp-c-divider); - border-radius: 4px; - width: calc(100% - 12px); - padding: 4px 8px; + border-radius: 8px; + width: calc(100% - 24px); + padding: 12px; + background-color: var(--vp-c-bg-soft); + color: var(--vp-c-text-1); + font-size: 14px; + font-family: var(--vp-font-family-mono); + line-height: 1.5; + transition: all 0.2s ease; + outline: none; + resize: vertical; +} + +.textarea:hover { + border-color: var(--vp-c-brand-1); + background-color: var(--vp-c-bg); +} + +.textarea:focus { + border-color: var(--vp-c-brand-1); + box-shadow: 0 0 0 3px var(--vp-c-brand-soft); } .input { display: block; border: 1px solid var(--vp-c-divider); - border-radius: 4px; + border-radius: 8px; width: 100%; - padding: 4px 8px; + padding: 10px 12px; + background-color: var(--vp-c-bg-soft); + color: var(--vp-c-text-1); + font-size: 14px; + transition: all 0.2s ease; + outline: none; + box-sizing: border-box; +} + +.input:hover { + border-color: var(--vp-c-brand-1); + background-color: var(--vp-c-bg); +} + +.input:focus { + border-color: var(--vp-c-brand-1); + box-shadow: 0 0 0 3px var(--vp-c-brand-soft); +} + +/* Radio button styles */ +input[type="radio"] { + appearance: none; + width: 18px; + height: 18px; + border: 2px solid var(--vp-c-border); + border-radius: 50%; + background-color: var(--vp-c-bg); + cursor: pointer; + position: relative; + vertical-align: middle; + margin-right: 6px; + transition: all 0.2s ease; +} + +input[type="radio"]:hover { + border-color: var(--vp-c-brand-1); + background-color: var(--vp-c-bg-soft); +} + +input[type="radio"]:checked { + border-color: var(--vp-c-brand-1); + background-color: var(--vp-c-brand-1); +} + +input[type="radio"]:checked::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--vp-c-bg); +} + +input[type="radio"]:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Checkbox styles */ +input[type="checkbox"] { + appearance: none; + width: 18px; + height: 18px; + border: 2px solid var(--vp-c-border); + border-radius: 4px; + background-color: var(--vp-c-bg); + cursor: pointer; + position: relative; + vertical-align: middle; + margin-right: 6px; + transition: all 0.2s ease; +} + +input[type="checkbox"]:hover { + border-color: var(--vp-c-brand-1); + background-color: var(--vp-c-bg-soft); +} + +input[type="checkbox"]:checked { + border-color: var(--vp-c-brand-1); + background-color: var(--vp-c-brand-1); +} + +input[type="checkbox"]:checked::after { + content: ''; + position: absolute; + top: 2px; + left: 5px; + width: 4px; + height: 8px; + border: solid var(--vp-c-bg); + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +input[type="checkbox"]:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Label styles */ +label { + cursor: pointer; + user-select: none; + color: var(--vp-c-text-1); + font-size: 14px; + line-height: 1.5; + transition: color 0.2s ease; +} + +label:hover { + color: var(--vp-c-brand-1); +} + +input[type="radio"]:disabled + label, +input[type="checkbox"]:disabled + label { + opacity: 0.5; + cursor: not-allowed; +} + +input[type="radio"]:disabled + label:hover, +input[type="checkbox"]:disabled + label:hover { + color: var(--vp-c-text-1); } .command-container { diff --git a/docs/.vitepress/components/Contributors.vue b/docs/.vitepress/components/Contributors.vue new file mode 100644 index 000000000..c1f474b0c --- /dev/null +++ b/docs/.vitepress/components/Contributors.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index bf210a996..8ddc9c1f2 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -4,7 +4,7 @@ import sidebarZh from "./sidebar.zh"; // https://vitepress.dev/reference/site-config export default { - title: "static-php-cli", + title: "Static PHP", description: "Build single static PHP binary, with PHP project together, with popular extensions included.", locales: { en: { @@ -44,6 +44,7 @@ export default { }, themeConfig: { // https://vitepress.dev/reference/default-theme-config + logo: '/images/static-php_nobg.png', nav: [], socialLinks: [ {icon: 'github', link: 'https://github.com/crazywhalecc/static-php-cli'} diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css index b09fc6828..ccf440746 100644 --- a/docs/.vitepress/theme/style.css +++ b/docs/.vitepress/theme/style.css @@ -4,3 +4,21 @@ max-width: 1000px !important; } + +.vp-doc .contributors-header h2 { + padding-top: 0; + border-top: none; +} + +.vp-doc .sponsors-header h2 { + padding-top: 0; + border-top: none; +} + +.dark .VPImage.logo { + filter: contrast(0.7); +} + +.dark .VPImage.image-src { + filter: contrast(0.7); +} diff --git a/docs/en/develop/source-module.md b/docs/en/develop/source-module.md index 4b1b1fc0f..51c3ba3ca 100644 --- a/docs/en/develop/source-module.md +++ b/docs/en/develop/source-module.md @@ -36,6 +36,7 @@ The following is the source download configuration corresponding to the `libeven The most important field here is `type`. Currently, the types it supports are: - `url`: Directly use URL to download, for example: `https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`. +- `pie`: Download PHP extensions from Packagist using the PIE (PHP Installer for Extensions) standard. - `ghrel`: Use the GitHub Release API to download, download the artifacts uploaded from the latest version released by maintainers. - `ghtar`: Use the GitHub Release API to download. Different from `ghrel`, `ghtar` is downloaded from the `source code (tar.gz)` in the latest Release of the project. @@ -89,6 +90,37 @@ Example (download the imagick extension and extract it to the extension storage } ``` +## Download type - pie + +PIE (PHP Installer for Extensions) type sources refer to downloading PHP extensions from Packagist that follow the PIE standard. +This method automatically fetches extension information from the Packagist repository and downloads the appropriate distribution file. + +The parameters included are: + +- `repo`: The Packagist vendor/package name, such as `vendor/package-name` + +Example (download a PHP extension from Packagist using PIE): + +```json +{ + "ext-example": { + "type": "pie", + "repo": "vendor/example-extension", + "path": "php-src/ext/example", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +::: tip +The PIE download type will automatically detect the extension information from Packagist metadata, +including the download URL, version, and distribution type. +The extension must be marked as `type: php-ext` or contain `php-ext` metadata in its Packagist package definition. +::: + ## Download type - ghrel ghrel will download files from Assets uploaded in GitHub Release. diff --git a/docs/en/faq/index.md b/docs/en/faq/index.md index a942244cd..b65dca463 100644 --- a/docs/en/faq/index.md +++ b/docs/en/faq/index.md @@ -44,6 +44,11 @@ So on macOS, you can **directly** use SPC to build statically compiled PHP binar 2. You will get `buildroot/modules/xdebug.so` and `buildroot/bin/php`. 3. The `xdebug.so` file could be used for php that version and thread-safe are the same. +For the Windows platform, since officially built extensions (such as `php_yaml.dll`) force the use of the `php8.dll` dynamic library as a link, and statically built PHP does not include any dynamic libraries other than system libraries, +php.exe built by static-php cannot load officially built dynamic extensions. Since static-php-cli does not yet support building dynamic extensions, there is currently no way to load dynamic extensions with static-php. + +However, Windows can normally use the `FFI` extension to load other dll files and call them. + ## Can it support Oracle database extension? Some extensions that rely on closed source libraries, such as `oci8`, `sourceguardian`, etc., diff --git a/docs/en/index.md b/docs/en/index.md index 3aee06126..7d80bf2f7 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -3,11 +3,14 @@ layout: home hero: - name: "static-php-cli" + name: "Static PHP" tagline: "Build standalone PHP binary on Linux, macOS, FreeBSD, Windows, with PHP project together, with popular extensions included." + image: + src: /images/static-php_nobg.png + alt: Static PHP CLI Logo actions: - theme: brand - text: Guide + text: Get Started link: ./guide/ features: @@ -19,3 +22,121 @@ features: details: static-php-cli comes with dependency management and supports installation of different types of PHP extensions. --- + + +
+
+

Special Sponsors

+

+ Thank you to our amazing sponsors for supporting this project! +

+
+ +
+ + + + diff --git a/docs/index.md b/docs/index.md index f667ce448..b2b25dedf 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,8 +3,11 @@ layout: home hero: - name: "static-php-cli" + name: "Static PHP" tagline: "Build standalone PHP binary on Linux, macOS, FreeBSD, Windows, with PHP project together, with popular extensions included." + image: + src: /images/static-php_nobg.png + alt: Static PHP CLI Logo actions: - theme: brand text: Get Started @@ -24,13 +27,121 @@ features: -## Special Sponsors +
+
+

Special Sponsors

+

+ Thank you to our amazing sponsors for supporting this project! +

+
+ +
+ + - + diff --git a/docs/public/images/static-php_nobg.png b/docs/public/images/static-php_nobg.png new file mode 100644 index 000000000..64b6695c0 Binary files /dev/null and b/docs/public/images/static-php_nobg.png differ diff --git a/docs/zh/develop/source-module.md b/docs/zh/develop/source-module.md index 00feb3c42..769ffa08b 100644 --- a/docs/zh/develop/source-module.md +++ b/docs/zh/develop/source-module.md @@ -30,6 +30,7 @@ static-php-cli 的下载资源模块是一个主要的功能,它包含了所 这里最主要的字段是 `type`,目前它支持的类型有: - `url`: 直接使用 URL 下载,例如:`https://download.libsodium.org/libsodium/releases/libsodium-1.0.18.tar.gz`。 +- `pie`: 使用 PIE(PHP Installer for Extensions)标准从 Packagist 下载 PHP 扩展。 - `ghrel`: 使用 GitHub Release API 下载,即从 GitHub 项目发布的最新版本中上传的附件下载。 - `ghtar`: 使用 GitHub Release API 下载,与 `ghrel` 不同的是,`ghtar` 是从项目的最新 Release 中找 `source code (tar.gz)` 下载的。 - `ghtagtar`: 使用 GitHub Release API 下载,与 `ghtar` 相比,`ghtagtar` 可以从 `tags` 列表找最新的,并下载 `tar.gz` 格式的源码(因为有些项目只使用了 `tag` 发布版本)。 @@ -77,6 +78,36 @@ url 类型的资源指的是从 URL 直接下载文件。 } ``` +## 下载类型 - pie + +PIE(PHP Installer for Extensions)类型的资源是从 Packagist 下载遵循 PIE 标准的 PHP 扩展。 +该方法会自动从 Packagist 仓库获取扩展信息,并下载相应的分发文件。 + +包含的参数有: + +- `repo`: Packagist 的 vendor/package 名称,如 `vendor/package-name` + +例子(使用 PIE 从 Packagist 下载 PHP 扩展): + +```json +{ + "ext-example": { + "type": "pie", + "repo": "vendor/example-extension", + "path": "php-src/ext/example", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} +``` + +::: tip +PIE 下载类型会自动从 Packagist 元数据中检测扩展信息,包括下载 URL、版本和分发类型。 +扩展必须在其 Packagist 包定义中标记为 `type: php-ext` 或包含 `php-ext` 元数据。 +::: + ## 下载类型 - ghrel ghrel 会从 GitHub Release 中上传的 Assets 下载文件。首先使用 GitHub Release API 获取最新版本,然后根据正则匹配方式下载相应的文件。 diff --git a/docs/zh/faq/index.md b/docs/zh/faq/index.md index 27611a50a..142a1503f 100644 --- a/docs/zh/faq/index.md +++ b/docs/zh/faq/index.md @@ -41,6 +41,11 @@ buildroot/bin/php -d "zend_extension=/path/to/php{PHP_VER}-{ts/nts}/xdebug.so" - 2. 你将获得 `buildroot/modules/xdebug.so` 和 `buildroot/bin/php`。 3. `xdebug.so` 文件可用于版本和线程安全相同的 php。 +对于 Windows 平台,由于官方构建的扩展(如 `php_yaml.dll`)强制使用了 `php8.dll` 动态库作为链接,静态构建的 PHP 不包含任何系统库以外的动态库, +所以 Windows 下无法加载官方构建的动态扩展。 由于 static-php-cli 还暂未支持构建动态扩展,所以目前还没有让 static-php 加载动态扩展的方法。 + +不过,Windows 可以正常使用 `FFI` 扩展加载其他的 dll 文件并调用。 + ## 可以支持 Oracle 数据库扩展吗? 部分依赖库闭源的扩展,如 `oci8`、`sourceguardian` 等,它们没有提供纯静态编译的依赖库文件(`.a`),仅提供了动态依赖库文件(`.so`), diff --git a/docs/zh/index.md b/docs/zh/index.md index bfa81cf4e..265d39745 100644 --- a/docs/zh/index.md +++ b/docs/zh/index.md @@ -3,18 +3,140 @@ layout: home hero: - name: "static-php-cli" + name: "Static PHP" tagline: "在 Linux、macOS、FreeBSD、Windows 上与 PHP 项目一起构建独立的 PHP 二进制文件,并包含流行的扩展。" + image: + src: /images/static-php_nobg.png + alt: Static PHP CLI Logo actions: - theme: brand - text: 指南 + text: 开始使用 link: ./guide/ features: -- title: 静态二进制 - details: 您可以轻松地编译一个独立的 PHP 二进制文件以供嵌入程序使用。包括 cli、fpm、micro。 -- title: phpmicro 自执行二进制 - details: 您可以使用 micro SAPI 编译一个自解压的可执行文件,并将 PHP 代码与二进制文件打包为一个文件。 -- title: 依赖管理 - details: static-php-cli 附带依赖项管理,支持安装不同类型的 PHP 扩展和不同的依赖库。 + - title: 静态 CLI 二进制 + details: 您可以轻松地编译一个独立的 PHP 二进制文件以供通用使用,包括 CLI、FPM SAPI。 + - title: Micro 自解压可执行文件 + details: 您可以编译一个自解压的可执行文件,并将 PHP 源代码与二进制文件打包在一起。 + - title: 依赖管理 + details: static-php-cli 附带依赖项管理,支持安装不同类型的 PHP 扩展。 --- + + + +
+
+

特别赞助商

+

+ 感谢我们出色的赞助商对本项目的支持! +

+
+ +
+ + + + diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 023b541e6..1b1d3e199 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.7.4'; + public const string VERSION = '2.7.9'; public function __construct() { diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index e6e27cda9..fdba936d7 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -128,27 +128,6 @@ public function getExts(bool $including_shared = true): array return array_filter($this->exts, fn ($ext) => $ext->isBuildStatic()); } - /** - * Check if there is a cpp extensions or libraries. - */ - public function hasCpp(): bool - { - // judge cpp-extension - $exts = array_keys($this->getExts(false)); - foreach ($exts as $ext) { - if (Config::getExt($ext, 'cpp-extension', false) === true) { - return true; - } - } - $libs = array_keys($this->getLibs()); - foreach ($libs as $lib) { - if (Config::getLib($lib, 'cpp-library', false) === true) { - return true; - } - } - return false; - } - /** * Set libs only mode. * diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index ac7efd1ec..08b403e61 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -4,17 +4,13 @@ namespace SPC\builder; +use SPC\builder\unix\UnixBuilderBase; use SPC\exception\EnvironmentException; -use SPC\exception\FileSystemException; use SPC\exception\SPCException; use SPC\exception\ValidationException; use SPC\exception\WrongUsageException; use SPC\store\Config; use SPC\store\FileSystem; -use SPC\toolchain\ClangNativeToolchain; -use SPC\toolchain\GccNativeToolchain; -use SPC\toolchain\ToolchainManager; -use SPC\toolchain\ZigToolchain; use SPC\util\SPCConfigUtil; use SPC\util\SPCTarget; @@ -225,13 +221,25 @@ public function patchBeforeSharedConfigure(): bool */ public function patchBeforeSharedMake(): bool { - $config = (new SPCConfigUtil($this->builder))->config([$this->getName()], array_map(fn ($l) => $l->getName(), $this->builder->getLibs())); - [$staticLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); - FileSystem::replaceFileRegex( - $this->source_dir . '/Makefile', - '/^(.*_SHARED_LIBADD\s*=.*)$/m', - '$1 ' . trim($staticLibs) - ); + $config = (new SPCConfigUtil($this->builder))->getExtensionConfig($this); + [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); + $lstdcpp = str_contains($sharedLibs, '-l:libstdc++.a') ? '-l:libstdc++.a' : null; + $lstdcpp ??= str_contains($sharedLibs, '-lstdc++') ? '-lstdc++' : ''; + + $makefileContent = file_get_contents($this->source_dir . '/Makefile'); + if (preg_match('/^(.*_SHARED_LIBADD\s*=\s*)(.*)$/m', $makefileContent, $matches)) { + $prefix = $matches[1]; + $currentLibs = trim($matches[2]); + $newLibs = trim("{$currentLibs} {$staticLibs} {$lstdcpp}"); + $deduplicatedLibs = deduplicate_flags($newLibs); + + FileSystem::replaceFileRegex( + $this->source_dir . '/Makefile', + '/^(.*_SHARED_LIBADD\s*=.*)$/m', + $prefix . $deduplicatedLibs + ); + } + if ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS')) { FileSystem::replaceFileRegex( $this->source_dir . '/Makefile', @@ -298,7 +306,7 @@ public function runCliCheckUnix(): void // Run compile check if build target is cli // If you need to run some check, overwrite this or add your assert in src/globals/ext-tests/{extension_name}.php $sharedExtensions = $this->getSharedExtensionLoadString(); - [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"'); + [$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"'); if ($ret !== 0) { throw new ValidationException( "extension {$this->getName()} failed compile check: php-cli returned {$ret}", @@ -328,7 +336,7 @@ public function runCliCheckWindows(): void { // Run compile check if build target is cli // If you need to run some check, overwrite this or add your assert in src/globals/ext-tests/{extension_name}.php - [$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -n --ri "' . $this->getDistName() . '"', false); + [$ret] = cmd()->execWithResult(BUILD_BIN_PATH . '/php.exe -n --ri "' . $this->getDistName() . '"', false); if ($ret !== 0) { throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: "Extension {$this->getName()} sanity check"); } @@ -405,42 +413,7 @@ public function buildShared(array $visited = []): void */ public function buildUnixShared(): void { - $config = (new SPCConfigUtil($this->builder))->config( - [$this->getName()], - array_map(fn ($l) => $l->getName(), $this->getLibraryDependencies(recursive: true)), - $this->builder->getOption('with-suggested-exts'), - $this->builder->getOption('with-suggested-libs'), - ); - [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); - $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; - $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; - $env = [ - 'CFLAGS' => $config['cflags'], - 'CXXFLAGS' => $config['cflags'], - 'LDFLAGS' => $config['ldflags'], - 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), - 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, - ]; - if (str_contains($env['LIBS'], '-lstdc++') && SPCTarget::getTargetOS() === 'Linux') { - if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { - $env['SPC_COMPILER_EXTRA'] = '-lstdc++'; - } elseif (ToolchainManager::getToolchainClass() === GccNativeToolchain::class || ToolchainManager::getToolchainClass() === ClangNativeToolchain::class) { - try { - $content = FileSystem::readFile($this->source_dir . '/config.m4'); - if ($content && !str_contains($content, 'PHP_ADD_LIBRARY(stdc++')) { - $pattern = '/(PHP_NEW_EXTENSION\(' . $this->name . ',.*\))/m'; - $replacement = "$1\nPHP_ADD_LIBRARY(stdc++, 1, " . strtoupper($this->name) . '_SHARED_LIBADD)'; - FileSystem::replaceFileRegex( - $this->source_dir . '/config.m4', - $pattern, - $replacement - ); - } - } catch (FileSystemException) { - } - } - } - + $env = $this->getSharedExtensionEnv(); if ($this->patchBeforeSharedPhpize()) { logger()->info("Extension [{$this->getName()}] patched before shared phpize"); } @@ -455,13 +428,15 @@ public function buildUnixShared(): void logger()->info("Extension [{$this->getName()}] patched before shared configure"); } + $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + shell()->cd($this->source_dir) ->setEnv($env) ->appendEnv($this->getExtraEnv()) ->exec( './configure ' . $this->getUnixConfigureArg(true) . ' --with-php-config=' . BUILD_BIN_PATH . '/php-config ' . - '--enable-shared --disable-static' + "--enable-shared --disable-static {$phpvars}" ); if ($this->patchBeforeSharedMake()) { @@ -474,6 +449,15 @@ public function buildUnixShared(): void ->exec('make clean') ->exec('make -j' . $this->builder->concurrency) ->exec('make install'); + + // process *.so file + $soFile = BUILD_MODULES_PATH . '/' . $this->getName() . '.so'; + if (!file_exists($soFile)) { + throw new ValidationException("extension {$this->getName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getName()} build"); + } + /** @var UnixBuilderBase $builder */ + $builder = $this->builder; + $builder->deployBinary($soFile, $soFile, false); } /** @@ -512,6 +496,58 @@ public function isBuildStatic(): bool return $this->build_static; } + /** + * Get the library dependencies that current extension depends on. + * + * @param bool $recursive Whether it includes dependencies recursively + */ + public function getLibraryDependencies(bool $recursive = false): array + { + $ret = array_filter($this->dependencies, fn ($x) => $x instanceof LibraryBase); + if (!$recursive) { + return $ret; + } + + $deps = []; + + $added = 1; + while ($added !== 0) { + $added = 0; + foreach ($ret as $depName => $dep) { + foreach ($dep->getDependencies(true) as $depdepName => $depdep) { + if (!array_key_exists($depdepName, $deps)) { + $deps[$depdepName] = $depdep; + ++$added; + } + } + if (!array_key_exists($depName, $deps)) { + $deps[$depName] = $dep; + } + } + } + + return $deps; + } + + /** + * Returns the environment variables a shared extension needs to be built. + * CFLAGS, CXXFLAGS, LDFLAGS and so on. + */ + protected function getSharedExtensionEnv(): array + { + $config = (new SPCConfigUtil($this->builder))->getExtensionConfig($this); + [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); + $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; + $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; + return [ + 'CFLAGS' => $config['cflags'], + 'CXXFLAGS' => $config['cflags'], + 'LDFLAGS' => $config['ldflags'], + 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), + 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, + ]; + } + protected function addLibraryDependency(string $name, bool $optional = false): void { $depLib = $this->builder->getLib($name); @@ -521,7 +557,7 @@ protected function addLibraryDependency(string $name, bool $optional = false): v } logger()->info("enabling {$this->name} without library {$name}"); } else { - $this->dependencies[] = $depLib; + $this->dependencies[$name] = $depLib; } } @@ -534,7 +570,7 @@ protected function addExtensionDependency(string $name, bool $optional = false): } logger()->info("enabling {$this->name} without extension {$name}"); } else { - $this->dependencies[] = $depExt; + $this->dependencies[$name] = $depExt; } } @@ -569,37 +605,4 @@ protected function splitLibsIntoStaticAndShared(string $allLibs): array } return [trim($staticLibString), trim($sharedLibString)]; } - - private function getLibraryDependencies(bool $recursive = false): array - { - $ret = array_filter($this->dependencies, fn ($x) => $x instanceof LibraryBase); - if (!$recursive) { - return $ret; - } - - $deps = []; - - $added = 1; - while ($added !== 0) { - $added = 0; - foreach ($ret as $depName => $dep) { - foreach ($dep->getDependencies(true) as $depdepName => $depdep) { - if (!in_array($depdepName, array_keys($deps), true)) { - $deps[$depdepName] = $depdep; - ++$added; - } - } - if (!in_array($depName, array_keys($deps), true)) { - $deps[$depName] = $dep; - } - } - } - - if (array_key_exists(0, $deps)) { - $zero = [0 => $deps[0]]; - unset($deps[0]); - return $zero + $deps; - } - return $deps; - } } diff --git a/src/SPC/builder/extension/gettext.php b/src/SPC/builder/extension/gettext.php index dc4f58aaa..303cc3892 100644 --- a/src/SPC/builder/extension/gettext.php +++ b/src/SPC/builder/extension/gettext.php @@ -15,7 +15,11 @@ class gettext extends Extension public function patchBeforeBuildconf(): bool { if ($this->builder instanceof MacOSBuilder) { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/gettext/config.m4', 'AC_CHECK_LIB($GETTEXT_CHECK_IN_LIB', 'AC_CHECK_LIB(intl'); + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/ext/gettext/config.m4', + ['AC_CHECK_LIB($GETTEXT_CHECK_IN_LIB', 'AC_CHECK_LIB([$GETTEXT_CHECK_IN_LIB'], + ['AC_CHECK_LIB(intl', 'AC_CHECK_LIB([intl'] // new php versions use a bracket + ); } return true; } diff --git a/src/SPC/builder/extension/grpc.php b/src/SPC/builder/extension/grpc.php index 61af6c04d..fb31c85fe 100644 --- a/src/SPC/builder/extension/grpc.php +++ b/src/SPC/builder/extension/grpc.php @@ -43,7 +43,7 @@ public function patchBeforeBuildconf(): bool public function patchBeforeConfigure(): bool { $util = new SPCConfigUtil($this->builder, ['libs_only_deps' => true]); - $config = $util->config(['grpc']); + $config = $util->getExtensionConfig($this); $libs = $config['libs']; FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/configure', '-lgrpc', $libs); return true; @@ -56,4 +56,11 @@ public function patchBeforeMake(): bool GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes'); return true; } + + protected function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + $env['CPPFLAGS'] = $env['CXXFLAGS'] . ' -Wno-attributes'; + return $env; + } } diff --git a/src/SPC/builder/extension/maxminddb.php b/src/SPC/builder/extension/maxminddb.php new file mode 100644 index 000000000..a84224164 --- /dev/null +++ b/src/SPC/builder/extension/maxminddb.php @@ -0,0 +1,25 @@ +source_dir; + FileSystem::copyDir($original . '/ext', SOURCE_PATH . '/php-src/ext/maxminddb'); + $this->source_dir = SOURCE_PATH . '/php-src/ext/maxminddb'; + return true; + } + $this->source_dir = SOURCE_PATH . '/php-src/ext/maxminddb'; + return false; + } +} diff --git a/src/SPC/builder/extension/rdkafka.php b/src/SPC/builder/extension/rdkafka.php index 575ab3092..58af50ffe 100644 --- a/src/SPC/builder/extension/rdkafka.php +++ b/src/SPC/builder/extension/rdkafka.php @@ -7,6 +7,7 @@ use SPC\builder\Extension; use SPC\store\FileSystem; use SPC\util\CustomExt; +use SPC\util\SPCConfigUtil; #[CustomExt('rdkafka')] class rdkafka extends Extension @@ -15,6 +16,7 @@ public function patchBeforeBuildconf(): bool { FileSystem::replaceFileStr("{$this->source_dir}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\n", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm \$RDKAFKA_LIBS\n"); FileSystem::replaceFileStr("{$this->source_dir}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\"\n", '-L$RDKAFKA_DIR/$PHP_LIBDIR -lm $RDKAFKA_LIBS"'); + FileSystem::replaceFileStr("{$this->source_dir}/config.m4", 'PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,', 'AC_CHECK_LIB([$LIBNAME], [$LIBSYMBOL],'); return true; } @@ -37,8 +39,7 @@ public function patchBeforeMake(): bool public function getUnixConfigureArg(bool $shared = false): string { - $pkgconf_libs = shell()->execWithResult('pkg-config --libs --static rdkafka')[1]; - $pkgconf_libs = trim(implode('', $pkgconf_libs)); - return '--with-rdkafka=' . ($shared ? 'shared,' : '') . BUILD_ROOT_PATH . ' RDKAFKA_LIBS="' . $pkgconf_libs . '"'; + $pkgconf_libs = (new SPCConfigUtil($this->builder, ['no_php' => true, 'libs_only_deps' => true]))->getExtensionConfig($this); + return '--with-rdkafka=' . ($shared ? 'shared,' : '') . BUILD_ROOT_PATH . " RDKAFKA_LIBS=\"{$pkgconf_libs['libs']}\""; } } diff --git a/src/SPC/builder/extension/readline.php b/src/SPC/builder/extension/readline.php index a8ee48aa0..035d7b438 100644 --- a/src/SPC/builder/extension/readline.php +++ b/src/SPC/builder/extension/readline.php @@ -5,6 +5,7 @@ namespace SPC\builder\extension; use SPC\builder\Extension; +use SPC\exception\ValidationException; use SPC\store\FileSystem; use SPC\util\CustomExt; @@ -23,12 +24,7 @@ public function patchBeforeConfigure(): bool public function getUnixConfigureArg(bool $shared = false): string { - $enable = '--without-libedit --with-readline=' . BUILD_ROOT_PATH; - if ($this->builder->getPHPVersionID() < 84000) { - // the check uses `char rl_pending_input()` instead of `extern int rl_pending_input`, which makes LTO fail - $enable .= ' ac_cv_lib_readline_rl_pending_input=yes'; - } - return $enable; + return '--with-libedit --without-readline'; } public function buildUnixShared(): void @@ -39,4 +35,13 @@ public function buildUnixShared(): void } parent::buildUnixShared(); } + + public function runCliCheckUnix(): void + { + parent::runCliCheckUnix(); + [$ret, $out] = shell()->execWithResult('printf "exit\n" | ' . BUILD_BIN_PATH . '/php -a'); + if ($ret !== 0 || !str_contains(implode("\n", $out), 'Interactive shell')) { + throw new ValidationException("readline extension failed sanity check. Code: {$ret}, output: " . implode("\n", $out)); + } + } } diff --git a/src/SPC/builder/extension/simdjson.php b/src/SPC/builder/extension/simdjson.php index 71796ebee..914fd674f 100644 --- a/src/SPC/builder/extension/simdjson.php +++ b/src/SPC/builder/extension/simdjson.php @@ -6,6 +6,8 @@ use SPC\builder\Extension; use SPC\store\FileSystem; +use SPC\toolchain\ToolchainManager; +use SPC\toolchain\ZigToolchain; use SPC\util\CustomExt; #[CustomExt('simdjson')] @@ -17,7 +19,7 @@ public function patchBeforeBuildconf(): bool FileSystem::replaceFileRegex( SOURCE_PATH . '/php-src/ext/simdjson/config.m4', '/php_version=(`.*`)$/m', - 'php_version=' . strval($php_ver) + 'php_version=' . $php_ver ); FileSystem::replaceFileStr( SOURCE_PATH . '/php-src/ext/simdjson/config.m4', @@ -31,4 +33,18 @@ public function patchBeforeBuildconf(): bool ); return true; } + + public function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { + $extra = getenv('SPC_COMPILER_EXTRA'); + if (!str_contains((string) $extra, '-lstdc++')) { + f_putenv('SPC_COMPILER_EXTRA=' . clean_spaces($extra . ' -lstdc++')); + } + $env['CFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + $env['CXXFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + } + return $env; + } } diff --git a/src/SPC/builder/extension/snmp.php b/src/SPC/builder/extension/snmp.php new file mode 100644 index 000000000..488fc81ec --- /dev/null +++ b/src/SPC/builder/extension/snmp.php @@ -0,0 +1,29 @@ +builder->getPHPVersionID() < 80400) { + FileSystem::copy(ROOT_DIR . '/src/globals/extra/snmp-ext-config-old.m4', "{$this->source_dir}/config.m4"); + } + $libs = implode(' ', PkgConfigUtil::getLibsArray('netsnmp')); + FileSystem::replaceFileStr( + "{$this->source_dir}/config.m4", + 'PHP_EVAL_LIBLINE([$SNMP_LIBS], [SNMP_SHARED_LIBADD])', + "SNMP_LIBS=\"{$libs}\"\nPHP_EVAL_LIBLINE([\$SNMP_LIBS], [SNMP_SHARED_LIBADD])" + ); + return true; + } +} diff --git a/src/SPC/builder/extension/spx.php b/src/SPC/builder/extension/spx.php index f5e736d58..a5a8ad71a 100644 --- a/src/SPC/builder/extension/spx.php +++ b/src/SPC/builder/extension/spx.php @@ -13,7 +13,7 @@ class spx extends Extension { public function getUnixConfigureArg(bool $shared = false): string { - $arg = '--enable-spx' . ($shared ? '=shared' : ''); + $arg = '--enable-SPX' . ($shared ? '=shared' : ''); if ($this->builder->getLib('zlib') !== null) { $arg .= ' --with-zlib-dir=' . BUILD_ROOT_PATH; } @@ -29,4 +29,20 @@ public function patchBeforeConfigure(): bool ); return true; } + + public function patchBeforeBuildconf(): bool + { + FileSystem::replaceFileStr( + $this->source_dir . '/config.m4', + 'CFLAGS="$CFLAGS -Werror -Wall -O3 -pthread -std=gnu90"', + 'CFLAGS="$CFLAGS -pthread"' + ); + FileSystem::replaceFileStr( + $this->source_dir . '/src/php_spx.h', + "extern zend_module_entry spx_module_entry;\n", + "extern zend_module_entry spx_module_entry;;\n#define phpext_spx_ptr &spx_module_entry\n" + ); + FileSystem::copy($this->source_dir . '/src/php_spx.h', $this->source_dir . '/php_spx.h'); + return true; + } } diff --git a/src/SPC/builder/extension/swoole.php b/src/SPC/builder/extension/swoole.php index a4a531cf3..f6ff5931d 100644 --- a/src/SPC/builder/extension/swoole.php +++ b/src/SPC/builder/extension/swoole.php @@ -69,12 +69,15 @@ public function getUnixConfigureArg(bool $shared = false): string $arg .= $this->builder->getExt('swoole-hook-pgsql') ? ' --enable-swoole-pgsql' : ' --disable-swoole-pgsql'; $arg .= $this->builder->getExt('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd'; $arg .= $this->builder->getExt('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite'; - if ($this->builder->getExt('swoole-hook-odbc')) { - $config = (new SPCConfigUtil($this->builder, ['libs_only_deps' => true]))->config([], ['unixodbc']); + $config = (new SPCConfigUtil($this->builder))->getLibraryConfig($this->builder->getLib('unixodbc')); $arg .= ' --with-swoole-odbc=unixODBC,' . BUILD_ROOT_PATH . ' SWOOLE_ODBC_LIBS="' . $config['libs'] . '"'; } + if ($this->getExtVersion() >= '6.1.0') { + $arg .= ' --enable-swoole-stdext'; + } + if (SPCTarget::getTargetOS() === 'Darwin') { $arg .= ' ac_cv_lib_pthread_pthread_barrier_init=no'; } diff --git a/src/SPC/builder/extension/trader.php b/src/SPC/builder/extension/trader.php new file mode 100644 index 000000000..030215297 --- /dev/null +++ b/src/SPC/builder/extension/trader.php @@ -0,0 +1,19 @@ +getOption('no-strip', false)) { $shell->exec('strip sapi/cli/php'); } - $this->deployBinary(BUILD_TARGET_CLI); + $this->deploySAPIBinary(BUILD_TARGET_CLI); } /** @@ -184,7 +184,7 @@ protected function buildMicro(): void if (!$this->getOption('no-strip', false)) { shell()->cd(SOURCE_PATH . '/php-src/sapi/micro')->exec('strip --strip-unneeded micro.sfx'); } - $this->deployBinary(BUILD_TARGET_MICRO); + $this->deploySAPIBinary(BUILD_TARGET_MICRO); if ($this->phar_patched) { SourcePatcher::unpatchMicroPhar(); @@ -206,7 +206,7 @@ protected function buildFpm(): void if (!$this->getOption('no-strip', false)) { $shell->exec('strip sapi/fpm/php-fpm'); } - $this->deployBinary(BUILD_TARGET_FPM); + $this->deploySAPIBinary(BUILD_TARGET_FPM); } /** diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 9bd271670..0d6f77fba 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -7,6 +7,8 @@ use SPC\builder\unix\UnixBuilderBase; use SPC\exception\PatchException; use SPC\exception\WrongUsageException; +use SPC\store\Config; +use SPC\store\DirDiff; use SPC\store\FileSystem; use SPC\store\SourcePatcher; use SPC\util\GlobalEnvManager; @@ -89,12 +91,14 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void // prepare build php envs // $musl_flag = SPCTarget::getLibc() === 'musl' ? ' -D__MUSL__' : ' -U__MUSL__'; $php_configure_env = SystemUtil::makeEnvVarString([ - 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS'), + 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'CPPFLAGS' => '-I' . BUILD_INCLUDE_PATH, // . ' -Dsomethinghere', // . $musl_flag, 'LDFLAGS' => '-L' . BUILD_LIB_PATH, // 'LIBS' => SPCTarget::getRuntimeLibs(), // do not pass static libraries here yet, they may contain polyfills for libc functions! ]); + $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; if ($embed_type !== 'static' && SPCTarget::isStatic()) { throw new WrongUsageException( @@ -105,18 +109,19 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $this->seekPhpSrcLogFileOnException(fn () => shell()->cd(SOURCE_PATH . '/php-src')->exec( $php_configure_env . ' ' . - getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . - ($enableCli ? '--enable-cli ' : '--disable-cli ') . - ($enableFpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') . - ($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') . - ($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') . - ($enableCgi ? '--enable-cgi ' : '--disable-cgi ') . - $config_file_path . - $config_file_scan_dir . - $json_74 . - $zts . - $maxExecutionTimers . - $this->makeStaticExtensionArgs() . ' ' + getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . + ($enableCli ? '--enable-cli ' : '--disable-cli ') . + ($enableFpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') . + ($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') . + ($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') . + ($enableCgi ? '--enable-cgi ' : '--disable-cgi ') . + $config_file_path . + $config_file_scan_dir . + $json_74 . + $zts . + $maxExecutionTimers . + $phpvars . ' ' . + $this->makeStaticExtensionArgs() . ' ' )); $this->emitPatchPoint('before-php-make'); @@ -153,7 +158,14 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void } $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); if (!empty($shared_extensions)) { - logger()->info('Building shared extensions ...'); + if (SPCTarget::isStatic()) { + throw new WrongUsageException( + "You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" . + 'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" . + 'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.' + ); + } + logger()->info('Building shared extensions...'); $this->buildSharedExts(); } } @@ -169,47 +181,32 @@ public function testPHP(int $build_target = BUILD_TARGET_NONE) */ protected function buildCli(): void { - if ($this->getExt('readline')) { + if ($this->getExt('readline') && SPCTarget::isStatic()) { SourcePatcher::patchFile('musl_static_readline.patch', SOURCE_PATH . '/php-src'); } + $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cli"); + ->exec("make {$concurrency} {$vars} cli"); - if ($this->getExt('readline')) { + if ($this->getExt('readline') && SPCTarget::isStatic()) { SourcePatcher::patchFile('musl_static_readline.patch', SOURCE_PATH . '/php-src', true); } - if (!$this->getOption('no-strip', false)) { - shell()->cd(SOURCE_PATH . '/php-src/sapi/cli')->exec('strip --strip-unneeded php'); - } - if ($this->getOption('with-upx-pack')) { - shell()->cd(SOURCE_PATH . '/php-src/sapi/cli') - ->exec(getenv('UPX_EXEC') . ' --best php'); - } - - $this->deployBinary(BUILD_TARGET_CLI); + $this->deploySAPIBinary(BUILD_TARGET_CLI); } protected function buildCgi(): void { $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cgi"); - - if (!$this->getOption('no-strip', false)) { - shell()->cd(SOURCE_PATH . '/php-src/sapi/cgi')->exec('strip --strip-unneeded php-cgi'); - } - if ($this->getOption('with-upx-pack')) { - shell()->cd(SOURCE_PATH . '/php-src/sapi/cgi') - ->exec(getenv('UPX_EXEC') . ' --best php-cgi'); - } + ->exec("make {$concurrency} {$vars} cgi"); - $this->deployBinary(BUILD_TARGET_CGI); + $this->deploySAPIBinary(BUILD_TARGET_CGI); } /** @@ -220,29 +217,33 @@ protected function buildMicro(): void if ($this->getPHPVersionID() < 80000) { throw new WrongUsageException('phpmicro only support PHP >= 8.0!'); } - if ($this->getExt('phar')) { - $this->phar_patched = true; - SourcePatcher::patchMicroPhar($this->getPHPVersionID()); - } - - $enable_fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - $vars = $this->getMakeExtraVars(); + try { + if ($this->getExt('phar')) { + $this->phar_patched = true; + SourcePatcher::patchMicroPhar($this->getPHPVersionID()); + } - // patch fake cli for micro - $vars['EXTRA_CFLAGS'] .= $enable_fake_cli; - $vars = SystemUtil::makeEnvVarString($vars); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; + $enable_fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; + $vars = $this->getMakeExtraVars(); - shell()->cd(SOURCE_PATH . '/php-src') - ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} micro"); + // patch fake cli for micro + $vars['EXTRA_CFLAGS'] .= $enable_fake_cli; + $vars = SystemUtil::makeEnvVarString($vars); + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; - $this->processMicroUPX(); + shell()->cd(SOURCE_PATH . '/php-src') + ->exec('sed -i "s|//lib|/lib|g" Makefile') + ->exec("make {$concurrency} {$vars} micro"); - $this->deployBinary(BUILD_TARGET_MICRO); + // deploy micro.sfx + $dst = $this->deploySAPIBinary(BUILD_TARGET_MICRO); - if ($this->phar_patched) { - SourcePatcher::unpatchMicroPhar(); + // patch after UPX-ed micro.sfx + $this->processUpxedMicroSfx($dst); + } finally { + if ($this->phar_patched) { + SourcePatcher::unpatchMicroPhar(); + } } } @@ -252,19 +253,12 @@ protected function buildMicro(): void protected function buildFpm(): void { $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') ->exec('sed -i "s|//lib|/lib|g" Makefile') - ->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} fpm"); + ->exec("make {$concurrency} {$vars} fpm"); - if (!$this->getOption('no-strip', false)) { - shell()->cd(SOURCE_PATH . '/php-src/sapi/fpm')->exec('strip --strip-unneeded php-fpm'); - } - if ($this->getOption('with-upx-pack')) { - shell()->cd(SOURCE_PATH . '/php-src/sapi/fpm') - ->exec(getenv('UPX_EXEC') . ' --best php-fpm'); - } - $this->deployBinary(BUILD_TARGET_FPM); + $this->deploySAPIBinary(BUILD_TARGET_FPM); } /** @@ -272,17 +266,69 @@ protected function buildFpm(): void */ protected function buildEmbed(): void { + $sharedExts = array_filter($this->exts, static fn ($ext) => $ext->isBuildShared()); + $sharedExts = array_filter($sharedExts, static function ($ext) { + return Config::getExt($ext->getName(), 'build-with-php') === true; + }); + $install_modules = $sharedExts ? 'install-modules' : ''; + + // detect changes in module path + $diff = new DirDiff(BUILD_MODULES_PATH, true); $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); - + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; shell()->cd(SOURCE_PATH . '/php-src') ->exec('sed -i "s|//lib|/lib|g" Makefile') ->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile') - ->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . ' INSTALL_ROOT=' . BUILD_ROOT_PATH . " {$vars} install"); + ->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi {$install_modules} install-build install-headers install-programs"); + + // process libphp.so for shared embed + $libphpSo = BUILD_LIB_PATH . '/libphp.so'; + if (file_exists($libphpSo)) { + // post actions: rename libphp.so to libphp-.so if -release is set in LDFLAGS + $this->processLibphpSoFile($libphpSo); + // deploy libphp.so + $this->deployBinary($libphpSo, $libphpSo, false); + } + + // process shared extensions build-with-php + $increment_files = $diff->getChangedFiles(); + foreach ($increment_files as $increment_file) { + $this->deployBinary($increment_file, $increment_file, false); + } + + // process libphp.a for static embed + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { + $AR = getenv('AR') ?: 'ar'; + f_passthru("{$AR} -t " . BUILD_LIB_PATH . "/libphp.a | grep '\\.a$' | xargs -n1 {$AR} d " . BUILD_LIB_PATH . '/libphp.a'); + // export dynamic symbols + SystemUtil::exportDynamicSymbols(BUILD_LIB_PATH . '/libphp.a'); + } + + // patch embed php scripts + $this->patchPhpScripts(); + } + + /** + * Return extra variables for php make command. + */ + private function getMakeExtraVars(): array + { + $config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); + $static = SPCTarget::isStatic() ? '-all-static' : ''; + $lib = BUILD_LIB_PATH; + return array_filter([ + 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), + 'EXTRA_LIBS' => $config['libs'], + 'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), + 'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie", + ]); + } + private function processLibphpSoFile(string $libphpSo): void + { $ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: ''; $libDir = BUILD_LIB_PATH; $modulesDir = BUILD_MODULES_PATH; - $libphpSo = "{$libDir}/libphp.so"; $realLibName = 'libphp.so'; $cwd = getcwd(); @@ -344,60 +390,29 @@ protected function buildEmbed(): void } } } - - if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { - $AR = getenv('AR') ?: 'ar'; - f_passthru("{$AR} -t " . BUILD_LIB_PATH . "/libphp.a | grep '\\.a$' | xargs -n1 {$AR} d " . BUILD_LIB_PATH . '/libphp.a'); - // export dynamic symbols - SystemUtil::exportDynamicSymbols(BUILD_LIB_PATH . '/libphp.a'); - } - - if (!$this->getOption('no-strip', false) && file_exists(BUILD_LIB_PATH . '/' . $realLibName)) { - shell()->cd(BUILD_LIB_PATH)->exec("strip --strip-unneeded {$realLibName}"); - } - $this->patchPhpScripts(); - } - - /** - * Return extra variables for php make command. - */ - private function getMakeExtraVars(): array - { - $config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); - $static = SPCTarget::isStatic() ? '-all-static' : ''; - $lib = BUILD_LIB_PATH; - return [ - 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), - 'EXTRA_LIBS' => $config['libs'], - 'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), - 'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie", - ]; } /** - * Strip micro.sfx for Linux. - * The micro.sfx does not support UPX directly, but we can remove UPX-info segment to adapt. - * This will also make micro.sfx with upx-packed more like a malware fore antivirus :( + * Patch micro.sfx after UPX compression. + * micro needs special section handling in LinuxBuilder. + * The micro.sfx does not support UPX directly, but we can remove UPX + * info segment to adapt. + * This will also make micro.sfx with upx-packed more like a malware fore antivirus */ - private function processMicroUPX(): void + private function processUpxedMicroSfx(string $dst): void { - if (version_compare($this->getMicroVersion(), '0.2.0') >= 0 && !$this->getOption('no-strip', false)) { - shell()->exec('strip --strip-unneeded ' . SOURCE_PATH . '/php-src/sapi/micro/micro.sfx'); - - if ($this->getOption('with-upx-pack')) { - // strip first - shell()->exec(getenv('UPX_EXEC') . ' --best ' . SOURCE_PATH . '/php-src/sapi/micro/micro.sfx'); - // cut binary with readelf - [$ret, $out] = shell()->execWithResult('readelf -l ' . SOURCE_PATH . '/php-src/sapi/micro/micro.sfx | awk \'/LOAD|GNU_STACK/ {getline; print $1, $2, $3, $4, $6, $7}\''); - $out[1] = explode(' ', $out[1]); - $offset = $out[1][0]; - if ($ret !== 0 || !str_starts_with($offset, '0x')) { - throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); - } - $offset = hexdec($offset); - // remove upx extra wastes - file_put_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx', substr(file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx'), 0, $offset)); + if ($this->getOption('with-upx-pack') && version_compare($this->getMicroVersion(), '0.2.0') >= 0) { + // strip first + // cut binary with readelf + [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \$1, \$2, \$3, \$4, \$6, \$7}'"); + $out[1] = explode(' ', $out[1]); + $offset = $out[1][0]; + if ($ret !== 0 || !str_starts_with($offset, '0x')) { + throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); } + $offset = hexdec($offset); + // remove upx extra wastes + file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); } } } diff --git a/src/SPC/builder/linux/library/idn2.php b/src/SPC/builder/linux/library/idn2.php new file mode 100644 index 000000000..a271760d2 --- /dev/null +++ b/src/SPC/builder/linux/library/idn2.php @@ -0,0 +1,12 @@ + getenv('SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS'), + 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'CPPFLAGS' => '-I' . BUILD_INCLUDE_PATH, 'LDFLAGS' => '-L' . BUILD_LIB_PATH, ]); @@ -186,12 +188,9 @@ protected function buildCli(): void $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; - $shell->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cli"); - if (!$this->getOption('no-strip', false)) { - $shell->exec('dsymutil -f sapi/cli/php')->exec('strip -S sapi/cli/php'); - } - $this->deployBinary(BUILD_TARGET_CLI); + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; + $shell->exec("make {$concurrency} {$vars} cli"); + $this->deploySAPIBinary(BUILD_TARGET_CLI); } protected function buildCgi(): void @@ -199,12 +198,9 @@ protected function buildCgi(): void $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make'; - $shell->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cgi"); - if (!$this->getOption('no-strip', false)) { - $shell->exec('dsymutil -f sapi/cgi/php-cgi')->exec('strip -S sapi/cgi/php-cgi'); - } - $this->deployBinary(BUILD_TARGET_CGI); + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; + $shell->exec("make {$concurrency} {$vars} cgi"); + $this->deploySAPIBinary(BUILD_TARGET_CGI); } /** @@ -215,30 +211,30 @@ protected function buildMicro(): void if ($this->getPHPVersionID() < 80000) { throw new WrongUsageException('phpmicro only support PHP >= 8.0!'); } - if ($this->getExt('phar')) { - $this->phar_patched = true; - SourcePatcher::patchMicroPhar($this->getPHPVersionID()); - } - $enable_fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - $vars = $this->getMakeExtraVars(); + try { + if ($this->getExt('phar')) { + $this->phar_patched = true; + SourcePatcher::patchMicroPhar($this->getPHPVersionID()); + } - // patch fake cli for micro - $vars['EXTRA_CFLAGS'] .= $enable_fake_cli; - $vars = SystemUtil::makeEnvVarString($vars); + $enable_fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; + $vars = $this->getMakeExtraVars(); - $shell = shell()->cd(SOURCE_PATH . '/php-src'); - // build - $shell->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . " {$vars} micro"); - // strip - if (!$this->getOption('no-strip', false)) { - $shell->exec('dsymutil -f sapi/micro/micro.sfx')->exec('strip -S sapi/micro/micro.sfx'); - } + // patch fake cli for micro + $vars['EXTRA_CFLAGS'] .= $enable_fake_cli; + $vars = SystemUtil::makeEnvVarString($vars); - $this->deployBinary(BUILD_TARGET_MICRO); + $shell = shell()->cd(SOURCE_PATH . '/php-src'); + // build + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; + $shell->exec("make {$concurrency} {$vars} micro"); - if ($this->phar_patched) { - SourcePatcher::unpatchMicroPhar(); + $this->deploySAPIBinary(BUILD_TARGET_MICRO); + } finally { + if ($this->phar_patched) { + SourcePatcher::unpatchMicroPhar(); + } } } @@ -250,11 +246,9 @@ protected function buildFpm(): void $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); $shell = shell()->cd(SOURCE_PATH . '/php-src'); - $shell->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . " {$vars} fpm"); - if (!$this->getOption('no-strip', false)) { - $shell->exec('dsymutil -f sapi/fpm/php-fpm')->exec('strip -S sapi/fpm/php-fpm'); - } - $this->deployBinary(BUILD_TARGET_FPM); + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; + $shell->exec("make {$concurrency} {$vars} fpm"); + $this->deploySAPIBinary(BUILD_TARGET_FPM); } /** @@ -262,10 +256,31 @@ protected function buildFpm(): void */ protected function buildEmbed(): void { + $sharedExts = array_filter($this->exts, static fn ($ext) => $ext->isBuildShared()); + $sharedExts = array_filter($sharedExts, static function ($ext) { + return Config::getExt($ext->getName(), 'build-with-php') === true; + }); + $install_modules = $sharedExts ? 'install-modules' : ''; $vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars()); + $concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : ''; + + $diff = new DirDiff(BUILD_MODULES_PATH, true); shell()->cd(SOURCE_PATH . '/php-src') - ->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . ' INSTALL_ROOT=' . BUILD_ROOT_PATH . " {$vars} install"); + ->exec('sed -i "" "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile') + ->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi {$install_modules} install-build install-headers install-programs"); + + $libphp = BUILD_LIB_PATH . '/libphp.dylib'; + if (file_exists($libphp)) { + $this->deployBinary($libphp, $libphp, false); + // macOS currently have no -release option for dylib, so we just rename it here + } + + // process shared extensions build-with-php + $increment_files = $diff->getChangedFiles(); + foreach ($increment_files as $increment_file) { + $this->deployBinary($increment_file, $increment_file, false); + } if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { $AR = getenv('AR') ?: 'ar'; @@ -279,10 +294,10 @@ protected function buildEmbed(): void private function getMakeExtraVars(): array { $config = (new SPCConfigUtil($this, ['libs_only_deps' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs')); - return [ + return array_filter([ 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'EXTRA_LDFLAGS_PROGRAM' => '-L' . BUILD_LIB_PATH, 'EXTRA_LIBS' => $config['libs'], - ]; + ]); } } diff --git a/src/SPC/builder/macos/library/idn2.php b/src/SPC/builder/macos/library/idn2.php new file mode 100644 index 000000000..a41eeeb15 --- /dev/null +++ b/src/SPC/builder/macos/library/idn2.php @@ -0,0 +1,12 @@ +lib_list = $sorted_libraries; } + /** + * Strip unneeded symbols from binary file. + */ + public function stripBinary(string $binary_path): void + { + shell()->exec(match (PHP_OS_FAMILY) { + 'Darwin' => "strip -S {$binary_path}", + 'Linux' => "strip --strip-unneeded {$binary_path}", + default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'), + }); + } + + /** + * Extract debug information from binary file. + * + * @param string $binary_path the path to the binary file, including executables, shared libraries, etc + */ + public function extractDebugInfo(string $binary_path): string + { + $target_dir = BUILD_ROOT_PATH . '/debug'; + FileSystem::createDir($target_dir); + $basename = basename($binary_path); + $debug_file = "{$target_dir}/{$basename}" . (PHP_OS_FAMILY === 'Darwin' ? '.dwarf' : '.debug'); + if (PHP_OS_FAMILY === 'Darwin') { + shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}"); + } elseif (PHP_OS_FAMILY === 'Linux') { + if ($eu_strip = SystemUtil::findCommand('eu-strip')) { + shell() + ->exec("{$eu_strip} -f {$debug_file} {$binary_path}") + ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); + } else { + shell() + ->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}") + ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); + } + } else { + throw new SPCInternalException('extractDebugInfo is only supported on Linux and macOS'); + } + return $debug_file; + } + + /** + * Deploy the binary file from src to dst. + */ + public function deployBinary(string $src, string $dst, bool $executable = true): string + { + logger()->debug('Deploying binary from ' . $src . ' to ' . $dst); + + // file must exists + if (!file_exists($src)) { + throw new SPCInternalException("Deploy failed. Cannot find file: {$src}"); + } + // dst dir must exists + FileSystem::createDir(dirname($dst)); + + // ignore copy to self + if (realpath($src) !== realpath($dst)) { + shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg($dst)); + } + + // file exist + if (!file_exists($dst)) { + throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}"); + } + + // extract debug info + $this->extractDebugInfo($dst); + + // strip + if (!$this->getOption('no-strip')) { + $this->stripBinary($dst); + } + + // UPX for linux + $upx_option = $this->getOption('with-upx-pack'); + if ($upx_option && PHP_OS_FAMILY === 'Linux' && $executable) { + if ($this->getOption('no-strip')) { + logger()->warning('UPX compression is not recommended when --no-strip is enabled.'); + } + logger()->info("Compressing {$dst} with UPX"); + shell()->exec(getenv('UPX_EXEC') . " --best {$dst}"); + } + + return $dst; + } + /** * Sanity check after build complete. */ @@ -208,23 +296,20 @@ protected function sanityCheck(int $build_target): void } /** - * Deploy the binary file to the build bin path. - * - * @param int $type Type integer, one of BUILD_TARGET_CLI, BUILD_TARGET_MICRO, BUILD_TARGET_FPM + * Deploy binaries that produces executable SAPI */ - protected function deployBinary(int $type): bool + protected function deploySAPIBinary(int $type): string { $src = match ($type) { BUILD_TARGET_CLI => SOURCE_PATH . '/php-src/sapi/cli/php', BUILD_TARGET_MICRO => SOURCE_PATH . '/php-src/sapi/micro/micro.sfx', BUILD_TARGET_FPM => SOURCE_PATH . '/php-src/sapi/fpm/php-fpm', BUILD_TARGET_CGI => SOURCE_PATH . '/php-src/sapi/cgi/php-cgi', + BUILD_TARGET_FRANKENPHP => BUILD_BIN_PATH . '/frankenphp', default => throw new SPCInternalException("Deployment does not accept type {$type}"), }; - logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file'); - FileSystem::createDir(BUILD_BIN_PATH); - shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg(BUILD_BIN_PATH)); - return true; + $dst = BUILD_BIN_PATH . '/' . basename($src); + return $this->deployBinary($src, $dst); } /** @@ -265,22 +350,75 @@ protected function patchPhpScripts(): void } } + /** + * Process the --with-frankenphp-app option + * Creates app.tar and app.checksum in source/frankenphp directory + */ + protected function processFrankenphpApp(): void + { + $frankenphpSourceDir = getenv('FRANKENPHP_SOURCE_PATH') ?: SOURCE_PATH . '/frankenphp'; + if (!is_dir($frankenphpSourceDir)) { + SourceManager::initSource(['frankenphp'], ['frankenphp']); + } + $frankenphpAppPath = $this->getOption('with-frankenphp-app'); + + if ($frankenphpAppPath) { + if (!is_dir($frankenphpAppPath)) { + throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); + } + $appTarPath = $frankenphpSourceDir . '/app.tar'; + logger()->info("Creating app.tar from {$frankenphpAppPath}"); + + shell()->exec('tar -cf ' . escapeshellarg($appTarPath) . ' -C ' . escapeshellarg($frankenphpAppPath) . ' .'); + + $checksum = hash_file('md5', $appTarPath); + file_put_contents($frankenphpSourceDir . '/app_checksum.txt', $checksum); + } else { + FileSystem::removeFileIfExists($frankenphpSourceDir . '/app.tar'); + FileSystem::removeFileIfExists($frankenphpSourceDir . '/app_checksum.txt'); + file_put_contents($frankenphpSourceDir . '/app.tar', ''); + file_put_contents($frankenphpSourceDir . '/app_checksum.txt', ''); + } + } + + protected function getFrankenPHPVersion(): string + { + if ($version = getenv('FRANKENPHP_VERSION')) { + return $version; + } + $frankenphpSourceDir = getenv('FRANKENPHP_SOURCE_PATH') ?: SOURCE_PATH . '/frankenphp'; + $goModPath = $frankenphpSourceDir . '/caddy/go.mod'; + + if (!file_exists($goModPath)) { + throw new SPCInternalException("FrankenPHP caddy/go.mod file not found at {$goModPath}, why did we not download FrankenPHP?"); + } + + $content = file_get_contents($goModPath); + if (preg_match('/github\.com\/dunglas\/frankenphp\s+v?(\d+\.\d+\.\d+)/', $content, $matches)) { + return $matches[1]; + } + + throw new SPCInternalException('Could not find FrankenPHP version in caddy/go.mod'); + } + protected function buildFrankenphp(): void { GlobalEnvManager::addPathIfNotExists(GoXcaddy::getPath()); + $this->processFrankenphpApp(); $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; $xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES'); - // make it possible to build from a different frankenphp directory! - if (!str_contains($xcaddyModules, '--with github.com/dunglas/frankenphp')) { - $xcaddyModules = '--with github.com/dunglas/frankenphp ' . $xcaddyModules; - } + $frankenphpSourceDir = getenv('FRANKENPHP_SOURCE_PATH') ?: SOURCE_PATH . '/frankenphp'; + + $xcaddyModules = preg_replace('#--with github.com/dunglas/frankenphp\S*#', '', $xcaddyModules); + $xcaddyModules = "--with github.com/dunglas/frankenphp={$frankenphpSourceDir} " . + "--with github.com/dunglas/frankenphp/caddy={$frankenphpSourceDir}/caddy {$xcaddyModules}"; if ($this->getLib('brotli') === null && str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) { logger()->warning('caddy-cbrotli module is enabled, but brotli library is not built. Disabling caddy-cbrotli.'); $xcaddyModules = str_replace('--with github.com/dunglas/caddy-cbrotli', '', $xcaddyModules); } - [, $out] = shell()->execWithResult('go list -m github.com/dunglas/frankenphp@latest'); - $frankenPhpVersion = str_replace('github.com/dunglas/frankenphp v', '', $out[0]); + + $frankenPhpVersion = $this->getFrankenPHPVersion(); $libphpVersion = $this->getPHPVersion(); $dynamic_exports = ''; if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { @@ -290,20 +428,18 @@ protected function buildFrankenphp(): void $dynamic_exports = ' ' . $dynamicSymbolsArgument; } } - $debugFlags = $this->getOption('no-strip') ? '-w -s ' : ''; - $extLdFlags = "-extldflags '-pie{$dynamic_exports}'"; + $extLdFlags = "-extldflags '-pie{$dynamic_exports} {$this->arch_ld_flags}'"; $muslTags = ''; $staticFlags = ''; if (SPCTarget::isStatic()) { - $extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports}'"; + $extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports} {$this->arch_ld_flags}'"; $muslTags = 'static_build,'; $staticFlags = '-static-pie'; } $config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list); - $cflags = "{$this->arch_c_flags} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'); + $cflags = "{$this->arch_c_flags} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -DFRANKENPHP_VERSION=' . $frankenPhpVersion; $libs = $config['libs']; - $libs .= PHP_OS_FAMILY === 'Linux' ? ' -lrt' : ''; // Go's gcc driver doesn't automatically link against -lgcov or -lrt. Ugly, but necessary fix. if ((str_contains((string) getenv('SPC_DEFAULT_C_FLAGS'), '-fprofile') || str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), '-fprofile')) && @@ -316,7 +452,7 @@ protected function buildFrankenphp(): void 'CGO_CFLAGS' => clean_spaces($cflags), 'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}", 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . - '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . $debugFlags . + '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . "v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", @@ -326,13 +462,7 @@ protected function buildFrankenphp(): void ->setEnv($env) ->exec("xcaddy build --output frankenphp {$xcaddyModules}"); - if (!$this->getOption('no-strip', false) && file_exists(BUILD_BIN_PATH . '/frankenphp')) { - if (PHP_OS_FAMILY === 'Linux') { - shell()->cd(BUILD_BIN_PATH)->exec('strip --strip-unneeded frankenphp'); - } else { // macOS doesn't understand strip-unneeded - shell()->cd(BUILD_BIN_PATH)->exec('strip -S frankenphp'); - } - } + $this->deploySAPIBinary(BUILD_TARGET_FRANKENPHP); } /** diff --git a/src/SPC/builder/unix/library/attr.php b/src/SPC/builder/unix/library/attr.php index b33cf9870..67ac5feb6 100644 --- a/src/SPC/builder/unix/library/attr.php +++ b/src/SPC/builder/unix/library/attr.php @@ -17,7 +17,7 @@ protected function build(): void ->exec('libtoolize --force --copy') ->exec('./autogen.sh || autoreconf -if') ->configure('--disable-nls') - ->make(); + ->make('install-attributes_h install-data install-libattr_h install-libLTLIBRARIES install-pkgincludeHEADERS install-pkgconfDATA', with_install: false); $this->patchPkgconfPrefix(['libattr.pc'], PKGCONF_PATCH_PREFIX); } } diff --git a/src/SPC/builder/unix/library/curl.php b/src/SPC/builder/unix/library/curl.php index 1b5eaed1d..caa97b8c6 100644 --- a/src/SPC/builder/unix/library/curl.php +++ b/src/SPC/builder/unix/library/curl.php @@ -23,6 +23,8 @@ protected function build(): void ->optionalLib('zstd', ...cmake_boolean_args('CURL_ZSTD')) ->optionalLib('idn2', ...cmake_boolean_args('USE_LIBIDN2')) ->optionalLib('psl', ...cmake_boolean_args('CURL_USE_LIBPSL')) + ->optionalLib('krb5', ...cmake_boolean_args('CURL_USE_GSSAPI')) + ->optionalLib('idn2', ...cmake_boolean_args('CURL_USE_IDN2')) ->optionalLib('libcares', '-DENABLE_ARES=ON') ->addConfigureArgs( '-DBUILD_CURL_EXE=OFF', diff --git a/src/SPC/builder/unix/library/gettext.php b/src/SPC/builder/unix/library/gettext.php index f0fdae547..332e25c96 100644 --- a/src/SPC/builder/unix/library/gettext.php +++ b/src/SPC/builder/unix/library/gettext.php @@ -31,7 +31,7 @@ protected function build(): void $autoconf->addConfigureArgs('--disable-threads'); } - $autoconf->configure()->make(); + $autoconf->configure()->make(dir: $this->getSourceDir() . '/gettext-runtime/intl'); $this->patchLaDependencyPrefix(); } } diff --git a/src/SPC/builder/unix/library/idn2.php b/src/SPC/builder/unix/library/idn2.php new file mode 100644 index 000000000..829565340 --- /dev/null +++ b/src/SPC/builder/unix/library/idn2.php @@ -0,0 +1,27 @@ +configure( + '--disable-nls', + '--disable-doc', + '--enable-year2038', + '--disable-rpath' + ) + ->optionalLib('libiconv', "--with-libiconv-prefix={$this->getBuildRootPath()}") + ->optionalLib('libunistring', "--with-libunistring-prefix={$this->getBuildRootPath()}") + ->optionalLib('gettext', "--with-libnintl-prefix={$this->getBuildRootPath()}") + ->make(); + $this->patchPkgconfPrefix(['libidn2.pc']); + $this->patchLaDependencyPrefix(); + } +} diff --git a/src/SPC/builder/unix/library/imagemagick.php b/src/SPC/builder/unix/library/imagemagick.php index 2d3125498..42064e2d8 100644 --- a/src/SPC/builder/unix/library/imagemagick.php +++ b/src/SPC/builder/unix/library/imagemagick.php @@ -12,6 +12,11 @@ trait imagemagick { protected function build(): void { + $original_ldflags = $this->builder->arch_ld_flags; + if (str_contains($this->builder->arch_ld_flags, '-Wl,--as-needed')) { + $this->builder->arch_ld_flags = str_replace('-Wl,--as-needed', '', $original_ldflags); + } + $ac = UnixAutoconfExecutor::create($this) ->optionalLib('libzip', ...ac_with_args('zip')) ->optionalLib('libjpeg', ...ac_with_args('jpeg')) @@ -32,7 +37,7 @@ protected function build(): void ); // special: linux-static target needs `-static` - $ldflags = SPCTarget::isStatic() ? ('-static -ldl') : '-ldl'; + $ldflags = SPCTarget::isStatic() ? '-static -ldl' : '-ldl'; // special: macOS needs -iconv $libs = SPCTarget::getTargetOS() === 'Darwin' ? '-liconv' : ''; @@ -45,6 +50,8 @@ protected function build(): void $ac->configure()->make(); + $this->builder->arch_ld_flags = $original_ldflags; + $filelist = [ 'ImageMagick.pc', 'ImageMagick-7.Q16HDRI.pc', diff --git a/src/SPC/builder/unix/library/krb5.php b/src/SPC/builder/unix/library/krb5.php new file mode 100644 index 000000000..798346459 --- /dev/null +++ b/src/SPC/builder/unix/library/krb5.php @@ -0,0 +1,57 @@ +source_dir; + $this->source_dir .= '/src'; + shell()->cd($this->source_dir)->exec('autoreconf -if'); + $libs = array_map(fn ($x) => $x->getName(), $this->getDependencies(true)); + $spc = new SPCConfigUtil($this->builder, ['no_php' => true, 'libs_only_deps' => true]); + $config = $spc->config(libraries: $libs, include_suggest_lib: $this->builder->getOption('with-suggested-libs', false)); + $extraEnv = [ + 'CFLAGS' => '-fcommon', + 'LIBS' => $config['libs'], + ]; + if (getenv('SPC_LD_LIBRARY_PATH') && getenv('SPC_LIBRARY_PATH')) { + $extraEnv = [...$extraEnv, ...[ + 'LD_LIBRARY_PATH' => getenv('SPC_LD_LIBRARY_PATH'), + 'LIBRARY_PATH' => getenv('SPC_LIBRARY_PATH'), + ]]; + } + $args = [ + '--disable-nls', + '--disable-rpath', + '--without-system-verto', + ]; + if (PHP_OS_FAMILY === 'Darwin') { + $extraEnv['LDFLAGS'] = '-framework Kerberos'; + $args[] = 'ac_cv_func_secure_getenv=no'; + } + UnixAutoconfExecutor::create($this) + ->appendEnv($extraEnv) + ->optionalLib('ldap', '--with-ldap', '--without-ldap') + ->optionalLib('libedit', '--with-libedit', '--without-libedit') + ->configure(...$args) + ->make(); + $this->patchPkgconfPrefix([ + 'krb5-gssapi.pc', + 'krb5.pc', + 'kadm-server.pc', + 'kadm-client.pc', + 'kdb.pc', + 'mit-krb5-gssapi.pc', + 'mit-krb5.pc', + 'gssrpc.pc', + ]); + $this->source_dir = $origin_source_dir; + } +} diff --git a/src/SPC/builder/unix/library/libacl.php b/src/SPC/builder/unix/library/libacl.php index 223e09fc1..8c5d699d3 100644 --- a/src/SPC/builder/unix/library/libacl.php +++ b/src/SPC/builder/unix/library/libacl.php @@ -26,7 +26,7 @@ protected function build(): void ->exec('libtoolize --force --copy') ->exec('./autogen.sh || autoreconf -if') ->configure('--disable-nls', '--disable-tests') - ->make(); + ->make('install-acl_h install-libacl_h install-data install-libLTLIBRARIES install-pkgincludeHEADERS install-sysincludeHEADERS install-pkgconfDATA', with_install: false); $this->patchPkgconfPrefix(['libacl.pc'], PKGCONF_PATCH_PREFIX); } } diff --git a/src/SPC/builder/unix/library/libedit.php b/src/SPC/builder/unix/library/libedit.php new file mode 100644 index 000000000..2de9077fb --- /dev/null +++ b/src/SPC/builder/unix/library/libedit.php @@ -0,0 +1,30 @@ +source_dir . '/src/sys.h', + '|//#define\s+strl|', + '#define strl' + ); + return true; + } + + protected function build(): void + { + UnixAutoconfExecutor::create($this) + ->appendEnv(['CFLAGS' => '-D__STDC_ISO_10646__=201103L']) + ->configure() + ->make(); + $this->patchPkgconfPrefix(['libedit.pc']); + } +} diff --git a/src/SPC/builder/unix/library/libiconv.php b/src/SPC/builder/unix/library/libiconv.php index fd68e309d..5ccc94843 100644 --- a/src/SPC/builder/unix/library/libiconv.php +++ b/src/SPC/builder/unix/library/libiconv.php @@ -10,7 +10,13 @@ trait libiconv { protected function build(): void { - UnixAutoconfExecutor::create($this)->configure('--enable-extra-encodings')->make(); + UnixAutoconfExecutor::create($this) + ->configure( + '--enable-extra-encodings', + '--enable-year2038', + ) + ->make('install-lib', with_install: false) + ->make('install-lib', with_install: false, dir: $this->getSourceDir() . '/libcharset'); $this->patchLaDependencyPrefix(); } } diff --git a/src/SPC/builder/unix/library/libmaxminddb.php b/src/SPC/builder/unix/library/libmaxminddb.php new file mode 100644 index 000000000..44915f274 --- /dev/null +++ b/src/SPC/builder/unix/library/libmaxminddb.php @@ -0,0 +1,20 @@ +addConfigureArgs( + '-DBUILD_TESTING=OFF', + '-DMAXMINDDB_BUILD_BINARIES=OFF', + ) + ->build(); + } +} diff --git a/src/SPC/builder/unix/library/librdkafka.php b/src/SPC/builder/unix/library/librdkafka.php index f2bea09f1..222760d0b 100644 --- a/src/SPC/builder/unix/library/librdkafka.php +++ b/src/SPC/builder/unix/library/librdkafka.php @@ -5,7 +5,7 @@ namespace SPC\builder\unix\library; use SPC\store\FileSystem; -use SPC\util\executor\UnixAutoconfExecutor; +use SPC\util\executor\UnixCMakeExecutor; trait librdkafka { @@ -26,34 +26,18 @@ public function patchBeforeBuild(): bool protected function build(): void { - UnixAutoconfExecutor::create($this) - ->appendEnv(['CFLAGS' => '-Wno-int-conversion -Wno-unused-but-set-variable -Wno-unused-variable']) - ->optionalLib( - 'zstd', - function ($lib) { - putenv("STATIC_LIB_libzstd={$lib->getLibDir()}/libzstd.a"); - return ''; - }, - '--disable-zstd' + UnixCMakeExecutor::create($this) + ->optionalLib('zstd', ...cmake_boolean_args('WITH_ZSTD')) + ->optionalLib('curl', ...cmake_boolean_args('WITH_CURL')) + ->optionalLib('openssl', ...cmake_boolean_args('WITH_SSL')) + ->optionalLib('zlib', ...cmake_boolean_args('WITH_ZLIB')) + ->optionalLib('liblz4', ...cmake_boolean_args('ENABLE_LZ4_EXT')) + ->addConfigureArgs( + '-DWITH_SASL=OFF', + '-DRDKAFKA_BUILD_STATIC=ON', + '-DRDKAFKA_BUILD_EXAMPLES=OFF', + '-DRDKAFKA_BUILD_TESTS=OFF', ) - ->removeConfigureArgs( - '--with-pic', - '--enable-pic', - ) - ->configure( - '--disable-curl', - '--disable-sasl', - '--disable-valgrind', - '--disable-zlib', - '--disable-ssl', - ) - ->make(); - - $this->patchPkgconfPrefix(['rdkafka.pc', 'rdkafka-static.pc', 'rdkafka++.pc', 'rdkafka++-static.pc']); - // remove dynamic libs - shell() - ->exec("rm -rf {$this->getLibDir()}/*.so.*") - ->exec("rm -rf {$this->getLibDir()}/*.so") - ->exec("rm -rf {$this->getLibDir()}/*.dylib"); + ->build(); } } diff --git a/src/SPC/builder/unix/library/libunistring.php b/src/SPC/builder/unix/library/libunistring.php new file mode 100644 index 000000000..1d410a8c5 --- /dev/null +++ b/src/SPC/builder/unix/library/libunistring.php @@ -0,0 +1,18 @@ +configure('--disable-nls') + ->make(); + $this->patchLaDependencyPrefix(); + } +} diff --git a/src/SPC/builder/unix/library/libzip.php b/src/SPC/builder/unix/library/libzip.php index 931bfcc35..ad0befea2 100644 --- a/src/SPC/builder/unix/library/libzip.php +++ b/src/SPC/builder/unix/library/libzip.php @@ -22,6 +22,7 @@ protected function build(): void '-DBUILD_EXAMPLES=OFF', '-DBUILD_REGRESS=OFF', '-DBUILD_TOOLS=OFF', + '-DBUILD_OSSFUZZ=OFF', ) ->build(); $this->patchPkgconfPrefix(['libzip.pc'], PKGCONF_PATCH_PREFIX); diff --git a/src/SPC/builder/unix/library/net_snmp.php b/src/SPC/builder/unix/library/net_snmp.php new file mode 100644 index 000000000..df464d523 --- /dev/null +++ b/src/SPC/builder/unix/library/net_snmp.php @@ -0,0 +1,49 @@ +source_dir}/configure", 'LIBS="-lssl ${OPENSSL_LIBS}"', 'LIBS="-lssl ${OPENSSL_LIBS} -lpthread -ldl"'); + return true; + } + return false; + } + + protected function build(): void + { + // use --static for PKG_CONFIG + UnixAutoconfExecutor::create($this) + ->setEnv(['PKG_CONFIG' => getenv('PKG_CONFIG') . ' --static']) + ->configure( + '--disable-mibs', + '--without-nl', + '--disable-agent', + '--disable-applications', + '--disable-manuals', + '--disable-scripts', + '--disable-embedded-perl', + '--without-perl-modules', + '--with-out-mib-modules="if-mib host disman/event-mib ucd-snmp/diskio mibII"', + '--with-out-transports="Unix"', + '--with-mib-modules=""', + '--enable-mini-agent', + '--with-default-snmp-version="3"', + '--with-sys-contact="@@no.where"', + '--with-sys-location="Unknown"', + '--with-logfile="/var/log/snmpd.log"', + '--with-persistent-directory="/var/lib/net-snmp"', + '--with-openssl=' . BUILD_ROOT_PATH, + '--with-zlib=' . BUILD_ROOT_PATH, + )->make(with_install: 'installheaders installlibs install_pkgconfig'); + $this->patchPkgconfPrefix(); + } +} diff --git a/src/SPC/builder/unix/library/postgresql.php b/src/SPC/builder/unix/library/postgresql.php index 3bc6835dd..6e0cb606e 100644 --- a/src/SPC/builder/unix/library/postgresql.php +++ b/src/SPC/builder/unix/library/postgresql.php @@ -4,93 +4,83 @@ namespace SPC\builder\unix\library; -use SPC\builder\linux\library\LinuxLibraryBase; -use SPC\exception\BuildFailureException; use SPC\exception\FileSystemException; use SPC\store\FileSystem; +use SPC\util\PkgConfigUtil; +use SPC\util\SPCConfigUtil; use SPC\util\SPCTarget; trait postgresql { - protected function build(): void + public function patchBeforeBuild(): bool { - $builddir = BUILD_ROOT_PATH; - $envs = ''; - $packages = 'zlib openssl readline libxml-2.0'; - $optional_packages = [ - 'zstd' => 'libzstd', - 'ldap' => 'ldap', - 'libxslt' => 'libxslt', - 'icu' => 'icu-i18n', - ]; - $error_exec_cnt = 0; - - foreach ($optional_packages as $lib => $pkg) { - if ($this->getBuilder()->getLib($lib)) { - $packages .= ' ' . $pkg; - $output = shell()->execWithResult("pkg-config --static {$pkg}"); - $error_exec_cnt += $output[0] === 0 ? 0 : 1; - logger()->info(var_export($output[1], true)); + // fix aarch64 build on glibc 2.17 (e.g. CentOS 7) + if (SPCTarget::getLibcVersion() === '2.17' && GNU_ARCH === 'aarch64') { + try { + FileSystem::replaceFileStr("{$this->source_dir}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0'); + FileSystem::replaceFileStr( + "{$this->source_dir}/src/port/pg_crc32c_armv8_choose.c", + '#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32)', + '#if defined(__linux__) && !defined(HWCAP_CRC32)' + ); + } catch (FileSystemException) { + // allow file not-existence to make it compatible with old and new version } } + // skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries + FileSystem::replaceFileStr("{$this->source_dir}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); + // disable shared libs build + FileSystem::replaceFileStr( + "{$this->source_dir}/src/Makefile.shlib", + [ + '$(LINK.shared) -o $@ $(OBJS) $(LDFLAGS) $(LDFLAGS_SL) $(SHLIB_LINK)', + '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(pkglibdir)/$(shlib)\'', + '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(libdir)/$(shlib)\'', + '$(INSTALL_SHLIB) $< \'$(DESTDIR)$(bindir)/$(shlib)\'', + ], + '' + ); + return true; + } - $output = shell()->execWithResult("pkg-config --cflags-only-I --static {$packages}"); - $error_exec_cnt += $output[0] === 0 ? 0 : 1; - $macos_15_bug_cflags = PHP_OS_FAMILY === 'Darwin' ? ' -Wno-unguarded-availability-new' : ''; - $cflags = ''; - if (!empty($output[1][0])) { - $cflags = $output[1][0]; - $envs .= ' CPPFLAGS="-DPIC"'; - $cflags = "{$cflags} -fno-ident{$macos_15_bug_cflags}"; - } - $output = shell()->execWithResult("pkg-config --libs-only-L --static {$packages}"); - $error_exec_cnt += $output[0] === 0 ? 0 : 1; - if (!empty($output[1][0])) { - $ldflags = $output[1][0]; - $envs .= SPCTarget::isStatic() ? " LDFLAGS=\"{$ldflags} -static\" " : " LDFLAGS=\"{$ldflags}\" "; - } - $output = shell()->execWithResult("pkg-config --libs-only-l --static {$packages}"); - $error_exec_cnt += $output[0] === 0 ? 0 : 1; - if (!empty($output[1][0])) { - $libs = $output[1][0]; - $libcpp = ''; - if ($this->builder->getLib('icu')) { - $libcpp = $this instanceof LinuxLibraryBase ? ' -lstdc++' : ' -lc++'; - } - $envs .= " LIBS=\"{$libs}{$libcpp}\" "; - } - if ($error_exec_cnt > 0) { - throw new BuildFailureException('Failed to get pkg-config information!'); + protected function build(): void + { + $libs = array_map(fn ($x) => $x->getName(), $this->getDependencies(true)); + $spc = new SPCConfigUtil($this->builder, ['no_php' => true, 'libs_only_deps' => true]); + $config = $spc->config(libraries: $libs, include_suggest_lib: $this->builder->getOption('with-suggested-libs', false)); + + $env_vars = [ + 'CFLAGS' => $config['cflags'], + 'CPPFLAGS' => '-DPIC', + 'LDFLAGS' => $config['ldflags'], + 'LIBS' => $config['libs'], + ]; + + if ($ldLibraryPath = getenv('SPC_LD_LIBRARY_PATH')) { + $env_vars['LD_LIBRARY_PATH'] = $ldLibraryPath; } FileSystem::resetDir($this->source_dir . '/build'); - $version = $this->getVersion(); - // 16.1 workaround - if (version_compare($version, '16.1') >= 0) { - # 有静态链接配置 参考文件: src/interfaces/libpq/Makefile - shell()->cd($this->source_dir . '/build') - ->exec('sed -i.backup "s/invokes exit\'; exit 1;/invokes exit\';/" ../src/interfaces/libpq/Makefile') - ->exec('sed -i.backup "278 s/^/# /" ../src/Makefile.shlib') - ->exec('sed -i.backup "402 s/^/# /" ../src/Makefile.shlib'); - } else { - throw new BuildFailureException('Unsupported version for postgresql: ' . $version . ' !'); - } + // php source relies on the non-private encoding functions in libpgcommon.a + FileSystem::replaceFileStr( + "{$this->source_dir}/src/common/Makefile", + '$(OBJS_FRONTEND): CPPFLAGS += -DUSE_PRIVATE_ENCODING_FUNCS', + '$(OBJS_FRONTEND): CPPFLAGS += -UUSE_PRIVATE_ENCODING_FUNCS -DFRONTEND', + ); // configure - shell()->cd($this->source_dir . '/build')->initializeEnv($this) - ->appendEnv(['CFLAGS' => $cflags]) + $shell = shell()->cd("{$this->source_dir}/build")->initializeEnv($this) + ->appendEnv($env_vars) ->exec( - "{$envs} ../configure " . - "--prefix={$builddir} " . - ($this->builder->getOption('enable-zts') ? '--enable-thread-safety ' : '--disable-thread-safety ') . + '../configure ' . + "--prefix={$this->getBuildRootPath()} " . '--enable-coverage=no ' . '--with-ssl=openssl ' . '--with-readline ' . '--with-libxml ' . ($this->builder->getLib('icu') ? '--with-icu ' : '--without-icu ') . ($this->builder->getLib('ldap') ? '--with-ldap ' : '--without-ldap ') . - // '--without-ldap ' . ($this->builder->getLib('libxslt') ? '--with-libxslt ' : '--without-libxslt ') . ($this->builder->getLib('zstd') ? '--with-zstd ' : '--without-zstd ') . '--without-lz4 ' . @@ -99,32 +89,29 @@ protected function build(): void '--without-pam ' . '--without-bonjour ' . '--without-tcl ' - ) - ->exec($envs . ' make -C src/bin/pg_config install') - ->exec($envs . ' make -C src/include install') - ->exec($envs . ' make -C src/common install') - ->exec($envs . ' make -C src/port install') - ->exec($envs . ' make -C src/interfaces/libpq install'); + ); + + // patch ldap lib + if ($this->builder->getLib('ldap')) { + $libs = PkgConfigUtil::getLibsArray('ldap'); + $libs = clean_spaces(implode(' ', $libs)); + FileSystem::replaceFileStr($this->source_dir . '/build/config.status', '-lldap', $libs); + FileSystem::replaceFileStr($this->source_dir . '/build/src/Makefile.global', '-lldap', $libs); + } + + $shell + ->exec('make -C src/bin/pg_config install') + ->exec('make -C src/include install') + ->exec('make -C src/common install') + ->exec('make -C src/port install') + ->exec('make -C src/interfaces/libpq install'); // remove dynamic libs shell()->cd($this->source_dir . '/build') - ->exec("rm -rf {$builddir}/lib/*.so.*") - ->exec("rm -rf {$builddir}/lib/*.so") - ->exec("rm -rf {$builddir}/lib/*.dylib"); + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so.*") + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so") + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.dylib"); - FileSystem::replaceFileStr(BUILD_LIB_PATH . '/pkgconfig/libpq.pc', '-lldap', '-lldap -llber'); - } - - private function getVersion(): string - { - try { - $file = FileSystem::readFile($this->source_dir . '/meson.build'); - if (preg_match("/^\\s+version:\\s?'(.*)'/m", $file, $match)) { - return $match[1]; - } - return 'unknown'; - } catch (FileSystemException) { - return 'unknown'; - } + FileSystem::replaceFileStr("{$this->getLibDir()}/pkgconfig/libpq.pc", '-lldap', '-lldap -llber'); } } diff --git a/src/SPC/builder/windows/WindowsBuilder.php b/src/SPC/builder/windows/WindowsBuilder.php index 3f21f639b..e4f1578c4 100644 --- a/src/SPC/builder/windows/WindowsBuilder.php +++ b/src/SPC/builder/windows/WindowsBuilder.php @@ -57,6 +57,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; $enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; $enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; + $enableCgi = ($build_target & BUILD_TARGET_CGI) === BUILD_TARGET_CGI; SourcePatcher::patchBeforeBuildconf($this); @@ -66,21 +67,6 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $zts = $this->zts ? '--enable-zts=yes ' : '--enable-zts=no '; - // with-upx-pack for phpmicro - if ($enableMicro && version_compare($this->getMicroVersion(), '0.2.0') < 0) { - $makefile = FileSystem::convertPath(SOURCE_PATH . '/php-src/sapi/micro/Makefile.frag.w32'); - if ($this->getOption('with-upx-pack', false)) { - if (!file_exists($makefile . '.originfile')) { - copy($makefile, $makefile . '.originfile'); - FileSystem::replaceFileStr($makefile, '$(MICRO_SFX):', '_MICRO_UPX = ' . getenv('UPX_EXEC') . " --best $(MICRO_SFX)\n$(MICRO_SFX):"); - FileSystem::replaceFileStr($makefile, '@$(_MICRO_MT)', "@$(_MICRO_MT)\n\t@$(_MICRO_UPX)"); - } - } elseif (file_exists($makefile . '.originfile')) { - copy($makefile . '.originfile', $makefile); - unlink($makefile . '.originfile'); - } - } - $opcache_jit = !$this->getOption('disable-opcache-jit', false); $opcache_jit_arg = $opcache_jit ? '--enable-opcache-jit=yes ' : '--enable-opcache-jit=no '; @@ -102,13 +88,13 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void ->exec( "{$this->sdk_prefix} configure.bat --task-args \"" . '--disable-all ' . - '--disable-cgi ' . '--with-php-build=' . BUILD_ROOT_PATH . ' ' . '--with-extra-includes=' . BUILD_INCLUDE_PATH . ' ' . '--with-extra-libs=' . BUILD_LIB_PATH . ' ' . - ($enableCli ? '--enable-cli=yes ' : '--enable-cli=no ') . - ($enableMicro ? ('--enable-micro=yes ' . $micro_logo . $micro_w32) : '--enable-micro=no ') . - ($enableEmbed ? '--enable-embed=yes ' : '--enable-embed=no ') . + ($enableCli ? '--enable-cli ' : '--disable-cli ') . + ($enableMicro ? ('--enable-micro ' . $micro_logo . $micro_w32) : '--disable-micro ') . + ($enableEmbed ? '--enable-embed ' : '--disable-embed ') . + ($enableCgi ? '--enable-cgi ' : '--disable-cgi ') . $config_file_scan_dir . $opcache_jit_arg . "{$this->makeStaticExtensionArgs()} " . @@ -127,6 +113,10 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void if ($enableFpm) { logger()->warning('Windows does not support fpm SAPI, I will skip it.'); } + if ($enableCgi) { + logger()->info('building cgi'); + $this->buildCgi(); + } if ($enableMicro) { logger()->info('building micro'); $this->buildMicro(); @@ -140,7 +130,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void } } - public function testPHP(int $build_target = BUILD_TARGET_NONE) + public function testPHP(int $build_target = BUILD_TARGET_NONE): void { $this->sanityCheck($build_target); } @@ -151,12 +141,52 @@ public function buildCli(): void $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + // Add debug symbols for release build if --no-strip is specified + // We need to modify CFLAGS to replace /Ox with /Zi and add /DEBUG to LDFLAGS + $debug_overrides = ''; + if ($this->getOption('no-strip', false)) { + // Read current CFLAGS from Makefile and replace optimization flags + $makefile_content = file_get_contents(SOURCE_PATH . '\php-src\Makefile'); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + // Replace /Ox (full optimization) with /Zi (debug info) and /Od (disable optimization) + // Keep optimization for speed: /O2 /Zi instead of /Od /Zi + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CLI=/DEBUG" '; + } + } + // add nmake wrapper - FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_cli_wrapper.bat', "nmake /nologo LIBS_CLI=\"ws2_32.lib shell32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= %*"); + FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_cli_wrapper.bat', "nmake /nologo {$debug_overrides}LIBS_CLI=\"ws2_32.lib shell32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= %*"); cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_cli_wrapper.bat --task-args php.exe"); - $this->deployBinary(BUILD_TARGET_CLI); + $this->deploySAPIBinary(BUILD_TARGET_CLI); + } + + public function buildCgi(): void + { + SourcePatcher::patchWindowsCGITarget(); + + $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($this->getOption('no-strip', false)) { + $makefile_content = file_get_contents(SOURCE_PATH . '\php-src\Makefile'); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CGI=/DEBUG" '; + } + } + + // add nmake wrapper + FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_cgi_wrapper.bat', "nmake /nologo {$debug_overrides}LIBS_CGI=\"ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= %*"); + + cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_cgi_wrapper.bat --task-args php-cgi.exe"); + + $this->deploySAPIBinary(BUILD_TARGET_CGI); } public function buildEmbed(): void @@ -183,9 +213,20 @@ public function buildMicro(): void $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + if ($this->getOption('no-strip', false) && !$this->getOption('debug', false)) { + $makefile_content = file_get_contents(SOURCE_PATH . '\php-src\Makefile'); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_MICRO=/DEBUG" '; + } + } + // add nmake wrapper $fake_cli = $this->getOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI" ' : ''; - $wrapper = "nmake /nologo LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" %*"; + $wrapper = "nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" %*"; FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_micro_wrapper.bat', $wrapper); // phar patch for micro @@ -202,7 +243,7 @@ public function buildMicro(): void } } - $this->deployBinary(BUILD_TARGET_MICRO); + $this->deploySAPIBinary(BUILD_TARGET_MICRO); } public function proveLibs(array $sorted_libraries): void @@ -265,7 +306,7 @@ public function sanityCheck(mixed $build_target): void // sanity check for php-cli if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { logger()->info('running cli sanity check'); - [$ret, $output] = cmd()->execWithResult(BUILD_ROOT_PATH . '\bin\php.exe -n -r "echo \"hello\";"'); + [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php.exe -n -r "echo \"hello\";"'); if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { throw new ValidationException('cli failed sanity check', validation_module: 'php-cli function check'); } @@ -284,7 +325,7 @@ public function sanityCheck(mixed $build_target): void if (file_exists($test_file)) { @unlink($test_file); } - file_put_contents($test_file, file_get_contents(BUILD_ROOT_PATH . '\bin\micro.sfx') . $task['content']); + file_put_contents($test_file, file_get_contents(BUILD_BIN_PATH . '\micro.sfx') . $task['content']); chmod($test_file, 0755); [$ret, $out] = cmd()->execWithResult($test_file); foreach ($task['conditions'] as $condition => $closure) { @@ -298,6 +339,17 @@ public function sanityCheck(mixed $build_target): void } } } + + // sanity check for php-cgi + if (($build_target & BUILD_TARGET_CGI) === BUILD_TARGET_CGI) { + logger()->info('running cgi sanity check'); + FileSystem::writeFile(SOURCE_PATH . '\php-cgi-test.php', 'Hello, World!"; ?>'); + [$ret, $output] = cmd()->execWithResult(BUILD_BIN_PATH . '\php-cgi.exe -n -f ' . SOURCE_PATH . '\php-cgi-test.php'); + $raw_output = implode("\n", $output); + if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!')) { + throw new ValidationException("cgi failed sanity check. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi sanity check'); + } + } } /** @@ -305,27 +357,53 @@ public function sanityCheck(mixed $build_target): void * * @param int $type Deploy type */ - public function deployBinary(int $type): bool + public function deploySAPIBinary(int $type): void { + logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file'); + + $debug_dir = BUILD_ROOT_PATH . '\debug'; + $bin_path = BUILD_BIN_PATH; + + // create dirs + FileSystem::createDir($debug_dir); + FileSystem::createDir($bin_path); + + // determine source path for different SAPI + $rel_type = 'Release'; // TODO: Debug build support $ts = $this->zts ? '_TS' : ''; $src = match ($type) { - BUILD_TARGET_CLI => SOURCE_PATH . "\\php-src\\x64\\Release{$ts}\\php.exe", - BUILD_TARGET_MICRO => SOURCE_PATH . "\\php-src\\x64\\Release{$ts}\\micro.sfx", + BUILD_TARGET_CLI => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'php.exe', 'php.pdb'], + BUILD_TARGET_MICRO => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'micro.sfx', 'micro.pdb'], + BUILD_TARGET_CGI => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'php-cgi.exe', 'php-cgi.pdb'], default => throw new SPCInternalException("Deployment does not accept type {$type}"), }; + $src = "{$src[0]}\\{$src[1]}"; + $dst = BUILD_BIN_PATH . '\\' . basename($src); + + // file must exists + if (!file_exists($src)) { + throw new SPCInternalException("Deploy failed. Cannot find file: {$src}"); + } + // dst dir must exists + FileSystem::createDir(dirname($dst)); + + // copy file + if (realpath($src) !== realpath($dst)) { + cmd()->exec('copy ' . escapeshellarg($src) . ' ' . escapeshellarg($dst)); + } + + // extract debug info in buildroot/debug + if ($this->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { + cmd()->exec('copy ' . escapeshellarg("{$src[0]}\\{$src[2]}") . ' ' . escapeshellarg($debug_dir)); + } + // with-upx-pack for cli and micro if ($this->getOption('with-upx-pack', false)) { - if ($type === BUILD_TARGET_CLI || ($type === BUILD_TARGET_MICRO && version_compare($this->getMicroVersion(), '0.2.0') >= 0)) { - cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($src)); + if ($type === BUILD_TARGET_CLI || $type === BUILD_TARGET_CGI || ($type === BUILD_TARGET_MICRO && version_compare($this->getMicroVersion(), '0.2.0') >= 0)) { + cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst)); } } - - logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file'); - FileSystem::createDir(BUILD_ROOT_PATH . '\bin'); - - cmd()->exec('copy ' . escapeshellarg($src) . ' ' . escapeshellarg(BUILD_ROOT_PATH . '\bin\\')); - return true; } /** diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 3ffc54cef..ad884b3ba 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -33,7 +33,7 @@ public function configure(): void $this->addOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)'); $this->addOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)'); $this->addOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)'); - $this->addOption('build-cgi', null, null, 'Build cgi SAPI (not available on Windows)'); + $this->addOption('build-cgi', null, null, 'Build cgi SAPI'); $this->addOption('build-all', null, null, 'Build all SAPI'); $this->addOption('no-strip', null, null, 'build without strip, keep symbols to debug'); $this->addOption('disable-opcache-jit', null, null, 'disable opcache jit'); @@ -48,6 +48,7 @@ public function configure(): void $this->addOption('with-upx-pack', null, null, 'Compress / pack binary using UPX tool (linux/windows only)'); $this->addOption('with-micro-logo', null, InputOption::VALUE_REQUIRED, 'Use custom .ico for micro.sfx (windows only)'); $this->addOption('enable-micro-win32', null, null, 'Enable win32 mode for phpmicro (Windows only)'); + $this->addOption('with-frankenphp-app', null, InputOption::VALUE_REQUIRED, 'Path to a folder to be embedded in FrankenPHP'); } public function handle(): int @@ -222,11 +223,9 @@ public function handle(): int // ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ---------- $build_root_path = BUILD_ROOT_PATH; - $cwd = getcwd(); $fixed = ''; + $build_root_path = get_display_path($build_root_path); if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) { - str_replace($cwd, '', $build_root_path); - $build_root_path = getenv('SPC_FIX_DEPLOY_ROOT') . '/' . basename($build_root_path); $fixed = ' (host system)'; } if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { diff --git a/src/SPC/command/DownloadCommand.php b/src/SPC/command/DownloadCommand.php index b9bae6a57..00bcc1948 100644 --- a/src/SPC/command/DownloadCommand.php +++ b/src/SPC/command/DownloadCommand.php @@ -106,7 +106,7 @@ public function handle(): int } // retry - $retry = intval($this->getOption('retry')); + $retry = (int) $this->getOption('retry'); f_putenv('SPC_DOWNLOAD_RETRIES=' . $retry); // Use shallow-clone can reduce git resource download @@ -265,7 +265,7 @@ private function downloadFromZip(string $path): int f_passthru((PHP_OS_FAMILY === 'Windows' ? 'rmdir /s /q ' : 'rm -rf ') . DOWNLOAD_PATH); } // unzip command check - if (PHP_OS_FAMILY !== 'Windows' && !$this->findCommand('unzip')) { + if (PHP_OS_FAMILY !== 'Windows' && !self::findCommand('unzip')) { $this->output->writeln('Missing unzip command, you need to install it first !'); $this->output->writeln('You can use "bin/spc doctor" command to check and install required tools'); return static::FAILURE; diff --git a/src/SPC/doctor/item/LinuxMuslCheck.php b/src/SPC/doctor/item/LinuxMuslCheck.php index 76d59e230..c11bd4c41 100644 --- a/src/SPC/doctor/item/LinuxMuslCheck.php +++ b/src/SPC/doctor/item/LinuxMuslCheck.php @@ -22,7 +22,7 @@ class LinuxMuslCheck public static function optionalCheck(): bool { return getenv('SPC_TOOLCHAIN') === MuslToolchain::class || - (getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !SystemUtil::isMuslDist()); + (getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !SystemUtil::isMuslDist() && !str_contains((string) getenv('SPC_TARGET'), 'gnu')); } /** @noinspection PhpUnused */ diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php index 2f6e866d8..ab8144a21 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -40,7 +40,7 @@ class LinuxToolCheckList 'tar', 'unzip', 'gzip', 'gcc', 'g++', 'bzip2', 'cmake', 'patch', 'which', 'xz', 'libtool', 'gettext-devel', - 'patchelf', + 'patchelf', 'file', ]; public const TOOLS_ARCH = [ @@ -82,14 +82,14 @@ public function checkCliTools(): ?CheckResult return CheckResult::ok(); } - #[AsCheckItem('if cmake version >= 3.18', limit_os: 'Linux')] + #[AsCheckItem('if cmake version >= 3.22', limit_os: 'Linux')] public function checkCMakeVersion(): ?CheckResult { $ver = get_cmake_version(); if ($ver === null) { return CheckResult::fail('Failed to get cmake version'); } - if (version_compare($ver, '3.18.0') < 0) { + if (version_compare($ver, '3.22.0') < 0) { return CheckResult::fail('cmake version is too low (' . $ver . '), please update it manually!'); } return CheckResult::ok($ver); diff --git a/src/SPC/exception/ExceptionHandler.php b/src/SPC/exception/ExceptionHandler.php index 44b82b063..df5e4bf0c 100644 --- a/src/SPC/exception/ExceptionHandler.php +++ b/src/SPC/exception/ExceptionHandler.php @@ -137,13 +137,18 @@ public static function handleSPCException(SPCException $e): void self::logError("\n----------------------------------------\n"); - self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_OUTPUT_LOG)); + // convert log file path if in docker + $spc_log_convert = get_display_path(SPC_OUTPUT_LOG); + $shell_log_convert = get_display_path(SPC_SHELL_LOG); + $spc_logs_dir_convert = get_display_path(SPC_LOGS_DIR); + + self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($spc_log_convert)); if (file_exists(SPC_SHELL_LOG)) { - self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_SHELL_LOG)); + self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($shell_log_convert)); } if ($e->getExtraLogFiles() !== []) { foreach ($e->getExtraLogFiles() as $key => $file) { - self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none(SPC_LOGS_DIR . "/{$file}")); + self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none("{$spc_logs_dir_convert}/{$file}")); } } if (!defined('DEBUG_MODE')) { @@ -154,7 +159,9 @@ public static function handleSPCException(SPCException $e): void public static function handleDefaultException(\Throwable $e): void { $class = get_class($e); - self::logError("✗ Unhandled exception {$class}:\n\t{$e->getMessage()}\n"); + $file = $e->getFile(); + $line = $e->getLine(); + self::logError("✗ Unhandled exception {$class} on {$file} line {$line}:\n\t{$e->getMessage()}\n"); self::logError('Stack trace:'); self::logError(ConsoleColor::gray($e->getTraceAsString()) . PHP_EOL, 4); self::logError('⚠ Please report this exception to: https://github.com/crazywhalecc/static-php-cli/issues'); diff --git a/src/SPC/store/DirDiff.php b/src/SPC/store/DirDiff.php new file mode 100644 index 000000000..8cd5c1d40 --- /dev/null +++ b/src/SPC/store/DirDiff.php @@ -0,0 +1,95 @@ +reset(); + } + + /** + * Reset the baseline to current state. + */ + public function reset(): void + { + $this->before = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; + + if ($this->track_content_changes) { + $this->before_file_hashes = []; + foreach ($this->before as $file) { + $this->before_file_hashes[$file] = md5_file($this->dir . DIRECTORY_SEPARATOR . $file); + } + } + } + + /** + * Get the list of incremented files. + * + * @param bool $relative Return relative paths or absolute paths + * @return array List of incremented files + */ + public function getIncrementFiles(bool $relative = false): array + { + $after = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; + $diff = array_diff($after, $this->before); + if ($relative) { + return $diff; + } + return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $diff); + } + + /** + * Get the list of changed files (including new files). + * + * @param bool $relative Return relative paths or absolute paths + * @param bool $include_new_files Include new files as changed files + * @return array List of changed files + */ + public function getChangedFiles(bool $relative = false, bool $include_new_files = true): array + { + $after = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; + $changed = []; + foreach ($after as $file) { + if (isset($this->before_file_hashes[$file])) { + $after_hash = md5_file($this->dir . DIRECTORY_SEPARATOR . $file); + if ($after_hash !== $this->before_file_hashes[$file]) { + $changed[] = $file; + } + } elseif ($include_new_files) { + // New file, consider as changed + $changed[] = $file; + } + } + if ($relative) { + return $changed; + } + return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $changed); + } + + /** + * Get the list of removed files. + * + * @param bool $relative Return relative paths or absolute paths + * @return array List of removed files + */ + public function getRemovedFiles(bool $relative = false): array + { + $after = FileSystem::scanDirFiles($this->dir, relative: true) ?: []; + $removed = array_diff($this->before, $after); + if ($relative) { + return $removed; + } + return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $removed); + } +} diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index d659253e9..63bec8075 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -16,6 +16,42 @@ */ class Downloader { + /** + * Get latest version from PIE config (Packagist) + * + * @param string $name Source name + * @param array $source Source meta info: [repo] + * @return array [url, filename] + */ + public static function getPIEInfo(string $name, array $source): array + { + $packagist_url = "https://repo.packagist.org/p2/{$source['repo']}.json"; + logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); + $data = json_decode(self::curlExec( + url: $packagist_url, + retries: self::getRetryAttempts() + ), true); + if (!isset($data['packages'][$source['repo']]) || !is_array($data['packages'][$source['repo']])) { + throw new DownloaderException("failed to find {$name} repo info from packagist"); + } + // get the first version + $first = $data['packages'][$source['repo']][0] ?? []; + // check 'type' => 'php-ext' or contains 'php-ext' key + if (!isset($first['php-ext'])) { + throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); + } + // get download link from dist + $dist_url = $first['dist']['url'] ?? null; + $dist_type = $first['dist']['type'] ?? null; + if (!$dist_url || !$dist_type) { + throw new DownloaderException("failed to find {$name} dist info from packagist"); + } + $name = str_replace('/', '_', $source['repo']); + $version = $first['version'] ?? 'unknown'; + // file name use: $name-$version.$dist_type + return [$dist_url, "{$name}-{$version}.{$dist_type}"]; + } + /** * Get latest version from BitBucket tag * @@ -65,19 +101,19 @@ public static function getLatestGithubTarball(string $name, array $source, strin url: "https://api.github.com/repos/{$source['repo']}/{$type}", hooks: [[CurlHook::class, 'setupGithubToken']], retries: self::getRetryAttempts() - ), true); + ), true, 512, JSON_THROW_ON_ERROR); $url = null; - for ($i = 0; $i < count($data); ++$i) { - if (($data[$i]['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) { + foreach ($data as $rel) { + if (($rel['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) { continue; } if (!($source['match'] ?? null)) { - $url = $data[$i]['tarball_url'] ?? null; + $url = $rel['tarball_url'] ?? null; break; } - if (preg_match('|' . $source['match'] . '|', $data[$i]['tarball_url'])) { - $url = $data[$i]['tarball_url']; + if (preg_match('|' . $source['match'] . '|', $rel['tarball_url'])) { + $url = $rel['tarball_url']; break; } } @@ -232,7 +268,8 @@ public static function downloadGit(string $name, string $url, string $branch, ?a $quiet = !defined('DEBUG_MODE') ? '-q --quiet' : ''; $git = SPC_GIT_EXEC; $shallow = defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : ''; - $recursive = ($submodules === null) ? '--recursive' : ''; + $recursive = ($submodules === null && defined('GIT_SHALLOW_CLONE')) ? '--recursive --shallow-submodules' : null; + $recursive ??= $submodules === null ? '--recursive' : ''; try { self::registerCancelEvent(function () use ($download_path) { @@ -243,8 +280,9 @@ public static function downloadGit(string $name, string $url, string $branch, ?a }); f_passthru("{$git} clone {$quiet} --config core.autocrlf=false --branch \"{$branch}\" {$shallow} {$recursive} \"{$url}\" \"{$download_path}\""); if ($submodules !== null) { + $depth_flag = defined('GIT_SHALLOW_CLONE') ? '--depth 1' : ''; foreach ($submodules as $submodule) { - f_passthru("cd \"{$download_path}\" && {$git} submodule update --init " . escapeshellarg($submodule)); + f_passthru("cd \"{$download_path}\" && {$git} submodule update --init {$depth_flag} " . escapeshellarg($submodule)); } } } catch (SPCException $e) { @@ -315,91 +353,14 @@ public static function downloadPackage(string $name, ?array $pkg = null, bool $f if (self::isAlreadyDownloaded($name, $force, SPC_DOWNLOAD_PACKAGE)) { return; } - - try { - switch ($pkg['type']) { - case 'bitbuckettag': // BitBucket Tag - [$url, $filename] = self::getLatestBitbucketTag($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE); - break; - case 'ghtar': // GitHub Release (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghtagtar': // GitHub Tag (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $pkg, 'tags'); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghrel': // GitHub Release (uploaded) - [$url, $filename] = self::getLatestGithubRelease($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); - break; - case 'filelist': // Basic File List (regex based crawler) - [$url, $filename] = self::getFromFileList($name, $pkg); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE); - break; - case 'url': // Direct download URL - $url = $pkg['url']; - $filename = $pkg['filename'] ?? basename($pkg['url']); - self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE); - break; - case 'git': // Git repo - self::downloadGit( - $name, - $pkg['url'], - $pkg['rev'], - $pkg['submodules'] ?? null, - $pkg['extract'] ?? null, - self::getRetryAttempts(), - SPC_DOWNLOAD_PRE_BUILT - ); - break; - case 'local': - // Local directory, do nothing, just lock it - logger()->debug("Locking local source {$name}"); - LockFile::lockSource($name, [ - 'source_type' => SPC_SOURCE_LOCAL, - 'dirname' => $pkg['dirname'], - 'move_path' => $pkg['extract'] ?? null, - 'lock_as' => SPC_DOWNLOAD_PACKAGE, - ]); - break; - case 'custom': // Custom download method, like API-based download or other - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'); - if (isset($pkg['func']) && is_callable($pkg['func'])) { - $pkg['name'] = $name; - $pkg['func']($force, $pkg, SPC_DOWNLOAD_PACKAGE); - break; - } - foreach ($classes as $class) { - if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { - $cls = new $class(); - if (in_array($name, $cls->getSupportName())) { - (new $class())->fetch($name, $force, $pkg); - break; - } - } - } - break; - default: - throw new DownloaderException('unknown source type: ' . $pkg['type']); - } - } catch (\Throwable $e) { - // Because sometimes files downloaded through the command line are not automatically deleted after a failure. - // Here we need to manually delete the file if it is detected to exist. - if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { - logger()->warning('Deleting download file: ' . $filename); - unlink(DOWNLOAD_PATH . '/' . $filename); - } - throw new DownloaderException('Download failed! ' . $e->getMessage()); - } + self::downloadByType($pkg['type'], $name, $pkg, $force, SPC_DOWNLOAD_PACKAGE); } /** * Download source * * @param string $name source name - * @param null|array{ + * @param null|array{ * type: string, * repo: ?string, * url: ?string, @@ -414,7 +375,7 @@ public static function downloadPackage(string $name, ?array $pkg = null, bool $f * path: ?string, * text: ?string * } - * } $source source meta info: [type, path, rev, url, filename, regex, license] + * } $source source meta info: [type, path, rev, url, filename, regex, license] * @param bool $force Whether to force download (default: false) * @param int $download_as Lock source type (default: SPC_LOCK_SOURCE) */ @@ -437,80 +398,7 @@ public static function downloadSource(string $name, ?array $source = null, bool return; } - try { - switch ($source['type']) { - case 'bitbuckettag': // BitBucket Tag - [$url, $filename] = self::getLatestBitbucketTag($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as); - break; - case 'ghtar': // GitHub Release (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghtagtar': // GitHub Tag (tar) - [$url, $filename] = self::getLatestGithubTarball($name, $source, 'tags'); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); - break; - case 'ghrel': // GitHub Release (uploaded) - [$url, $filename] = self::getLatestGithubRelease($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); - break; - case 'filelist': // Basic File List (regex based crawler) - [$url, $filename] = self::getFromFileList($name, $source); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as); - break; - case 'url': // Direct download URL - $url = $source['url']; - $filename = $source['filename'] ?? basename($source['url']); - self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as); - break; - case 'git': // Git repo - self::downloadGit( - $name, - $source['url'], - $source['rev'], - $source['submodules'] ?? null, - $source['path'] ?? null, - self::getRetryAttempts(), - $download_as - ); - break; - case 'local': - // Local directory, do nothing, just lock it - logger()->debug("Locking local source {$name}"); - LockFile::lockSource($name, [ - 'source_type' => SPC_SOURCE_LOCAL, - 'dirname' => $source['dirname'], - 'move_path' => $source['extract'] ?? null, - 'lock_as' => $download_as, - ]); - break; - case 'custom': // Custom download method, like API-based download or other - if (isset($source['func']) && is_callable($source['func'])) { - $source['name'] = $name; - $source['func']($force, $source, $download_as); - break; - } - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'); - foreach ($classes as $class) { - if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) { - (new $class())->fetch($force, $source, $download_as); - break; - } - } - break; - default: - throw new DownloaderException('unknown source type: ' . $source['type']); - } - } catch (\Throwable $e) { - // Because sometimes files downloaded through the command line are not automatically deleted after a failure. - // Here we need to manually delete the file if it is detected to exist. - if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { - logger()->warning('Deleting download file: ' . $filename); - unlink(DOWNLOAD_PATH . '/' . $filename); - } - throw new DownloaderException('Download failed! ' . $e->getMessage()); - } + self::downloadByType($source['type'], $name, $source, $force, $download_as); } /** @@ -711,4 +599,109 @@ private static function isAlreadyDownloaded(string $name, bool $force, int $down } return false; } + + /** + * Download by type. + * + * @param string $type Types + * @param string $name Download item name + * @param array{ + * url?: string, + * repo?: string, + * rev?: string, + * path?: string, + * filename?: string, + * dirname?: string, + * match?: string, + * prefer-stable?: bool, + * extract?: string, + * submodules?: array, + * provide-pre-built?: bool, + * func?: ?callable, + * license?: array + * } $conf Download item config + * @param bool $force Force download + * @param int $download_as Lock source type + */ + private static function downloadByType(string $type, string $name, array $conf, bool $force, int $download_as): void + { + try { + switch ($type) { + case 'pie': // Packagist + [$url, $filename] = self::getPIEInfo($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); + break; + case 'bitbuckettag': // BitBucket Tag + [$url, $filename] = self::getLatestBitbucketTag($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); + break; + case 'ghtar': // GitHub Release (tar) + [$url, $filename] = self::getLatestGithubTarball($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); + break; + case 'ghtagtar': // GitHub Tag (tar) + [$url, $filename] = self::getLatestGithubTarball($name, $conf, 'tags'); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]); + break; + case 'ghrel': // GitHub Release (uploaded) + [$url, $filename] = self::getLatestGithubRelease($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]); + break; + case 'filelist': // Basic File List (regex based crawler) + [$url, $filename] = self::getFromFileList($name, $conf); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); + break; + case 'url': // Direct download URL + $url = $conf['url']; + $filename = $conf['filename'] ?? basename($conf['url']); + self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as); + break; + case 'git': // Git repo + self::downloadGit($name, $conf['url'], $conf['rev'], $conf['submodules'] ?? null, $conf['path'] ?? $conf['extract'] ?? null, self::getRetryAttempts(), $download_as); + break; + case 'local': // Local directory, do nothing, just lock it + LockFile::lockSource($name, [ + 'source_type' => SPC_SOURCE_LOCAL, + 'dirname' => $conf['dirname'], + 'move_path' => $conf['path'] ?? $conf['extract'] ?? null, + 'lock_as' => $download_as, + ]); + break; + case 'custom': // Custom download method, like API-based download or other + if (isset($conf['func'])) { + $conf['name'] = $name; + $conf['func']($force, $conf, $download_as); + break; + } + $classes = [ + ...FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'), + ...FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'), + ]; + foreach ($classes as $class) { + if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) { + (new $class())->fetch($force, $conf, $download_as); + break; + } + if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { + $cls = new $class(); + if (in_array($name, $cls->getSupportName())) { + (new $class())->fetch($name, $force, $conf); + break; + } + } + } + break; + default: + throw new DownloaderException("Unknown download type: {$type}"); + } + } catch (\Throwable $e) { + // Because sometimes files downloaded through the command line are not automatically deleted after a failure. + // Here we need to manually delete the file if it is detected to exist. + if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) { + logger()->warning("Deleting download file: {$filename}"); + unlink(DOWNLOAD_PATH . '/' . $filename); + } + throw new DownloaderException("Download failed: {$e->getMessage()}"); + } + } } diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index 7a574925d..86daefc43 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -174,6 +174,9 @@ public static function copy(string $from, string $to): void logger()->debug("Copying file from {$from} to {$to}"); $dst_path = FileSystem::convertPath($to); $src_path = FileSystem::convertPath($from); + if ($src_path === $dst_path) { + return; + } if (!copy($src_path, $dst_path)) { throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path); } @@ -584,7 +587,7 @@ private static function extractArchive(string $filename, string $target): void 'tar', 'xz', 'txz' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), 'tgz', 'gz' => f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"), 'bz2' => f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1"), - 'zip' => f_passthru("unzip {$filename} -d {$target}"), + 'zip' => self::unzipWithStrip($filename, $target), default => throw new FileSystemException('unknown archive format: ' . $filename), }; } elseif (PHP_OS_FAMILY === 'Windows') { @@ -599,7 +602,7 @@ private static function extractArchive(string $filename, string $target): void match (self::extname($filename)) { 'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), 'xz', 'txz', 'gz', 'tgz', 'bz2' => cmd()->execWithResult("\"{$_7z}\" x -so {$filename} | tar -f - -x -C \"{$target}\" --strip-components 1"), - 'zip' => f_passthru("\"{$_7z}\" x {$filename} -o{$target} -y"), + 'zip' => self::unzipWithStrip($filename, $target), default => throw new FileSystemException("unknown archive format: {$filename}"), }; } @@ -635,7 +638,7 @@ private static function emitSourceExtractHook(string $name, string $target): voi private static function extractWithType(string $source_type, string $filename, string $extract_path): void { - logger()->debug('Extracting source [' . $source_type . ']: ' . $filename); + logger()->debug("Extracting source [{$source_type}]: {$filename}"); /* @phpstan-ignore-next-line */ match ($source_type) { SPC_SOURCE_ARCHIVE => self::extractArchive($filename, $extract_path), @@ -644,4 +647,107 @@ private static function extractWithType(string $source_type, string $filename, s SPC_SOURCE_LOCAL => symlink(self::convertPath($filename), $extract_path), }; } + + /** + * Move file or directory, handling cross-device scenarios + * Uses rename() if possible, falls back to copy+delete for cross-device moves + * + * @param string $source Source path + * @param string $dest Destination path + */ + private static function moveFileOrDir(string $source, string $dest): void + { + $source = self::convertPath($source); + $dest = self::convertPath($dest); + + // Try rename first (fast, atomic) + if (@rename($source, $dest)) { + return; + } + + if (is_dir($source)) { + self::copyDir($source, $dest); + self::removeDir($source); + } else { + if (!copy($source, $dest)) { + throw new FileSystemException("Failed to copy file from {$source} to {$dest}"); + } + if (!unlink($source)) { + throw new FileSystemException("Failed to remove source file: {$source}"); + } + } + } + + /** + * Unzip file with stripping top-level directory + */ + private static function unzipWithStrip(string $zip_file, string $extract_path): void + { + $temp_dir = self::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16))); + $zip_file = self::convertPath($zip_file); + $extract_path = self::convertPath($extract_path); + + // extract to temp dir + self::createDir($temp_dir); + + if (PHP_OS_FAMILY === 'Windows') { + $mute = defined('DEBUG_MODE') ? '' : ' > NUL'; + // use php-sdk-binary-tools/bin/7za.exe + $_7z = self::convertPath(getenv('PHP_SDK_PATH') . '/bin/7za.exe'); + f_passthru("\"{$_7z}\" x {$zip_file} -o{$temp_dir} -y{$mute}"); + } else { + $mute = defined('DEBUG_MODE') ? '' : ' > /dev/null'; + f_passthru("unzip \"{$zip_file}\" -d \"{$temp_dir}\"{$mute}"); + } + // scan first level dirs (relative, not recursive, include dirs) + $contents = self::scanDirFiles($temp_dir, false, true, true); + if ($contents === false) { + throw new FileSystemException('Cannot scan unzip temp dir: ' . $temp_dir); + } + // if extract path already exists, remove it + if (is_dir($extract_path)) { + self::removeDir($extract_path); + } + // if only one dir, move its contents to extract_path + $subdir = self::convertPath("{$temp_dir}/{$contents[0]}"); + if (count($contents) === 1 && is_dir($subdir)) { + self::moveFileOrDir($subdir, $extract_path); + } else { + // else, if it contains only one dir, strip dir and copy other files + $dircount = 0; + $dir = []; + $top_files = []; + foreach ($contents as $item) { + if (is_dir(self::convertPath("{$temp_dir}/{$item}"))) { + ++$dircount; + $dir[] = $item; + } else { + $top_files[] = $item; + } + } + // extract dir contents to extract_path + self::createDir($extract_path); + // extract move dir + if ($dircount === 1) { + $sub_contents = self::scanDirFiles("{$temp_dir}/{$dir[0]}", false, true, true); + if ($sub_contents === false) { + throw new FileSystemException("Cannot scan unzip temp sub-dir: {$dir[0]}"); + } + foreach ($sub_contents as $sub_item) { + self::moveFileOrDir(self::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"), self::convertPath("{$extract_path}/{$sub_item}")); + } + } else { + foreach ($dir as $item) { + self::moveFileOrDir(self::convertPath("{$temp_dir}/{$item}"), self::convertPath("{$extract_path}/{$item}")); + } + } + // move top-level files to extract_path + foreach ($top_files as $top_file) { + self::moveFileOrDir(self::convertPath("{$temp_dir}/{$top_file}"), self::convertPath("{$extract_path}/{$top_file}")); + } + } + + // Clean up temp directory + self::removeDir($temp_dir); + } } diff --git a/src/SPC/store/LockFile.php b/src/SPC/store/LockFile.php index cab5b9da8..88ecd6cb0 100644 --- a/src/SPC/store/LockFile.php +++ b/src/SPC/store/LockFile.php @@ -155,8 +155,8 @@ public static function putLockSourceHash(array $lock_options, string $destinatio * @param string $name Source name * @param array{ * source_type: string, - * dirname: ?string, - * filename: ?string, + * dirname?: ?string, + * filename?: ?string, * move_path: ?string, * lock_as: int * } $data Source data diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index c08204f5d..f628544fe 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -44,7 +44,7 @@ public static function patchBeforeBuildconf(BuilderBase $builder): void } foreach ($builder->getLibs() as $lib) { if ($lib->patchBeforeBuildconf() === true) { - logger()->info("Library [{$lib->getName()}]patched before buildconf"); + logger()->info("Library [{$lib->getName()}] patched before buildconf"); } } // patch windows php 8.1 bug @@ -549,6 +549,39 @@ public static function patchWindowsCLITarget(): void FileSystem::writeFile(SOURCE_PATH . '/php-src/Makefile', implode("\r\n", $lines)); } + /** + * Patch cgi SAPI Makefile for Windows. + */ + public static function patchWindowsCGITarget(): void + { + // search Makefile code line contains "$(BUILD_DIR)\php-cgi.exe:" + $content = FileSystem::readFile(SOURCE_PATH . '/php-src/Makefile'); + $lines = explode("\r\n", $content); + $line_num = 0; + $found = false; + foreach ($lines as $v) { + if (str_contains($v, '$(BUILD_DIR)\php-cgi.exe:')) { + $found = $line_num; + break; + } + ++$line_num; + } + if ($found === false) { + throw new PatchException('Windows Makefile patching for php-cgi.exe target', 'Cannot patch windows CGI Makefile, Makefile does not contain "$(BUILD_DIR)\php-cgi.exe:" line'); + } + // cli: $(BUILD_DIR)\php.exe: $(DEPS_CLI) $(CLI_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest + // $lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(CLI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest'; + // cgi: $(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(BUILD_DIR)\$(PHPLIB) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest + $lines[$line_num] = '$(BUILD_DIR)\php-cgi.exe: $(DEPS_CGI) $(CGI_GLOBAL_OBJS) $(PHP_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php-cgi.exe.res $(BUILD_DIR)\php-cgi.exe.manifest'; + + // cli: @"$(LINK)" /nologo $(CGI_GLOBAL_OBJS_RESP) $(BUILD_DIR)\$(PHPLIB) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) + $lines[$line_num + 1] = "\t" . '@"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CGI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CGI) $(BUILD_DIR)\php-cgi.exe.res /out:$(BUILD_DIR)\php-cgi.exe $(LDFLAGS) $(LDFLAGS_CGI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; + FileSystem::writeFile(SOURCE_PATH . '/php-src/Makefile', implode("\r\n", $lines)); + + // Patch cgi-static, comment ZEND_TSRMLS_CACHE_DEFINE() + FileSystem::replaceFileRegex(SOURCE_PATH . '\php-src\sapi\cgi\cgi_main.c', '/^ZEND_TSRMLS_CACHE_DEFINE\(\)/m', '// ZEND_TSRMLS_CACHE_DEFINE()'); + } + public static function patchPhpLibxml212(): bool { $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); diff --git a/src/SPC/store/scripts/zig-cc.sh b/src/SPC/store/scripts/zig-cc.sh index d4090a0f3..6b340bc1d 100644 --- a/src/SPC/store/scripts/zig-cc.sh +++ b/src/SPC/store/scripts/zig-cc.sh @@ -19,9 +19,15 @@ while [[ $# -gt 0 ]]; do ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG") ;; - -march=*|-mcpu=*) # replace -march=x86-64 with -march=x86_64 + -march=*|-mcpu=*) OPT_NAME="${1%%=*}" OPT_VALUE="${1#*=}" + # Skip armv8- flags entirely as Zig doesn't support them + if [[ "$OPT_VALUE" == armv8-* ]]; then + shift + continue + fi + # replace -march=x86-64 with -march=x86_64 OPT_VALUE="${OPT_VALUE//-/_}" PARSED_ARGS+=("${OPT_NAME}=${OPT_VALUE}") shift diff --git a/src/SPC/store/source/PhpSource.php b/src/SPC/store/source/PhpSource.php index c33e172f3..27e8bb891 100644 --- a/src/SPC/store/source/PhpSource.php +++ b/src/SPC/store/source/PhpSource.php @@ -15,12 +15,10 @@ class PhpSource extends CustomSourceBase public function fetch(bool $force = false, ?array $config = null, int $lock_as = SPC_DOWNLOAD_SOURCE): void { $major = defined('SPC_BUILD_PHP_VERSION') ? SPC_BUILD_PHP_VERSION : '8.4'; - if ($major === '8.5') { - Downloader::downloadSource('php-src', ['type' => 'url', 'url' => 'https://downloads.php.net/~edorian/php-8.5.0beta1.tar.xz'], $force); - } elseif ($major === 'git') { + if ($major === 'git') { Downloader::downloadSource('php-src', ['type' => 'git', 'url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $force); } else { - Downloader::downloadSource('php-src', self::getLatestPHPInfo($major), $force); + Downloader::downloadSource('php-src', $this->getLatestPHPInfo($major), $force); } } @@ -33,7 +31,7 @@ public function getLatestPHPInfo(string $major_version): array // 查找最新的小版本号 $info = json_decode(Downloader::curlExec( url: "https://www.php.net/releases/index.php?json&version={$major_version}", - retries: intval(getenv('SPC_DOWNLOAD_RETRIES') ?: 0) + retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0 ), true); if (!isset($info['version'])) { throw new DownloaderException("Version {$major_version} not found."); diff --git a/src/SPC/toolchain/ZigToolchain.php b/src/SPC/toolchain/ZigToolchain.php index 8f2e261e9..bb423db20 100644 --- a/src/SPC/toolchain/ZigToolchain.php +++ b/src/SPC/toolchain/ZigToolchain.php @@ -64,6 +64,11 @@ public function afterInit(): void $extra_libs = trim($extra_libs . ' -lunwind'); GlobalEnvManager::putenv("SPC_EXTRA_LIBS={$extra_libs}"); } + $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; + $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); + if (!$has_avx512) { + GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no'); + } } public function getCompilerInfo(): ?string diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php index 58dedea6e..445c6242f 100644 --- a/src/SPC/util/ConfigValidator.php +++ b/src/SPC/util/ConfigValidator.php @@ -11,6 +11,150 @@ class ConfigValidator { + /** + * Global field type definitions + * Maps field names to their expected types and validation rules + * Note: This only includes fields used in config files (source.json, lib.json, ext.json, pkg.json, pre-built.json) + */ + private const array FIELD_TYPES = [ + // String fields + 'url' => 'string', // url + 'regex' => 'string', // regex pattern + 'rev' => 'string', // revision/branch + 'repo' => 'string', // repository name + 'match' => 'string', // match pattern (aaa*bbb) + 'filename' => 'string', // filename + 'path' => 'string', // copy path + 'extract' => 'string', // copy path (alias of path) + 'dirname' => 'string', // directory name for local source + 'source' => 'string', // the source name that this item uses + 'match-pattern-linux' => 'string', // pre-built match pattern for linux + 'match-pattern-macos' => 'string', // pre-built match pattern for macos + 'match-pattern-windows' => 'string', // pre-built match pattern for windows + + // Boolean fields + 'prefer-stable' => 'bool', // prefer stable releases + 'provide-pre-built' => 'bool', // provide pre-built binaries + 'notes' => 'bool', // whether to show notes in docs + 'cpp-library' => 'bool', // whether this is a C++ library + 'cpp-extension' => 'bool', // whether this is a C++ extension + 'build-with-php' => 'bool', // whether if this extension can be built to shared with PHP source together + 'zend-extension' => 'bool', // whether this is a zend extension + 'unix-only' => 'bool', // whether this extension is only for unix-like systems + + // Array fields + 'submodules' => 'array', // git submodules list (for git source type) + 'lib-depends' => 'list', + 'lib-suggests' => 'list', + 'ext-depends' => 'list', + 'ext-suggests' => 'list', + 'static-libs' => 'list', + 'pkg-configs' => 'list', // required pkg-config files without suffix (e.g. [libwebp]) + 'headers' => 'list', // required header files + 'bin' => 'list', // required binary files + 'frameworks' => 'list', // shared library frameworks (macOS) + + // Object/assoc array fields + 'support' => 'object', // extension OS support docs + 'extract-files' => 'object', // pkg.json extract files mapping with match pattern + 'alt' => 'object|bool', // alternative source/package + 'license' => 'object|array', // license information + 'target' => 'array', // extension build targets (default: [static], alternate: [shared] or both) + + // Special/mixed fields + 'func' => 'callable', // custom download function for custom source/package type + 'type' => 'string', // type field (validated separately) + ]; + + /** + * Source/Package download type validation rules + * Maps type names to [required_props, optional_props] + */ + private const array SOURCE_TYPE_FIELDS = [ + 'filelist' => [['url', 'regex'], []], + 'git' => [['url', 'rev'], ['path', 'extract', 'submodules']], + 'ghtagtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']], + 'ghtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']], + 'ghrel' => [['repo', 'match'], ['path', 'extract', 'prefer-stable']], + 'url' => [['url'], ['filename', 'path', 'extract']], + 'bitbuckettag' => [['repo'], ['path', 'extract']], + 'local' => [['dirname'], ['path', 'extract']], + 'pie' => [['repo'], ['path']], + 'custom' => [[], ['func']], + ]; + + /** + * Source.json specific fields [field_name => required] + * Note: 'type' is validated separately in validateSourceTypeConfig + * Field types are defined in FIELD_TYPES constant + */ + private const array SOURCE_FIELDS = [ + 'type' => true, // source type (must be SOURCE_TYPE_FIELDS key) + 'provide-pre-built' => false, // whether to provide pre-built binaries + 'alt' => false, // alternative source configuration + 'license' => false, // license information for source + // ... other fields are validated based on source type + ]; + + /** + * Lib.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array LIB_FIELDS = [ + 'type' => false, // lib type (lib/package/target/root) + 'source' => false, // the source name that this lib uses + 'lib-depends' => false, // required libraries + 'lib-suggests' => false, // suggested libraries + 'static-libs' => false, // Generated static libraries + 'pkg-configs' => false, // Generated pkg-config files + 'cpp-library' => false, // whether this is a C++ library + 'headers' => false, // Generated header files + 'bin' => false, // Generated binary files + 'frameworks' => false, // Used shared library frameworks (macOS) + ]; + + /** + * Ext.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array EXT_FIELDS = [ + 'type' => true, // extension type (builtin/external/addon/wip) + 'source' => false, // the source name that this extension uses + 'support' => false, // extension OS support docs + 'notes' => false, // whether to show notes in docs + 'cpp-extension' => false, // whether this is a C++ extension + 'build-with-php' => false, // whether if this extension can be built to shared with PHP source together + 'target' => false, // extension build targets (default: [static], alternate: [shared] or both) + 'lib-depends' => false, + 'lib-suggests' => false, + 'ext-depends' => false, + 'ext-suggests' => false, + 'frameworks' => false, + 'zend-extension' => false, // whether this is a zend extension + 'unix-only' => false, // whether this extension is only for unix-like systems + ]; + + /** + * Pkg.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array PKG_FIELDS = [ + 'type' => true, // package type (same as source type) + 'extract-files' => false, // files to extract mapping (source pattern => target path) + ]; + + /** + * Pre-built.json specific fields [field_name => required] + * Field types are defined in FIELD_TYPES constant + */ + private const array PRE_BUILT_FIELDS = [ + 'repo' => true, // repository name for pre-built binaries + 'prefer-stable' => false, // prefer stable releases + 'match-pattern-linux' => false, // pre-built match pattern for linux + 'match-pattern-macos' => false, // pre-built match pattern for macos + 'match-pattern-windows' => false, // pre-built match pattern for windows + ]; + /** * Validate source.json * @@ -22,33 +166,20 @@ public static function validateSource(array $data): void // Validate basic source type configuration self::validateSourceTypeConfig($src, $name, 'source'); - // Check source-specific fields - // check if alt is valid - if (isset($src['alt'])) { - if (!is_assoc_array($src['alt']) && !is_bool($src['alt'])) { - throw new ValidationException("source {$name} alt must be object or boolean"); - } - if (is_assoc_array($src['alt'])) { - // validate alt source recursively - self::validateSource([$name . '_alt' => $src['alt']]); - } - } + // Validate all source-specific fields using unified method + self::validateConfigFields($src, $name, 'source', self::SOURCE_FIELDS); - // check if provide-pre-built is boolean - if (isset($src['provide-pre-built']) && !is_bool($src['provide-pre-built'])) { - throw new ValidationException("source {$name} provide-pre-built must be boolean"); - } + // Check for unknown fields + self::validateAllowedFields($src, $name, 'source', self::SOURCE_FIELDS); - // check if prefer-stable is boolean - if (isset($src['prefer-stable']) && !is_bool($src['prefer-stable'])) { - throw new ValidationException("source {$name} prefer-stable must be boolean"); + // check if alt is valid + if (isset($src['alt']) && is_assoc_array($src['alt'])) { + // validate alt source recursively + self::validateSource([$name . '_alt' => $src['alt']]); } // check if license is valid if (isset($src['license'])) { - if (!is_array($src['license'])) { - throw new ValidationException("source {$name} license must be an object or array"); - } if (is_assoc_array($src['license'])) { self::checkSingleLicense($src['license'], $name); } elseif (is_list_array($src['license'])) { @@ -58,8 +189,6 @@ public static function validateSource(array $data): void } self::checkSingleLicense($license, $name); } - } else { - throw new ValidationException("source {$name} license must be an object or array"); } } } @@ -71,9 +200,8 @@ public static function validateLibs(mixed $data, array $source_data = []): void if (!is_array($data)) { throw new ValidationException('lib.json is broken'); } - // check each lib + foreach ($data as $name => $lib) { - // check if lib is an assoc array if (!is_assoc_array($lib)) { throw new ValidationException("lib {$name} is not an object"); } @@ -89,36 +217,22 @@ public static function validateLibs(mixed $data, array $source_data = []): void if (isset($lib['source']) && !empty($source_data) && !isset($source_data[$lib['source']])) { throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}"); } - // check if source is string - if (isset($lib['source']) && !is_string($lib['source'])) { - throw new ValidationException("lib {$name} source must be string"); - } - // check if [lib-depends|lib-suggests|static-libs|headers|bin][-windows|-unix|-macos|-linux] are valid list array + + // Validate basic fields using unified method + self::validateConfigFields($lib, $name, 'lib', self::LIB_FIELDS); + + // Validate list array fields with suffixes $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; - foreach ($suffixes as $suffix) { - if (isset($lib['lib-depends' . $suffix]) && !is_list_array($lib['lib-depends' . $suffix])) { - throw new ValidationException("lib {$name} lib-depends must be a list"); - } - if (isset($lib['lib-suggests' . $suffix]) && !is_list_array($lib['lib-suggests' . $suffix])) { - throw new ValidationException("lib {$name} lib-suggests must be a list"); - } - if (isset($lib['static-libs' . $suffix]) && !is_list_array($lib['static-libs' . $suffix])) { - throw new ValidationException("lib {$name} static-libs must be a list"); - } - if (isset($lib['pkg-configs' . $suffix]) && !is_list_array($lib['pkg-configs' . $suffix])) { - throw new ValidationException("lib {$name} pkg-configs must be a list"); - } - if (isset($lib['headers' . $suffix]) && !is_list_array($lib['headers' . $suffix])) { - throw new ValidationException("lib {$name} headers must be a list"); - } - if (isset($lib['bin' . $suffix]) && !is_list_array($lib['bin' . $suffix])) { - throw new ValidationException("lib {$name} bin must be a list"); - } - } - // check if frameworks is a list array - if (isset($lib['frameworks']) && !is_list_array($lib['frameworks'])) { - throw new ValidationException("lib {$name} frameworks must be a list"); + $fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin']; + self::validateListArrayFields($lib, $name, 'lib', $fields, $suffixes); + + // Validate frameworks (special case without suffix) + if (isset($lib['frameworks'])) { + self::validateFieldType('frameworks', $lib['frameworks'], $name, 'lib'); } + + // Check for unknown fields + self::validateAllowedFields($lib, $name, 'lib', self::LIB_FIELDS); } } @@ -127,61 +241,34 @@ public static function validateExts(mixed $data): void if (!is_array($data)) { throw new ValidationException('ext.json is broken'); } - // check each extension + foreach ($data as $name => $ext) { - // check if ext is an assoc array if (!is_assoc_array($ext)) { throw new ValidationException("ext {$name} is not an object"); } - // check if ext has valid type + if (!in_array($ext['type'] ?? '', ['builtin', 'external', 'addon', 'wip'])) { throw new ValidationException("ext {$name} type is invalid"); } - // check if external ext has source + + // Check source field requirement if (($ext['type'] ?? '') === 'external' && !isset($ext['source'])) { throw new ValidationException("ext {$name} does not assign any source"); } - // check if source is string - if (isset($ext['source']) && !is_string($ext['source'])) { - throw new ValidationException("ext {$name} source must be string"); - } - // check if support is valid - if (isset($ext['support']) && !is_assoc_array($ext['support'])) { - throw new ValidationException("ext {$name} support must be an object"); - } - // check if notes is boolean - if (isset($ext['notes']) && !is_bool($ext['notes'])) { - throw new ValidationException("ext {$name} notes must be boolean"); - } - // check if [lib-depends|lib-suggests|ext-depends][-windows|-unix|-macos|-linux] are valid list array + + // Validate basic fields using unified method + self::validateConfigFields($ext, $name, 'ext', self::EXT_FIELDS); + + // Validate list array fields with suffixes $suffixes = ['', '-windows', '-unix', '-macos', '-linux']; - foreach ($suffixes as $suffix) { - if (isset($ext['lib-depends' . $suffix]) && !is_list_array($ext['lib-depends' . $suffix])) { - throw new ValidationException("ext {$name} lib-depends must be a list"); - } - if (isset($ext['lib-suggests' . $suffix]) && !is_list_array($ext['lib-suggests' . $suffix])) { - throw new ValidationException("ext {$name} lib-suggests must be a list"); - } - if (isset($ext['ext-depends' . $suffix]) && !is_list_array($ext['ext-depends' . $suffix])) { - throw new ValidationException("ext {$name} ext-depends must be a list"); - } - } - // check if arg-type is valid - if (isset($ext['arg-type'])) { - $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; - if (!in_array($ext['arg-type'], $valid_arg_types)) { - throw new ValidationException("ext {$name} arg-type is invalid"); - } - } - // check if arg-type with suffix is valid - foreach ($suffixes as $suffix) { - if (isset($ext['arg-type' . $suffix])) { - $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; - if (!in_array($ext['arg-type' . $suffix], $valid_arg_types)) { - throw new ValidationException("ext {$name} arg-type{$suffix} is invalid"); - } - } - } + $fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests']; + self::validateListArrayFields($ext, $name, 'ext', $fields, $suffixes); + + // Validate arg-type fields + self::validateArgTypeFields($ext, $name, $suffixes); + + // Check for unknown fields + self::validateAllowedFields($ext, $name, 'ext', self::EXT_FIELDS); } } @@ -200,12 +287,11 @@ public static function validatePkgs(mixed $data): void // Validate basic source type configuration (reuse from source validation) self::validateSourceTypeConfig($pkg, $name, 'pkg'); - // Check pkg-specific fields - // check if extract-files is valid + // Validate all pkg-specific fields using unified method + self::validateConfigFields($pkg, $name, 'pkg', self::PKG_FIELDS); + + // Validate extract-files content (object validation is done by validateFieldType) if (isset($pkg['extract-files'])) { - if (!is_assoc_array($pkg['extract-files'])) { - throw new ValidationException("pkg {$name} extract-files must be an object"); - } // check each extract file mapping foreach ($pkg['extract-files'] as $source => $target) { if (!is_string($source) || !is_string($target)) { @@ -213,6 +299,9 @@ public static function validatePkgs(mixed $data): void } } } + + // Check for unknown fields + self::validateAllowedFields($pkg, $name, 'pkg', self::PKG_FIELDS); } } @@ -227,18 +316,11 @@ public static function validatePreBuilt(mixed $data): void throw new ValidationException('pre-built.json is broken'); } - // Check required fields - if (!isset($data['repo'])) { - throw new ValidationException('pre-built.json must have [repo] field'); - } - if (!is_string($data['repo'])) { - throw new ValidationException('pre-built.json [repo] must be string'); - } + // Validate all fields using unified method + self::validateConfigFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS); - // Check optional prefer-stable field - if (isset($data['prefer-stable']) && !is_bool($data['prefer-stable'])) { - throw new ValidationException('pre-built.json [prefer-stable] must be boolean'); - } + // Check for unknown fields + self::validateAllowedFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS); // Check match pattern fields (at least one must exist) $pattern_fields = ['match-pattern-linux', 'match-pattern-macos', 'match-pattern-windows']; @@ -247,9 +329,6 @@ public static function validatePreBuilt(mixed $data): void foreach ($pattern_fields as $field) { if (isset($data[$field])) { $has_pattern = true; - if (!is_string($data[$field])) { - throw new ValidationException("pre-built.json [{$field}] must be string"); - } // Validate pattern contains required placeholders if (!str_contains($data[$field], '{name}')) { throw new ValidationException("pre-built.json [{$field}] must contain {name} placeholder"); @@ -403,6 +482,52 @@ public static function validateAndParseCraftFile(mixed $craft_file, Command $com return $craft; } + /** + * Validate a field based on its global type definition + * + * @param string $field Field name + * @param mixed $value Field value + * @param string $name Item name (for error messages) + * @param string $type Item type (for error messages) + * @return bool Returns true if validation passes + */ + private static function validateFieldType(string $field, mixed $value, string $name, string $type): bool + { + // Check if field exists in FIELD_TYPES + if (!isset(self::FIELD_TYPES[$field])) { + // Try to strip suffix and check base field name + $suffixes = ['-windows', '-unix', '-macos', '-linux']; + $base_field = $field; + foreach ($suffixes as $suffix) { + if (str_ends_with($field, $suffix)) { + $base_field = substr($field, 0, -strlen($suffix)); + break; + } + } + + if (!isset(self::FIELD_TYPES[$base_field])) { + // Unknown field is not allowed - strict validation + throw new ValidationException("{$type} {$name} has unknown field [{$field}]"); + } + + // Use base field type for validation + $expected_type = self::FIELD_TYPES[$base_field]; + } else { + $expected_type = self::FIELD_TYPES[$field]; + } + + return match ($expected_type) { + 'string' => is_string($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be string"), + 'bool' => is_bool($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be boolean"), + 'array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be array"), + 'list' => is_list_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be a list"), + 'object' => is_assoc_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object"), + 'object|bool' => (is_assoc_array($value) || is_bool($value)) ?: throw new ValidationException("{$type} {$name} [{$field}] must be object or boolean"), + 'object|array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object or array"), + 'callable' => true, // Skip validation for callable + }; + } + private static function checkSingleLicense(array $license, string $name): void { if (!is_assoc_array($license)) { @@ -414,9 +539,6 @@ private static function checkSingleLicense(array $license, string $name): void if (!in_array($license['type'], ['file', 'text'])) { throw new ValidationException("source {$name} license type is invalid"); } - if (!in_array($license['type'], ['file', 'text'])) { - throw new ValidationException("source {$name} license type is invalid"); - } if ($license['type'] === 'file' && !isset($license['path'])) { throw new ValidationException("source {$name} license file must have path"); } @@ -440,68 +562,127 @@ private static function validateSourceTypeConfig(array $item, string $name, stri if (!is_string($item['type'])) { throw new ValidationException("{$config_type} {$name} type prop must be string"); } - if (!in_array($item['type'], ['filelist', 'git', 'ghtagtar', 'ghtar', 'ghrel', 'url', 'custom'])) { + + if (!isset(self::SOURCE_TYPE_FIELDS[$item['type']])) { throw new ValidationException("{$config_type} {$name} type [{$item['type']}] is invalid"); } - // Validate type-specific requirements - switch ($item['type']) { - case 'filelist': - if (!isset($item['url'], $item['regex'])) { - throw new ValidationException("{$config_type} {$name} needs [url] and [regex] props"); - } - if (!is_string($item['url']) || !is_string($item['regex'])) { - throw new ValidationException("{$config_type} {$name} [url] and [regex] must be string"); - } - break; - case 'git': - if (!isset($item['url'], $item['rev'])) { - throw new ValidationException("{$config_type} {$name} needs [url] and [rev] props"); - } - if (!is_string($item['url']) || !is_string($item['rev'])) { - throw new ValidationException("{$config_type} {$name} [url] and [rev] must be string"); - } - if (isset($item['path']) && !is_string($item['path'])) { - throw new ValidationException("{$config_type} {$name} [path] must be string"); - } - break; - case 'ghtagtar': - case 'ghtar': - if (!isset($item['repo'])) { - throw new ValidationException("{$config_type} {$name} needs [repo] prop"); - } - if (!is_string($item['repo'])) { - throw new ValidationException("{$config_type} {$name} [repo] must be string"); - } - if (isset($item['path']) && !is_string($item['path'])) { - throw new ValidationException("{$config_type} {$name} [path] must be string"); - } - break; - case 'ghrel': - if (!isset($item['repo'], $item['match'])) { - throw new ValidationException("{$config_type} {$name} needs [repo] and [match] props"); - } - if (!is_string($item['repo']) || !is_string($item['match'])) { - throw new ValidationException("{$config_type} {$name} [repo] and [match] must be string"); - } - break; - case 'url': - if (!isset($item['url'])) { - throw new ValidationException("{$config_type} {$name} needs [url] prop"); - } - if (!is_string($item['url'])) { - throw new ValidationException("{$config_type} {$name} [url] must be string"); + [$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']]; + + // Check required fields exist + foreach ($required as $prop) { + if (!isset($item[$prop])) { + $props = implode('] and [', $required); + throw new ValidationException("{$config_type} {$name} needs [{$props}] props"); + } + } + + // Validate field types using global field type definitions + foreach (array_merge($required, $optional) as $prop) { + if (isset($item[$prop])) { + self::validateFieldType($prop, $item[$prop], $name, $config_type); + } + } + } + + /** + * Validate that fields with suffixes are list arrays + */ + private static function validateListArrayFields(array $item, string $name, string $type, array $fields, array $suffixes): void + { + foreach ($fields as $field) { + foreach ($suffixes as $suffix) { + $key = $field . $suffix; + if (isset($item[$key])) { + self::validateFieldType($key, $item[$key], $name, $type); } - if (isset($item['filename']) && !is_string($item['filename'])) { - throw new ValidationException("{$config_type} {$name} [filename] must be string"); + } + } + } + + /** + * Validate arg-type fields with suffixes + */ + private static function validateArgTypeFields(array $item, string $name, array $suffixes): void + { + $valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path']; + + foreach (array_merge([''], $suffixes) as $suffix) { + $key = 'arg-type' . $suffix; + if (isset($item[$key]) && !in_array($item[$key], $valid_arg_types)) { + throw new ValidationException("ext {$name} {$key} is invalid"); + } + } + } + + /** + * Unified method to validate config fields based on field definitions + * + * @param array $item Item data to validate + * @param string $name Item name for error messages + * @param string $type Config type (source, lib, ext, pkg, pre-built) + * @param array $field_definitions Field definitions [field_name => required (bool)] + */ + private static function validateConfigFields(array $item, string $name, string $type, array $field_definitions): void + { + foreach ($field_definitions as $field => $required) { + if ($required && !isset($item[$field])) { + throw new ValidationException("{$type} {$name} must have [{$field}] field"); + } + + if (isset($item[$field])) { + self::validateFieldType($field, $item[$field], $name, $type); + } + } + } + + /** + * Validate that item only contains allowed fields + * This method checks for unknown fields based on the config type + * + * @param array $item Item data to validate + * @param string $name Item name for error messages + * @param string $type Config type (source, lib, ext, pkg, pre-built) + * @param array $field_definitions Field definitions [field_name => required (bool)] + */ + private static function validateAllowedFields(array $item, string $name, string $type, array $field_definitions): void + { + // For source and pkg types, we need to check SOURCE_TYPE_FIELDS as well + $allowed_fields = array_keys($field_definitions); + + // For source/pkg, add allowed fields from SOURCE_TYPE_FIELDS based on the type + if (in_array($type, ['source', 'pkg']) && isset($item['type'], self::SOURCE_TYPE_FIELDS[$item['type']])) { + [$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']]; + $allowed_fields = array_merge($allowed_fields, $required, $optional); + } + + // For lib and ext types, add fields with suffixes + if (in_array($type, ['lib', 'ext'])) { + $suffixes = ['-windows', '-unix', '-macos', '-linux']; + $base_fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin']; + if ($type === 'ext') { + $base_fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests']; + // Add arg-type fields + foreach (array_merge([''], $suffixes) as $suffix) { + $allowed_fields[] = 'arg-type' . $suffix; } - if (isset($item['path']) && !is_string($item['path'])) { - throw new ValidationException("{$config_type} {$name} [path] must be string"); + } + foreach ($base_fields as $field) { + foreach ($suffixes as $suffix) { + $allowed_fields[] = $field . $suffix; } - break; - case 'custom': - // custom type has no specific requirements - break; + } + // frameworks is lib-only + if ($type === 'lib') { + $allowed_fields[] = 'frameworks'; + } + } + + // Check each field in item + foreach (array_keys($item) as $field) { + if (!in_array($field, $allowed_fields)) { + throw new ValidationException("{$type} {$name} has unknown field [{$field}]"); + } } } } diff --git a/src/SPC/util/GlobalEnvManager.php b/src/SPC/util/GlobalEnvManager.php index 9f74d2df7..4ce3384dd 100644 --- a/src/SPC/util/GlobalEnvManager.php +++ b/src/SPC/util/GlobalEnvManager.php @@ -40,7 +40,12 @@ public static function init(): void if (is_unix()) { self::addPathIfNotExists(BUILD_BIN_PATH); self::addPathIfNotExists(PKG_ROOT_PATH . '/bin'); - self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . '/pkgconfig'); + $pkgConfigPath = getenv('PKG_CONFIG_PATH'); + if ($pkgConfigPath !== false) { + self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . "/pkgconfig:{$pkgConfigPath}"); + } else { + self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . '/pkgconfig'); + } } $ini = self::readIniFile(); diff --git a/src/SPC/util/SPCConfigUtil.php b/src/SPC/util/SPCConfigUtil.php index d19cc8583..a74d6a24c 100644 --- a/src/SPC/util/SPCConfigUtil.php +++ b/src/SPC/util/SPCConfigUtil.php @@ -6,6 +6,8 @@ use SPC\builder\BuilderBase; use SPC\builder\BuilderProvider; +use SPC\builder\Extension; +use SPC\builder\LibraryBase; use SPC\exception\WrongUsageException; use SPC\store\Config; use Symfony\Component\Console\Input\ArgvInput; @@ -52,6 +54,9 @@ public function __construct(?BuilderBase $builder = null, array $options = []) */ public function config(array $extensions = [], array $libraries = [], bool $include_suggest_ext = false, bool $include_suggest_lib = false): array { + logger()->debug('config extensions: ' . implode(',', $extensions)); + logger()->debug('config libs: ' . implode(',', $libraries)); + logger()->debug('config suggest for [ext, lib]: ' . ($include_suggest_ext ? 'true' : 'false') . ',' . ($include_suggest_lib ? 'true' : 'false')); $extra_exts = []; foreach ($extensions as $ext) { $extra_exts = array_merge($extra_exts, Config::getExt($ext, 'ext-suggests', [])); @@ -87,7 +92,7 @@ public function config(array $extensions = [], array $libraries = [], bool $incl if (SPCTarget::getTargetOS() === 'Darwin') { $libs .= " {$this->getFrameworksString($extensions)}"; } - if ($this->builder->hasCpp()) { + if ($this->hasCpp($extensions, $libraries)) { $libcpp = SPCTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; } @@ -123,6 +128,84 @@ public function config(array $extensions = [], array $libraries = [], bool $incl ]; } + /** + * [Helper function] + * Get configuration for a specific extension(s) dependencies. + * + * @param Extension|Extension[] $extension Extension instance or list + * @param bool $include_suggest_ext Whether to include suggested extensions + * @param bool $include_suggest_lib Whether to include suggested libraries + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getExtensionConfig(array|Extension $extension, bool $include_suggest_ext = false, bool $include_suggest_lib = false): array + { + if (!is_array($extension)) { + $extension = [$extension]; + } + $libs = array_map(fn ($y) => $y->getName(), array_merge(...array_map(fn ($x) => $x->getLibraryDependencies(true), $extension))); + return $this->config( + extensions: array_map(fn ($x) => $x->getName(), $extension), + libraries: $libs, + include_suggest_ext: $include_suggest_ext ?: $this->builder?->getOption('with-suggested-exts') ?? false, + include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, + ); + } + + /** + * [Helper function] + * Get configuration for a specific library(s) dependencies. + * + * @param LibraryBase|LibraryBase[] $lib Library instance or list + * @param bool $include_suggest_lib Whether to include suggested libraries + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getLibraryConfig(array|LibraryBase $lib, bool $include_suggest_lib = false): array + { + if (!is_array($lib)) { + $lib = [$lib]; + } + $save_no_php = $this->no_php; + $this->no_php = true; + $save_libs_only_deps = $this->libs_only_deps; + $this->libs_only_deps = true; + $ret = $this->config( + libraries: array_map(fn ($x) => $x->getName(), $lib), + include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false, + ); + $this->no_php = $save_no_php; + $this->libs_only_deps = $save_libs_only_deps; + return $ret; + } + + private function hasCpp(array $extensions, array $libraries): bool + { + // judge cpp-extension + $builderExtNames = array_keys($this->builder->getExts(false)); + $exts = array_unique([...$builderExtNames, ...$extensions]); + + foreach ($exts as $ext) { + if (Config::getExt($ext, 'cpp-extension', false) === true) { + return true; + } + } + $builderLibNames = array_keys($this->builder->getLibs()); + $libs = array_unique([...$builderLibNames, ...$libraries]); + foreach ($libs as $lib) { + if (Config::getLib($lib, 'cpp-library', false) === true) { + return true; + } + } + return false; + } + private function getIncludesString(array $libraries): string { $base = BUILD_INCLUDE_PATH; diff --git a/src/SPC/util/executor/UnixAutoconfExecutor.php b/src/SPC/util/executor/UnixAutoconfExecutor.php index 075d7fd32..e04fe4f9c 100644 --- a/src/SPC/util/executor/UnixAutoconfExecutor.php +++ b/src/SPC/util/executor/UnixAutoconfExecutor.php @@ -50,18 +50,22 @@ public function getConfigureArgsString(): string * @param bool $with_clean Whether to clean before building * @param array $after_env_vars Environment variables postfix */ - public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = []): static + public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = [], ?string $dir = null): static { - return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars) { + return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars, $dir) { + $shell = $this->shell; + if ($dir) { + $shell = $shell->cd($dir); + } if ($with_clean) { - $this->shell->exec('make clean'); + $shell->exec('make clean'); } $after_env_vars_str = $after_env_vars !== [] ? shell()->setEnv($after_env_vars)->getEnvString() : ''; - $this->shell->exec("make -j{$this->library->getBuilder()->concurrency} {$target} {$after_env_vars_str}"); + $shell->exec("make -j{$this->library->getBuilder()->concurrency} {$target} {$after_env_vars_str}"); if ($with_install !== false) { - $this->shell->exec("make {$with_install}"); + $shell->exec("make {$with_install}"); } - return $this->shell; + return $shell; }); } @@ -111,6 +115,12 @@ public function removeConfigureArgs(...$args): static return $this; } + public function setEnv(array $env): static + { + $this->shell->setEnv($env); + return $this; + } + public function appendEnv(array $env): static { $this->shell->appendEnv($env); diff --git a/src/SPC/util/executor/UnixCMakeExecutor.php b/src/SPC/util/executor/UnixCMakeExecutor.php index 3e29addf6..eceab9014 100644 --- a/src/SPC/util/executor/UnixCMakeExecutor.php +++ b/src/SPC/util/executor/UnixCMakeExecutor.php @@ -205,7 +205,7 @@ private function makeCmakeToolchainFile(): string SET(CMAKE_INSTALL_LIBDIR "lib") set(PKG_CONFIG_EXECUTABLE "{$pkgConfigExecutable}") -list(APPEND PKG_CONFIG_EXECUTABLE "--static") +set(PKG_CONFIG_ARGN "--static" CACHE STRING "Extra arguments for pkg-config" FORCE) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) diff --git a/src/SPC/util/shell/UnixShell.php b/src/SPC/util/shell/UnixShell.php index 75a0be3d6..2fdc1b077 100644 --- a/src/SPC/util/shell/UnixShell.php +++ b/src/SPC/util/shell/UnixShell.php @@ -8,6 +8,7 @@ use SPC\builder\linux\library\LinuxLibraryBase; use SPC\builder\macos\library\MacOSLibraryBase; use SPC\exception\SPCInternalException; +use SPC\util\SPCTarget; use ZM\Logger\ConsoleColor; /** @@ -28,6 +29,7 @@ public function __construct(?bool $debug = null) public function exec(string $cmd): static { + $cmd = clean_spaces($cmd); /* @phpstan-ignore-next-line */ logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); $original_command = $cmd; @@ -48,7 +50,7 @@ public function initializeEnv(BSDLibraryBase|LinuxLibraryBase|MacOSLibraryBase $ 'CFLAGS' => $library->getLibExtraCFlags(), 'CXXFLAGS' => $library->getLibExtraCXXFlags(), 'LDFLAGS' => $library->getLibExtraLdFlags(), - 'LIBS' => $library->getLibExtraLibs(), + 'LIBS' => $library->getLibExtraLibs() . SPCTarget::getRuntimeLibs(), ]); return $this; } diff --git a/src/globals/extra/snmp-ext-config-old.m4 b/src/globals/extra/snmp-ext-config-old.m4 new file mode 100644 index 000000000..22cab40ad --- /dev/null +++ b/src/globals/extra/snmp-ext-config-old.m4 @@ -0,0 +1,92 @@ +PHP_ARG_WITH([snmp], + [for SNMP support], + [AS_HELP_STRING([[--with-snmp[=DIR]]], + [Include SNMP support. Use PKG_CONFIG_PATH (or SNMP_CFLAGS and SNMP_LIBS) + environment variables, or alternatively the optional DIR argument to + customize where to look for the net-snmp-config utility of the NET-SNMP + library.])]) + +if test "$PHP_SNMP" != "no"; then + snmp_found=no + AS_VAR_IF([PHP_SNMP], [yes], + [PKG_CHECK_MODULES([SNMP], [netsnmp >= 5.3], [snmp_found=yes], [:])]) + + AS_VAR_IF([snmp_found], [no], [ + AS_VAR_IF([PHP_SNMP], [yes], + [AC_PATH_PROG([SNMP_CONFIG], [net-snmp-config],, [/usr/local/bin:$PATH])], + [SNMP_CONFIG="$PHP_SNMP/bin/net-snmp-config"]) + + AS_IF([test ! -x "$SNMP_CONFIG"], + [AC_MSG_ERROR(m4_text_wrap([ + Could not find net-snmp-config binary. Please check your net-snmp + installation. + ]))]) + + snmp_version=$($SNMP_CONFIG --version) + AS_VERSION_COMPARE([$snmp_version], [5.3], + [AC_MSG_ERROR(m4_text_wrap([ + Net-SNMP version 5.3 or greater required (detected $snmp_version). + ]))]) + + SNMP_PREFIX=$($SNMP_CONFIG --prefix) + SNMP_CFLAGS="-I${SNMP_PREFIX}/include" + SNMP_LIBS=$($SNMP_CONFIG --netsnmp-libs) + SNMP_LIBS="$SNMP_LIBS $($SNMP_CONFIG --external-libs)" + + AS_IF([test -z "$SNMP_LIBS" || test -z "$SNMP_PREFIX"], + [AC_MSG_ERROR(m4_text_wrap([ + Could not find the required paths. Please check your net-snmp + installation. + ]))]) + ]) + + PHP_EVAL_INCLINE([$SNMP_CFLAGS]) + PHP_EVAL_LIBLINE([$SNMP_LIBS], [SNMP_SHARED_LIBADD]) + SNMP_LIBNAME=netsnmp + + dnl Test build. + PHP_CHECK_LIBRARY([$SNMP_LIBNAME], [init_snmp], + [AC_DEFINE([HAVE_SNMP], [1], + [Define to 1 if the PHP extension 'snmp' is available.])], + [AC_MSG_FAILURE([SNMP sanity check failed.])], + [$SNMP_SHARED_LIBADD]) + + dnl Check whether shutdown_snmp_logging() exists. + PHP_CHECK_LIBRARY([$SNMP_LIBNAME], [shutdown_snmp_logging], + [AC_DEFINE([HAVE_SHUTDOWN_SNMP_LOGGING], [1], + [Define to 1 if SNMP library has the 'shutdown_snmp_logging' function.])], + [], + [$SNMP_SHARED_LIBADD]) + + CFLAGS_SAVE=$CFLAGS + LIBS_SAVE=$LIBS + CFLAGS="$CFLAGS $SNMP_CFLAGS" + LIBS="$LIBS $SNMP_LIBS" + + AC_CHECK_DECL([usmHMAC192SHA256AuthProtocol], + [AC_DEFINE([HAVE_SNMP_SHA256], [1], + [Define to 1 if SNMP library has the 'usmHMAC192SHA256AuthProtocol' + array.])], + [], + [ + #include + #include + ]) + + AC_CHECK_DECL([usmHMAC384SHA512AuthProtocol], + [AC_DEFINE([HAVE_SNMP_SHA512], [1], + [Define to 1 if SNMP library has the 'usmHMAC384SHA512AuthProtocol' + array.])], + [], + [ + #include + #include + ]) + + CFLAGS=$CFLAGS_SAVE + LIBS=$LIBS_SAVE + + PHP_NEW_EXTENSION([snmp], [snmp.c], [$ext_shared]) + PHP_ADD_EXTENSION_DEP(snmp, spl) + PHP_SUBST([SNMP_SHARED_LIBADD]) +fi diff --git a/src/globals/functions.php b/src/globals/functions.php index 711279600..c8c6a8d0f 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -245,6 +245,23 @@ function clean_spaces(string $string): string return trim(preg_replace('/\s+/', ' ', $string)); } +/** + * Deduplicate flags in a string. Only the last occurence of each flag will be kept. + * E.g. `-lintl -lstdc++ -lphp -lstdc++` becomes `-lintl -lphp -lstdc++` + * + * @param string $flags the string containing flags to deduplicate + * @return string the deduplicated string with no duplicate flags + */ +function deduplicate_flags(string $flags): string +{ + $tokens = preg_split('/\s+/', trim($flags)); + + // Reverse, unique, reverse back - keeps last occurrence of duplicates + $deduplicated = array_reverse(array_unique(array_reverse($tokens))); + + return implode(' ', $deduplicated); +} + /** * Register a callback function to handle keyboard interrupts (Ctrl+C). * @@ -283,3 +300,20 @@ function strip_ansi_colors(string $text): string // Including color codes, cursor control, clear screen and other control sequences return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', $text); } + +/** + * Convert to a real path for display purposes, used in docker volumes. + */ +function get_display_path(string $path): string +{ + $deploy_root = getenv('SPC_FIX_DEPLOY_ROOT'); + if ($deploy_root === false) { + return $path; + } + $cwd = WORKING_DIR; + // replace build root with deploy root, only if path starts with build root + if (str_starts_with($path, $cwd)) { + return $deploy_root . substr($path, strlen($cwd)); + } + throw new WrongUsageException("Cannot convert path: {$path}"); +} diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 8358d26fe..1d31d5202 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,29 +13,29 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - // '8.1', - // '8.2', - // '8.3', + '8.1', + '8.2', + '8.3', '8.4', - // '8.5', + '8.5', // 'git', ]; -// test os (macos-13, macos-14, macos-15, ubuntu-latest, windows-latest are available) +// test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available) $test_os = [ - 'macos-13', // bin/spc for x86_64 - // 'macos-14', // bin/spc for arm64 + 'macos-15-intel', // bin/spc for x86_64 'macos-15', // bin/spc for arm64 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - // 'ubuntu-24.04', // bin/spc for x86_64 + 'ubuntu-24.04', // bin/spc for x86_64 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 - // 'ubuntu-24.04-arm', // bin/spc for arm64 - // 'windows-latest', // .\bin\spc.ps1 + 'ubuntu-24.04-arm', // bin/spc for arm64 + // 'windows-2022', // .\bin\spc.ps1 + // 'windows-2025', ]; // whether enable thread safe -$zts = true; +$zts = false; $no_strip = false; @@ -50,19 +50,19 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'readline', - 'Windows' => 'bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,ftp,iconv,xml,mbstring,mbregex,mysqlnd,openssl,pdo,pdo_mysql,pdo_sqlite,phar,session,simplexml,soap,sockets,sqlite3,tokenizer,xmlwriter,xmlreader,zlib,zip', + 'Linux', 'Darwin' => 'maxminddb', + 'Windows' => 'bcmath', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => 'zip', + 'Linux' => '', 'Darwin' => '', 'Windows' => '', }; // If you want to test lib-suggests for all extensions and libraries, set it to true. -$with_suggested_libs = true; +$with_suggested_libs = false; // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { @@ -74,7 +74,7 @@ // You can use `common`, `bulk`, `minimal` or `none`. // note: combination is only available for *nix platform. Windows must use `none` combination $base_combination = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'none', + 'Linux', 'Darwin' => 'minimal', 'Windows' => 'none', }; @@ -156,17 +156,13 @@ function quote2(string $param): string switch ($argv[2] ?? null) { case 'ubuntu-22.04': case 'ubuntu-22.04-arm': + case 'macos-15': + case 'macos-15-intel': $shared_cmd = ' --build-shared=' . quote2($shared_extensions) . ' '; break; case 'ubuntu-24.04': case 'ubuntu-24.04-arm': break; - case 'macos-13': - case 'macos-14': - case 'macos-15': - $shared_cmd = ' --build-shared=' . quote2($shared_extensions) . ' '; - $no_strip = true; - break; default: $shared_cmd = ''; break; @@ -213,7 +209,7 @@ function quote2(string $param): string passthru($prefix . $down_cmd, $retcode); break; case 'build_cmd': - passthru($prefix . $build_cmd . ' --build-cli --build-micro', $retcode); + passthru($prefix . $build_cmd . ' --build-cli --build-micro --build-cgi', $retcode); break; case 'build_embed_cmd': if ($frankenphp) { diff --git a/tests/SPC/builder/BuilderTest.php b/tests/SPC/builder/BuilderTest.php index d28bd4d33..b4b6258fd 100644 --- a/tests/SPC/builder/BuilderTest.php +++ b/tests/SPC/builder/BuilderTest.php @@ -62,12 +62,6 @@ public function testGetExtAndGetExts() $this->assertInstanceOf(Extension::class, $this->builder->getExt('mbregex')); } - public function testHasCpp() - { - // mbregex doesn't have cpp - $this->assertFalse($this->builder->hasCpp()); - } - public function testMakeExtensionArgs() { $this->assertStringContainsString('--enable-mbstring', $this->builder->makeStaticExtensionArgs()); diff --git a/tests/SPC/util/ConfigValidatorTest.php b/tests/SPC/util/ConfigValidatorTest.php index f132636fd..aba611a42 100644 --- a/tests/SPC/util/ConfigValidatorTest.php +++ b/tests/SPC/util/ConfigValidatorTest.php @@ -50,7 +50,6 @@ public function testValidateSourceGood(): void 'filename' => 'test.tar.gz', 'path' => 'test/path', 'provide-pre-built' => true, - 'prefer-stable' => false, 'license' => [ 'type' => 'file', 'path' => 'LICENSE', diff --git a/tests/SPC/util/SPCConfigUtilTest.php b/tests/SPC/util/SPCConfigUtilTest.php index 92353824c..c4b1427a2 100644 --- a/tests/SPC/util/SPCConfigUtilTest.php +++ b/tests/SPC/util/SPCConfigUtilTest.php @@ -44,6 +44,9 @@ public function testConstruct(): void public function testConfig(): void { + if (PHP_OS_FAMILY !== 'Linux') { + $this->markTestSkipped('SPCConfigUtil tests are only applicable on Linux.'); + } // normal $result = (new SPCConfigUtil())->config(['bcmath']); $this->assertStringContainsString(BUILD_ROOT_PATH . '/include', $result['cflags']);