From 1468bb99f0d367c6473f8fc46a500d50654684d8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 14 Jun 2025 02:06:12 +0800 Subject: [PATCH 01/48] Add commit tests {craft} extensions: curl,ast sapi: cli,micro {/craft} --- .github/workflows/commit-tests.yml | 128 +++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 .github/workflows/commit-tests.yml diff --git a/.github/workflows/commit-tests.yml b/.github/workflows/commit-tests.yml new file mode 100644 index 00000000..a99d8631 --- /dev/null +++ b/.github/workflows/commit-tests.yml @@ -0,0 +1,128 @@ +name: Single Test +on: + push: + +permissions: read-all + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + parse-commit: + runs-on: ubuntu-latest + outputs: + skip_craft: ${{ steps.parse_commit.outputs.skip_craft }} + craft: ${{ steps.parse_commit.outputs.craft }} + skip_bash: ${{ steps.parse_commit.outputs.skip_bash }} + bash: ${{ steps.parse_commit.outputs.bash }} + build_os: ${{ steps.parse_commit.outputs.build_os }} + spc_prefix: ${{ steps.parse_commit.outputs.spc_prefix }} + steps: + - name: "Parse commit message" + id: parse_commit + run: | + # parse the commit message, see if it has {craft} and {/craft} tags + COMMIT_MESSAGE=$(git log -1 --pretty=%B) + # judge it, it it's not exist, then skip this test + if [[ "$COMMIT_MESSAGE" != *"{craft}"* ]] || [[ "$COMMIT_MESSAGE" != *"{/craft}"* ]]; then + echo "No {craft} tags found in commit message. Skipping test." + echo "skip_craft=yes" >> $GITHUB_OUTPUT + exit 0 + else + echo "\e[32mCraft tags found in commit message.\e[0m" + # get the craft content + CRAFT_CONTENT=$(echo "$COMMIT_MESSAGE" | sed -nz 's/.*{craft}\(.*\){\/craft}.*/\1/p') + echo "Craft content: $CRAFT_CONTENT" + # set the output variable + echo "craft=$CRAFT_CONTENT" >> $GITHUB_OUTPUT + fi + + # parse the bash test script from the commit message + if [[ "$COMMIT_MESSAGE" != *"{bash}"* ]] || [[ "$COMMIT_MESSAGE" != *"{/bash}"* ]]; then + echo "No {bash} tags found in commit message. Skipping bash test." + echo "skip_bash=yes" >> $GITHUB_OUTPUT + else + echo "\e[32mBash tags found in commit message.\e[0m" + # get the bash content + BASH_CONTENT=$(echo "$COMMIT_MESSAGE" | sed -nz 's/.*{bash}\(.*\){\/bash}.*/\1/p') + echo "Bash content: $BASH_CONTENT" + # set the output variable + echo "bash=$BASH_CONTENT" >> $GITHUB_OUTPUT + fi + + # parse spc_prefix from commit message, e.g. [spc_prefix:bin/spc-gnu-docker], default: bin/spc + if [[ "$COMMIT_MESSAGE" =~ \[spc_prefix:([^\]]+)\] ]]; then + SPC_PREFIX=${BASH_REMATCH[1]} + echo "SPC prefix found: $SPC_PREFIX" + else + SPC_PREFIX="bin/spc" + echo "No SPC prefix found, using default: $SPC_PREFIX" + fi + echo "spc_prefix=$SPC_PREFIX" >> $GITHUB_OUTPUT + + # parse build_os from commit message, e.g. [build_os:ubuntu-latest], default: ubuntu-latest + if [[ "$COMMIT_MESSAGE" =~ \[build_os:([^\]]+)\] ]]; then + BUILD_OS=${BASH_REMATCH[1]} + echo "Build OS found: $BUILD_OS" + else + BUILD_OS="ubuntu-latest" + echo "No Build OS found, using default: $BUILD_OS" + fi + echo "build_os=$BUILD_OS" >> $GITHUB_OUTPUT + + craft-test: + needs: parse-commit + if: needs.parse-commit.outputs.skip_craft != 'yes' + runs-on: ${{ needs.parse-commit.outputs.build_os }} + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: curl, openssl, mbstring + ini-values: memory_limit=-1 + tools: composer + + - name: "Install Dependencies" + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + - name: "Doctor" + run: ${{ needs.parse-commit.outputs.spc_prefix }} doctor --auto-fix --debug + + - name: "Run Craft Test" + run: | + echo "Running craft test with content:" + echo "${{ needs.parse-commit.outputs.craft }}" + echo "${{ needs.parse-commit.outputs.craft }}" > craft.yml + ${{ needs.parse-commit.outputs.spc_prefix }} craft --debug + + bash-test: + needs: parse-commit + if: needs.parse-commit.outputs.skip_bash != 'yes' + runs-on: ${{ needs.parse-commit.outputs.build_os }} + steps: + - name: "Checkout" + uses: actions/checkout@v4 + + - name: "Setup PHP" + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: curl, openssl, mbstring + ini-values: memory_limit=-1 + tools: composer + + - name: "Install Dependencies" + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + - name: "Doctor" + run: ${{ needs.parse-commit.outputs.spc_prefix }} doctor --auto-fix --debug + + - name: "Run Bash Test" + run: | + echo "Running bash test with content:" + echo "${{ needs.parse-commit.outputs.bash }}" + echo "${{ needs.parse-commit.outputs.bash }}" | bash From 45ec0cef242a8baea6fbd7564596b9446e1bc426 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 14 Jun 2025 02:20:52 +0800 Subject: [PATCH 02/48] Add checkout {craft} extensions: curl,ast sapi: cli,micro {/craft} --- .github/workflows/commit-tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/commit-tests.yml b/.github/workflows/commit-tests.yml index a99d8631..45a5c391 100644 --- a/.github/workflows/commit-tests.yml +++ b/.github/workflows/commit-tests.yml @@ -18,6 +18,9 @@ jobs: build_os: ${{ steps.parse_commit.outputs.build_os }} spc_prefix: ${{ steps.parse_commit.outputs.spc_prefix }} steps: + - name: "Checkout" + uses: actions/checkout@v4 + - name: "Parse commit message" id: parse_commit run: | From 3a0d21eb44d2a9a79fd81fe44eec4d55f204f51c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 14 Jun 2025 02:27:48 +0800 Subject: [PATCH 03/48] Support multi-line {craft} extensions: curl,ast sapi: cli,micro {/craft} --- .github/workflows/commit-tests.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/commit-tests.yml b/.github/workflows/commit-tests.yml index 45a5c391..8878fac1 100644 --- a/.github/workflows/commit-tests.yml +++ b/.github/workflows/commit-tests.yml @@ -32,12 +32,14 @@ jobs: echo "skip_craft=yes" >> $GITHUB_OUTPUT exit 0 else - echo "\e[32mCraft tags found in commit message.\e[0m" + echo -e "\e[32mCraft tags found in commit message.\e[0m" # get the craft content CRAFT_CONTENT=$(echo "$COMMIT_MESSAGE" | sed -nz 's/.*{craft}\(.*\){\/craft}.*/\1/p') echo "Craft content: $CRAFT_CONTENT" # set the output variable - echo "craft=$CRAFT_CONTENT" >> $GITHUB_OUTPUT + echo "craft<> $GITHUB_OUTPUT + echo "$CRAFT_CONTENT" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT fi # parse the bash test script from the commit message @@ -45,12 +47,14 @@ jobs: echo "No {bash} tags found in commit message. Skipping bash test." echo "skip_bash=yes" >> $GITHUB_OUTPUT else - echo "\e[32mBash tags found in commit message.\e[0m" + echo -e "\e[32mBash tags found in commit message.\e[0m" # get the bash content BASH_CONTENT=$(echo "$COMMIT_MESSAGE" | sed -nz 's/.*{bash}\(.*\){\/bash}.*/\1/p') echo "Bash content: $BASH_CONTENT" # set the output variable - echo "bash=$BASH_CONTENT" >> $GITHUB_OUTPUT + echo "bash<> $GITHUB_OUTPUT + echo "$BASH_CONTENT" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT fi # parse spc_prefix from commit message, e.g. [spc_prefix:bin/spc-gnu-docker], default: bin/spc From fe455bf9013a69566113b0dde62c1b9054c22d31 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 14 Jun 2025 02:30:00 +0800 Subject: [PATCH 04/48] Fix shared-extensions as optional {craft} extensions: curl,ast sapi: cli,micro {/craft} --- src/SPC/command/CraftCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/command/CraftCommand.php b/src/SPC/command/CraftCommand.php index 8d3cc2be..9a2ac441 100644 --- a/src/SPC/command/CraftCommand.php +++ b/src/SPC/command/CraftCommand.php @@ -49,7 +49,7 @@ class CraftCommand extends BaseCommand } $static_extensions = implode(',', $craft['extensions']); - $shared_extensions = implode(',', $craft['shared-extensions']); + $shared_extensions = implode(',', $craft['shared-extensions'] ?? []); $libs = implode(',', $craft['libs']); // init log From 6253b7a912d2fef9cc679cbea098f56dd754eb4a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 14 Jun 2025 02:35:37 +0800 Subject: [PATCH 05/48] Next pr, I won't run --- docs/deps-craft-yml.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/deps-craft-yml.md b/docs/deps-craft-yml.md index 9e660be8..1b824e0b 100644 --- a/docs/deps-craft-yml.md +++ b/docs/deps-craft-yml.md @@ -42,6 +42,9 @@ build-options: # Set micro SAPI as win32 mode, without this, micro SAPI will be compiled as a console application (only for Windows, default: false) enable-micro-win32: false +# Build options for shared extensions (same as `build-shared` command options, all options are optional) +shared-extensions: [ ] + # Download options download-options: # Use custom url for specified sources, format: "{source-name}:{url}" (e.g. "php-src:https://example.com/php-8.4.0.tar.gz") From e5cd3adf97ccf74983dc925e367a3dd096553364 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 14 Jun 2025 02:36:47 +0800 Subject: [PATCH 06/48] Next pr, I won't run --- .github/workflows/commit-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/commit-tests.yml b/.github/workflows/commit-tests.yml index 8878fac1..3b5dbcc7 100644 --- a/.github/workflows/commit-tests.yml +++ b/.github/workflows/commit-tests.yml @@ -10,6 +10,8 @@ env: jobs: parse-commit: runs-on: ubuntu-latest + # if the commit message does not contain {craft} or {bash} tags, then skip the craft-test and bash-test jobs + if: github.event_name == 'push' && (contains(github.event.head_commit.message, '{craft}') || contains(github.event.head_commit.message, '{bash}')) outputs: skip_craft: ${{ steps.parse_commit.outputs.skip_craft }} craft: ${{ steps.parse_commit.outputs.craft }} From 0e88cdb25876b6a274ebaa2a0ff9fe47c768b707 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 14 Jun 2025 13:49:29 +0800 Subject: [PATCH 07/48] Add shared extension parser {craft} extensions: bcmath shared-extensions: xdebug,swoole sapi: cli {/craft} [spc_prefix:bin/spc-gnu-docker] --- docs/deps-craft-yml.md | 2 +- src/SPC/util/ConfigValidator.php | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/deps-craft-yml.md b/docs/deps-craft-yml.md index 1b824e0b..0ee0e11d 100644 --- a/docs/deps-craft-yml.md +++ b/docs/deps-craft-yml.md @@ -42,7 +42,7 @@ build-options: # Set micro SAPI as win32 mode, without this, micro SAPI will be compiled as a console application (only for Windows, default: false) enable-micro-win32: false -# Build options for shared extensions (same as `build-shared` command options, all options are optional) +# Build options for shared extensions (list or comma-separated are both accepted) shared-extensions: [ ] # Download options diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php index eae2ef2b..dbc8ce23 100644 --- a/src/SPC/util/ConfigValidator.php +++ b/src/SPC/util/ConfigValidator.php @@ -164,6 +164,12 @@ class ConfigValidator if (is_string($craft['extensions'])) { $craft['extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['extensions']))); } + if (!isset($craft['shared-extensions'])) { + $craft['shared-extensions'] = []; + } + if (is_string($craft['shared-extensions'] ?? [])) { + $craft['shared-extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['shared-extensions']))); + } // check libs if (isset($craft['libs']) && is_string($craft['libs'])) { $craft['libs'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['libs']))); From 3a64feefd0ade9c3ce611055add2282fe3f99c48 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 11:34:51 +0800 Subject: [PATCH 08/48] Change test strategy for commit tests --- .github/pull_request_template.md | 12 ++- .github/workflows/commit-tests.yml | 137 ------------------------- .github/workflows/ext-matrix-tests.yml | 21 +++- 3 files changed, 24 insertions(+), 146 deletions(-) delete mode 100644 .github/workflows/commit-tests.yml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index a7166ec2..07e7017b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,7 +7,11 @@ > If your PR involves the changes mentioned below and completed the action, please tick the corresponding option. > If a modification is not involved, please skip it directly. -- [ ] If you modified `*.php`, run `composer cs-fix` at local machine. -- [ ] If it's an extension or dependency update, make sure adding related extensions in `src/global/test-extensions.php`. -- [ ] If you changed the behavior of static-php-cli, update docs in `./docs/`. -- [ ] If you updated `config/xxx.json` content, run `bin/spc dev:sort-config xxx`. +- If you modified `*.php` or `*.json`, run them locally to ensure your changes are valid: + - [ ] `PHP_CS_FIXER_IGNORE_ENV=1 composer cs-fix` + - [ ] `composer analyse` + - [ ] `composer test` + - [ ] `bin/spc dev:sort-config` +- If it's an extension or dependency update, please ensure the following: + - [ ] Add your test combination to `src/globals/test-extensions.php`. + - [ ] If adding new or fixing bugs, add commit message containing `fix` or `test` to trigger full test suite. diff --git a/.github/workflows/commit-tests.yml b/.github/workflows/commit-tests.yml deleted file mode 100644 index 3b5dbcc7..00000000 --- a/.github/workflows/commit-tests.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: Single Test -on: - push: - -permissions: read-all - -env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - parse-commit: - runs-on: ubuntu-latest - # if the commit message does not contain {craft} or {bash} tags, then skip the craft-test and bash-test jobs - if: github.event_name == 'push' && (contains(github.event.head_commit.message, '{craft}') || contains(github.event.head_commit.message, '{bash}')) - outputs: - skip_craft: ${{ steps.parse_commit.outputs.skip_craft }} - craft: ${{ steps.parse_commit.outputs.craft }} - skip_bash: ${{ steps.parse_commit.outputs.skip_bash }} - bash: ${{ steps.parse_commit.outputs.bash }} - build_os: ${{ steps.parse_commit.outputs.build_os }} - spc_prefix: ${{ steps.parse_commit.outputs.spc_prefix }} - steps: - - name: "Checkout" - uses: actions/checkout@v4 - - - name: "Parse commit message" - id: parse_commit - run: | - # parse the commit message, see if it has {craft} and {/craft} tags - COMMIT_MESSAGE=$(git log -1 --pretty=%B) - # judge it, it it's not exist, then skip this test - if [[ "$COMMIT_MESSAGE" != *"{craft}"* ]] || [[ "$COMMIT_MESSAGE" != *"{/craft}"* ]]; then - echo "No {craft} tags found in commit message. Skipping test." - echo "skip_craft=yes" >> $GITHUB_OUTPUT - exit 0 - else - echo -e "\e[32mCraft tags found in commit message.\e[0m" - # get the craft content - CRAFT_CONTENT=$(echo "$COMMIT_MESSAGE" | sed -nz 's/.*{craft}\(.*\){\/craft}.*/\1/p') - echo "Craft content: $CRAFT_CONTENT" - # set the output variable - echo "craft<> $GITHUB_OUTPUT - echo "$CRAFT_CONTENT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - fi - - # parse the bash test script from the commit message - if [[ "$COMMIT_MESSAGE" != *"{bash}"* ]] || [[ "$COMMIT_MESSAGE" != *"{/bash}"* ]]; then - echo "No {bash} tags found in commit message. Skipping bash test." - echo "skip_bash=yes" >> $GITHUB_OUTPUT - else - echo -e "\e[32mBash tags found in commit message.\e[0m" - # get the bash content - BASH_CONTENT=$(echo "$COMMIT_MESSAGE" | sed -nz 's/.*{bash}\(.*\){\/bash}.*/\1/p') - echo "Bash content: $BASH_CONTENT" - # set the output variable - echo "bash<> $GITHUB_OUTPUT - echo "$BASH_CONTENT" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - fi - - # parse spc_prefix from commit message, e.g. [spc_prefix:bin/spc-gnu-docker], default: bin/spc - if [[ "$COMMIT_MESSAGE" =~ \[spc_prefix:([^\]]+)\] ]]; then - SPC_PREFIX=${BASH_REMATCH[1]} - echo "SPC prefix found: $SPC_PREFIX" - else - SPC_PREFIX="bin/spc" - echo "No SPC prefix found, using default: $SPC_PREFIX" - fi - echo "spc_prefix=$SPC_PREFIX" >> $GITHUB_OUTPUT - - # parse build_os from commit message, e.g. [build_os:ubuntu-latest], default: ubuntu-latest - if [[ "$COMMIT_MESSAGE" =~ \[build_os:([^\]]+)\] ]]; then - BUILD_OS=${BASH_REMATCH[1]} - echo "Build OS found: $BUILD_OS" - else - BUILD_OS="ubuntu-latest" - echo "No Build OS found, using default: $BUILD_OS" - fi - echo "build_os=$BUILD_OS" >> $GITHUB_OUTPUT - - craft-test: - needs: parse-commit - if: needs.parse-commit.outputs.skip_craft != 'yes' - runs-on: ${{ needs.parse-commit.outputs.build_os }} - steps: - - name: "Checkout" - uses: actions/checkout@v4 - - - name: "Setup PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - extensions: curl, openssl, mbstring - ini-values: memory_limit=-1 - tools: composer - - - name: "Install Dependencies" - run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - - - name: "Doctor" - run: ${{ needs.parse-commit.outputs.spc_prefix }} doctor --auto-fix --debug - - - name: "Run Craft Test" - run: | - echo "Running craft test with content:" - echo "${{ needs.parse-commit.outputs.craft }}" - echo "${{ needs.parse-commit.outputs.craft }}" > craft.yml - ${{ needs.parse-commit.outputs.spc_prefix }} craft --debug - - bash-test: - needs: parse-commit - if: needs.parse-commit.outputs.skip_bash != 'yes' - runs-on: ${{ needs.parse-commit.outputs.build_os }} - steps: - - name: "Checkout" - uses: actions/checkout@v4 - - - name: "Setup PHP" - uses: shivammathur/setup-php@v2 - with: - php-version: '8.4' - extensions: curl, openssl, mbstring - ini-values: memory_limit=-1 - tools: composer - - - name: "Install Dependencies" - run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - - - name: "Doctor" - run: ${{ needs.parse-commit.outputs.spc_prefix }} doctor --auto-fix --debug - - - name: "Run Bash Test" - run: | - echo "Running bash test with content:" - echo "${{ needs.parse-commit.outputs.bash }}" - echo "${{ needs.parse-commit.outputs.bash }}" | bash diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml index da30d008..dc5de0d9 100644 --- a/.github/workflows/ext-matrix-tests.yml +++ b/.github/workflows/ext-matrix-tests.yml @@ -1,16 +1,27 @@ name: "Extension matrix tests" + +# Only run if: +# - the workflow is manually triggered +# - or a pull request is made to the main branch that modifies this workflow file or commit message contains "fix" or "test" on: - workflow_dispatch: - pull_request: - branches: [ "main" ] - paths: - - '.github/workflows/ext-matrix-tests.yml' + workflow_dispatch: + pull_request: + branches: [ "main" ] + paths: + - '.github/workflows/ext-matrix-tests.yml' + - 'src/**' + - 'config/**' + - 'bin/**' + - 'composer.json' + - 'box.json' + - '.php-cs-fixer.php' jobs: test: name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})" runs-on: ${{ matrix.operating-system }} + if: contains(github.event.head_commit.message, 'fix') || contains(github.event.head_commit.message, 'test') strategy: fail-fast: false matrix: From 68548cf248ac2f4011c9da0fbe0afbfe3eab7dd0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 11:42:32 +0800 Subject: [PATCH 09/48] Wrap it to test test test --- .github/workflows/ext-matrix-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml index dc5de0d9..ae7388a9 100644 --- a/.github/workflows/ext-matrix-tests.yml +++ b/.github/workflows/ext-matrix-tests.yml @@ -21,7 +21,7 @@ jobs: test: name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})" runs-on: ${{ matrix.operating-system }} - if: contains(github.event.head_commit.message, 'fix') || contains(github.event.head_commit.message, 'test') + if: ${{ contains(github.event.head_commit.message, 'fix') || contains(github.event.head_commit.message, 'test') }} strategy: fail-fast: false matrix: From 2bfc8e92ef5a5805cadaf0e8d9109b3bb6a39235 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 11:49:42 +0800 Subject: [PATCH 10/48] Test test --- .github/workflows/ext-matrix-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml index ae7388a9..7efb6e3b 100644 --- a/.github/workflows/ext-matrix-tests.yml +++ b/.github/workflows/ext-matrix-tests.yml @@ -21,7 +21,7 @@ jobs: test: name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})" runs-on: ${{ matrix.operating-system }} - if: ${{ contains(github.event.head_commit.message, 'fix') || contains(github.event.head_commit.message, 'test') }} + if: github.event_name == 'push' && (contains(github.event.head_commit.message, 'fix') || contains(github.event.head_commit.message, 'test')) strategy: fail-fast: false matrix: From cb0a90d1d9b403114c2cda9243b6981faa55ea2d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 14 Jun 2025 16:09:48 +0800 Subject: [PATCH 11/48] Add source hash comparator & refactor download lock --- tests/bootstrap.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index d573204b..c2fb6eee 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,5 +2,7 @@ declare(strict_types=1); +putenv('SPC_IGNORE_BAD_HASH=yes'); + require_once __DIR__ . '/../src/globals/internal-env.php'; require_once __DIR__ . '/mock/SPC_store.php'; From 57b22782d35b35b72ad574c4b045d966462e4287 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Jun 2025 12:34:18 +0800 Subject: [PATCH 12/48] Define env in phpunit.xml --- tests/bootstrap.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/bootstrap.php b/tests/bootstrap.php index c2fb6eee..d573204b 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,7 +2,5 @@ declare(strict_types=1); -putenv('SPC_IGNORE_BAD_HASH=yes'); - require_once __DIR__ . '/../src/globals/internal-env.php'; require_once __DIR__ . '/mock/SPC_store.php'; From 5cb107b844322de5cf1b953cf6ab230900dcdf5c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 11:53:17 +0800 Subject: [PATCH 13/48] Test test --- .github/workflows/ext-matrix-tests.yml | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml index 7efb6e3b..ae164094 100644 --- a/.github/workflows/ext-matrix-tests.yml +++ b/.github/workflows/ext-matrix-tests.yml @@ -1,27 +1,14 @@ name: "Extension matrix tests" - -# Only run if: -# - the workflow is manually triggered -# - or a pull request is made to the main branch that modifies this workflow file or commit message contains "fix" or "test" on: - workflow_dispatch: - pull_request: - branches: [ "main" ] - paths: - - '.github/workflows/ext-matrix-tests.yml' - - 'src/**' - - 'config/**' - - 'bin/**' - - 'composer.json' - - 'box.json' - - '.php-cs-fixer.php' + workflow_dispatch: + push: jobs: test: name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})" runs-on: ${{ matrix.operating-system }} - if: github.event_name == 'push' && (contains(github.event.head_commit.message, 'fix') || contains(github.event.head_commit.message, 'test')) + if: contains(github.event.head_commit.message, 'extension test') strategy: fail-fast: false matrix: From 7057a135cf7db382b225fd2139ecb8a64912622d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 11:54:10 +0800 Subject: [PATCH 14/48] Trigger extension test --- .github/workflows/ext-matrix-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml index ae164094..265c8b2e 100644 --- a/.github/workflows/ext-matrix-tests.yml +++ b/.github/workflows/ext-matrix-tests.yml @@ -1,4 +1,4 @@ -name: "Extension matrix tests" +name: "Extension Matrix Tests" on: workflow_dispatch: From 71783088c05335e58249079f23cf49d19ed90f90 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 11:55:13 +0800 Subject: [PATCH 15/48] Use new trigger test message --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 07e7017b..c1d4eb16 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,4 +14,4 @@ - [ ] `bin/spc dev:sort-config` - If it's an extension or dependency update, please ensure the following: - [ ] Add your test combination to `src/globals/test-extensions.php`. - - [ ] If adding new or fixing bugs, add commit message containing `fix` or `test` to trigger full test suite. + - [ ] If adding new or fixing bugs, add commit message containing `extension test` to trigger full test suite. From c1870af1b1a24be25d8f982cc7d9961aa8cce582 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 10:48:09 +0700 Subject: [PATCH 16/48] add frankenphp sapi --- config/env.ini | 3 + src/SPC/builder/linux/LinuxBuilder.php | 5 ++ src/SPC/builder/traits/UnixGoCheckTrait.php | 83 +++++++++++++++++++++ src/SPC/builder/unix/UnixBuilderBase.php | 30 ++++++++ src/SPC/command/BuildPHPCommand.php | 5 +- src/SPC/doctor/AsCheckItem.php | 3 +- src/SPC/doctor/item/BSDToolCheckList.php | 6 ++ src/SPC/doctor/item/LinuxToolCheckList.php | 8 ++ src/SPC/doctor/item/MacOSToolCheckList.php | 8 ++ src/globals/defines.php | 3 +- 10 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 src/SPC/builder/traits/UnixGoCheckTrait.php diff --git a/config/env.ini b/config/env.ini index ba8652d5..112d0187 100644 --- a/config/env.ini +++ b/config/env.ini @@ -42,6 +42,9 @@ SPC_CONCURRENCY=${CPU_COUNT} 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/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli" + ; EXTENSION_DIR where the built php will look for extension when a .ini instructs to load them ; only useful for builds targeting not pure-static linking ; default paths diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 315d7df9..fc131214 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -114,6 +114,7 @@ class LinuxBuilder extends UnixBuilderBase $enable_fpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; $enable_micro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; $enable_embed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; + $enable_frankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; $mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : ''; // prepare build php envs @@ -175,6 +176,10 @@ class LinuxBuilder extends UnixBuilderBase } $this->buildEmbed(); } + if ($enable_frankenphp) { + logger()->info('building frankenphp'); + $this->buildFrankenphp(); + } } public function testPHP(int $build_target = BUILD_TARGET_NONE) diff --git a/src/SPC/builder/traits/UnixGoCheckTrait.php b/src/SPC/builder/traits/UnixGoCheckTrait.php new file mode 100644 index 00000000..12e3d005 --- /dev/null +++ b/src/SPC/builder/traits/UnixGoCheckTrait.php @@ -0,0 +1,83 @@ +findCommand('go', $paths) === null) { + $this->installGo(); + } + + $gobin = getenv('GOBIN') ?: (getenv('HOME') . '/go/bin'); + putenv("GOBIN={$gobin}"); + + $paths[] = $gobin; + + if ($this->findCommand('xcaddy', $paths) === null) { + shell(true)->exec('go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest'); + } + + return CheckResult::ok(); + } + + private function installGo(): bool + { + $prefix = ''; + if (get_current_user() !== 'root') { + $prefix = 'sudo '; + logger()->warning('Current user is not root, using sudo for running command'); + } + + $arch = php_uname('m'); + $go_arch = match ($arch) { + 'x86_64' => 'amd64', + 'aarch64' => 'arm64', + default => $arch + }; + $os = strtolower(PHP_OS_FAMILY); + + $go_version = '1.24.4'; + $go_filename = "go{$go_version}.{$os}-{$go_arch}.tar.gz"; + $go_url = "https://go.dev/dl/{$go_filename}"; + + logger()->info("Downloading Go {$go_version} for {$go_arch}"); + + try { + // Download Go binary + Downloader::downloadFile('go', $go_url, $go_filename); + + // Extract the tarball + FileSystem::extractSource('go', SPC_SOURCE_ARCHIVE, DOWNLOAD_PATH . "/{$go_filename}"); + + // Move to /usr/local/go + logger()->info('Installing Go to /usr/local/go'); + shell()->exec("{$prefix}rm -rf /usr/local/go"); + shell()->exec("{$prefix}mv " . SOURCE_PATH . '/go /usr/local/'); + + if (!str_contains(getenv('PATH'), '/usr/local/go/bin')) { + logger()->info('Adding Go to PATH'); + shell()->exec("{$prefix}echo 'export PATH=\$PATH:/usr/local/go/bin' >> /etc/profile"); + putenv('PATH=' . getenv('PATH') . ':/usr/local/go/bin'); + } + + logger()->info('Go has been installed successfully'); + return true; + } catch (RuntimeException $e) { + logger()->error('Failed to install Go: ' . $e->getMessage()); + return false; + } + } +} diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 0d367de1..19880bf1 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -277,4 +277,34 @@ abstract class UnixBuilderBase extends BuilderBase FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str); } } + + protected function buildFrankenphp(): void + { + $path = getenv('PATH'); + $xcaddyPath = getenv('GOBIN') ?: (getenv('HOME') . '/go/bin'); + if (!str_contains($path, $xcaddyPath)) { + $path = $path . ':' . $xcaddyPath; + } + $path = BUILD_BIN_PATH . ':' . $path; + f_putenv("PATH={$path}"); + + $brotliLibs = $this->getLib('brotli') !== null ? '-lbrotlienc -lbrotlidec -lbrotlicommon' : ''; + $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; + $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; + + $env = [ + 'CGO_ENABLED' => '1', + 'CGO_CFLAGS' => '$(php-config --includes) -I$(php-config --include-dir)/..', + 'CGO_LDFLAGS' => "$(php-config --ldflags) $(php-config --libs) {$brotliLibs} -lwatcher-c -lphp -Wl,-rpath=" . BUILD_LIB_PATH, + 'XCADDY_GO_BUILD_FLAGS' => "-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" . $nobrotli . $nowatcher, + ]; + shell()->cd(BUILD_BIN_PATH) + ->setEnv($env) + ->exec( + 'xcaddy build ' . + '--output frankenphp ' . + '--with github.com/dunglas/frankenphp/caddy ' . + getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES') + ); + } } diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 353605b1..0ef9dab3 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -33,6 +33,7 @@ class BuildPHPCommand extends BuildCommand $this->addOption('build-cli', null, null, 'Build cli SAPI'); $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-all', null, null, 'Build all SAPI'); $this->addOption('no-strip', null, null, 'build without strip, in order to debug and load external extensions'); $this->addOption('disable-opcache-jit', null, null, 'disable opcache jit'); @@ -83,7 +84,8 @@ class BuildPHPCommand extends BuildCommand $this->output->writeln("\t--build-micro\tBuild phpmicro SAPI"); $this->output->writeln("\t--build-fpm\tBuild php-fpm SAPI"); $this->output->writeln("\t--build-embed\tBuild embed SAPI/libphp"); - $this->output->writeln("\t--build-all\tBuild all SAPI: cli, micro, fpm, embed"); + $this->output->writeln("\t--build-frankenphp\tBuild FrankenPHP SAPI/libphp"); + $this->output->writeln("\t--build-all\tBuild all SAPI: cli, micro, fpm, embed, frankenphp"); return static::FAILURE; } if ($rule === BUILD_TARGET_ALL) { @@ -304,6 +306,7 @@ class BuildPHPCommand extends BuildCommand $rule |= ($this->getOption('build-micro') ? BUILD_TARGET_MICRO : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-fpm') ? BUILD_TARGET_FPM : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-embed') || !empty($shared_extensions) ? BUILD_TARGET_EMBED : BUILD_TARGET_NONE); + $rule |= ($this->getOption('build-frankenphp') || !empty($shared_extensions) ? BUILD_TARGET_FRANKENPHP : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE); return $rule; } diff --git a/src/SPC/doctor/AsCheckItem.php b/src/SPC/doctor/AsCheckItem.php index f64d914b..0417bcfa 100644 --- a/src/SPC/doctor/AsCheckItem.php +++ b/src/SPC/doctor/AsCheckItem.php @@ -14,5 +14,6 @@ class AsCheckItem public ?string $limit_os = null, public int $level = 100, public bool $manual = false, - ) {} + ) { + } } diff --git a/src/SPC/doctor/item/BSDToolCheckList.php b/src/SPC/doctor/item/BSDToolCheckList.php index 2505227b..97f0ccf9 100644 --- a/src/SPC/doctor/item/BSDToolCheckList.php +++ b/src/SPC/doctor/item/BSDToolCheckList.php @@ -47,6 +47,12 @@ class BSDToolCheckList return CheckResult::ok(); } + #[AsCheckItem('if xcaddy is installed', limit_os: 'BSD')] + public function checkXcaddy(): ?CheckResult + { + return $this->checkGoAndXcaddy(); + } + #[AsFixItem('build-tools-bsd')] public function fixBuildTools(array $missing): bool { diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php index 56235b0c..07f6b5fb 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace SPC\doctor\item; use SPC\builder\linux\SystemUtil; +use SPC\builder\traits\UnixGoCheckTrait; use SPC\builder\traits\UnixSystemUtilTrait; use SPC\doctor\AsCheckItem; use SPC\doctor\AsFixItem; @@ -14,6 +15,7 @@ use SPC\exception\RuntimeException; class LinuxToolCheckList { use UnixSystemUtilTrait; + use UnixGoCheckTrait; public const TOOLS_ALPINE = [ 'make', 'bison', 'flex', @@ -87,6 +89,12 @@ class LinuxToolCheckList return CheckResult::ok(); } + #[AsCheckItem('if xcaddy is installed', limit_os: 'Linux')] + public function checkXcaddy(): ?CheckResult + { + return $this->checkGoAndXcaddy(); + } + #[AsCheckItem('if cmake version >= 3.18', limit_os: 'Linux')] public function checkCMakeVersion(): ?CheckResult { diff --git a/src/SPC/doctor/item/MacOSToolCheckList.php b/src/SPC/doctor/item/MacOSToolCheckList.php index b4043a1d..57ba8157 100644 --- a/src/SPC/doctor/item/MacOSToolCheckList.php +++ b/src/SPC/doctor/item/MacOSToolCheckList.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace SPC\doctor\item; +use SPC\builder\traits\UnixGoCheckTrait; use SPC\builder\traits\UnixSystemUtilTrait; use SPC\doctor\AsCheckItem; use SPC\doctor\AsFixItem; @@ -13,6 +14,7 @@ use SPC\exception\RuntimeException; class MacOSToolCheckList { use UnixSystemUtilTrait; + use UnixGoCheckTrait; /** @var string[] MacOS 环境下编译依赖的命令 */ public const REQUIRED_COMMANDS = [ @@ -34,6 +36,12 @@ class MacOSToolCheckList 'glibtoolize', ]; + #[AsCheckItem('if xcaddy is installed', limit_os: 'Darwin')] + public function checkXcaddy(): ?CheckResult + { + return $this->checkGoAndXcaddy(); + } + #[AsCheckItem('if homebrew has installed', limit_os: 'Darwin', level: 998)] public function checkBrew(): ?CheckResult { diff --git a/src/globals/defines.php b/src/globals/defines.php index eab2fcc4..aebd4d5f 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -62,7 +62,8 @@ const BUILD_TARGET_CLI = 1; // build cli const BUILD_TARGET_MICRO = 2; // build micro const BUILD_TARGET_FPM = 4; // build fpm const BUILD_TARGET_EMBED = 8; // build embed -const BUILD_TARGET_ALL = 15; // build all +const BUILD_TARGET_FRANKENPHP = BUILD_TARGET_EMBED | 16; // build frankenphp +const BUILD_TARGET_ALL = BUILD_TARGET_CLI | BUILD_TARGET_MICRO | BUILD_TARGET_FPM | BUILD_TARGET_EMBED | BUILD_TARGET_FRANKENPHP; // build all // doctor error fix policy const FIX_POLICY_DIE = 1; // die directly From f64eb0dea5e3ee609de49d7127aa5c5c0297c752 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 11:20:05 +0700 Subject: [PATCH 17/48] build for bsd and macos too --- src/SPC/builder/freebsd/BSDBuilder.php | 5 ++++ src/SPC/builder/linux/LinuxBuilder.php | 32 +++++++++++++------------- src/SPC/builder/macos/MacOSBuilder.php | 5 ++++ src/SPC/command/BuildPHPCommand.php | 2 +- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/SPC/builder/freebsd/BSDBuilder.php b/src/SPC/builder/freebsd/BSDBuilder.php index 04fd43d3..65ebea57 100644 --- a/src/SPC/builder/freebsd/BSDBuilder.php +++ b/src/SPC/builder/freebsd/BSDBuilder.php @@ -96,6 +96,7 @@ class BSDBuilder extends UnixBuilderBase $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; + $enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; shell()->cd(SOURCE_PATH . '/php-src') ->exec( @@ -143,6 +144,10 @@ class BSDBuilder extends UnixBuilderBase } $this->buildEmbed(); } + if ($enableFrankenphp) { + logger()->info('building frankenphp'); + $this->buildFrankenphp(); + } } public function testPHP(int $build_target = BUILD_TARGET_NONE) diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index fc131214..addd2f57 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -110,11 +110,11 @@ class LinuxBuilder extends UnixBuilderBase $config_file_scan_dir = $this->getOption('with-config-file-scan-dir', false) ? ('--with-config-file-scan-dir=' . $this->getOption('with-config-file-scan-dir') . ' ') : ''; - $enable_cli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI; - $enable_fpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM; - $enable_micro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO; - $enable_embed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED; - $enable_frankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; + $enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI; + $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; + $enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; $mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : ''; // prepare build php envs @@ -126,7 +126,7 @@ class LinuxBuilder extends UnixBuilderBase ]); // process micro upx patch if micro sapi enabled - if ($enable_micro) { + if ($enableMicro) { if (version_compare($this->getMicroVersion(), '0.2.0') < 0) { // for phpmicro 0.1.x $this->processMicroUPXLegacy(); @@ -138,10 +138,10 @@ class LinuxBuilder extends UnixBuilderBase shell()->cd(SOURCE_PATH . '/php-src') ->exec( getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' . - ($enable_cli ? '--enable-cli ' : '--disable-cli ') . - ($enable_fpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') . - ($enable_embed ? "--enable-embed={$embed_type} " : '--disable-embed ') . - ($enable_micro ? '--enable-micro=all-static ' : '--disable-micro ') . + ($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 ') . $config_file_path . $config_file_scan_dir . $disable_jit . @@ -157,26 +157,26 @@ class LinuxBuilder extends UnixBuilderBase $this->cleanMake(); - if ($enable_cli) { + if ($enableCli) { logger()->info('building cli'); $this->buildCli(); } - if ($enable_fpm) { + if ($enableFpm) { logger()->info('building fpm'); $this->buildFpm(); } - if ($enable_micro) { + if ($enableMicro) { logger()->info('building micro'); $this->buildMicro(); } - if ($enable_embed) { + if ($enableEmbed) { logger()->info('building embed'); - if ($enable_micro) { + if ($enableMicro) { FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); } $this->buildEmbed(); } - if ($enable_frankenphp) { + if ($enableFrankenphp) { logger()->info('building frankenphp'); $this->buildFrankenphp(); } diff --git a/src/SPC/builder/macos/MacOSBuilder.php b/src/SPC/builder/macos/MacOSBuilder.php index a0522120..153da08c 100644 --- a/src/SPC/builder/macos/MacOSBuilder.php +++ b/src/SPC/builder/macos/MacOSBuilder.php @@ -122,6 +122,7 @@ class MacOSBuilder extends UnixBuilderBase $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; + $enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; // prepare build php envs $mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : ''; @@ -180,6 +181,10 @@ class MacOSBuilder extends UnixBuilderBase } $this->buildEmbed(); } + if ($enableFrankenphp) { + logger()->info('building frankenphp'); + $this->buildFrankenphp(); + } } public function testPHP(int $build_target = BUILD_TARGET_NONE) diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 0ef9dab3..96a93653 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -306,7 +306,7 @@ class BuildPHPCommand extends BuildCommand $rule |= ($this->getOption('build-micro') ? BUILD_TARGET_MICRO : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-fpm') ? BUILD_TARGET_FPM : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-embed') || !empty($shared_extensions) ? BUILD_TARGET_EMBED : BUILD_TARGET_NONE); - $rule |= ($this->getOption('build-frankenphp') || !empty($shared_extensions) ? BUILD_TARGET_FRANKENPHP : BUILD_TARGET_NONE); + $rule |= ($this->getOption('build-frankenphp') ? BUILD_TARGET_FRANKENPHP : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE); return $rule; } From c1e68323c70e412c286650fe76d0c7e6eddd85de Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 11:21:50 +0700 Subject: [PATCH 18/48] cs fix --- src/SPC/doctor/AsCheckItem.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SPC/doctor/AsCheckItem.php b/src/SPC/doctor/AsCheckItem.php index 0417bcfa..f64d914b 100644 --- a/src/SPC/doctor/AsCheckItem.php +++ b/src/SPC/doctor/AsCheckItem.php @@ -14,6 +14,5 @@ class AsCheckItem public ?string $limit_os = null, public int $level = 100, public bool $manual = false, - ) { - } + ) {} } From 92338d478e470d34d1fc63b5039513799f650b2e Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 11:30:04 +0700 Subject: [PATCH 19/48] don't bake the rpath in, otherwise we might run into issues when loading frankenphp after compiling a different version --- src/SPC/builder/unix/UnixBuilderBase.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 19880bf1..666c09c8 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -289,14 +289,16 @@ abstract class UnixBuilderBase extends BuilderBase f_putenv("PATH={$path}"); $brotliLibs = $this->getLib('brotli') !== null ? '-lbrotlienc -lbrotlidec -lbrotlicommon' : ''; + $watcherLibs = $this->getLib('brotli') !== null ? '-lwatcher-c' : ''; $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; $env = [ 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => '$(php-config --includes) -I$(php-config --include-dir)/..', - 'CGO_LDFLAGS' => "$(php-config --ldflags) $(php-config --libs) {$brotliLibs} -lwatcher-c -lphp -Wl,-rpath=" . BUILD_LIB_PATH, + 'CGO_LDFLAGS' => "$(php-config --ldflags) $(php-config --libs) {$brotliLibs} {$watcherLibs} -lphp", 'XCADDY_GO_BUILD_FLAGS' => "-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" . $nobrotli . $nowatcher, + 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; shell()->cd(BUILD_BIN_PATH) ->setEnv($env) From c46f8513dd15b8bda79ae7651417ff46dfcace73 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 11:34:05 +0700 Subject: [PATCH 20/48] watcher... --- src/SPC/builder/unix/UnixBuilderBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 666c09c8..4ef2502b 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -289,7 +289,7 @@ abstract class UnixBuilderBase extends BuilderBase f_putenv("PATH={$path}"); $brotliLibs = $this->getLib('brotli') !== null ? '-lbrotlienc -lbrotlidec -lbrotlicommon' : ''; - $watcherLibs = $this->getLib('brotli') !== null ? '-lwatcher-c' : ''; + $watcherLibs = $this->getLib('watcher') !== null ? '-lwatcher-c' : ''; $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; From abf3bfb98e8acf1ed6b5cd8afc2d4236697ad641 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 11:36:58 +0700 Subject: [PATCH 21/48] suggest watcher --- config/lib.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config/lib.json b/config/lib.json index dc792c3b..ba76f4a9 100644 --- a/config/lib.json +++ b/config/lib.json @@ -12,8 +12,12 @@ "lib-base", "micro" ], + "lib-suggests-unix": [ + "watcher" + ], "lib-suggests-linux": [ - "libacl" + "libacl", + "watcher" ] }, "micro": { From dca43d6d8d3d7a24cc85d9f9cdded0a0590ec327 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 11:39:22 +0700 Subject: [PATCH 22/48] nicer escaping --- src/SPC/builder/unix/UnixBuilderBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 4ef2502b..39ea69ec 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -297,7 +297,7 @@ abstract class UnixBuilderBase extends BuilderBase 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => '$(php-config --includes) -I$(php-config --include-dir)/..', 'CGO_LDFLAGS' => "$(php-config --ldflags) $(php-config --libs) {$brotliLibs} {$watcherLibs} -lphp", - 'XCADDY_GO_BUILD_FLAGS' => "-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" . $nobrotli . $nowatcher, + 'XCADDY_GO_BUILD_FLAGS' => "-ldflags='-w -s' -tags=nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; shell()->cd(BUILD_BIN_PATH) From d635b10e248a9c6adc1ec36d70c970333ac4e807 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 11:47:05 +0700 Subject: [PATCH 23/48] specify system gcc to build xcaddy in spc-gnu-docker --- bin/spc-gnu-docker | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/spc-gnu-docker b/bin/spc-gnu-docker index b69e0710..9583b4dd 100755 --- a/bin/spc-gnu-docker +++ b/bin/spc-gnu-docker @@ -94,7 +94,7 @@ ENV PATH="/app/bin:/cmake/bin:$PATH" ENV SPC_LIBC=glibc ADD ./config/env.ini /app/config/env.ini -RUN bin/spc doctor --auto-fix --debug +RUN CC=gcc bin/spc doctor --auto-fix --debug RUN curl -o make.tgz -fsSL https://ftp.gnu.org/gnu/make/make-4.4.tar.gz && \ tar -zxvf make.tgz && \ From d094824d76132a6de6c23a82f02416aafa63a492 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 11:54:03 +0700 Subject: [PATCH 24/48] --with github.com/dunglas/caddy-cbrotli requires brotli --- config/env.ini | 2 +- src/SPC/builder/unix/UnixBuilderBase.php | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/config/env.ini b/config/env.ini index 112d0187..03e07b47 100644 --- a/config/env.ini +++ b/config/env.ini @@ -43,7 +43,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/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" ; EXTENSION_DIR where the built php will look for extension when a .ini instructs to load them ; only useful for builds targeting not pure-static linking diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 39ea69ec..ac493b73 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -292,6 +292,10 @@ abstract class UnixBuilderBase extends BuilderBase $watcherLibs = $this->getLib('watcher') !== null ? '-lwatcher-c' : ''; $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; + $xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES'); + if ($this->getLib('brotli') !== null && !str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) { + $xcaddyModules .= ' --with github.com/dunglas/caddy-cbrotli'; + } $env = [ 'CGO_ENABLED' => '1', @@ -302,11 +306,6 @@ abstract class UnixBuilderBase extends BuilderBase ]; shell()->cd(BUILD_BIN_PATH) ->setEnv($env) - ->exec( - 'xcaddy build ' . - '--output frankenphp ' . - '--with github.com/dunglas/frankenphp/caddy ' . - getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES') - ); + ->exec('xcaddy build --output frankenphp --with github.com/dunglas/frankenphp/caddy ' . $xcaddyModules); } } From e71f76288b21ace475673ad29313e63fabf7b335 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 12:02:37 +0700 Subject: [PATCH 25/48] support building static frankenphp --- src/SPC/builder/unix/UnixBuilderBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index ac493b73..92ee2518 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -300,7 +300,7 @@ abstract class UnixBuilderBase extends BuilderBase $env = [ 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => '$(php-config --includes) -I$(php-config --include-dir)/..', - 'CGO_LDFLAGS' => "$(php-config --ldflags) $(php-config --libs) {$brotliLibs} {$watcherLibs} -lphp", + 'CGO_LDFLAGS' => '$(php-config --ldflags) -L' . BUILD_LIB_PATH . " $(php-config --libs) {$brotliLibs} {$watcherLibs} -lphp -lrt", 'XCADDY_GO_BUILD_FLAGS' => "-ldflags='-w -s' -tags=nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; From f37c863092804eb5fc57528e23fa0ca9f4948802 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 12:04:01 +0700 Subject: [PATCH 26/48] only needed on linux --- src/SPC/builder/unix/UnixBuilderBase.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 92ee2518..83de392c 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -296,11 +296,12 @@ abstract class UnixBuilderBase extends BuilderBase if ($this->getLib('brotli') !== null && !str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) { $xcaddyModules .= ' --with github.com/dunglas/caddy-cbrotli'; } + $lrt = PHP_OS_FAMILY === 'Linux' ? '-lrt' : ''; $env = [ 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => '$(php-config --includes) -I$(php-config --include-dir)/..', - 'CGO_LDFLAGS' => '$(php-config --ldflags) -L' . BUILD_LIB_PATH . " $(php-config --libs) {$brotliLibs} {$watcherLibs} -lphp -lrt", + 'CGO_LDFLAGS' => '$(php-config --ldflags) -L' . BUILD_LIB_PATH . " $(php-config --libs) {$brotliLibs} {$watcherLibs} -lphp {$lrt}", 'XCADDY_GO_BUILD_FLAGS' => "-ldflags='-w -s' -tags=nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; From d58534b07d70e4d388f39961d66ec03e470882fe Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 12:19:33 +0700 Subject: [PATCH 27/48] add support for frankenphp directory from file system, instead of pulling latest xcaddy module --- config/env.ini | 2 +- src/SPC/builder/unix/UnixBuilderBase.php | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/config/env.ini b/config/env.ini index 03e07b47..7acc8730 100644 --- a/config/env.ini +++ b/config/env.ini @@ -43,7 +43,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/mercure/caddy --with github.com/dunglas/vulcain/caddy" +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" ; EXTENSION_DIR where the built php will look for extension when a .ini instructs to load them ; only useful for builds targeting not pure-static linking diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 83de392c..758efaa6 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -293,8 +293,13 @@ abstract class UnixBuilderBase extends BuilderBase $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; $xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES'); - if ($this->getLib('brotli') !== null && !str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) { - $xcaddyModules .= ' --with github.com/dunglas/caddy-cbrotli'; + // 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; + } + if ($this->getLib('brotli') === null && str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) { + logger()->warning('caddy-cbrotli module is enabled, but broli library is not built. Disabling caddy-cbrotli.'); + $xcaddyModules = str_replace('--with github.com/dunglas/caddy-cbrotli', '', $xcaddyModules); } $lrt = PHP_OS_FAMILY === 'Linux' ? '-lrt' : ''; @@ -307,6 +312,6 @@ abstract class UnixBuilderBase extends BuilderBase ]; shell()->cd(BUILD_BIN_PATH) ->setEnv($env) - ->exec('xcaddy build --output frankenphp --with github.com/dunglas/frankenphp/caddy ' . $xcaddyModules); + ->exec('xcaddy build --output frankenphp ' . $xcaddyModules); } } From 82ee6f0dee53815f09f258c7f4c92feaae26a428 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 12:35:48 +0700 Subject: [PATCH 28/48] allow specifying if we want to build embed shared or static --- src/SPC/command/BuildPHPCommand.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 96a93653..72d6a686 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -32,7 +32,7 @@ class BuildPHPCommand extends BuildCommand $this->addOption('build-micro', null, null, 'Build micro SAPI'); $this->addOption('build-cli', null, null, 'Build cli SAPI'); $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-embed', null, InputOption::VALUE_OPTIONAL, 'Build embed SAPI (not available on Windows)'); $this->addOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)'); $this->addOption('build-all', null, null, 'Build all SAPI'); $this->addOption('no-strip', null, null, 'build without strip, in order to debug and load external extensions'); @@ -305,7 +305,17 @@ class BuildPHPCommand extends BuildCommand $rule |= ($this->getOption('build-cli') ? BUILD_TARGET_CLI : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-micro') ? BUILD_TARGET_MICRO : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-fpm') ? BUILD_TARGET_FPM : BUILD_TARGET_NONE); - $rule |= ($this->getOption('build-embed') || !empty($shared_extensions) ? BUILD_TARGET_EMBED : BUILD_TARGET_NONE); + $embed = $this->getOption('build-embed'); + if (!$embed && !empty($shared_extensions)) { + $embed = true; + } + if ($embed) { + if ($embed === true) { + $embed = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; + } + $rule |= BUILD_TARGET_EMBED; + f_putenv('SPC_CMD_VAR_PHP_EMBED_TYPE=' . ($embed === 'static' ? 'static' : 'shared')); + } $rule |= ($this->getOption('build-frankenphp') ? BUILD_TARGET_FRANKENPHP : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE); return $rule; From a1e76d9d02e928cca769743e8e63828ca475f742 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 12:41:27 +0700 Subject: [PATCH 29/48] remove watcher suggestion --- config/lib.json | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/config/lib.json b/config/lib.json index ba76f4a9..dc792c3b 100644 --- a/config/lib.json +++ b/config/lib.json @@ -12,12 +12,8 @@ "lib-base", "micro" ], - "lib-suggests-unix": [ - "watcher" - ], "lib-suggests-linux": [ - "libacl", - "watcher" + "libacl" ] }, "micro": { From 8c6a708764912a7897607a82021f046b23e8725c Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 12:46:05 +0700 Subject: [PATCH 30/48] ah, the infamous arm64 bug with -fpic vs -fPIC it's a bit slower, but oh well --- config/env.ini | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/env.ini b/config/env.ini index ba8652d5..d6217f5f 100644 --- a/config/env.ini +++ b/config/env.ini @@ -68,8 +68,8 @@ CXX=${SPC_LINUX_DEFAULT_CXX} AR=${SPC_LINUX_DEFAULT_AR} LD=ld.gold ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build -SPC_DEFAULT_C_FLAGS="-fpic -Os" -SPC_DEFAULT_CXX_FLAGS="-fpic -Os" +SPC_DEFAULT_C_FLAGS="-fPIC -Os" +SPC_DEFAULT_CXX_FLAGS="-fPIC -Os" ; extra libs for building php executable, used in `make` command for building php (this value may changed by extension build process, space separated) SPC_EXTRA_LIBS= ; upx executable path @@ -89,7 +89,7 @@ SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; *** default build vars for building php *** ; CFLAGS for configuring php -SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -fpie" +SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -fPIE" ; CPPFLAGS for configuring php SPC_CMD_VAR_PHP_CONFIGURE_CPPFLAGS="-I${BUILD_INCLUDE_PATH}" ; LDFLAGS for configuring php @@ -97,7 +97,7 @@ SPC_CMD_VAR_PHP_CONFIGURE_LDFLAGS="-L${BUILD_LIB_PATH}" ; LIBS for configuring php SPC_CMD_VAR_PHP_CONFIGURE_LIBS="-ldl -lpthread -lm" ; EXTRA_CFLAGS for `make` php -SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fpie ${SPC_DEFAULT_C_FLAGS}" +SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_C_FLAGS}" ; EXTRA_LIBS for `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_LIBS="-ldl -lpthread -lm" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so From b4168d09b562e33138b82f3f9a5a6929b0993c27 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 14:06:36 +0800 Subject: [PATCH 31/48] Add test extensions as trigger --- .github/workflows/ext-matrix-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml index 265c8b2e..1c04f5c9 100644 --- a/.github/workflows/ext-matrix-tests.yml +++ b/.github/workflows/ext-matrix-tests.yml @@ -8,7 +8,7 @@ jobs: test: name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})" runs-on: ${{ matrix.operating-system }} - if: contains(github.event.head_commit.message, 'extension test') + if: contains(github.event.head_commit.message, 'extension test') || contains(github.event.head_commit.message, 'test extensions') strategy: fail-fast: false matrix: From f7a3f80689ce39906f50d021fce62be0260b36ee Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 14:06:59 +0800 Subject: [PATCH 32/48] Add test extensions as trigger --- .github/pull_request_template.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index c1d4eb16..13441a81 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -14,4 +14,4 @@ - [ ] `bin/spc dev:sort-config` - If it's an extension or dependency update, please ensure the following: - [ ] Add your test combination to `src/globals/test-extensions.php`. - - [ ] If adding new or fixing bugs, add commit message containing `extension test` to trigger full test suite. + - [ ] If adding new or fixing bugs, add commit message containing `extension test` or `test extensions` to trigger full test suite. From f10ba862188ecb56885dca84a379500577616907 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 14:18:01 +0700 Subject: [PATCH 33/48] add extension test for frankenphp --- src/globals/test-extensions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index cfde83f6..44243140 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -177,7 +177,7 @@ if ($argv[1] === 'build_cmd' || $argv[1] === 'build_embed_cmd') { $build_cmd .= $no_strip ? '--no-strip ' : ''; $build_cmd .= $upx ? '--with-upx-pack ' : ''; $build_cmd .= $final_libs === '' ? '' : ('--with-libs=' . quote2($final_libs) . ' '); - $build_cmd .= str_starts_with($argv[2], 'windows-') ? '' : '--build-fpm '; + $build_cmd .= str_starts_with($argv[2], 'windows-') ? '' : '--build-fpm --build-frankenphp'; $build_cmd .= '--debug '; } From 65b828c424b87dbc76c76a1acc24c084456ab4be Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 15:50:55 +0700 Subject: [PATCH 34/48] embed version information --- src/SPC/builder/unix/UnixBuilderBase.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 758efaa6..75e3d77d 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -12,6 +12,7 @@ use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; use SPC\store\Config; +use SPC\store\Downloader; use SPC\store\FileSystem; use SPC\util\DependencyUtil; use SPC\util\SPCConfigUtil; @@ -278,6 +279,10 @@ abstract class UnixBuilderBase extends BuilderBase } } + /** + * @throws WrongUsageException + * @throws RuntimeException + */ protected function buildFrankenphp(): void { $path = getenv('PATH'); @@ -302,12 +307,20 @@ abstract class UnixBuilderBase extends BuilderBase $xcaddyModules = str_replace('--with github.com/dunglas/caddy-cbrotli', '', $xcaddyModules); } $lrt = PHP_OS_FAMILY === 'Linux' ? '-lrt' : ''; + $releaseInfo = json_decode(Downloader::curlExec('https://api.github.com/repos/php/frankenphp/releases/latest'), true); + $frankenPhpVersion = $releaseInfo['tag_name']; + $libphpVersion = $this->getPHPVersion(); + $debugFlags = $this->getOption('--with-debug') ? "'-w -s' " : ''; $env = [ 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => '$(php-config --includes) -I$(php-config --include-dir)/..', 'CGO_LDFLAGS' => '$(php-config --ldflags) -L' . BUILD_LIB_PATH . " $(php-config --libs) {$brotliLibs} {$watcherLibs} -lphp {$lrt}", - 'XCADDY_GO_BUILD_FLAGS' => "-ldflags='-w -s' -tags=nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", + 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . + '-ldflags \\"-linkmode=external -extldflags \'-pie\' '. $debugFlags . + '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . + "{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . + "-tags=nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; shell()->cd(BUILD_BIN_PATH) From eee2ff6d614bdaaaccc737203de4b1937ddad626 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Wed, 18 Jun 2025 15:55:14 +0700 Subject: [PATCH 35/48] don't embed minor version when loading libphp.so --- src/SPC/builder/unix/UnixBuilderBase.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 75e3d77d..9801d47b 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -310,6 +310,9 @@ abstract class UnixBuilderBase extends BuilderBase $releaseInfo = json_decode(Downloader::curlExec('https://api.github.com/repos/php/frankenphp/releases/latest'), true); $frankenPhpVersion = $releaseInfo['tag_name']; $libphpVersion = $this->getPHPVersion(); + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { + $libphpVersion = preg_replace('/\.\d$/', '', $libphpVersion); + } $debugFlags = $this->getOption('--with-debug') ? "'-w -s' " : ''; $env = [ From ae569316ff3234aadaa3331e9eccd8b583045120 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 20:54:01 +0800 Subject: [PATCH 36/48] Remove go download from doctor --- src/SPC/builder/traits/UnixGoCheckTrait.php | 83 --------------------- src/SPC/doctor/item/BSDToolCheckList.php | 6 -- src/SPC/doctor/item/LinuxToolCheckList.php | 8 -- src/SPC/doctor/item/MacOSToolCheckList.php | 8 -- 4 files changed, 105 deletions(-) delete mode 100644 src/SPC/builder/traits/UnixGoCheckTrait.php diff --git a/src/SPC/builder/traits/UnixGoCheckTrait.php b/src/SPC/builder/traits/UnixGoCheckTrait.php deleted file mode 100644 index 12e3d005..00000000 --- a/src/SPC/builder/traits/UnixGoCheckTrait.php +++ /dev/null @@ -1,83 +0,0 @@ -findCommand('go', $paths) === null) { - $this->installGo(); - } - - $gobin = getenv('GOBIN') ?: (getenv('HOME') . '/go/bin'); - putenv("GOBIN={$gobin}"); - - $paths[] = $gobin; - - if ($this->findCommand('xcaddy', $paths) === null) { - shell(true)->exec('go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest'); - } - - return CheckResult::ok(); - } - - private function installGo(): bool - { - $prefix = ''; - if (get_current_user() !== 'root') { - $prefix = 'sudo '; - logger()->warning('Current user is not root, using sudo for running command'); - } - - $arch = php_uname('m'); - $go_arch = match ($arch) { - 'x86_64' => 'amd64', - 'aarch64' => 'arm64', - default => $arch - }; - $os = strtolower(PHP_OS_FAMILY); - - $go_version = '1.24.4'; - $go_filename = "go{$go_version}.{$os}-{$go_arch}.tar.gz"; - $go_url = "https://go.dev/dl/{$go_filename}"; - - logger()->info("Downloading Go {$go_version} for {$go_arch}"); - - try { - // Download Go binary - Downloader::downloadFile('go', $go_url, $go_filename); - - // Extract the tarball - FileSystem::extractSource('go', SPC_SOURCE_ARCHIVE, DOWNLOAD_PATH . "/{$go_filename}"); - - // Move to /usr/local/go - logger()->info('Installing Go to /usr/local/go'); - shell()->exec("{$prefix}rm -rf /usr/local/go"); - shell()->exec("{$prefix}mv " . SOURCE_PATH . '/go /usr/local/'); - - if (!str_contains(getenv('PATH'), '/usr/local/go/bin')) { - logger()->info('Adding Go to PATH'); - shell()->exec("{$prefix}echo 'export PATH=\$PATH:/usr/local/go/bin' >> /etc/profile"); - putenv('PATH=' . getenv('PATH') . ':/usr/local/go/bin'); - } - - logger()->info('Go has been installed successfully'); - return true; - } catch (RuntimeException $e) { - logger()->error('Failed to install Go: ' . $e->getMessage()); - return false; - } - } -} diff --git a/src/SPC/doctor/item/BSDToolCheckList.php b/src/SPC/doctor/item/BSDToolCheckList.php index 97f0ccf9..2505227b 100644 --- a/src/SPC/doctor/item/BSDToolCheckList.php +++ b/src/SPC/doctor/item/BSDToolCheckList.php @@ -47,12 +47,6 @@ class BSDToolCheckList return CheckResult::ok(); } - #[AsCheckItem('if xcaddy is installed', limit_os: 'BSD')] - public function checkXcaddy(): ?CheckResult - { - return $this->checkGoAndXcaddy(); - } - #[AsFixItem('build-tools-bsd')] public function fixBuildTools(array $missing): bool { diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php index 07f6b5fb..56235b0c 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace SPC\doctor\item; use SPC\builder\linux\SystemUtil; -use SPC\builder\traits\UnixGoCheckTrait; use SPC\builder\traits\UnixSystemUtilTrait; use SPC\doctor\AsCheckItem; use SPC\doctor\AsFixItem; @@ -15,7 +14,6 @@ use SPC\exception\RuntimeException; class LinuxToolCheckList { use UnixSystemUtilTrait; - use UnixGoCheckTrait; public const TOOLS_ALPINE = [ 'make', 'bison', 'flex', @@ -89,12 +87,6 @@ class LinuxToolCheckList return CheckResult::ok(); } - #[AsCheckItem('if xcaddy is installed', limit_os: 'Linux')] - public function checkXcaddy(): ?CheckResult - { - return $this->checkGoAndXcaddy(); - } - #[AsCheckItem('if cmake version >= 3.18', limit_os: 'Linux')] public function checkCMakeVersion(): ?CheckResult { diff --git a/src/SPC/doctor/item/MacOSToolCheckList.php b/src/SPC/doctor/item/MacOSToolCheckList.php index 57ba8157..b4043a1d 100644 --- a/src/SPC/doctor/item/MacOSToolCheckList.php +++ b/src/SPC/doctor/item/MacOSToolCheckList.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace SPC\doctor\item; -use SPC\builder\traits\UnixGoCheckTrait; use SPC\builder\traits\UnixSystemUtilTrait; use SPC\doctor\AsCheckItem; use SPC\doctor\AsFixItem; @@ -14,7 +13,6 @@ use SPC\exception\RuntimeException; class MacOSToolCheckList { use UnixSystemUtilTrait; - use UnixGoCheckTrait; /** @var string[] MacOS 环境下编译依赖的命令 */ public const REQUIRED_COMMANDS = [ @@ -36,12 +34,6 @@ class MacOSToolCheckList 'glibtoolize', ]; - #[AsCheckItem('if xcaddy is installed', limit_os: 'Darwin')] - public function checkXcaddy(): ?CheckResult - { - return $this->checkGoAndXcaddy(); - } - #[AsCheckItem('if homebrew has installed', limit_os: 'Darwin', level: 998)] public function checkBrew(): ?CheckResult { From 8e2dffc3b5cfcf1587a77fe8136ad6b5ff2551ab Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 20:54:54 +0800 Subject: [PATCH 37/48] Add frankenphp sapi embed build at build command, not constant --- src/SPC/builder/BuilderBase.php | 23 +++++++++++++++++++++++ src/SPC/command/BuildPHPCommand.php | 5 ++++- src/globals/defines.php | 2 +- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index 7fc78e44..fd4788f8 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -404,6 +404,9 @@ abstract class BuilderBase if (($type & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) { $ls[] = 'embed'; } + if (($type & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) { + $ls[] = 'frankenphp'; + } return implode(', ', $ls); } @@ -510,6 +513,26 @@ abstract class BuilderBase } } + public function checkBeforeBuildPHP(int $rule): void + { + if (($rule & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) { + // frankenphp only support linux and macOS + if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { + throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); + } + // frankenphp needs package go-mod-frankenphp installed + $pkg_dir = PKG_ROOT_PATH . '/go-mod-frankenphp-' . arch2gnu(php_uname('m')) . '-' . osfamily2shortname(); + if (!file_exists("{$pkg_dir}/bin/go") || !file_exists("{$pkg_dir}/bin/xcaddy")) { + global $argv; + throw new WrongUsageException("FrankenPHP SAPI requires go-mod-frankenphp package, please install it first: {$argv[0]} install-pkg go-mod-frankenphp"); + } + // frankenphp needs libxml2 libs + if (!$this->getLib('libxml2')) { + throw new WrongUsageException('FrankenPHP SAPI requires libxml2 library, please include `xml` extension in your build.'); + } + } + } + /** * Generate micro extension test php code. */ diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 72d6a686..29cedf8f 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -196,6 +196,9 @@ class BuildPHPCommand extends BuildCommand // validate libs and extensions $builder->validateLibsAndExts(); + // check some things before building all the things + $builder->checkBeforeBuildPHP($rule); + // clean builds and sources if ($this->input->getOption('with-clean')) { logger()->info('Cleaning source and previous build dir...'); @@ -316,7 +319,7 @@ class BuildPHPCommand extends BuildCommand $rule |= BUILD_TARGET_EMBED; f_putenv('SPC_CMD_VAR_PHP_EMBED_TYPE=' . ($embed === 'static' ? 'static' : 'shared')); } - $rule |= ($this->getOption('build-frankenphp') ? BUILD_TARGET_FRANKENPHP : BUILD_TARGET_NONE); + $rule |= ($this->getOption('build-frankenphp') ? (BUILD_TARGET_FRANKENPHP | BUILD_TARGET_EMBED) : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE); return $rule; } diff --git a/src/globals/defines.php b/src/globals/defines.php index aebd4d5f..ab37ace9 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -62,7 +62,7 @@ const BUILD_TARGET_CLI = 1; // build cli const BUILD_TARGET_MICRO = 2; // build micro const BUILD_TARGET_FPM = 4; // build fpm const BUILD_TARGET_EMBED = 8; // build embed -const BUILD_TARGET_FRANKENPHP = BUILD_TARGET_EMBED | 16; // build frankenphp +const BUILD_TARGET_FRANKENPHP = 16; // build frankenphp const BUILD_TARGET_ALL = BUILD_TARGET_CLI | BUILD_TARGET_MICRO | BUILD_TARGET_FPM | BUILD_TARGET_EMBED | BUILD_TARGET_FRANKENPHP; // build all // doctor error fix policy From f709f3bb18406b50ac102326478b99f4e8ef6dd1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 20:55:24 +0800 Subject: [PATCH 38/48] Add custom package downloader and extractor --- src/SPC/store/Downloader.php | 22 ++++++++++++++++++---- src/SPC/store/PackageManager.php | 15 +++++++++++++++ src/SPC/store/pkg/CustomPackage.php | 17 +++++++++++++++++ src/globals/functions.php | 11 +++++++++++ 4 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 src/SPC/store/pkg/CustomPackage.php diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index b0c663d3..e5cc6aae 100644 --- a/src/SPC/store/Downloader.php +++ b/src/SPC/store/Downloader.php @@ -9,6 +9,7 @@ use SPC\exception\DownloaderException; use SPC\exception\FileSystemException; use SPC\exception\RuntimeException; use SPC\exception\WrongUsageException; +use SPC\store\pkg\CustomPackage; use SPC\store\source\CustomSourceBase; /** @@ -385,10 +386,13 @@ class Downloader ]); break; case 'custom': // Custom download method, like API-based download or other - $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'); + $classes = 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); + 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; } } @@ -708,7 +712,6 @@ class Downloader } } // If lock file exists for current arch and glibc target, skip downloading - if (!$force && $download_as === SPC_DOWNLOAD_PRE_BUILT && isset($lock[$lock_name = self::getPreBuiltLockName($name)])) { // lock name with env if ( @@ -719,6 +722,17 @@ class Downloader return true; } } + + // If lock file exists, skip downloading for source mode + if (!$force && $download_as === SPC_DOWNLOAD_PACKAGE && isset($lock[$name])) { + if ( + $lock[$name]['source_type'] === SPC_SOURCE_ARCHIVE && file_exists(DOWNLOAD_PATH . '/' . $lock[$name]['filename']) || + $lock[$name]['source_type'] === SPC_SOURCE_GIT && is_dir(DOWNLOAD_PATH . '/' . $lock[$name]['dirname']) + ) { + logger()->notice("Package [{$name}] already downloaded: " . ($lock[$name]['filename'] ?? $lock[$name]['dirname'])); + return true; + } + } return false; } } diff --git a/src/SPC/store/PackageManager.php b/src/SPC/store/PackageManager.php index ca930228..7e8ae3fd 100644 --- a/src/SPC/store/PackageManager.php +++ b/src/SPC/store/PackageManager.php @@ -6,6 +6,7 @@ namespace SPC\store; use SPC\exception\FileSystemException; use SPC\exception\WrongUsageException; +use SPC\store\pkg\CustomPackage; class PackageManager { @@ -32,6 +33,20 @@ class PackageManager // Download package Downloader::downloadPackage($pkg_name, $config, $force); + if (Config::getPkg($pkg_name)['type'] === 'custom') { + // Custom extract function + $classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'); + foreach ($classes as $class) { + if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) { + $cls = new $class(); + if (in_array($pkg_name, $cls->getSupportName())) { + (new $class())->extract($pkg_name); + break; + } + } + } + return; + } // After download, read lock file name $lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true); $source_type = $lock[$pkg_name]['source_type']; diff --git a/src/SPC/store/pkg/CustomPackage.php b/src/SPC/store/pkg/CustomPackage.php new file mode 100644 index 00000000..89edb17e --- /dev/null +++ b/src/SPC/store/pkg/CustomPackage.php @@ -0,0 +1,17 @@ + 'win', + 'Darwin' => 'macos', + 'Linux' => 'linux', + 'BSD' => 'bsd', + default => throw new WrongUsageException('Not support os: ' . PHP_OS_FAMILY), + }; +} + function shell(?bool $debug = null): UnixShell { /* @noinspection PhpUnhandledExceptionInspection */ From 92284e92c9aaf73a997e2662a02da4f9d7bb6bb6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 20:56:07 +0800 Subject: [PATCH 39/48] Refactor go and frankenphp downloads and builds --- config/pkg.json | 12 +++++ src/SPC/builder/unix/UnixBuilderBase.php | 52 +++++++++++++------ src/SPC/store/pkg/GoModFrankenphp.php | 64 ++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 src/SPC/store/pkg/GoModFrankenphp.php diff --git a/config/pkg.json b/config/pkg.json index 5760c0b1..a2ec8a14 100644 --- a/config/pkg.json +++ b/config/pkg.json @@ -42,5 +42,17 @@ "extract-files": { "upx-*-win64/upx.exe": "{pkg_root_path}/bin/upx.exe" } + }, + "go-mod-frankenphp-x86_64-linux": { + "type": "custom" + }, + "go-mod-frankenphp-aarch64-linux": { + "type": "custom" + }, + "go-mod-frankenphp-x86_64-macos": { + "type": "custom" + }, + "go-mod-frankenphp-aarch64-macos": { + "type": "custom" } } diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 9801d47b..44756955 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -219,6 +219,19 @@ abstract class UnixBuilderBase extends BuilderBase throw new RuntimeException('embed failed sanity check: run failed. Error message: ' . implode("\n", $output)); } } + + // sanity check for frankenphp + if (($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) { + logger()->info('running frankenphp sanity check'); + $frankenphp = BUILD_BIN_PATH . '/frankenphp'; + if (!file_exists($frankenphp)) { + throw new RuntimeException('FrankenPHP binary not found: ' . $frankenphp); + } + [$ret, $output] = shell()->execWithResult($frankenphp . ' -v'); + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { + throw new RuntimeException('FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']'); + } + } } /** @@ -285,16 +298,19 @@ abstract class UnixBuilderBase extends BuilderBase */ protected function buildFrankenphp(): void { - $path = getenv('PATH'); - $xcaddyPath = getenv('GOBIN') ?: (getenv('HOME') . '/go/bin'); - if (!str_contains($path, $xcaddyPath)) { - $path = $path . ':' . $xcaddyPath; - } - $path = BUILD_BIN_PATH . ':' . $path; - f_putenv("PATH={$path}"); + $os = match (PHP_OS_FAMILY) { + 'Linux' => 'linux', + 'Windows' => 'win', + 'Darwin' => 'macos', + 'BSD' => 'freebsd', + default => throw new RuntimeException('Unsupported OS: ' . PHP_OS_FAMILY), + }; + $arch = arch2gnu(php_uname('m')); + + // define executables for go and xcaddy + $go_exec = PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin/go"; + $xcaddy_exec = PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin/xcaddy"; - $brotliLibs = $this->getLib('brotli') !== null ? '-lbrotlienc -lbrotlidec -lbrotlicommon' : ''; - $watcherLibs = $this->getLib('watcher') !== null ? '-lwatcher-c' : ''; $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; $xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES'); @@ -303,7 +319,7 @@ abstract class UnixBuilderBase extends BuilderBase $xcaddyModules = '--with github.com/dunglas/frankenphp ' . $xcaddyModules; } if ($this->getLib('brotli') === null && str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) { - logger()->warning('caddy-cbrotli module is enabled, but broli library is not built. Disabling 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); } $lrt = PHP_OS_FAMILY === 'Linux' ? '-lrt' : ''; @@ -313,14 +329,20 @@ abstract class UnixBuilderBase extends BuilderBase if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { $libphpVersion = preg_replace('/\.\d$/', '', $libphpVersion); } - $debugFlags = $this->getOption('--with-debug') ? "'-w -s' " : ''; + $debugFlags = $this->getOption('--with-debug') ? "'-w -s' " : ''; + + $config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list, with_dependencies: true); $env = [ + 'PATH' => PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin:" . getenv('PATH'), + 'GOROOT' => PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}", + 'GOBIN' => PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin", + 'GOPATH' => PKG_ROOT_PATH . '/go', 'CGO_ENABLED' => '1', - 'CGO_CFLAGS' => '$(php-config --includes) -I$(php-config --include-dir)/..', - 'CGO_LDFLAGS' => '$(php-config --ldflags) -L' . BUILD_LIB_PATH . " $(php-config --libs) {$brotliLibs} {$watcherLibs} -lphp {$lrt}", + 'CGO_CFLAGS' => $config['cflags'], + 'CGO_LDFLAGS' => "{$config['ldflags']} {$config['libs']} {$lrt}", 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . - '-ldflags \\"-linkmode=external -extldflags \'-pie\' '. $debugFlags . + '-ldflags \"-linkmode=external -extldflags \'-pie\' ' . $debugFlags . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . "{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags=nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", @@ -328,6 +350,6 @@ abstract class UnixBuilderBase extends BuilderBase ]; shell()->cd(BUILD_BIN_PATH) ->setEnv($env) - ->exec('xcaddy build --output frankenphp ' . $xcaddyModules); + ->exec("{$xcaddy_exec} build --output frankenphp {$xcaddyModules}"); } } diff --git a/src/SPC/store/pkg/GoModFrankenphp.php b/src/SPC/store/pkg/GoModFrankenphp.php new file mode 100644 index 00000000..bede5d22 --- /dev/null +++ b/src/SPC/store/pkg/GoModFrankenphp.php @@ -0,0 +1,64 @@ + 'amd64', + 'aarch64' => 'arm64', + default => throw new \InvalidArgumentException('Unsupported architecture: ' . $name), + }; + $os = match (explode('-', $name)[4]) { + 'linux' => 'linux', + 'macos' => 'darwin', + default => throw new \InvalidArgumentException('Unsupported OS: ' . $name), + }; + $go_version = '1.24.4'; + $config = [ + 'type' => 'url', + 'url' => "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz", + ]; + Downloader::downloadPackage($name, $config, $force); + } + + public function extract(string $name): void + { + $pkgroot = PKG_ROOT_PATH; + $lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true); + $source_type = $lock[$name]['source_type']; + $filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']); + $extract = $lock[$name]['move_path'] === null ? (PKG_ROOT_PATH . "{$pkgroot}/{$name}") : $lock[$name]['move_path']; + + FileSystem::extractPackage($name, $source_type, $filename, $extract); + + // install xcaddy + $go_exec = PKG_ROOT_PATH . "{$pkgroot}/{$name}/bin/go"; + // $xcaddy_exec = PKG_ROOT_PATH . "$pkgroot/$name/bin/xcaddy"; + shell()->appendEnv([ + 'PATH' => "{$pkgroot}/{$name}/bin:" . getenv('PATH'), + 'GOROOT' => "{$pkgroot}/{$name}", + 'GOBIN' => "{$pkgroot}/{$name}/bin", + 'GOPATH' => "{$pkgroot}/go", + ]) + ->exec("{$go_exec} install github.com/caddyserver/xcaddy/cmd/xcaddy@latest"); + // TODO: Here to download dependencies for xcaddy and frankenphp first + } +} From d6858e18df118c9160e97301fa871038593a478a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 20:57:14 +0800 Subject: [PATCH 40/48] phpstan fix --- src/globals/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globals/functions.php b/src/globals/functions.php index 32f0514e..998b2d1c 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -105,7 +105,7 @@ function osfamily2dir(): string function osfamily2shortname(): string { return match (PHP_OS_FAMILY) { - 'Windows', 'WINNT', 'Cygwin' => 'win', + 'Windows' => 'win', 'Darwin' => 'macos', 'Linux' => 'linux', 'BSD' => 'bsd', From 74b1dda884a447258df20c7696fa9a07872367d7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 21:06:08 +0800 Subject: [PATCH 41/48] Fix test-extensions.php --- src/globals/test-extensions.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 44243140..77e0f71d 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -40,6 +40,9 @@ $no_strip = false; // compress with upx $upx = false; +// whether to test frankenphp build, only available for macos and linux +$frankenphp = false; + // prefer downloading pre-built packages to speed up the build process $prefer_pre_built = false; @@ -177,7 +180,7 @@ if ($argv[1] === 'build_cmd' || $argv[1] === 'build_embed_cmd') { $build_cmd .= $no_strip ? '--no-strip ' : ''; $build_cmd .= $upx ? '--with-upx-pack ' : ''; $build_cmd .= $final_libs === '' ? '' : ('--with-libs=' . quote2($final_libs) . ' '); - $build_cmd .= str_starts_with($argv[2], 'windows-') ? '' : '--build-fpm --build-frankenphp'; + $build_cmd .= str_starts_with($argv[2], 'windows-') ? '' : '--build-fpm '; $build_cmd .= '--debug '; } @@ -208,7 +211,7 @@ switch ($argv[1] ?? null) { passthru($prefix . $build_cmd . ' --build-cli --build-micro', $retcode); break; case 'build_embed_cmd': - passthru($prefix . $build_cmd . (str_starts_with($argv[2], 'windows-') ? ' --build-cli' : ' --build-embed'), $retcode); + passthru($prefix . $build_cmd . (str_starts_with($argv[2], 'windows-') ? ' --build-cli' : (' --build-embed' . ($frankenphp ? ' --build-frankenphp' : ''))), $retcode); break; case 'doctor_cmd': passthru($prefix . $doctor_cmd, $retcode); From 4ecaffd9085846091e825d300b1b29d64056b763 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 21:08:05 +0800 Subject: [PATCH 42/48] Fix test-extensions.php --- src/globals/test-extensions.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 77e0f71d..79acc984 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -41,7 +41,7 @@ $no_strip = false; $upx = false; // whether to test frankenphp build, only available for macos and linux -$frankenphp = false; +$frankenphp = true; // prefer downloading pre-built packages to speed up the build process $prefer_pre_built = false; @@ -211,6 +211,12 @@ switch ($argv[1] ?? null) { passthru($prefix . $build_cmd . ' --build-cli --build-micro', $retcode); break; case 'build_embed_cmd': + if ($frankenphp) { + passthru("{$prefix}install-pkg go-mod-frankenphp --debug", $retcode); + if ($retcode !== 0) { + break; + } + } passthru($prefix . $build_cmd . (str_starts_with($argv[2], 'windows-') ? ' --build-cli' : (' --build-embed' . ($frankenphp ? ' --build-frankenphp' : ''))), $retcode); break; case 'doctor_cmd': From becee5b42671d2b54316460bad699c8cb18aead1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 21:27:07 +0800 Subject: [PATCH 43/48] Use version instead of -v --- src/SPC/builder/unix/UnixBuilderBase.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 44756955..d424c594 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -227,7 +227,7 @@ abstract class UnixBuilderBase extends BuilderBase if (!file_exists($frankenphp)) { throw new RuntimeException('FrankenPHP binary not found: ' . $frankenphp); } - [$ret, $output] = shell()->execWithResult($frankenphp . ' -v'); + [$ret, $output] = shell()->execWithResult("{$frankenphp} version"); if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { throw new RuntimeException('FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']'); } From a76f49f92739d03c683e08ed7cc6e66a9849c83d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Jun 2025 21:56:36 +0800 Subject: [PATCH 44/48] Remove libxml2 requirement for linux --- src/SPC/builder/BuilderBase.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index fd4788f8..daa27765 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -527,8 +527,8 @@ abstract class BuilderBase throw new WrongUsageException("FrankenPHP SAPI requires go-mod-frankenphp package, please install it first: {$argv[0]} install-pkg go-mod-frankenphp"); } // frankenphp needs libxml2 libs - if (!$this->getLib('libxml2')) { - throw new WrongUsageException('FrankenPHP SAPI requires libxml2 library, please include `xml` extension in your build.'); + if (PHP_OS_FAMILY === 'Darwin' && !$this->getLib('libxml2')) { + throw new WrongUsageException('FrankenPHP SAPI for macOS requires libxml2 library, please include `xml` extension in your build.'); } } } From 15979d4636b060f90c624900778d3858f1a034ac Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Thu, 19 Jun 2025 08:59:56 +0700 Subject: [PATCH 45/48] fix double path --- src/SPC/store/pkg/GoModFrankenphp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/store/pkg/GoModFrankenphp.php b/src/SPC/store/pkg/GoModFrankenphp.php index bede5d22..3b93ceac 100644 --- a/src/SPC/store/pkg/GoModFrankenphp.php +++ b/src/SPC/store/pkg/GoModFrankenphp.php @@ -45,7 +45,7 @@ class GoModFrankenphp extends CustomPackage $lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true); $source_type = $lock[$name]['source_type']; $filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']); - $extract = $lock[$name]['move_path'] === null ? (PKG_ROOT_PATH . "{$pkgroot}/{$name}") : $lock[$name]['move_path']; + $extract = $lock[$name]['move_path'] === null ? "{$pkgroot}/{$name}" : $lock[$name]['move_path']; FileSystem::extractPackage($name, $source_type, $filename, $extract); From cb010d81ac5f943ce8b2f344060efcb0576ee030 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Thu, 19 Jun 2025 09:03:40 +0700 Subject: [PATCH 46/48] there's no documented functionality to download without building - xcaddy is meant to do both in one step --- src/SPC/store/pkg/GoModFrankenphp.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/SPC/store/pkg/GoModFrankenphp.php b/src/SPC/store/pkg/GoModFrankenphp.php index 3b93ceac..59507b6c 100644 --- a/src/SPC/store/pkg/GoModFrankenphp.php +++ b/src/SPC/store/pkg/GoModFrankenphp.php @@ -52,13 +52,13 @@ class GoModFrankenphp extends CustomPackage // install xcaddy $go_exec = PKG_ROOT_PATH . "{$pkgroot}/{$name}/bin/go"; // $xcaddy_exec = PKG_ROOT_PATH . "$pkgroot/$name/bin/xcaddy"; - shell()->appendEnv([ - 'PATH' => "{$pkgroot}/{$name}/bin:" . getenv('PATH'), - 'GOROOT' => "{$pkgroot}/{$name}", - 'GOBIN' => "{$pkgroot}/{$name}/bin", - 'GOPATH' => "{$pkgroot}/go", - ]) + shell() + ->appendEnv([ + 'PATH' => "{$pkgroot}/{$name}/bin:" . getenv('PATH'), + 'GOROOT' => "{$pkgroot}/{$name}", + 'GOBIN' => "{$pkgroot}/{$name}/bin", + 'GOPATH' => "{$pkgroot}/go", + ]) ->exec("{$go_exec} install github.com/caddyserver/xcaddy/cmd/xcaddy@latest"); - // TODO: Here to download dependencies for xcaddy and frankenphp first } } From b42409efd183f68141865589479e00ae85e9674b Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Thu, 19 Jun 2025 09:08:42 +0700 Subject: [PATCH 47/48] LD_LIBRARY_PATH for frankenphp sanity check --- src/SPC/builder/unix/UnixBuilderBase.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index d424c594..adde474a 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -227,7 +227,9 @@ abstract class UnixBuilderBase extends BuilderBase if (!file_exists($frankenphp)) { throw new RuntimeException('FrankenPHP binary not found: ' . $frankenphp); } - [$ret, $output] = shell()->execWithResult("{$frankenphp} version"); + [$ret, $output] = shell() + ->setEnv(['LD_LIBRARY_PATH' => BUILD_LIB_PATH]) + ->execWithResult("{$frankenphp} version"); if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { throw new RuntimeException('FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']'); } @@ -308,7 +310,6 @@ abstract class UnixBuilderBase extends BuilderBase $arch = arch2gnu(php_uname('m')); // define executables for go and xcaddy - $go_exec = PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin/go"; $xcaddy_exec = PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin/xcaddy"; $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; From 804468f7b913ce64563b3c486666fa0d443aa003 Mon Sep 17 00:00:00 2001 From: DubbleClick Date: Thu, 19 Jun 2025 09:14:39 +0700 Subject: [PATCH 48/48] refactor common exec code out --- src/SPC/util/UnixShell.php | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/SPC/util/UnixShell.php b/src/SPC/util/UnixShell.php index 0f320d50..bb4e8d5e 100644 --- a/src/SPC/util/UnixShell.php +++ b/src/SPC/util/UnixShell.php @@ -44,14 +44,7 @@ class UnixShell { /* @phpstan-ignore-next-line */ logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); - logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); - $env_str = $this->getEnvString(); - if (!empty($env_str)) { - $cmd = "{$env_str} {$cmd}"; - } - if ($this->cd !== null) { - $cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd; - } + $cmd = $this->getExecString($cmd); if (!$this->debug) { $cmd .= ' 1>/dev/null 2>&1'; } @@ -99,10 +92,7 @@ class UnixShell /* @phpstan-ignore-next-line */ logger()->debug(ConsoleColor::blue('[EXEC] ') . ConsoleColor::gray($cmd)); } - logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); - if ($this->cd !== null) { - $cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd; - } + $cmd = $this->getExecString($cmd); exec($cmd, $out, $code); return [$code, $out]; } @@ -126,4 +116,21 @@ class UnixShell } return trim($str); } + + /** + * @param string $cmd + * @return string + */ + private function getExecString(string $cmd): string + { + logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); + $env_str = $this->getEnvString(); + if (!empty($env_str)) { + $cmd = "{$env_str} {$cmd}"; + } + if ($this->cd !== null) { + $cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd; + } + return $cmd; + } }