Compare commits

..

145 Commits

Author SHA1 Message Date
henderkes
5a5f54bdcd brilliant to test php 8.1 2026-03-30 01:37:08 +07:00
henderkes
8f7897e13b test 2026-03-30 01:06:31 +07:00
henderkes
daae5f2a7c libjpeg-turbo mustn't compile zlib symbols on its own 2026-03-30 00:56:51 +07:00
Marc
766f7fa34f hard code protobuf version while we're on v2 (#1075) 2026-03-26 12:37:32 +07:00
henderkes
ecf712b2b7 hard code protobuf version while we're on v2 2026-03-26 12:32:27 +07:00
Jerry Ma
d35cbd7bf8 Add Windows builders for libaom and brotli (#1072) 2026-03-23 16:21:46 +08:00
crazywhalecc
d076df6b04 Bump version number 2026-03-23 16:21:24 +08:00
crazywhalecc
a99b6bebae Remove freetype useless lib suggestions 2026-03-23 16:08:09 +08:00
Hendrik Mennen
864678ab46 Merge branch 'main' into feat/windows-libaom-brotli 2026-03-22 13:12:47 +01:00
Hendrik Mennen
c03508a84b Improve zlib Windows library detection for future zlib versions (#1070)
Co-authored-by: Hendrik Mennen <hmennen@gambio.ec1.de>
2026-03-22 20:11:29 +08:00
Hendrik Mennen
963e2a084a Add Windows builders for libaom and brotli libraries
Both libraries are listed in lib.json and used as transitive dependencies
(libaom via libavif, brotli via freetype/curl) but had no Windows builder,
causing builds to fail with "library [X] is in the lib.json list but not
supported to compile".

libaom: Uses builddir instead of build to avoid collision with the
source tree's build/cmake/ directory. Matches the Unix builder's
AOM_TARGET_CPU=generic setting for portability.

brotli: Standard CMake build with shared libs and tools disabled.

Also adds static-libs-windows entry for libaom in lib.json.
2026-03-22 09:07:47 +01:00
Marc
4c6b7a3d55 fix libde265 on ancient debian OS (#1064) 2026-03-20 19:48:01 +07:00
Marc
5404926a14 Merge branch 'main' into fix/lide265 2026-03-20 19:47:47 +07:00
Nils Silbernagel
295df19484 Add shared-extensions, frankenphp and zts to build-unix workflow (#1062)
Co-authored-by: crazywhalecc <jesse2061@outlook.com>
2026-03-20 20:37:25 +08:00
Jerry Ma
b970bf8e3a Fix gd build on PHP 8.5 (#1043) 2026-03-20 20:00:42 +08:00
Jerry Ma
54915028d7 Fix zlib produced lib file names from different zlib version (#1066) 2026-03-20 19:35:51 +08:00
henderkes
823fe96942 attempt 2026-03-18 23:26:57 +07:00
henderkes
f2fa29809a why is it not failing here? 2026-03-18 18:37:16 +07:00
Marc
463ec546fa Merge branch 'main' into fix/lide265 2026-03-18 15:51:03 +07:00
henderkes
60b2aea09e fix libde265 on ancient debian OS? 2026-03-18 11:57:27 +07:00
Marc
4625c6a885 update default php version to 8.5 (#1058) 2026-03-11 15:09:07 +07:00
henderkes
85b0cd8b4b only disable when building ftp static, shared is fine 2026-03-11 13:54:24 +07:00
henderkes
1fcb74ad9b swoole-ftp conflicts with ftp 2026-03-11 13:42:38 +07:00
henderkes
1049a3ce66 curl is always supported now (swoole no longer supports php < 8.1) 2026-03-11 10:32:58 +07:00
henderkes
1b8b53d47f update swoole args for 6.2 2026-03-11 10:19:08 +07:00
henderkes
a232f578a4 test bulk 2026-03-11 10:11:39 +07:00
henderkes
70285cb53b actually update to 8.5 2026-03-11 09:48:50 +07:00
henderkes
a335d050cf cs fix 2026-03-11 09:46:41 +07:00
henderkes
ef4b2997a7 test 2026-03-11 09:45:56 +07:00
henderkes
901da8fa41 remove ldtl from odbc libs private (using built in ltdl) 2026-03-11 09:43:02 +07:00
henderkes
e49a5d7a50 make php 8.5 default 2026-03-11 09:42:39 +07:00
Jerry Ma
281b958075 Fix grpc build (#1055) 2026-03-10 20:35:51 +09:00
Marc
e31f64864e fix: FrankenPHP build args (#1057) 2026-03-10 18:13:34 +07:00
Kévin Dunglas
92f5b56c74 fix: FrankenPHP build args 2026-03-10 11:57:23 +01:00
Jerry Ma
2350d2d5ca Merge branch 'main' into fix/grpc-build 2026-03-10 14:52:21 +09:00
crazywhalecc
086c855a43 Use custom config.m4 for grpc extension 2026-03-10 13:49:53 +08:00
crazywhalecc
4fa5292913 Use custom config.m4 for grpc extension 2026-03-10 13:49:34 +08:00
Marc
9634b8bcda set custom binary name for frankenphp, allow linking against system openssl (fix mssql issues) (#1056) 2026-03-10 11:11:36 +07:00
Marc
5d5a50a33c Update src/SPC/builder/LibraryBase.php
Co-authored-by: Jerry Ma <jesse2061@outlook.com>
2026-03-10 10:57:49 +07:00
henderkes
1edf14e642 set custom binary name for frankenphp 2026-03-10 08:52:15 +07:00
henderkes
2277390a1a fix removeConfigureArgs in UnixAutoconfExecutor.php 2026-03-10 08:49:56 +07:00
henderkes
f93ad27c17 allow using some libs as system provided (work around mssql linking vs system openssl) 2026-03-10 08:47:38 +07:00
henderkes
b690566b39 simplify rm command 2026-03-10 08:43:48 +07:00
henderkes
16e772e1a8 add back in zig workaround as 0.16.x is not released yet 2026-03-10 08:42:17 +07:00
crazywhalecc
ad356b4a23 Fix grpc build 2026-03-09 20:12:14 +08:00
Jerry Ma
8c4e3d58a3 Add php-src mirror and use gmp mirror site (#1048) 2026-03-06 15:25:38 +09:00
Marc
8a51d64685 Add condition for ffi patch (#1050) 2026-03-06 12:59:23 +07:00
crazywhalecc
055bc7bc3c Add condition for ffi patch 2026-03-06 13:46:55 +08:00
Marc
7d7902e0e9 Update build flags for FrankenPHP in UnixBuilderBase (#1042) 2026-02-24 06:08:14 +07:00
Kévin Dunglas
2a8fa7d155 Update build flags for FrankenPHP in UnixBuilderBase 2026-02-23 16:29:43 +01:00
Marc
67ef8f6608 fix redownloading go-xcaddy every time, version 2.8.3 (#1034) 2026-02-17 22:36:27 +07:00
henderkes
d83a597689 unquote the string in case a shell script passes it stupidly 2026-02-17 21:49:30 +07:00
henderkes
5623fed37f fix redownloading go-xcaddy every time 2026-02-17 21:05:18 +07:00
Marc
38140d115f libavif needs at least one encoder to work (#1033) 2026-02-17 19:59:46 +07:00
henderkes
98117c3a04 remove pre built 2026-02-17 19:58:03 +07:00
Marc
b01d3ce12c Merge branch 'main' into feat/avif-dec 2026-02-17 19:18:38 +07:00
henderkes
608c915e14 should depend on it instead 2026-02-17 19:14:29 +07:00
henderkes
c680299654 libavif needs at least one encoder to work 2026-02-17 19:12:19 +07:00
Marc
794ab16b32 add input with-suggested-libs for build command (#1032) 2026-02-16 18:38:35 +07:00
tricker
661723c99a change logs name
Co-authored-by: Marc <m@pyc.ac>
2026-02-16 12:26:49 +01:00
Yoram
d9834d05c6 upload debug logs on 'build php' failures 2026-02-16 11:35:42 +01:00
Yoram
9a53ef3498 add input with-suggested-libs for build command 2026-02-13 14:35:01 +01:00
Marc
f680731f9d fix: Postgres build with ancient libc (#1029) 2026-02-11 17:42:36 +01:00
Jerry Ma
0fe1442f7e Bump version from 2.8.0 to 2.8.2 2026-02-12 00:02:38 +08:00
Jerry Ma
1e4780397b Update test-extensions.php for PHP versions and extensions
Commented out older PHP versions and Windows 2025 in the test configuration. Updated the extensions to test for Linux and Darwin.
2026-02-11 23:32:19 +08:00
Kévin Dunglas
6b67cb90fc fix: Postgres build with ancient libc 2026-02-11 16:24:13 +01:00
Jerry Ma
b89ff3c083 Add com_dotnet extension (#1023) 2026-02-03 19:08:19 +08:00
Marc
0cfa2036f0 fix spx shared libadd (#1022) 2026-01-30 20:23:18 +01:00
henderkes
c5882c1f8e fix gettext v1.0 release 2026-01-30 19:41:39 +01:00
henderkes
4531c9fe57 add option to allow linking musl dynamically on alpine 2026-01-27 00:57:58 +01:00
henderkes
223dd10ac6 fix spx shared libadd 2026-01-24 20:26:19 +01:00
Marc
1c28f0f455 bunch of fixes/changes to make packages build (#1006) 2026-01-19 12:46:24 +01:00
henderkes
b3c450291a up version 2026-01-19 12:00:06 +01:00
crazywhalecc
372760e469 Update patch point docs 2026-01-19 18:56:28 +08:00
henderkes
6cf4c40cd2 Merge remote-tracking branch 'origin/main' into henderkes-patch-1 2026-01-19 11:22:03 +01:00
henderkes
af75ffaf24 suggestions, change openssldir 2026-01-19 10:22:33 +01:00
Marc
ae0217b3a1 add excimer extension (#1018) 2026-01-19 09:25:30 +01:00
henderkes
1e2b4017ac test excimer 2026-01-17 11:28:47 +01:00
henderkes
19f941797e zig now supports -Wl,-exported_symbols_list 2026-01-17 11:28:05 +01:00
Marc
0b863cbc70 Merge branch 'main' into feat/excimer 2026-01-17 10:53:35 +01:00
henderkes
b09337de09 add excimer extension 2026-01-17 10:51:21 +01:00
henderkes
d902e70b4d fix arm64 builds 2026-01-16 12:28:41 +01:00
Jerry Ma
cd2dc5bce4 Fix nghttp2 and curl build configurations for static linking (#1014) 2026-01-13 16:51:57 +08:00
henderkes
34910d18e9 add patch point for shared ext build 2026-01-04 02:31:41 +01:00
henderkes
3a17cec521 deploy extensions with -release flag too 2026-01-03 19:15:57 +01:00
henderkes
94644d374f fix 2026-01-03 19:12:16 +01:00
henderkes
f8b0c2c980 add release thing to extension build too 2026-01-03 19:08:14 +01:00
henderkes
6bbb3c969c remove -release handling functionality 2026-01-03 17:03:43 +01:00
henderkes
76025b95c1 missing space 2026-01-03 16:46:14 +01:00
henderkes
1be353fd13 more concise message 2026-01-03 16:45:55 +01:00
henderkes
54001ab868 simplify logic a bit 2026-01-03 16:33:40 +01:00
henderkes
890ff475f1 our memcache patch prevents shared building 2026-01-03 14:09:16 +01:00
henderkes
559a2909a9 use little trick to order libargon2 before libsodium 2026-01-02 23:21:29 +01:00
henderkes
fff2484529 postgresql doesn't build under c23 2026-01-02 22:52:03 +01:00
henderkes
d1b194999d use OPENSSL_CONF directory for openssl default configuration 2026-01-02 21:13:22 +01:00
henderkes
64f7a3553e don't need it anymore 2026-01-01 12:33:55 +01:00
henderkes
a06cc32491 pin libpng to released tags, not git 2025-12-30 11:58:57 +01:00
henderkes
022fdb2fc5 fix no-strip 2025-12-29 23:58:54 +01:00
henderkes
7688a55656 don't get zig master branch 2025-12-29 22:16:53 +01:00
henderkes
08388c0b15 force enable tailcall vm with zig 2025-12-29 22:12:25 +01:00
henderkes
e7a88f1df7 enable fat for gmp when next version releases 2025-12-29 21:15:53 +01:00
henderkes
2f3122627e make grpc php 8.5 compatible 2025-12-28 12:44:24 +01:00
henderkes
93a35908de factor grpc extension out to ext-grpc, keep library for now, even though unused 2025-12-28 12:11:56 +01:00
henderkes
5ef4623051 grpc will fail for php 8.5, it's not updated yet 2025-12-27 23:05:35 +01:00
henderkes
e952f1c76a we don't even need to build grpc library for grpc extension... 2025-12-27 22:36:24 +01:00
henderkes
09b89a30f9 WIP: use system libraries for grpc without building our own grpc lib 2025-12-27 22:20:02 +01:00
henderkes
9a681a9fa6 add mariadb mysqlnd plugins 2025-12-27 21:22:10 +01:00
Jerry Ma
8650ce4f8f Add MACOSX_DEPLOYMENT_TARGET to env.ini (#1009) 2025-12-26 17:15:45 +08:00
crazywhalecc
f7ca621efe Test 2025-12-26 15:03:54 +08:00
henderkes
6b5200002e fix downloader selecting drafts 2025-12-20 23:29:25 +01:00
henderkes
53f7cdefe0 fix swoole compilation with php 8.5.1 2025-12-18 20:12:01 +01:00
henderkes
e1a14bbb9f fix implicit include 2025-12-18 17:39:05 +01:00
henderkes
9e051c8c80 fix: check for link first before checking for is_dir 2025-12-18 17:32:02 +01:00
henderkes
e677be74d7 remove 2025-12-18 17:32:02 +01:00
henderkes
037d224fd7 why does phpstan think this is necessary? 2025-12-18 17:32:02 +01:00
henderkes
ce44e00bd4 @crazywhalecc how to use patch points to delete source dirs? 2025-12-18 17:32:01 +01:00
henderkes
0247458853 we were installing to wrong dir if source name != lib name 2025-12-18 17:32:01 +01:00
henderkes
656a58c3fa remove source dir after successful build in CI environment 2025-12-18 17:32:01 +01:00
Jerry Ma
9fdfef5057 macOS don't need to disable avx2 explicitly (#1007) 2025-12-18 21:21:47 +08:00
Marc
18c5ccfe9d the libwebp 1.6.0 bug affects centos 7 too (#1004) 2025-12-16 09:33:20 +01:00
henderkes
d064e1353c the libwebp 1.6.0 bug affects centos 7 too 2025-12-15 18:50:20 +01:00
Jerry Ma
3c89ce6c7f Update version to 2.7.10 (#997) 2025-12-10 17:14:27 +08:00
Marc
07ea1e2887 update libwebp and libxml2 (#982) 2025-12-10 10:01:24 +01:00
Jerry Ma
f0b5e4f59e Fix typo in ncurses.php enable-symlinks option (#994) 2025-12-10 15:43:24 +08:00
Marc
a54021bf19 Apply suggestion from @henderkes 2025-12-10 08:42:28 +01:00
henderkes
dce63d3c87 we need extensions to explicitly tell which c std they need 2025-12-06 11:18:10 +01:00
henderkes
47ab5d7584 use c17 for extensions as well? 2025-12-05 13:57:28 +01:00
henderkes
b2182b4fe1 use source extract hook for pdo_sqlsrv 2025-12-05 12:20:14 +01:00
henderkes
1d5aec037b c17 instead 2025-12-05 12:14:57 +01:00
henderkes
6b5f702719 ncurses can't build with std=c23 (default with gcc 15) 2025-12-05 11:43:51 +01:00
henderkes
7bdcda1d62 gmp can't build with std=c23 (default with gcc 15) 2025-12-05 11:37:35 +01:00
henderkes
66840a8eed update xdebug to use pie sources 2025-12-05 09:15:22 +01:00
henderkes
98773ee5a6 zig toolchain can always use libc 2025-12-03 15:02:14 +01:00
henderkes
719d818fd1 we need to check for structure of pdo_sqlsrv extension 2025-12-02 21:34:32 +01:00
henderkes
b8444070ee update go-xcaddy version automatically 2025-12-01 20:41:58 +01:00
henderkes
5b4f4f8e55 maybe? 2025-12-01 19:55:51 +01:00
henderkes
22d263c0a8 maybe explicit mavx2?! 2025-12-01 19:27:44 +01:00
henderkes
150d866c15 revert turning off sse for libwebp, need to check why debian fails building 2025-12-01 19:12:43 +01:00
henderkes
c051a48d56 don't add -l:libstdc++.a if we're not actually using gcc/clang 2025-12-01 17:28:59 +01:00
henderkes
b965ffcd82 don't build extra programs 2025-12-01 17:16:59 +01:00
henderkes
7f863d182f don't remove dir, just don't build tests 2025-12-01 17:10:56 +01:00
henderkes
d1041c57dc remove openssl source/test dir (4.1gb?!) 2025-12-01 17:05:50 +01:00
henderkes
14b822a185 don't build avx2 if we don't have it 2025-12-01 16:55:52 +01:00
Marc
7204d277b4 Update PHP extensions for Linux and Darwin 2025-12-01 11:39:56 +01:00
Marc
5a0fd40dc4 update libwebp and libxml2 2025-12-01 09:55:46 +01:00
294 changed files with 4438 additions and 26075 deletions

View File

@@ -29,6 +29,9 @@ on:
description: Extensions to build (comma separated)
required: true
type: string
shared-extensions:
description: Shared extensions to build (optional, comma separated)
type: string
extra-libs:
description: Extra libraries to build (optional, comma separated)
type: string
@@ -42,10 +45,22 @@ on:
build-fpm:
description: Build fpm binary
type: boolean
build-frankenphp:
description: Build frankenphp binary (requires ZTS)
type: boolean
default: false
enable-zts:
description: Enable ZTS
type: boolean
default: false
prefer-pre-built:
description: Prefer pre-built binaries (reduce build time)
type: boolean
default: true
with-suggested-libs:
description: Build with suggested libs
type: boolean
default: false
debug:
description: Show full build logs
type: boolean
@@ -69,6 +84,9 @@ on:
description: Extensions to build (comma separated)
required: true
type: string
shared-extensions:
description: Shared extensions to build (optional, comma separated)
type: string
extra-libs:
description: Extra libraries to build (optional, comma separated)
type: string
@@ -82,10 +100,22 @@ on:
build-fpm:
description: Build fpm binary
type: boolean
build-frankenphp:
description: Build frankenphp binary (requires ZTS)
type: boolean
default: false
enable-zts:
description: Enable ZTS
type: boolean
default: false
prefer-pre-built:
description: Prefer pre-built binaries (reduce build time)
type: boolean
default: true
with-suggested-libs:
description: Include suggested libs
type: boolean
default: false
debug:
description: Show full build logs
type: boolean
@@ -144,8 +174,19 @@ jobs:
RUNS_ON="macos-15"
;;
esac
DOWN_CMD="$DOWN_CMD --with-php=${{ inputs.php-version }} --for-extensions=${{ inputs.extensions }} --ignore-cache-sources=php-src"
BUILD_CMD="$BUILD_CMD ${{ inputs.extensions }}"
STATIC_EXTS="${{ inputs.extensions }}"
SHARED_EXTS="${{ inputs['shared-extensions'] }}"
BUILD_FRANKENPHP="${{ inputs['build-frankenphp'] }}"
ENABLE_ZTS="${{ inputs['enable-zts'] }}"
ALL_EXTS="$STATIC_EXTS"
if [ -n "$SHARED_EXTS" ]; then
ALL_EXTS="$ALL_EXTS,$SHARED_EXTS"
fi
DOWN_CMD="$DOWN_CMD --with-php=${{ inputs.php-version }} --for-extensions=$ALL_EXTS --ignore-cache-sources=php-src"
BUILD_CMD="$BUILD_CMD $STATIC_EXTS"
if [ -n "$SHARED_EXTS" ]; then
BUILD_CMD="$BUILD_CMD --build-shared=$SHARED_EXTS"
fi
if [ -n "${{ inputs.extra-libs }}" ]; then
DOWN_CMD="$DOWN_CMD --for-libs=${{ inputs.extra-libs }}"
BUILD_CMD="$BUILD_CMD --with-libs=${{ inputs.extra-libs }}"
@@ -157,6 +198,9 @@ jobs:
if [ ${{ inputs.prefer-pre-built }} == true ]; then
DOWN_CMD="$DOWN_CMD --prefer-pre-built"
fi
if [ ${{ inputs.with-suggested-libs }} == true ]; then
BUILD_CMD="$BUILD_CMD --with-suggested-libs"
fi
if [ ${{ inputs.build-cli }} == true ]; then
BUILD_CMD="$BUILD_CMD --build-cli"
fi
@@ -166,6 +210,12 @@ jobs:
if [ ${{ inputs.build-fpm }} == true ]; then
BUILD_CMD="$BUILD_CMD --build-fpm"
fi
if [ "$BUILD_FRANKENPHP" = "true" ]; then
BUILD_CMD="$BUILD_CMD --build-frankenphp"
fi
if [ "$ENABLE_ZTS" = "true" ]; then
BUILD_CMD="$BUILD_CMD --enable-zts"
fi
echo 'download='"$DOWN_CMD" >> "$GITHUB_OUTPUT"
echo 'build='"$BUILD_CMD" >> "$GITHUB_OUTPUT"
echo 'run='"$RUNS_ON" >> "$GITHUB_OUTPUT"
@@ -188,6 +238,27 @@ jobs:
env:
phpts: nts
- if: ${{ inputs['build-frankenphp'] == true }}
name: "Install go-xcaddy for FrankenPHP"
run: |
case "${{ inputs.os }}" in
linux-x86_64|linux-aarch64)
./bin/spc-alpine-docker install-pkg go-xcaddy
;;
linux-x86_64-glibc|linux-aarch64-glibc)
./bin/spc-gnu-docker install-pkg go-xcaddy
;;
macos-x86_64|macos-aarch64)
composer update --no-dev --classmap-authoritative
./bin/spc doctor --auto-fix
./bin/spc install-pkg go-xcaddy
;;
*)
echo "Unsupported OS for go-xcaddy install: ${{ inputs.os }}"
exit 1
;;
esac
# Cache downloaded source
- id: cache-download
uses: actions/cache@v4
@@ -202,6 +273,14 @@ jobs:
# if: ${{ failure() }}
# uses: mxschmitt/action-tmate@v3
# Upload debug logs
- if: ${{ inputs.debug && failure() }}
name: "Upload build logs on failure"
uses: actions/upload-artifact@v4
with:
name: spc-logs-${{ inputs.php-version }}-${{ inputs.os }}
path: log/*.log
# Upload cli executable
- if: ${{ inputs.build-cli == true }}
name: "Upload PHP cli SAPI"
@@ -226,7 +305,22 @@ jobs:
name: php-fpm-${{ inputs.php-version }}-${{ inputs.os }}
path: buildroot/bin/php-fpm
# Upload frankenphp executable
- if: ${{ inputs['build-frankenphp'] == true }}
name: "Upload FrankenPHP SAPI"
uses: actions/upload-artifact@v4
with:
name: php-frankenphp-${{ inputs.php-version }}-${{ inputs.os }}
path: buildroot/bin/frankenphp
# Upload extensions metadata
- if: ${{ inputs['shared-extensions'] != '' }}
name: "Upload shared extensions"
uses: actions/upload-artifact@v4
with:
name: php-shared-ext-${{ inputs.php-version }}-${{ inputs.os }}
path: |
buildroot/modules/*.so
- uses: actions/upload-artifact@v4
name: "Upload License Files"
with:

View File

@@ -2,7 +2,7 @@ name: Tests
on:
pull_request:
branches: [ "main", "v3" ]
branches: [ "main" ]
types: [ opened, synchronize, reopened ]
paths:
- 'src/**'

8
.gitignore vendored
View File

@@ -1,8 +1,8 @@
.idea
/runtime/
/docker/libraries/
/docker/extensions/
/docker/source/
runtime/
docker/libraries/
docker/extensions/
docker/source/
# Vendor files
/vendor/**

View File

@@ -69,6 +69,6 @@ return (new PhpCsFixer\Config())
'php_unit_data_provider_method_order' => false,
])
->setFinder(
PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/StaticPHP'])
PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/SPC'])
)
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect());

23
bin/spc
View File

@@ -1,9 +1,13 @@
#!/usr/bin/env php
<?php
use StaticPHP\ConsoleApplication;
use StaticPHP\Exception\ExceptionHandler;
use StaticPHP\Exception\SPCException;
use SPC\ConsoleApplication;
use SPC\exception\ExceptionHandler;
// Load custom php if exists
if (PHP_OS_FAMILY !== 'Windows' && PHP_BINARY !== (__DIR__ . '/php') && file_exists(__DIR__ . '/php') && is_executable(__DIR__ . '/php')) {
pcntl_exec(__DIR__ . '/php', $argv);
}
if (file_exists(dirname(__DIR__) . '/vendor/autoload.php')) {
// Current: ./bin (git/project mode)
@@ -13,6 +17,11 @@ if (file_exists(dirname(__DIR__) . '/vendor/autoload.php')) {
require_once dirname(__DIR__, 3) . '/autoload.php';
}
// 防止 Micro 打包状态下不支持中文的显示(虽然这个项目目前好像没输出过中文?)
if (PHP_OS_FAMILY === 'Windows' && Phar::running()) {
exec('CHCP 65001');
}
// Print deprecation notice on PHP < 8.4, use red and highlight background
if (PHP_VERSION_ID < 80400) {
echo "\e[43mDeprecation Notice: PHP < 8.4 is deprecated, please upgrade your PHP version.\e[0m\n";
@@ -20,11 +29,7 @@ if (PHP_VERSION_ID < 80400) {
try {
(new ConsoleApplication())->run();
} catch (SPCException $e) {
ExceptionHandler::handleSPCException($e);
exit(1);
} catch (\Throwable $e) {
ExceptionHandler::handleDefaultException($e);
} catch (Exception $e) {
ExceptionHandler::getInstance()->handle($e);
exit(1);
}

View File

@@ -3,7 +3,7 @@
set -e
# This file is using docker to run commands
SPC_DOCKER_VERSION=v7
SPC_DOCKER_VERSION=v6
# Detect docker can run
if ! which docker >/dev/null; then
@@ -108,8 +108,7 @@ RUN apk update; \
wget \
xz \
gettext-dev \
binutils-gold \
patchelf
binutils-gold
RUN curl -#fSL https://dl.static-php.dev/static-php-cli/bulk/php-8.4.4-cli-linux-\$(uname -m).tar.gz | tar -xz -C /usr/local/bin && \
chmod +x /usr/local/bin/php
@@ -123,7 +122,6 @@ COPY ./composer.* /app/
ADD ./bin /app/bin
RUN composer install --no-dev
ADD ./config /app/config
ADD ./spc.registry.json /app/spc.registry.json
RUN bin/spc doctor --auto-fix
RUN bin/spc install-pkg upx

View File

@@ -1,4 +0,0 @@
#!/usr/bin/env bash
# This script runs the 'spc' command with Xdebug enabled for debugging purposes.
php -d xdebug.mode=debug -d xdebug.client_host=127.0.0.1 -d xdebug.client_port=9003 -d xdebug.start_with_request=yes "$(dirname "$0")/../bin/spc" "$@"

View File

@@ -92,11 +92,6 @@ RUN echo "source scl_source enable devtoolset-10" >> /etc/bashrc
RUN source /etc/bashrc
RUN yum install -y which
RUN curl -fsSL -o patchelf.tgz https://github.com/NixOS/patchelf/releases/download/0.18.0/patchelf-0.18.0-$SPC_USE_ARCH.tar.gz && \
mkdir -p /patchelf && \
tar -xzf patchelf.tgz -C /patchelf --strip-components=1 && \
cp /patchelf/bin/patchelf /usr/bin/
RUN curl -o cmake.tgz -#fSL https://github.com/Kitware/CMake/releases/download/v3.31.4/cmake-3.31.4-linux-$SPC_USE_ARCH.tar.gz && \
mkdir /cmake && \
tar -xzf cmake.tgz -C /cmake --strip-components 1

View File

@@ -9,16 +9,14 @@
}
],
"require": {
"php": ">=8.4",
"php": ">= 8.3",
"ext-mbstring": "*",
"ext-zlib": "*",
"laravel/prompts": "~0.1",
"nette/php-generator": "^4.2",
"php-di/php-di": "^7.1",
"laravel/prompts": "^0.1.12",
"symfony/console": "^5.4 || ^6 || ^7",
"symfony/process": "^7.2",
"symfony/yaml": "^7.2",
"zhamao/logger": "^1.1.4"
"zhamao/logger": "^1.1.3"
},
"require-dev": {
"captainhook/captainhook-phar": "^5.23",
@@ -30,9 +28,7 @@
},
"autoload": {
"psr-4": {
"SPC\\": "src/SPC",
"StaticPHP\\": "src/StaticPHP",
"Package\\": "src/Package"
"SPC\\": "src/SPC"
},
"files": [
"src/globals/defines.php",
@@ -41,7 +37,7 @@
},
"autoload-dev": {
"psr-4": {
"Tests\\StaticPHP\\": "tests/StaticPHP"
"SPC\\Tests\\": "tests/SPC"
}
},
"bin": [

761
composer.lock generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -32,10 +32,9 @@
; GNU_ARCH: the GNU arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `aarch64`)
; MAC_ARCH: the MAC arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `arm64`)
; PKG_CONFIG: (*nix only) static-php-cli will set `$BUILD_BIN_PATH/pkg-config` to PKG_CONFIG.
; SPC_DEFAULT_CC: (*nix only) the default compiler for selected toolchain.
; SPC_DEFAULT_CXX: (*nix only) the default c++ compiler selected toolchain.
; SPC_DEFAULT_AR: (*nix only) the default archiver for selected toolchain.
; SPC_DEFAULT_LD: (*nix only) the default linker for selected toolchain.
; SPC_LINUX_DEFAULT_CC: (linux only) the default compiler for linux. (For alpine linux: `gcc`, default: `$GNU_ARCH-linux-musl-gcc`)
; SPC_LINUX_DEFAULT_CXX: (linux only) the default c++ compiler for linux. (For alpine linux: `g++`, default: `$GNU_ARCH-linux-musl-g++`)
; SPC_LINUX_DEFAULT_AR: (linux only) the default archiver for linux. (For alpine linux: `ar`, default: `$GNU_ARCH-linux-musl-ar`)
; SPC_EXTRA_PHP_VARS: (linux only) the extra vars for building php, used in `configure` and `make` command.
[global]
@@ -49,12 +48,6 @@ SPC_SKIP_DOCTOR_CHECK_ITEMS=""
SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli"
; The display message for php version output (PHP >= 8.4 available)
PHP_BUILD_PROVIDER="static-php-cli ${SPC_VERSION}"
; Whether to enable log file (if you are using vendor mode)
SPC_ENABLE_LOG_FILE="yes"
; The LOG DIR for spc logs
SPC_LOGS_DIR="${WORKING_DIR}/log"
; Preserve old logs when running new builds
SPC_PRESERVE_LOGS="no"
; 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
@@ -82,8 +75,10 @@ SPC_MICRO_PATCHES=static_extensions_win32,cli_checks,disable_huge_page,vcruntime
; - musl-native: used for alpine linux, can build `musl` and `musl -dynamic` target.
; - gnu-native: used for general linux distros, can build gnu target for the installed glibc version only.
; LEGACY option to specify the target
; option to specify the target, superceded by SPC_TARGET if set
SPC_LIBC=musl
; uncomment to link libc dynamically on musl
; SPC_MUSL_DYNAMIC=true
; Recommended: specify your target here. Zig toolchain will be used.
; examples:
@@ -122,17 +117,20 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE
; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so
SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS=""
; optional, path to openssl conf. This affects where openssl will look for the default CA.
; default on Debian/Alpine: /etc/ssl, default on RHEL: /etc/pki/tls
OPENSSLDIR=""
[macos]
; build target: macho or macho (possibly we could support macho-universal in the future)
; Currently we do not support universal and cross-compilation for macOS.
SPC_TARGET=native-macos
; compiler environments
CC=${SPC_LINUX_DEFAULT_CC}
CXX=${SPC_LINUX_DEFAULT_CXX}
AR=${SPC_LINUX_DEFAULT_AR}
LD=${SPC_LINUX_DEFAULT_LD}
CC=clang
CXX=clang++
AR=ar
LD=ld
; default compiler flags, used in CMake toolchain file, openssl and pkg-config build
; this will be added to all CFLAGS and CXXFLAGS for the library builds
SPC_DEFAULT_C_FLAGS="--target=${MAC_ARCH}-apple-darwin -Os"
SPC_DEFAULT_CXX_FLAGS="--target=${MAC_ARCH}-apple-darwin -Os"
SPC_DEFAULT_LD_FLAGS=""
@@ -150,3 +148,12 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable-
SPC_CMD_VAR_PHP_EMBED_TYPE="static"
; EXTRA_CFLAGS for `configure` and `make` php
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_C_FLAGS}"
; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so
SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS=""
; minimum compatible macOS version (LLVM vars, availability not guaranteed)
MACOSX_DEPLOYMENT_TARGET=12.0
[freebsd]
; compiler environments
CC=clang
CXX=clang++

View File

@@ -43,6 +43,14 @@
"calendar": {
"type": "builtin"
},
"com_dotnet": {
"support": {
"BSD": "no",
"Linux": "no",
"Darwin": "no"
},
"type": "builtin"
},
"ctype": {
"type": "builtin"
},
@@ -127,6 +135,14 @@
"sockets"
]
},
"excimer": {
"support": {
"Windows": "wip",
"BSD": "wip"
},
"type": "external",
"source": "ext-excimer"
},
"exif": {
"type": "builtin"
},
@@ -232,11 +248,14 @@
"BSD": "wip"
},
"type": "external",
"source": "grpc",
"source": "ext-grpc",
"arg-type-unix": "enable-path",
"cpp-extension": true,
"lib-depends": [
"grpc"
"grpc",
"zlib",
"openssl",
"libcares"
]
},
"iconv": {
@@ -408,8 +427,7 @@
"ext-depends": [
"zlib",
"session"
],
"build-with-php": true
]
},
"memcached": {
"support": {
@@ -487,6 +505,40 @@
"zlib"
]
},
"mysqlnd_ed25519": {
"type": "external",
"source": "mysqlnd_ed25519",
"arg-type": "enable",
"target": [
"shared"
],
"ext-depends": [
"mysqlnd"
],
"lib-depends": [
"libsodium"
],
"lib-suggests": [
"openssl"
]
},
"mysqlnd_parsec": {
"type": "external",
"source": "mysqlnd_parsec",
"arg-type": "enable",
"target": [
"shared"
],
"ext-depends": [
"mysqlnd"
],
"lib-depends": [
"libsodium"
],
"lib-suggests": [
"openssl"
]
},
"oci8": {
"type": "wip",
"support": {

View File

@@ -143,9 +143,7 @@
"zlib"
],
"lib-suggests": [
"libpng",
"bzip2",
"brotli"
"libpng"
]
},
"gettext": {
@@ -355,12 +353,18 @@
"static-libs-unix": [
"libaom.a"
],
"static-libs-windows": [
"aom.lib"
],
"cpp-library": true
},
"libargon2": {
"source": "libargon2",
"static-libs-unix": [
"libargon2.a"
],
"lib-suggests": [
"libsodium"
]
},
"libavif": {
@@ -370,6 +374,15 @@
],
"static-libs-windows": [
"avif.lib"
],
"lib-depends": [
"libaom"
],
"lib-suggests": [
"libwebp",
"libjpeg",
"libxml2",
"libpng"
]
},
"libcares": {
@@ -481,7 +494,7 @@
"static-libs-windows": [
"libjpeg_a.lib"
],
"lib-suggests-windows": [
"lib-depends": [
"zlib"
]
},
@@ -850,6 +863,9 @@
},
"openssl": {
"source": "openssl",
"pkg-configs": [
"openssl"
],
"static-libs-unix": [
"libssl.a",
"libcrypto.a"
@@ -962,6 +978,11 @@
},
"unixodbc": {
"source": "unixodbc",
"pkg-configs": [
"odbc",
"odbccr",
"odbcinst"
],
"static-libs-unix": [
"libodbc.a",
"libodbccr.a",
@@ -1003,6 +1024,9 @@
},
"zlib": {
"source": "zlib",
"pkg-configs": [
"zlib"
],
"static-libs-unix": [
"libz.a"
],
@@ -1016,6 +1040,9 @@
},
"zstd": {
"source": "zstd",
"pkg-configs": [
"libzstd"
],
"static-libs-unix": [
"libzstd.a"
],

File diff suppressed because it is too large Load Diff

105
config/pkg.json Normal file
View File

@@ -0,0 +1,105 @@
{
"go-xcaddy-aarch64-linux": {
"type": "custom"
},
"go-xcaddy-aarch64-macos": {
"type": "custom"
},
"go-xcaddy-x86_64-linux": {
"type": "custom"
},
"go-xcaddy-x86_64-macos": {
"type": "custom"
},
"musl-toolchain-aarch64-linux": {
"type": "url",
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz"
},
"musl-toolchain-x86_64-linux": {
"type": "url",
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz"
},
"nasm-x86_64-win": {
"type": "url",
"url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip",
"extract-files": {
"nasm.exe": "{php_sdk_path}/bin/nasm.exe",
"ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe"
}
},
"pkg-config-aarch64-linux": {
"type": "ghrel",
"repo": "static-php/static-php-cli-hosted",
"match": "pkg-config-aarch64-linux-musl-1.2.5.txz",
"extract-files": {
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
}
},
"pkg-config-aarch64-macos": {
"type": "ghrel",
"repo": "static-php/static-php-cli-hosted",
"match": "pkg-config-aarch64-darwin.txz",
"extract-files": {
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
}
},
"pkg-config-x86_64-linux": {
"type": "ghrel",
"repo": "static-php/static-php-cli-hosted",
"match": "pkg-config-x86_64-linux-musl-1.2.5.txz",
"extract-files": {
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
}
},
"pkg-config-x86_64-macos": {
"type": "ghrel",
"repo": "static-php/static-php-cli-hosted",
"match": "pkg-config-x86_64-darwin.txz",
"extract-files": {
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
}
},
"strawberry-perl-x86_64-win": {
"type": "url",
"url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip"
},
"upx-aarch64-linux": {
"type": "ghrel",
"repo": "upx/upx",
"match": "upx.+-arm64_linux\\.tar\\.xz",
"extract-files": {
"upx": "{pkg_root_path}/bin/upx"
}
},
"upx-x86_64-linux": {
"type": "ghrel",
"repo": "upx/upx",
"match": "upx.+-amd64_linux\\.tar\\.xz",
"extract-files": {
"upx": "{pkg_root_path}/bin/upx"
}
},
"upx-x86_64-win": {
"type": "ghrel",
"repo": "upx/upx",
"match": "upx.+-win64\\.zip",
"extract-files": {
"upx.exe": "{pkg_root_path}/bin/upx.exe"
}
},
"zig-aarch64-linux": {
"type": "custom"
},
"zig-aarch64-macos": {
"type": "custom"
},
"zig-x86_64-linux": {
"type": "custom"
},
"zig-x86_64-macos": {
"type": "custom"
},
"zig-x86_64-win": {
"type": "custom"
}
}

View File

@@ -1,992 +0,0 @@
{
"attr": {
"type": "library",
"artifact": "attr",
"license": {
"type": "file",
"path": "doc/COPYING.LGPL"
}
},
"brotli": {
"type": "library",
"headers": [
"brotli"
],
"pkg-configs": [
"libbrotlicommon",
"libbrotlidec",
"libbrotlienc"
],
"artifact": "brotli",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"bzip2": {
"type": "library",
"headers": [
"bzlib.h"
],
"artifact": "bzip2",
"license": {
"type": "text",
"text": "This program, \"bzip2\", the associated library \"libbzip2\", and all documentation, are copyright (C) 1996-2010 Julian R Seward. All rights reserved. \n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n 2. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.\n 3. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.\n 4. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nJulian Seward, jseward@bzip.org bzip2/libbzip2 version 1.0.6 of 6 September 2010\n\nPATENTS: To the best of my knowledge, bzip2 and libbzip2 do not use any patented algorithms. However, I do not have the resources to carry out a patent search. Therefore I cannot give any guarantee of the above statement."
}
},
"curl": {
"type": "library",
"depends@windows": [
"zlib",
"libssh2",
"nghttp2"
],
"depends": [
"openssl",
"zlib"
],
"suggests@windows": [
"brotli",
"zstd"
],
"suggests": [
"libssh2",
"brotli",
"nghttp2",
"nghttp3",
"ngtcp2",
"zstd",
"libcares",
"ldap"
],
"headers": [
"curl"
],
"frameworks": [
"CoreFoundation",
"CoreServices",
"SystemConfiguration"
],
"artifact": "curl",
"license": {
"type": "file",
"path": "COPYING"
}
},
"fastlz": {
"type": "library",
"headers": [
"fastlz/fastlz.h"
],
"artifact": "fastlz",
"license": {
"type": "file",
"path": "LICENSE.MIT"
}
},
"freetype": {
"type": "library",
"depends": [
"zlib"
],
"suggests": [
"libpng",
"bzip2",
"brotli"
],
"headers": [
"freetype2/freetype/freetype.h",
"freetype2/ft2build.h"
],
"artifact": "freetype",
"license": {
"type": "file",
"path": "LICENSE.TXT"
}
},
"gettext": {
"type": "library",
"depends": [
"libiconv"
],
"suggests": [
"ncurses",
"libxml2"
],
"frameworks": [
"CoreFoundation"
],
"artifact": "gettext",
"license": {
"type": "file",
"path": "gettext-runtime/intl/COPYING.LIB"
}
},
"glfw": {
"type": "library",
"frameworks": [
"CoreVideo",
"OpenGL",
"Cocoa",
"IOKit"
],
"artifact": "ext-glfw",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"gmp": {
"type": "library",
"headers": [
"gmp.h"
],
"artifact": "gmp",
"license": {
"type": "text",
"text": "Since version 6, GMP is distributed under the dual licenses, GNU LGPL v3 and GNU GPL v2. These licenses make the library free to use, share, and improve, and allow you to pass on the result. The GNU licenses give freedoms, but also set firm restrictions on the use with non-free programs."
}
},
"gmssl": {
"type": "library",
"frameworks": [
"Security"
],
"artifact": "gmssl",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"grpc": {
"type": "library",
"depends": [
"zlib",
"openssl",
"libcares"
],
"pkg-configs": [
"grpc"
],
"frameworks": [
"CoreFoundation"
],
"artifact": "grpc",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"icu": {
"type": "library",
"pkg-configs": [
"icu-uc",
"icu-i18n",
"icu-io"
],
"artifact": "icu",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"icu-static-win": {
"type": "library",
"headers@windows": [
"unicode"
],
"artifact": "icu-static-win",
"license": {
"type": "text",
"text": "none"
}
},
"imagemagick": {
"type": "library",
"depends": [
"zlib",
"libjpeg",
"libjxl",
"libpng",
"libwebp",
"freetype",
"libtiff",
"libheif",
"bzip2"
],
"suggests": [
"zstd",
"xz",
"libzip",
"libxml2"
],
"pkg-configs": [
"Magick++-7.Q16HDRI",
"MagickCore-7.Q16HDRI",
"MagickWand-7.Q16HDRI"
],
"artifact": "imagemagick",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"imap": {
"type": "library",
"suggests": [
"openssl"
],
"artifact": "imap",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"jbig": {
"type": "library",
"headers": [
"jbig.h",
"jbig85.h",
"jbig_ar.h"
],
"artifact": "jbig",
"license": {
"type": "file",
"path": "COPYING"
}
},
"ldap": {
"type": "library",
"depends": [
"openssl",
"zlib",
"gmp",
"libsodium"
],
"pkg-configs": [
"ldap",
"lber"
],
"artifact": "ldap",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"lerc": {
"type": "library",
"artifact": "lerc",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libacl": {
"type": "library",
"depends": [
"attr"
],
"artifact": "libacl",
"license": {
"type": "file",
"path": "doc/COPYING.LGPL"
}
},
"libaom": {
"type": "library",
"artifact": "libaom",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libargon2": {
"type": "library",
"artifact": "libargon2",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libavif": {
"type": "library",
"artifact": "libavif",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libcares": {
"type": "library",
"headers": [
"ares.h",
"ares_dns.h",
"ares_nameser.h"
],
"artifact": "libcares",
"license": {
"type": "file",
"path": "LICENSE.md"
}
},
"libde265": {
"type": "library",
"artifact": "libde265",
"license": {
"type": "file",
"path": "COPYING"
}
},
"libedit": {
"type": "library",
"depends": [
"ncurses"
],
"artifact": "libedit",
"license": {
"type": "file",
"path": "COPYING"
}
},
"libevent": {
"type": "library",
"depends": [
"openssl"
],
"artifact": "libevent",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libffi": {
"type": "library",
"headers@windows": [
"ffi.h",
"fficonfig.h",
"ffitarget.h"
],
"headers": [
"ffi.h",
"ffitarget.h"
],
"artifact": "libffi",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libffi-win": {
"type": "library",
"headers@windows": [
"ffi.h",
"ffitarget.h",
"fficonfig.h"
],
"artifact": "libffi-win",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libheif": {
"type": "library",
"depends": [
"libde265",
"libwebp",
"libaom",
"zlib",
"brotli"
],
"artifact": "libheif",
"license": {
"type": "file",
"path": "COPYING"
}
},
"libiconv": {
"type": "library",
"headers": [
"iconv.h",
"libcharset.h",
"localcharset.h"
],
"artifact": "libiconv",
"license": {
"type": "file",
"path": "COPYING.LIB"
}
},
"libiconv-win": {
"type": "library",
"artifact": "libiconv-win",
"license": {
"type": "file",
"path": "source/COPYING"
}
},
"libjpeg": {
"type": "library",
"suggests@windows": [
"zlib"
],
"artifact": "libjpeg",
"license": {
"type": "file",
"path": "LICENSE.md"
}
},
"libjxl": {
"type": "library",
"depends": [
"brotli",
"libjpeg",
"libpng",
"libwebp"
],
"pkg-configs": [
"libjxl",
"libjxl_cms",
"libjxl_threads",
"libhwy"
],
"artifact": "libjxl",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"liblz4": {
"type": "library",
"artifact": "liblz4",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libmemcached": {
"type": "library",
"artifact": "libmemcached",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libpng": {
"type": "library",
"depends": [
"zlib"
],
"headers@windows": [
"png.h",
"pngconf.h"
],
"headers": [
"png.h",
"pngconf.h",
"pnglibconf.h"
],
"artifact": "libpng",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"librabbitmq": {
"type": "library",
"depends": [
"openssl"
],
"artifact": "librabbitmq",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"librdkafka": {
"type": "library",
"suggests": [
"curl",
"liblz4",
"openssl",
"zlib",
"zstd"
],
"pkg-configs": [
"rdkafka++-static",
"rdkafka-static"
],
"artifact": "librdkafka",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libsodium": {
"type": "library",
"artifact": "libsodium",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"libssh2": {
"type": "library",
"depends": [
"openssl"
],
"headers": [
"libssh2.h",
"libssh2_publickey.h",
"libssh2_sftp.h"
],
"artifact": "libssh2",
"license": {
"type": "file",
"path": "COPYING"
}
},
"libtiff": {
"type": "library",
"depends": [
"zlib",
"libjpeg"
],
"suggests": [
"lerc",
"libwebp",
"jbig",
"xz",
"zstd"
],
"artifact": "libtiff",
"license": {
"type": "file",
"path": "LICENSE.md"
}
},
"liburing": {
"type": "library",
"headers@linux": [
"liburing/",
"liburing.h"
],
"pkg-configs": [
"liburing",
"liburing-ffi"
],
"artifact": "liburing",
"license": {
"type": "file",
"path": "COPYING"
}
},
"libuuid": {
"type": "library",
"headers": [
"uuid/uuid.h"
],
"artifact": "libuuid",
"license": {
"type": "file",
"path": "COPYING"
}
},
"libuv": {
"type": "library",
"artifact": "libuv",
"license": [
{
"type": "file",
"path": "LICENSE"
},
{
"type": "file",
"path": "LICENSE-extra"
}
]
},
"libwebp": {
"type": "library",
"pkg-configs": [
"libwebp",
"libwebpdecoder",
"libwebpdemux",
"libwebpmux",
"libsharpyuv"
],
"artifact": "libwebp",
"license": {
"type": "file",
"path": "COPYING"
}
},
"libxml2": {
"type": "library",
"depends@windows": [
"libiconv-win"
],
"depends": [
"libiconv"
],
"suggests@windows": [
"zlib"
],
"suggests": [
"xz",
"zlib"
],
"headers": [
"libxml2"
],
"pkg-configs": [
"libxml-2.0"
],
"artifact": "libxml2",
"license": {
"type": "file",
"path": "Copyright"
}
},
"libxslt": {
"type": "library",
"depends": [
"libxml2"
],
"artifact": "libxslt",
"license": {
"type": "file",
"path": "Copyright"
}
},
"libyaml": {
"type": "library",
"headers": [
"yaml.h"
],
"artifact": "libyaml",
"license": {
"type": "file",
"path": "License"
}
},
"libzip": {
"type": "library",
"depends@windows": [
"zlib",
"bzip2",
"xz"
],
"depends": [
"zlib"
],
"suggests@windows": [
"zstd",
"openssl"
],
"suggests": [
"bzip2",
"xz",
"zstd",
"openssl"
],
"headers": [
"zip.h",
"zipconf.h"
],
"artifact": "libzip",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"mimalloc": {
"type": "library",
"artifact": "mimalloc",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"ncurses": {
"type": "library",
"artifact": "ncurses",
"static-libs@unix": [
"libncurses.a"
],
"license": {
"type": "file",
"path": "COPYING"
}
},
"net-snmp": {
"type": "library",
"depends": [
"openssl",
"zlib"
],
"pkg-configs": [
"netsnmp",
"netsnmp-agent"
],
"artifact": "net-snmp",
"license": {
"type": "file",
"path": "COPYING"
}
},
"nghttp2": {
"type": "library",
"depends": [
"zlib",
"openssl"
],
"suggests": [
"libxml2",
"nghttp3",
"ngtcp2"
],
"headers": [
"nghttp2"
],
"artifact": "nghttp2",
"license": {
"type": "file",
"path": "COPYING"
}
},
"nghttp3": {
"type": "library",
"depends": [
"openssl"
],
"headers": [
"nghttp3"
],
"artifact": "nghttp3",
"license": {
"type": "file",
"path": "COPYING"
}
},
"ngtcp2": {
"type": "library",
"depends": [
"openssl"
],
"suggests": [
"nghttp3",
"brotli"
],
"headers": [
"ngtcp2"
],
"artifact": "ngtcp2",
"license": {
"type": "file",
"path": "COPYING"
}
},
"onig": {
"type": "library",
"headers": [
"oniggnu.h",
"oniguruma.h"
],
"artifact": "onig",
"license": {
"type": "file",
"path": "COPYING"
}
},
"openssl": {
"type": "library",
"depends": [
"zlib"
],
"headers": [
"openssl"
],
"artifact": "openssl",
"license": {
"type": "file",
"path": "LICENSE.txt"
}
},
"postgresql": {
"type": "library",
"depends": [
"libiconv",
"libxml2",
"openssl",
"zlib",
"libedit"
],
"suggests": [
"icu",
"libxslt",
"ldap",
"zstd"
],
"pkg-configs": [
"libpq"
],
"artifact": "postgresql",
"license": {
"type": "file",
"path": "COPYRIGHT"
}
},
"postgresql-win": {
"type": "library",
"artifact": "postgresql-win",
"license": {
"type": "text",
"text": "PostgreSQL Database Management System\n(also known as Postgres, formerly as Postgres95)\n\nPortions Copyright (c) 1996-2025, The PostgreSQL Global Development Group\n\nPortions Copyright (c) 1994, The Regents of the University of California\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose, without fee, and without a written\nagreement is hereby granted, provided that the above copyright notice\nand this paragraph and the following two paragraphs appear in all\ncopies.\n\nIN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY\nFOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,\nINCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\nDOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE.\n\nTHE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS\nON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS\nTO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS."
}
},
"pthreads4w": {
"type": "library",
"artifact": "pthreads4w",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"qdbm": {
"type": "library",
"headers@windows": [
"depot.h"
],
"artifact": "qdbm",
"license": {
"type": "file",
"path": "COPYING"
}
},
"re2c": {
"type": "library",
"artifact": "re2c",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"readline": {
"type": "library",
"depends": [
"ncurses"
],
"artifact": "readline",
"license": {
"type": "file",
"path": "COPYING"
}
},
"snappy": {
"type": "library",
"depends": [
"zlib"
],
"headers": [
"snappy.h",
"snappy-c.h",
"snappy-sinksource.h",
"snappy-stubs-public.h"
],
"artifact": "snappy",
"license": {
"type": "file",
"path": "COPYING"
}
},
"sqlite": {
"type": "library",
"headers": [
"sqlite3.h",
"sqlite3ext.h"
],
"artifact": "sqlite",
"license": {
"type": "text",
"text": "The author disclaims copyright to this source code. In place of\na legal notice, here is a blessing:\n\n * May you do good and not evil.\n * May you find forgiveness for yourself and forgive others.\n * May you share freely, never taking more than you give."
}
},
"tidy": {
"type": "library",
"artifact": "tidy",
"license": {
"type": "file",
"path": "README/LICENSE.md"
}
},
"unixodbc": {
"type": "library",
"depends": [
"libiconv"
],
"artifact": "unixodbc",
"license": {
"type": "file",
"path": "COPYING"
}
},
"watcher": {
"type": "library",
"headers": [
"wtr/watcher-c.h"
],
"artifact": "watcher",
"license": {
"type": "file",
"path": "license"
}
},
"xz": {
"type": "library",
"depends": [
"libiconv"
],
"headers@windows": [
"lzma",
"lzma.h"
],
"headers": [
"lzma"
],
"artifact": "xz",
"license": {
"type": "file",
"path": "COPYING"
}
},
"zlib": {
"type": "library",
"headers": [
"zlib.h",
"zconf.h"
],
"artifact": "zlib",
"license": {
"type": "text",
"text": "(C) 1995-2022 Jean-loup Gailly and Mark Adler\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not\n claim that you wrote the original software. If you use this software\n in a product, an acknowledgment in the product documentation would be\n appreciated but is not required.\n2. Altered source versions must be plainly marked as such, and must not be\n misrepresented as being the original software.\n3. This notice may not be removed or altered from any source distribution.\n\nJean-loup Gailly Mark Adler\njloup@gzip.org madler@alumni.caltech.edu"
}
},
"zstd": {
"type": "library",
"headers@windows": [
"zstd.h",
"zstd_errors.h"
],
"headers": [
"zdict.h",
"zstd.h",
"zstd_errors.h"
],
"artifact": "zstd",
"license": {
"type": "file",
"path": "LICENSE"
}
}
}

View File

@@ -1,95 +0,0 @@
{
"vswhere": {
"type": "target",
"artifact": "vswhere"
},
"pkg-config": {
"type": "target",
"static-bins": [
"pkg-config"
],
"artifact": "pkg-config"
},
"php": {
"type": "target",
"artifact": "php-src",
"depends@macos": [
"libxml2"
]
},
"php-cli": {
"type": "virtual-target",
"depends": [
"php"
]
},
"php-micro": {
"type": "virtual-target",
"artifact": "micro",
"depends": [
"php"
]
},
"php-cgi": {
"type": "virtual-target",
"depends": [
"php"
]
},
"php-fpm": {
"type": "virtual-target",
"depends": [
"php"
]
},
"php-embed": {
"type": "virtual-target",
"depends": [
"php"
]
},
"frankenphp": {
"type": "virtual-target",
"artifact": "frankenphp",
"depends": [
"php-embed",
"go-xcaddy"
],
"depends@macos": [
"php-embed",
"go-xcaddy",
"libxml2"
]
},
"go-xcaddy": {
"type": "target",
"artifact": "go-xcaddy",
"static-bins": [
"xcaddy"
]
},
"musl-toolchain": {
"type": "target",
"artifact": "musl-toolchain"
},
"strawberry-perl": {
"type": "target",
"artifact": "strawberry-perl"
},
"upx": {
"type": "target",
"artifact": "upx"
},
"zig": {
"type": "target",
"artifact": "zig"
},
"nasm": {
"type": "target",
"artifact": "nasm"
},
"php-sdk-binary-tools": {
"type": "target",
"artifact": "php-sdk-binary-tools"
}
}

View File

@@ -126,13 +126,23 @@
},
"ext-event": {
"type": "url",
"url": "https://bitbucket.org/osmanov/pecl-event/get/3.0.8.tar.gz",
"url": "https://bitbucket.org/osmanov/pecl-event/get/3.1.4.tar.gz",
"path": "php-src/ext/event",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"ext-excimer": {
"type": "url",
"url": "https://pecl.php.net/get/excimer",
"path": "php-src/ext/excimer",
"filename": "excimer.tgz",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"ext-glfw": {
"type": "git",
"url": "https://github.com/mario-deluna/php-glfw",
@@ -151,6 +161,18 @@
"path": "LICENSE"
}
},
"ext-grpc": {
"type": "url",
"url": "https://pecl.php.net/get/grpc",
"path": "php-src/ext/grpc",
"filename": "grpc.tgz",
"license": {
"type": "file",
"path": [
"LICENSE"
]
}
},
"ext-imagick": {
"type": "url",
"url": "https://pecl.php.net/get/imagick",
@@ -339,7 +361,7 @@
},
"gmp": {
"type": "filelist",
"url": "https://gmplib.org/download/gmp/",
"url": "https://ftp.gnu.org/gnu/gmp/",
"regex": "/href=\"(?<file>gmp-(?<version>[^\"]+)\\.tar\\.xz)\"/",
"provide-pre-built": true,
"alt": {
@@ -504,7 +526,7 @@
"libavif": {
"type": "ghtar",
"repo": "AOMediaCodec/libavif",
"provide-pre-built": true,
"provide-pre-built": false,
"license": {
"type": "file",
"path": "LICENSE"
@@ -619,6 +641,7 @@
"libjpeg": {
"type": "ghtar",
"repo": "libjpeg-turbo/libjpeg-turbo",
"prefer-stable": true,
"license": {
"type": "file",
"path": "LICENSE.md"
@@ -670,9 +693,10 @@
}
},
"libpng": {
"type": "git",
"url": "https://github.com/glennrp/libpng.git",
"rev": "libpng16",
"type": "ghtagtar",
"repo": "pnggroup/libpng",
"match": "v1\\.6\\.\\d+",
"query": "?per_page=150",
"provide-pre-built": true,
"license": {
"type": "file",
@@ -680,9 +704,9 @@
}
},
"librabbitmq": {
"type": "git",
"url": "https://github.com/alanxz/rabbitmq-c.git",
"rev": "master",
"type": "ghtar",
"repo": "alanxz/rabbitmq-c",
"prefer-stable": true,
"license": {
"type": "file",
"path": "LICENSE"
@@ -699,7 +723,7 @@
"libsodium": {
"type": "ghrel",
"repo": "jedisct1/libsodium",
"match": "libsodium-\\d+(\\.\\d+)*\\.tar\\.gz",
"match": "libsodium-(?!1\\.0\\.21)\\d+(\\.\\d+)*\\.tar\\.gz",
"prefer-stable": true,
"provide-pre-built": true,
"license": {
@@ -771,8 +795,9 @@
]
},
"libwebp": {
"type": "url",
"url": "https://github.com/webmproject/libwebp/archive/refs/tags/v1.3.2.tar.gz",
"type": "ghtagtar",
"repo": "webmproject/libwebp",
"match": "v1\\.\\d+\\.\\d+$",
"provide-pre-built": true,
"license": {
"type": "file",
@@ -780,8 +805,10 @@
}
},
"libxml2": {
"type": "url",
"url": "https://github.com/GNOME/libxml2/archive/refs/tags/v2.12.5.tar.gz",
"type": "ghtagtar",
"repo": "GNOME/libxml2",
"match": "v2\\.\\d+\\.\\d+$",
"provide-pre-built": false,
"license": {
"type": "file",
"path": "Copyright"
@@ -868,6 +895,24 @@
"path": "LICENSE"
}
},
"mysqlnd_ed25519": {
"type": "pie",
"repo": "mariadb/mysqlnd_ed25519",
"path": "php-src/ext/mysqlnd_ed25519",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"mysqlnd_parsec": {
"type": "pie",
"repo": "mariadb/mysqlnd_parsec",
"path": "php-src/ext/mysqlnd_parsec",
"license": {
"type": "file",
"path": "LICENSE"
}
},
"ncurses": {
"type": "filelist",
"url": "https://ftp.gnu.org/pub/gnu/ncurses/",
@@ -1010,7 +1055,7 @@
},
"protobuf": {
"type": "url",
"url": "https://pecl.php.net/get/protobuf",
"url": "https://pecl.php.net/get/protobuf-5.34.1.tgz",
"path": "php-src/ext/protobuf",
"filename": "protobuf.tgz",
"license": {
@@ -1169,9 +1214,8 @@
}
},
"xdebug": {
"type": "url",
"url": "https://pecl.php.net/get/xdebug",
"filename": "xdebug.tgz",
"type": "pie",
"repo": "xdebug/xdebug",
"license": {
"type": "file",
"path": "LICENSE"

View File

@@ -16,8 +16,10 @@ while also defining the extensions to compile.
1. Fork project.
2. Go to the Actions of the project and select `CI`.
3. Select `Run workflow`, fill in the PHP version you want to compile, the target type, and the list of extensions. (extensions comma separated, e.g. `bcmath,curl,mbstring`)
4. After waiting for about a period of time, enter the corresponding task and get `Artifacts`.
3. Select `Run workflow`, fill in the PHP version you want to compile, the target type, and the list of static extensions. (comma separated, e.g. `bcmath,curl,mbstring`)
4. If you need shared extensions (for example `xdebug`), set `shared-extensions` (comma separated, e.g. `xdebug`).
5. If you need FrankenPHP, enable `build-frankenphp` and also enable `enable-zts`.
6. After waiting for about a period of time, enter the corresponding task and get `Artifacts`.
If you enable `debug`, all logs will be output at build time, including compiled logs, for troubleshooting.

View File

@@ -549,22 +549,24 @@ otherwise it will be executed repeatedly in other events.
The following are the supported `patch_point` event names and corresponding locations:
| Event name | Event description |
|------------------------------|----------------------------------------------------------------------------------------------------|
| before-libs-extract | Triggered before the dependent libraries extracted |
| after-libs-extract | Triggered after the compiled dependent libraries extracted |
| before-php-extract | Triggered before PHP source code extracted |
| after-php-extract | Triggered after PHP source code extracted |
| before-micro-extract | Triggered before phpmicro extract |
| after-micro-extract | Triggered after phpmicro extracted |
| before-exts-extract | Triggered before the extension (to be compiled) extracted to the PHP source directory |
| after-exts-extract | Triggered after the extension extracted to the PHP source directory |
| before-library[*name*]-build | Triggered before the library named `name` is compiled (such as `before-library[postgresql]-build`) |
| after-library[*name*]-build | Triggered after the library named `name` is compiled |
| before-php-buildconf | Triggered before compiling PHP command `./buildconf` |
| before-php-configure | Triggered before compiling PHP command `./configure` |
| before-php-make | Triggered before compiling PHP command `make` |
| before-sanity-check | Triggered after compiling PHP but before running extended checks |
| Event name | Event description |
|---------------------------------|----------------------------------------------------------------------------------------------------|
| before-libs-extract | Triggered before the dependent libraries extracted |
| after-libs-extract | Triggered after the compiled dependent libraries extracted |
| before-php-extract | Triggered before PHP source code extracted |
| after-php-extract | Triggered after PHP source code extracted |
| before-micro-extract | Triggered before phpmicro extract |
| after-micro-extract | Triggered after phpmicro extracted |
| before-exts-extract | Triggered before the extension (to be compiled) extracted to the PHP source directory |
| after-exts-extract | Triggered after the extension extracted to the PHP source directory |
| before-library[*name*]-build | Triggered before the library named `name` is compiled (such as `before-library[postgresql]-build`) |
| after-library[*name*]-build | Triggered after the library named `name` is compiled |
| after-shared-ext[*name*]-build | Triggered after the shared extension named `name` is compiled |
| before-shared-ext[*name*]-build | Triggered before the shared extension named `name` is compiled |
| before-php-buildconf | Triggered before compiling PHP command `./buildconf` |
| before-php-configure | Triggered before compiling PHP command `./configure` |
| before-php-make | Triggered before compiling PHP command `make` |
| before-sanity-check | Triggered after compiling PHP but before running extended checks |
The following is a simple example of temporarily modifying the PHP source code.
Enable the CLI function to search for the `php.ini` configuration in the current working directory:

View File

@@ -14,7 +14,9 @@ Action 构建指的是直接使用 GitHub Action 进行编译。
1. Fork 本项目。
2. 进入项目的 Actions选择 CI 开头的 Workflow根据你需要的操作系统选择
3. 选择 `Run workflow`,填入你要编译的 PHP 版本、目标类型、扩展列表。(扩展列表使用英文逗号分割,例如 `bcmath,curl,mbstring`
4. 等待大约一段时间后,进入对应的任务中,获取 `Artifacts`
4. 如果需要共享扩展(例如 `xdebug`),请设置 `shared-extensions`(使用英文逗号分割,例如 `xdebug`
5. 如果需要 FrankenPHP请启用 `build-frankenphp`,同时也需要启用 `enable-zts`
6. 等待大约一段时间后,进入对应的任务中,获取 `Artifacts`
如果你选择了 `debug`,则会在构建时输出所有日志,包括编译的日志,以供排查错误。

View File

@@ -500,6 +500,8 @@ bin/spc dev:sort-config ext
| after-exts-extract | 在要编译的扩展解压到 PHP 源码目录后触发 |
| before-library[*name*]-build | 在名称为 `name` 的库编译前触发(如 `before-library[postgresql]-build` |
| after-library[*name*]-build | 在名称为 `name` 的库编译后触发 |
| after-shared-ext[*name*]-build | 在名称为 `name` 的共享扩展编译后触发(如 `after-shared-ext[redis]-build` |
| before-shared-ext[*name*]-build | 在名称为 `name` 的共享扩展编译前触发 |
| before-php-buildconf | 在编译 PHP 命令 `./buildconf` 前触发 |
| before-php-configure | 在编译 PHP 命令 `./configure` 前触发 |
| before-php-make | 在编译 PHP 命令 `make` 前触发 |

View File

@@ -17,4 +17,3 @@ parameters:
- ./src/globals/ext-tests/swoole.php
- ./src/globals/ext-tests/swoole.phpt
- ./src/globals/test-extensions.php
- ./src/SPC/

View File

@@ -1,32 +0,0 @@
{
"name": "internal",
"autoload": "vendor/autoload.php",
"doctor": {
"psr-4": {
"StaticPHP\\Doctor\\Item": "src/StaticPHP/Doctor/Item"
}
},
"package": {
"psr-4": {
"Package": "src/Package"
},
"config": [
"config/pkg.ext.json",
"config/pkg.lib.json",
"config/pkg.target.json"
]
},
"artifact": {
"config": [
"config/artifact.json"
],
"psr-4": {
"Package\\Artifact": "src/Package/Artifact"
}
},
"command": {
"psr-4": {
"Package\\Command": "src/Package/Command"
}
}
}

View File

@@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Artifact;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\System\LinuxUtil;
class go_xcaddy
{
#[CustomBinary('go-xcaddy', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function downBinary(ArtifactDownloader $downloader): DownloadResult
{
$pkgroot = PKG_ROOT_PATH;
$name = SystemTarget::getCurrentPlatformString();
$arch = match (explode('-', $name)[1]) {
'x86_64' => 'amd64',
'aarch64' => 'arm64',
default => throw new DownloaderException('Unsupported architecture: ' . $name),
};
$os = match (explode('-', $name)[0]) {
'linux' => 'linux',
'macos' => 'darwin',
default => throw new DownloaderException('Unsupported OS: ' . $name),
};
// get version and hash
[$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: '');
if ($version === '') {
throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text');
}
$page = default_shell()->executeCurl('https://go.dev/dl/');
if ($page === '' || $page === false) {
throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/');
}
$version_regex = str_replace('.', '\.', $version);
$pattern = "/href=\"\\/dl\\/{$version_regex}\\.{$os}-{$arch}\\.tar\\.gz\">.*?<tt>([a-f0-9]{64})<\\/tt>/s";
if (preg_match($pattern, $page, $matches)) {
$hash = $matches[1];
} else {
throw new DownloaderException("Failed to find download hash for Go {$version} {$os}-{$arch}");
}
$url = "https://go.dev/dl/{$version}.{$os}-{$arch}.tar.gz";
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "{$version}.{$os}-{$arch}.tar.gz";
default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
// verify hash
$file_hash = hash_file('sha256', $path);
if ($file_hash !== $hash) {
throw new DownloaderException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}");
}
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version);
}
#[AfterBinaryExtract('go-xcaddy', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function afterExtract(string $target_path): void
{
if (file_exists("{$target_path}/bin/go") && file_exists("{$target_path}/bin/xcaddy")) {
return;
}
$sanitizedPath = getenv('PATH');
if (PHP_OS_FAMILY === 'Linux' && !LinuxUtil::isMuslDist()) {
$sanitizedPath = preg_replace('#(:?/?[^:]*musl[^:]*)#', '', $sanitizedPath);
$sanitizedPath = preg_replace('#^:|:$|::#', ':', $sanitizedPath); // clean up colons
}
shell()->appendEnv([
'PATH' => "{$target_path}/bin:{$sanitizedPath}",
'GOROOT' => "{$target_path}",
'GOBIN' => "{$target_path}/bin",
'GOPATH' => "{$target_path}/go",
])->exec('CC=cc go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest');
GlobalEnvManager::addPathIfNotExists("{$target_path}/bin");
}
}

View File

@@ -1,98 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Artifact;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Runtime\SystemTarget;
class zig
{
#[CustomBinary('zig', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function downBinary(ArtifactDownloader $downloader): DownloadResult
{
$index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry());
$index_json = json_decode($index_json ?: '', true);
$latest_version = null;
foreach ($index_json as $version => $data) {
$latest_version = $version;
break;
}
if (!$latest_version) {
throw new DownloaderException('Could not determine latest Zig version');
}
$zig_arch = SystemTarget::getTargetArch();
$zig_os = match (SystemTarget::getTargetOS()) {
'Windows' => 'win',
'Darwin' => 'macos',
'Linux' => 'linux',
default => throw new DownloaderException('Unsupported OS for Zig: ' . SystemTarget::getTargetOS()),
};
$platform_key = "{$zig_arch}-{$zig_os}";
if (!isset($index_json[$latest_version][$platform_key])) {
throw new DownloaderException("No download available for {$platform_key} in Zig version {$latest_version}");
}
$download_info = $index_json[$latest_version][$platform_key];
$url = $download_info['tarball'];
$sha256 = $download_info['shasum'];
$filename = basename($url);
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
// verify hash
$file_hash = hash_file('sha256', $path);
if ($file_hash !== $sha256) {
throw new DownloaderException("Hash mismatch for downloaded Zig binary. Expected {$sha256}, got {$file_hash}");
}
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version);
}
#[AfterBinaryExtract('zig', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function postExtractZig(string $target_path): void
{
$files = ['zig', 'zig-cc', 'zig-c++', 'zig-ar', 'zig-ld.lld', 'zig-ranlib', 'zig-objcopy'];
$all_exist = true;
foreach ($files as $file) {
if (!file_exists("{$target_path}/{$file}")) {
$all_exist = false;
break;
}
}
if ($all_exist) {
return;
}
$script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh';
$script_content = file_get_contents($script_path);
file_put_contents("{$target_path}/zig-cc", $script_content);
chmod("{$target_path}/zig-cc", 0755);
$script_content = str_replace('zig cc', 'zig c++', $script_content);
file_put_contents("{$target_path}/zig-c++", $script_content);
file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@");
file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@");
file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@");
file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@");
chmod("{$target_path}/zig-c++", 0755);
chmod("{$target_path}/zig-ar", 0755);
chmod("{$target_path}/zig-ld.lld", 0755);
chmod("{$target_path}/zig-ranlib", 0755);
chmod("{$target_path}/zig-objcopy", 0755);
}
}

View File

@@ -1,121 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Command;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\DownloaderOptions;
use StaticPHP\Command\BaseCommand;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
#[AsCommand('switch-php-version', description: 'Switch downloaded PHP version')]
class SwitchPhpVersionCommand extends BaseCommand
{
protected bool $no_motd = true;
public function configure(): void
{
$this->addArgument(
'php-version',
InputArgument::REQUIRED,
'PHP version (e.g., 8.4, 8.3, 8.2, 8.1, 8.0, 7.4, or specific like 8.4.5)',
);
// Downloader options
$this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions());
// Additional options
$this->addOption('keep-source', null, null, 'Keep extracted source directory (do not remove source/php-src)');
}
public function handle(): int
{
$php_ver = $this->getArgument('php-version');
// Validate version format
if (!$this->isValidPhpVersion($php_ver)) {
$this->output->writeln("<error>Invalid PHP version '{$php_ver}'!</error>");
$this->output->writeln('<comment>Supported formats: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, or specific version like 8.4.5</comment>');
return static::FAILURE;
}
$cache = ApplicationContext::get(ArtifactCache::class);
// Check if php-src is already locked
$source_info = $cache->getSourceInfo('php-src');
if ($source_info !== null) {
$current_version = $source_info['version'] ?? 'unknown';
$this->output->writeln("<info>Current PHP version: {$current_version}, removing old PHP source cache...");
// Remove cache entry and optionally the downloaded file
$cache->removeSource('php-src', delete_file: true);
}
// Remove extracted source directory if exists and --keep-source not set
$source_dir = SOURCE_PATH . '/php-src';
if (!$this->getOption('keep-source') && is_dir($source_dir)) {
$this->output->writeln('<info>Removing extracted PHP source directory...</info>');
InteractiveTerm::indicateProgress('Removing: ' . $source_dir);
FileSystem::removeDir($source_dir);
InteractiveTerm::finish('Removed: ' . $source_dir);
}
// Download new PHP source
$this->output->writeln("<info>Downloading PHP {$php_ver} source...</info>");
$this->input->setOption('with-php', $php_ver);
$downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->input->getOptions());
$downloader = new ArtifactDownloader($downloaderOptions);
// Get php-src artifact from php package
$php_package = PackageLoader::getPackage('php');
$artifact = $php_package->getArtifact();
if ($artifact === null) {
$this->output->writeln('<error>Failed to get php-src artifact!</error>');
return static::FAILURE;
}
$downloader->add($artifact);
$downloader->download();
// Get the new version info
$new_source_info = $cache->getSourceInfo('php-src');
$new_version = $new_source_info['version'] ?? $php_ver;
$this->output->writeln('');
$this->output->writeln("<info>Successfully switched to PHP {$new_version}!</info>");
return static::SUCCESS;
}
/**
* Validate PHP version format.
*
* Accepts:
* - Major.Minor format: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4
* - Full version format: 8.4.5, 8.3.12, etc.
*/
private function isValidPhpVersion(string $version): bool
{
// Check major.minor format (e.g., 8.4)
if (in_array($version, ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'], true)) {
return true;
}
// Check full version format (e.g., 8.4.5)
if (preg_match('/^\d+\.\d+\.\d+$/', $version)) {
return true;
}
return false;
}
}

View File

@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Extension;
use Package\Target\php;
use StaticPHP\Attribute\Package\AfterStage;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Util\SourcePatcher;
#[Extension('readline')]
class readline
{
#[BeforeStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')]
#[PatchDescription('Fix readline static build with musl')]
public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
{
if ($toolchain->isStatic()) {
$php_src = $installer->getBuildPackage('php')->getSourceDir();
SourcePatcher::patchFile('musl_static_readline.patch', $php_src);
}
}
#[AfterStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')]
public function afterMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
{
if ($toolchain->isStatic()) {
$php_src = $installer->getBuildPackage('php')->getSourceDir();
SourcePatcher::patchFile('musl_static_readline.patch', $php_src, true);
}
}
}

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Library;
use Package\Target\php;
use StaticPHP\Attribute\Package\AfterStage;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
#[Library('imap')]
class imap
{
#[AfterStage('php', [php::class, 'patchEmbedScripts'], 'imap')]
#[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')]
public function afterPatchScripts(): void
{
if (SystemTarget::getLibc() === 'glibc') {
FileSystem::replaceFileRegex(BUILD_BIN_PATH . '/php-config', '/^libs="(.*)"$/m', 'libs="$1 -lcrypt"');
}
}
}

View File

@@ -1,37 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Library;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Util\FileSystem;
#[Library('libedit')]
class libedit extends LibraryPackage
{
#[BeforeStage(stage: 'build')]
public function patchBeforeBuild(): void
{
FileSystem::replaceFileRegex(
"{$this->getSourceDir()}/src/sys.h",
'|//#define\s+strl|',
'#define strl'
);
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(): void
{
UnixAutoconfExecutor::create($this)
->appendEnv(['CFLAGS' => '-D__STDC_ISO_10646__=201103L'])
->configure()
->make();
$this->patchPkgconfPrefix(['libedit.pc']);
}
}

View File

@@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Library;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
#[Library('libiconv')]
class libiconv
{
#[BuildFor('Darwin')]
public function build(LibraryPackage $package): void
{
UnixAutoconfExecutor::create($package)
->configure(
'--enable-extra-encodings',
'--enable-year2038',
)
->make('install-lib', with_install: false)
->make('install-lib', with_install: false, dir: "{$package->getSourceDir()}/libcharset");
$package->patchLaDependencyPrefix();
}
}

View File

@@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Library;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixCMakeExecutor;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
#[Library('libxml2')]
class libxml2
{
#[BuildFor('Darwin')]
public function build(LibraryPackage $package): void
{
$cmake = UnixCMakeExecutor::create($package)
->optionalPackage(
'zlib',
'-DLIBXML2_WITH_ZLIB=ON ' .
"-DZLIB_LIBRARY={$package->getLibDir()}/libz.a " .
"-DZLIB_INCLUDE_DIR={$package->getIncludeDir()}",
'-DLIBXML2_WITH_ZLIB=OFF',
)
->optionalPackage('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA'))
->addConfigureArgs(
'-DLIBXML2_WITH_ICONV=ON',
'-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md
'-DLIBXML2_WITH_PYTHON=OFF',
'-DLIBXML2_WITH_PROGRAMS=OFF',
'-DLIBXML2_WITH_TESTS=OFF',
);
if (SystemTarget::getTargetOS() === 'Linux') {
$cmake->addConfigureArgs('-DIconv_IS_BUILT_IN=OFF');
}
$cmake->build();
FileSystem::replaceFileStr(
BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc',
'-lxml2 -liconv',
'-lxml2'
);
FileSystem::replaceFileStr(
BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc',
'-lxml2',
'-lxml2 -liconv'
);
}
}

View File

@@ -1,61 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Library;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Util\DirDiff;
use StaticPHP\Util\FileSystem;
#[Library('ncurses')]
class ncurses
{
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(LibraryPackage $package, ToolchainInterface $toolchain): void
{
$dirdiff = new DirDiff(BUILD_BIN_PATH);
UnixAutoconfExecutor::create($package)
->appendEnv([
'LDFLAGS' => $toolchain->isStatic() ? '-static' : '',
])
->configure(
'--enable-overwrite',
'--with-curses-h',
'--enable-pc-files',
'--enable-echo',
'--disable-widec',
'--with-normal',
'--with-ticlib',
'--without-tests',
'--without-dlsym',
'--without-debug',
'--enable-symlinks',
"--bindir={$package->getBinDir()}",
"--includedir={$package->getIncludeDir()}",
"--libdir={$package->getLibDir()}",
"--prefix={$package->getBuildRootPath()}",
)
->make();
$new_files = $dirdiff->getIncrementFiles(true);
foreach ($new_files as $file) {
@unlink(BUILD_BIN_PATH . '/' . $file);
}
shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo');
shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo');
$pkgconf_list = ['form.pc', 'menu.pc', 'ncurses++.pc', 'ncurses.pc', 'panel.pc', 'tic.pc'];
$package->patchPkgconfPrefix($pkgconf_list);
foreach ($pkgconf_list as $pkgconf) {
FileSystem::replaceFileStr("{$package->getLibDir()}/pkgconfig/{$pkgconf}", "-L{$package->getLibDir()}", '-L${libdir}');
}
}
}

View File

@@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Library;
use Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\TargetPackage;
#[Library('postgresql')]
class postgresql
{
#[BeforeStage('php', [php::class, 'configureForUnix'], 'postgresql')]
#[PatchDescription('Patch to avoid explicit_bzero detection issues on some systems')]
public function patchBeforePHPConfigure(TargetPackage $package): void
{
shell()->cd($package->getSourceDir())
->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure');
}
}

View File

@@ -1,3 +0,0 @@
# Package Implementation
This directory contains the implementation of the `Package` module, which provides functionality for managing and manipulating packages within the system.

View File

@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Target;
use StaticPHP\Attribute\Package\InitPackage;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Util\GlobalEnvManager;
#[Target('go-xcaddy')]
class go_xcaddy
{
#[InitPackage]
public function init(): void
{
if (is_dir(PKG_ROOT_PATH . '/go-xcaddy/bin')) {
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '/go-xcaddy/bin');
GlobalEnvManager::putenv('GOROOT=' . PKG_ROOT_PATH . '/go-xcaddy');
GlobalEnvManager::putenv('GOBIN=' . PKG_ROOT_PATH . '/go-xcaddy/bin');
GlobalEnvManager::putenv('GOPATH=' . PKG_ROOT_PATH . '/go-xcaddy/go');
}
}
}

View File

@@ -1,22 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Target;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Util\FileSystem;
#[Target('php-micro')]
class micro
{
#[BeforeStage('php', [php::class, 'makeEmbedForUnix'], 'php-micro')]
#[PatchDescription('Patch Makefile to build only libphp.la for embedding')]
public function patchBeforeEmbed(TargetPackage $package): void
{
FileSystem::replaceFileStr("{$package->getSourceDir()}/Makefile", 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la');
}
}

View File

@@ -1,684 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Target;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Info;
use StaticPHP\Attribute\Package\InitPackage;
use StaticPHP\Attribute\Package\ResolveBuild;
use StaticPHP\Attribute\Package\Stage;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Attribute\Package\Validate;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\Package;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ToolchainManager;
use StaticPHP\Util\DirDiff;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\SourcePatcher;
use StaticPHP\Util\SPCConfigUtil;
use StaticPHP\Util\System\UnixUtil;
use StaticPHP\Util\V2CompatLayer;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use ZM\Logger\ConsoleColor;
#[Target('php')]
#[Target('php-cli')]
#[Target('php-fpm')]
#[Target('php-micro')]
#[Target('php-cgi')]
#[Target('php-embed')]
#[Target('frankenphp')]
class php extends TargetPackage
{
public static function getPHPVersionID(): int
{
$artifact = ArtifactLoader::getArtifactInstance('php-src');
if (!file_exists("{$artifact->getSourceDir()}/main/php_version.h")) {
throw new WrongUsageException('PHP source files are not available, you need to download them first');
}
$file = file_get_contents("{$artifact->getSourceDir()}/main/php_version.h");
if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0) {
return intval($match[1]);
}
throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again');
}
#[InitPackage]
public function init(TargetPackage $package): void
{
// universal build options (may move to base class later)
$package->addBuildOption('with-added-patch', 'P', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Inject patch script outside');
// basic build argument and options for PHP
$package->addBuildArgument('extensions', InputArgument::REQUIRED, 'Comma-separated list of static extensions to build');
$package->addBuildOption('no-strip', null, null, 'build without strip, keep symbols to debug');
$package->addBuildOption('with-upx-pack', null, null, 'Compress / pack binary using UPX tool (linux/windows only)');
// php configure and extra patch options
$package->addBuildOption('disable-opcache-jit', null, null, 'Disable opcache jit');
$package->addBuildOption('with-config-file-path', null, InputOption::VALUE_REQUIRED, 'Set the path in which to look for php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php');
$package->addBuildOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php/conf.d');
$package->addBuildOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI');
$package->addBuildOption('enable-zts', null, null, 'Enable thread safe support');
// phpmicro build options
if ($package->getName() === 'php' || $package->getName() === 'php-micro') {
$package->addBuildOption('with-micro-fake-cli', null, null, 'Let phpmicro\'s PHP_SAPI use "cli" instead of "micro"');
$package->addBuildOption('without-micro-ext-test', null, null, 'Disable phpmicro with extension test code');
$package->addBuildOption('with-micro-logo', null, InputOption::VALUE_REQUIRED, 'Use custom .ico for micro.sfx (windows only)');
$package->addBuildOption('enable-micro-win32', null, null, 'Enable win32 mode for phpmicro (Windows only)');
}
// frankenphp build options
if ($package->getName() === 'php' || $package->getName() === 'frankenphp') {
$package->addBuildOption('with-frankenphp-app', null, InputOption::VALUE_REQUIRED, 'Path to a folder to be embedded in FrankenPHP');
}
// embed build options
if ($package->getName() === 'php' || $package->getName() === 'php-embed') {
$package->addBuildOption('build-shared', 'D', InputOption::VALUE_REQUIRED, 'Shared extensions to build, comma separated', '');
}
// legacy php target build options
V2CompatLayer::addLegacyBuildOptionsForPhp($package);
if ($package->getName() === 'php') {
$package->addBuildOption('build-micro', null, null, 'Build micro SAPI');
$package->addBuildOption('build-cli', null, null, 'Build cli SAPI');
$package->addBuildOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)');
$package->addBuildOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)');
$package->addBuildOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)');
$package->addBuildOption('build-cgi', null, null, 'Build cgi SAPI');
$package->addBuildOption('build-all', null, null, 'Build all SAPI');
}
}
#[ResolveBuild]
public function resolveBuild(TargetPackage $package, PackageInstaller $installer): array
{
// Parse extensions and additional packages for all php-* targets
$static_extensions = parse_extension_list($package->getBuildArgument('extensions'));
$additional_libraries = parse_comma_list($package->getBuildOption('with-libs'));
$additional_packages = parse_comma_list($package->getBuildOption('with-packages'));
$additional_packages = array_merge($additional_libraries, $additional_packages);
$shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []);
$extensions_pkg = array_map(
fn ($x) => "ext-{$x}",
array_values(array_unique([...$static_extensions, ...$shared_extensions]))
);
// get instances
foreach ($extensions_pkg as $extension) {
$extname = substr($extension, 4);
$config = PackageConfig::get($extension, 'php-extension', []);
if (!PackageLoader::hasPackage($extension)) {
throw new WrongUsageException("Extension [{$extname}] does not exist. Please check your extension name.");
}
$instance = PackageLoader::getPackage($extension);
if (!$instance instanceof PhpExtensionPackage) {
throw new WrongUsageException("Package [{$extension}] is not a PHP extension package");
}
// set build static/shared
if (in_array($extname, $static_extensions)) {
if (($config['build-static'] ?? true) === false) {
throw new WrongUsageException("Extension [{$extname}] cannot be built as static extension.");
}
$instance->setBuildStatic();
}
if (in_array($extname, $shared_extensions)) {
if (($config['build-shared'] ?? true) === false) {
throw new WrongUsageException("Extension [{$extname}] cannot be built as shared extension, please remove it from --build-shared option.");
}
$instance->setBuildShared();
$instance->setBuildWithPhp($config['build-with-php'] ?? false);
}
}
// building shared extensions need embed SAPI
if (!empty($shared_extensions) && !$package->getBuildOption('build-embed', false) && $package->getName() === 'php') {
$installer->addBuildPackage('php-embed');
}
return [...$extensions_pkg, ...$additional_packages];
}
#[Validate]
public function validate(Package $package): void
{
// frankenphp
if ($package->getName() === 'frankenphp' && $package instanceof TargetPackage) {
if (!$package->getBuildOption('enable-zts')) {
throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!');
}
// frankenphp doesn't support windows, BSD is currently not supported by static-php-cli
if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) {
throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!');
}
}
// linux does not support loading shared libraries when target is pure static
$embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static';
if (SystemTarget::getTargetOS() === 'Linux' && ApplicationContext::get(ToolchainInterface::class)->isStatic() && $embed_type === 'shared') {
throw new WrongUsageException(
'Linux does not support loading shared libraries when linking libc statically. ' .
'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.'
);
}
}
#[Info]
public function info(Package $package, PackageInstaller $installer): array
{
/** @var TargetPackage $package */
if ($package->getName() !== 'php') {
return [];
}
$sapis = array_filter([
$installer->isPackageResolved('php-cli') ? 'cli' : null,
$installer->isPackageResolved('php-fpm') ? 'fpm' : null,
$installer->isPackageResolved('php-micro') ? 'micro' : null,
$installer->isPackageResolved('php-cgi') ? 'cgi' : null,
$installer->isPackageResolved('php-embed') ? 'embed' : null,
$installer->isPackageResolved('frankenphp') ? 'frankenphp' : null,
]);
$static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildStatic());
$shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []);
$install_packages = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() !== 'php-extension' && $x->getName() !== 'php' && !str_starts_with($x->getName(), 'php-'));
return [
'Build OS' => SystemTarget::getTargetOS() . ' (' . SystemTarget::getTargetArch() . ')',
'Build Target' => getenv('SPC_TARGET') ?: '',
'Build Toolchain' => ToolchainManager::getToolchainClass(),
'Build SAPI' => implode(', ', $sapis),
'Static Extensions (' . count($static_extensions) . ')' => implode(',', array_map(fn ($x) => substr($x->getName(), 4), $static_extensions)),
'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions),
'Install Packages (' . count($install_packages) . ')' => implode(',', array_map(fn ($x) => $x->getName(), $install_packages)),
];
}
#[BeforeStage('php', 'build')]
public function beforeBuild(PackageBuilder $builder, Package $package): void
{
// Process -I option
$custom_ini = [];
foreach ($builder->getOption('with-hardcoded-ini', []) as $value) {
[$source_name, $ini_value] = explode('=', $value, 2);
$custom_ini[$source_name] = $ini_value;
logger()->info("Adding hardcoded INI [{$source_name} = {$ini_value}]");
}
if (!empty($custom_ini)) {
SourcePatcher::patchHardcodedINI($package->getSourceDir(), $custom_ini);
}
// Patch StaticPHP version
// detect patch (remove this when 8.3 deprecated)
$file = FileSystem::readFile("{$package->getSourceDir()}/main/main.c");
if (!str_contains($file, 'static-php-cli.version')) {
$version = SPC_VERSION;
logger()->debug('Inserting static-php-cli.version to php-src');
$file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"static-php-cli.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file);
FileSystem::writeFile("{$package->getSourceDir()}/main/main.c", $file);
}
// clean old modules that may conflict with the new php build
FileSystem::removeDir(BUILD_MODULES_PATH);
}
#[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')]
#[PatchDescription('Patch configure.ac for musl and musl-toolchain')]
#[PatchDescription('Let php m4 tools use static pkg-config')]
public function patchBeforeBuildconf(TargetPackage $package): void
{
// patch configure.ac for musl and musl-toolchain
$musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl';
FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac');
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/configure.ac',
'if command -v ldd >/dev/null && ldd --version 2>&1 | grep ^musl >/dev/null 2>&1',
'if ' . ($musl ? 'true' : 'false')
);
// let php m4 tools use static pkg-config
FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC(');
}
#[Stage]
public function buildconfForUnix(TargetPackage $package): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf'));
V2CompatLayer::emitPatchPoint('before-php-buildconf');
shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF'));
}
#[Stage]
public function configureForUnix(TargetPackage $package, PackageInstaller $installer): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure'));
V2CompatLayer::emitPatchPoint('before-php-configure');
$cmd = getenv('SPC_CMD_PREFIX_PHP_CONFIGURE');
$args = [];
$version_id = self::getPHPVersionID();
// PHP JSON extension is built-in since PHP 8.0
if ($version_id < 80000) {
$args[] = '--enable-json';
}
// zts
if ($package->getBuildOption('enable-zts', false)) {
$args[] = '--enable-zts --disable-zend-signals';
if ($version_id >= 80100 && SystemTarget::getTargetOS() === 'Linux') {
$args[] = '--enable-zend-max-execution-timers';
}
}
// config-file-path and config-file-scan-dir
if ($option = $package->getBuildOption('with-config-file-path', false)) {
$args[] = "--with-config-file-path={$option}";
}
if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) {
$args[] = "--with-config-file-scan-dir={$option}";
}
// perform enable cli options
$args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli';
$args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm';
$args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) {
'Linux' => '--enable-micro=all-static',
default => '--enable-micro',
} : null;
$args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi';
$embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static';
$args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed';
$args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null;
$args = implode(' ', array_filter($args));
$static_extension_str = $this->makeStaticExtensionString($installer);
// run ./configure with args
$this->seekPhpSrcLogFileOnException(fn () => shell()->cd($package->getSourceDir())->setEnv([
'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'CPPFLAGS' => "-I{$package->getIncludeDir()}",
'LDFLAGS' => "-L{$package->getLibDir()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir());
}
#[Stage]
public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void
{
V2CompatLayer::emitPatchPoint('before-php-make');
logger()->info('cleaning up php-src build files');
shell()->cd($package->getSourceDir())->exec('make clean');
if ($installer->isPackageResolved('php-cli')) {
$package->runStage([self::class, 'makeCliForUnix']);
}
if ($installer->isPackageResolved('php-cgi')) {
$package->runStage([self::class, 'makeCgiForUnix']);
}
if ($installer->isPackageResolved('php-fpm')) {
$package->runStage([self::class, 'makeFpmForUnix']);
}
if ($installer->isPackageResolved('php-micro')) {
$package->runStage([self::class, 'makeMicroForUnix']);
}
if ($installer->isPackageResolved('php-embed')) {
$package->runStage([self::class, 'makeEmbedForUnix']);
}
}
#[Stage]
public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli'));
$concurrency = $builder->concurrency;
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} cli");
$builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php');
$package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php');
}
#[Stage]
public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi'));
$concurrency = $builder->concurrency;
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} cgi");
$builder->deployBinary("{$package->getSourceDir()}/sapi/cgi/php-cgi", BUILD_BIN_PATH . '/php-cgi');
$package->setOutput('Binary path for cgi SAPI', BUILD_BIN_PATH . '/php-cgi');
}
#[Stage]
public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm'));
$concurrency = $builder->concurrency;
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} fpm");
$builder->deployBinary("{$package->getSourceDir()}/sapi/fpm/php-fpm", BUILD_BIN_PATH . '/php-fpm');
$package->setOutput('Binary path for fpm SAPI', BUILD_BIN_PATH . '/php-fpm');
}
#[Stage]
#[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')]
public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
$phar_patched = false;
try {
if ($installer->isPackageResolved('ext-phar')) {
$phar_patched = true;
SourcePatcher::patchMicroPhar(self::getPHPVersionID());
}
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro'));
// apply --with-micro-fake-cli option
$vars = $this->makeVars($installer);
$vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : '';
// build
shell()->cd($package->getSourceDir())
->setEnv($vars)
->exec("make -j{$builder->concurrency} micro");
$builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', BUILD_BIN_PATH . '/micro.sfx');
$package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx');
} finally {
if ($phar_patched) {
SourcePatcher::unpatchMicroPhar();
}
}
}
#[Stage]
public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed'));
$shared_exts = array_filter(
$installer->getResolvedPackages(),
static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp()
);
$install_modules = $shared_exts ? 'install-modules' : '';
// detect changes in module path
$diff = new DirDiff(BUILD_MODULES_PATH, true);
$root = BUILD_ROOT_PATH;
$sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i';
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile')
->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs");
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared -------------
// process libphp.so for shared embed
$suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so';
$libphp_so = "{$package->getLibDir()}/libphp.{$suffix}";
if (file_exists($libphp_so)) {
// rename libphp.so if -release is set
if (SystemTarget::getTargetOS() === 'Linux') {
$this->processLibphpSoFile($libphp_so, $installer);
}
// deploy
$builder->deployBinary($libphp_so, $libphp_so, false);
$package->setOutput('Library path for embed SAPI', $libphp_so);
}
// process shared extensions that built-with-php
$increment_files = $diff->getChangedFiles();
$files = [];
foreach ($increment_files as $increment_file) {
$builder->deployBinary($increment_file, $increment_file, false);
$files[] = basename($increment_file);
}
if (!empty($files)) {
$package->setOutput('Built shared extensions', implode(', ', $files));
}
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static -------------
// process libphp.a for static embed
if (!file_exists("{$package->getLibDir()}/libphp.a")) {
return;
}
$ar = getenv('AR') ?: 'ar';
$libphp_a = "{$package->getLibDir()}/libphp.a";
shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}");
UnixUtil::exportDynamicSymbols($libphp_a);
// deploy embed php scripts
$package->runStage([$this, 'patchEmbedScripts']);
}
#[Stage]
public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void
{
// collect shared extensions
/** @var PhpExtensionPackage[] $shared_extensions */
$shared_extensions = array_filter(
$installer->getResolvedPackages(PhpExtensionPackage::class),
fn ($x) => $x->isBuildShared() && !$x->isBuildWithPhp()
);
if (!empty($shared_extensions)) {
if ($toolchain->isStatic()) {
throw new WrongUsageException(
"You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" .
'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" .
'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.'
);
}
FileSystem::createDir(BUILD_MODULES_PATH);
// backup
FileSystem::backupFile(BUILD_BIN_PATH . '/php-config');
FileSystem::backupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', 'extension_dir="' . BUILD_MODULES_PATH . '"');
FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes');
}
try {
logger()->debug('Building shared extensions...');
foreach ($shared_extensions as $extension) {
InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName()));
$extension->buildShared();
}
} finally {
// restore php-config
if (!empty($shared_extensions)) {
FileSystem::restoreBackupFile(BUILD_BIN_PATH . '/php-config');
FileSystem::restoreBackupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
}
}
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(TargetPackage $package): void
{
// virtual target, do nothing
if ($package->getName() !== 'php') {
return;
}
$package->runStage([$this, 'buildconfForUnix']);
$package->runStage([$this, 'configureForUnix']);
$package->runStage([$this, 'makeForUnix']);
$package->runStage([$this, 'unixBuildSharedExt']);
}
/**
* Patch phpize and php-config if needed
*/
#[Stage]
public function patchEmbedScripts(): void
{
// patch phpize
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
logger()->debug('Patching phpize prefix');
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
$this->setOutput('phpize script path for embed SAPI', BUILD_BIN_PATH . '/phpize');
}
// patch php-config
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
logger()->debug('Patching php-config prefix and libs order');
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
// move mimalloc to the beginning of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
// move lstdc++ to the end of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
$this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config');
}
}
/**
* Seek php-src/config.log when building PHP, add it to exception.
*/
protected function seekPhpSrcLogFileOnException(callable $callback, string $source_dir): void
{
try {
$callback();
} catch (SPCException $e) {
if (file_exists("{$source_dir}/config.log")) {
$e->addExtraLogFile('php-src config.log', 'php-src.config.log');
copy("{$source_dir}/config.log", SPC_LOGS_DIR . '/php-src.config.log');
}
throw $e;
}
}
private function makeStaticExtensionString(PackageInstaller $installer): string
{
$arg = [];
foreach ($installer->getResolvedPackages() as $package) {
/** @var PhpExtensionPackage $package */
if ($package->getType() !== 'php-extension' || !$package instanceof PhpExtensionPackage) {
continue;
}
// build-shared=true, build-static=false, build-with-php=true
if ($package->isBuildShared() && !$package->isBuildStatic() && $package->isBuildWithPhp()) {
$arg[] = $package->getPhpConfigureArg(SystemTarget::getTargetOS(), true);
} elseif ($package->isBuildStatic()) {
$arg[] = $package->getPhpConfigureArg(SystemTarget::getTargetOS(), false);
}
}
$str = implode(' ', $arg);
logger()->debug("Static extension configure args: {$str}");
return $str;
}
/**
* Make environment variables for php make.
* This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking.
*/
private function makeVars(PackageInstaller $installer): array
{
$config = (new SPCConfigUtil(['libs_only_deps' => true]))->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages()));
$static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : '';
$pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : '';
return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_LDFLAGS_PROGRAM' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . "{$config['ldflags']} {$static} {$pie}",
'EXTRA_LDFLAGS' => $config['ldflags'],
'EXTRA_LIBS' => $config['libs'],
]);
}
/**
* Rename libphp.so to libphp-<release>.so if -release is set in LDFLAGS.
*/
private function processLibphpSoFile(string $libphpSo, PackageInstaller $installer): void
{
$ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: '';
$libDir = BUILD_LIB_PATH;
$modulesDir = BUILD_MODULES_PATH;
$realLibName = 'libphp.so';
$cwd = getcwd();
if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) {
$release = $matches[1];
$realLibName = "libphp-{$release}.so";
$libphpRelease = "{$libDir}/{$realLibName}";
if (!file_exists($libphpRelease) && file_exists($libphpSo)) {
rename($libphpSo, $libphpRelease);
}
if (file_exists($libphpRelease)) {
chdir($libDir);
if (file_exists($libphpSo)) {
unlink($libphpSo);
}
symlink($realLibName, 'libphp.so');
shell()->exec(sprintf(
'patchelf --set-soname %s %s',
escapeshellarg($realLibName),
escapeshellarg($libphpRelease)
));
}
if (is_dir($modulesDir)) {
chdir($modulesDir);
foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) {
if (!$ext->isBuildShared()) {
continue;
}
$name = $ext->getName();
$versioned = "{$name}-{$release}.so";
$unversioned = "{$name}.so";
$src = "{$modulesDir}/{$versioned}";
$dst = "{$modulesDir}/{$unversioned}";
if (is_file($src)) {
rename($src, $dst);
shell()->exec(sprintf(
'patchelf --set-soname %s %s',
escapeshellarg($unversioned),
escapeshellarg($dst)
));
}
}
}
chdir($cwd);
}
$target = "{$libDir}/{$realLibName}";
if (file_exists($target)) {
[, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target));
$output = implode("\n", $output);
if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) {
$currentSoname = $sonameMatch[1];
if ($currentSoname !== basename($target)) {
shell()->exec(sprintf(
'patchelf --set-soname %s %s',
escapeshellarg(basename($target)),
escapeshellarg($target)
));
}
}
}
}
}

View File

@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace Package\Target;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\InitPackage;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
#[Target('pkg-config')]
class pkgconfig
{
#[InitPackage]
public function resolveBuild(): void
{
ApplicationContext::set('elephant', true);
}
#[BuildFor('Linux')]
#[BuildFor('Darwin')]
public function build(TargetPackage $package, ToolchainInterface $toolchain): void
{
UnixAutoconfExecutor::create($package)
->appendEnv([
'CFLAGS' => '-Wimplicit-function-declaration -Wno-int-conversion',
'LDFLAGS' => $toolchain->isStatic() ? '--static' : '',
])
->configure(
'--with-internal-glib',
'--disable-host-tool',
'--without-sysroot',
'--without-system-include-path',
'--without-system-library-path',
'--without-pc-path',
)
->make(with_install: 'install-exec');
shell()->exec('strip ' . BUILD_ROOT_PATH . '/bin/pkg-config');
}
}

View File

@@ -34,7 +34,7 @@ use Symfony\Component\Console\Application;
*/
final class ConsoleApplication extends Application
{
public const string VERSION = '3.0.0-dev';
public const string VERSION = '2.8.4';
public function __construct()
{

View File

@@ -96,7 +96,8 @@ class Extension
fn ($x) => $x->getStaticLibFiles(),
$this->getLibraryDependencies(recursive: true)
);
return implode(' ', $ret);
$libs = implode(' ', $ret);
return deduplicate_flags($libs);
}
/**
@@ -385,6 +386,9 @@ class Extension
logger()->info('Shared extension [' . $this->getName() . '] was already built, skipping (' . $this->getName() . '.so)');
return;
}
if ((string) Config::getExt($this->getName(), 'type') === 'addon') {
return;
}
logger()->info('Building extension [' . $this->getName() . '] as shared extension (' . $this->getName() . '.so)');
foreach ($this->dependencies as $dependency) {
if (!$dependency instanceof Extension) {
@@ -395,13 +399,12 @@ class Extension
$dependency->buildShared([...$visited, $this->getName()]);
}
}
if (Config::getExt($this->getName(), 'type') === 'addon') {
return;
}
$this->builder->emitPatchPoint('before-shared-ext[' . $this->getName() . ']-build');
match (PHP_OS_FAMILY) {
'Darwin', 'Linux' => $this->buildUnixShared(),
default => throw new WrongUsageException(PHP_OS_FAMILY . ' build shared extensions is not supported yet'),
};
$this->builder->emitPatchPoint('after-shared-ext[' . $this->getName() . ']-build');
} catch (SPCException $e) {
$e->bindExtensionInfo(['extension_name' => $this->getName()]);
throw $e;
@@ -452,12 +455,17 @@ class Extension
// process *.so file
$soFile = BUILD_MODULES_PATH . '/' . $this->getName() . '.so';
$soDest = $soFile;
preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches);
if (!empty($matches[1])) {
$soDest = str_replace('.so', '-' . $matches[1] . '.so', $soFile);
}
if (!file_exists($soFile)) {
throw new ValidationException("extension {$this->getName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getName()} build");
}
/** @var UnixBuilderBase $builder */
$builder = $this->builder;
$builder->deployBinary($soFile, $soFile, false);
$builder->deployBinary($soFile, $soDest, false);
}
/**
@@ -535,7 +543,7 @@ class Extension
*/
protected function getSharedExtensionEnv(): array
{
$config = (new SPCConfigUtil($this->builder))->getExtensionConfig($this);
$config = (new SPCConfigUtil($this->builder, ['no_php' => true]))->getExtensionConfig($this);
[$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
$preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group ';
$postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group ';
@@ -543,6 +551,7 @@ class Extension
'CFLAGS' => $config['cflags'],
'CXXFLAGS' => $config['cflags'],
'LDFLAGS' => $config['ldflags'],
'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"),
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
];

View File

@@ -184,18 +184,18 @@ abstract class LibraryBase
// extract first if not exists
if (!is_dir($this->source_dir)) {
$this->getBuilder()->emitPatchPoint('before-library[ ' . static::NAME . ']-extract');
$this->getBuilder()->emitPatchPoint('before-library[' . static::NAME . ']-extract');
SourceManager::initSource(libs: [static::NAME], source_only: true);
$this->getBuilder()->emitPatchPoint('after-library[ ' . static::NAME . ']-extract');
$this->getBuilder()->emitPatchPoint('after-library[' . static::NAME . ']-extract');
}
if (!$this->patched && $this->patchBeforeBuild()) {
file_put_contents($this->source_dir . '/.spc.patched', 'PATCHED!!!');
}
$this->getBuilder()->emitPatchPoint('before-library[ ' . static::NAME . ']-build');
$this->getBuilder()->emitPatchPoint('before-library[' . static::NAME . ']-build');
$this->build();
$this->installLicense();
$this->getBuilder()->emitPatchPoint('after-library[ ' . static::NAME . ']-build');
$this->getBuilder()->emitPatchPoint('after-library[' . static::NAME . ']-build');
return LIB_STATUS_OK;
}
@@ -346,25 +346,46 @@ abstract class LibraryBase
*/
protected function installLicense(): void
{
FileSystem::createDir(BUILD_ROOT_PATH . '/source-licenses/' . $this->getName());
$source = Config::getLib($this->getName(), 'source');
FileSystem::createDir(BUILD_ROOT_PATH . "/source-licenses/{$source}");
$license_files = Config::getSource($source)['license'] ?? [];
if (is_assoc_array($license_files)) {
$license_files = [$license_files];
}
foreach ($license_files as $index => $license) {
if ($license['type'] === 'text') {
FileSystem::writeFile(BUILD_ROOT_PATH . '/source-licenses/' . $this->getName() . "/{$index}.txt", $license['text']);
FileSystem::writeFile(BUILD_ROOT_PATH . "/source-licenses/{$source}/{$index}.txt", $license['text']);
continue;
}
if ($license['type'] === 'file') {
copy($this->source_dir . '/' . $license['path'], BUILD_ROOT_PATH . '/source-licenses/' . $this->getName() . "/{$index}.txt");
copy($this->source_dir . '/' . $license['path'], BUILD_ROOT_PATH . "/source-licenses/{$source}/{$index}.txt");
}
}
}
protected function isLibraryInstalled(): bool
{
if ($pkg_configs = Config::getLib(static::NAME, 'pkg-configs', [])) {
$pkg_config_path = getenv('PKG_CONFIG_PATH') ?: '';
$search_paths = array_unique(array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path)));
foreach ($pkg_configs as $name) {
$found = false;
foreach ($search_paths as $path) {
if (file_exists($path . "/{$name}.pc")) {
$found = true;
break;
}
}
if (!$found) {
return false;
}
}
// allow using system dependencies if pkg_config_path is explicitly defined
if (count($search_paths) > 1) {
return true;
}
}
foreach (Config::getLib(static::NAME, 'static-libs', []) as $name) {
if (!file_exists(BUILD_LIB_PATH . "/{$name}")) {
return false;
@@ -375,8 +396,17 @@ abstract class LibraryBase
return false;
}
}
$pkg_config_path = getenv('PKG_CONFIG_PATH') ?: '';
$search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path));
foreach (Config::getLib(static::NAME, 'pkg-configs', []) as $name) {
if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$name}.pc")) {
$found = false;
foreach ($search_paths as $path) {
if (file_exists($path . "/{$name}.pc")) {
$found = true;
break;
}
}
if (!$found) {
return false;
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\util\CustomExt;
#[CustomExt('com_dotnet')]
class com_dotnet extends Extension
{
public function getWindowsConfigureArg(bool $shared = false): string
{
return '--enable-com-dotnet=yes';
}
}

View File

@@ -5,11 +5,21 @@ declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\builder\linux\SystemUtil;
use SPC\store\SourcePatcher;
use SPC\util\CustomExt;
#[CustomExt('ffi')]
class ffi extends Extension
{
public function patchBeforeBuildconf(): bool
{
if (PHP_OS_FAMILY === 'Linux' && SystemUtil::getOSRelease()['dist'] === 'centos') {
return SourcePatcher::patchFfiCentos7FixO3strncmp();
}
return false;
}
public function getUnixConfigureArg(bool $shared = false): string
{
return '--with-ffi' . ($shared ? '=shared' : '') . ' --enable-zend-signals';

View File

@@ -11,7 +11,6 @@ use SPC\store\FileSystem;
use SPC\util\CustomExt;
use SPC\util\GlobalEnvManager;
use SPC\util\SPCConfigUtil;
use SPC\util\SPCTarget;
#[CustomExt('grpc')]
class grpc extends Extension
@@ -21,22 +20,50 @@ class grpc extends Extension
if ($this->builder instanceof WindowsBuilder) {
throw new ValidationException('grpc extension does not support windows yet');
}
if (file_exists(SOURCE_PATH . '/php-src/ext/grpc')) {
return false;
}
// soft link to the grpc source code
if (is_dir($this->source_dir . '/src/php/ext/grpc')) {
shell()->exec('ln -s ' . $this->source_dir . '/src/php/ext/grpc ' . SOURCE_PATH . '/php-src/ext/grpc');
} else {
throw new ValidationException('Cannot find grpc source code in ' . $this->source_dir . '/src/php/ext/grpc');
}
if (SPCTarget::getTargetOS() === 'Darwin') {
FileSystem::replaceFileRegex(
SOURCE_PATH . '/php-src/ext/grpc/config.m4',
'/GRPC_LIBDIR=.*$/m',
'GRPC_LIBDIR=' . BUILD_LIB_PATH . "\n" . 'LDFLAGS="$LDFLAGS -framework CoreFoundation"'
);
}
// Fix deprecated PHP API usage in call.c
FileSystem::replaceFileStr(
"{$this->source_dir}/src/php/ext/grpc/call.c",
'zend_exception_get_default(TSRMLS_C),',
'zend_ce_exception,',
);
$config_m4 = <<<'M4'
PHP_ARG_ENABLE(grpc, [whether to enable grpc support], [AS_HELP_STRING([--enable-grpc], [Enable grpc support])])
if test "$PHP_GRPC" != "no"; then
PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/include)
PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/src/php/ext/grpc)
GRPC_LIBDIR=@@build_lib_path@@
PHP_ADD_LIBPATH($GRPC_LIBDIR)
PHP_ADD_LIBRARY(grpc,,GRPC_SHARED_LIBADD)
LIBS="-lpthread $LIBS"
PHP_ADD_LIBRARY(pthread)
case $host in
*darwin*)
PHP_ADD_LIBRARY(c++,1,GRPC_SHARED_LIBADD)
;;
*)
PHP_ADD_LIBRARY(stdc++,1,GRPC_SHARED_LIBADD)
PHP_ADD_LIBRARY(rt,,GRPC_SHARED_LIBADD)
PHP_ADD_LIBRARY(rt)
;;
esac
PHP_NEW_EXTENSION(grpc, @grpc_c_files@, $ext_shared, , -DGRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK=1)
PHP_SUBST(GRPC_SHARED_LIBADD)
PHP_INSTALL_HEADERS([ext/grpc], [php_grpc.h])
fi
M4;
$replace = get_pack_replace();
// load grpc c files from src/php/ext/grpc
$c_files = glob($this->source_dir . '/src/php/ext/grpc/*.c');
$replace['@grpc_c_files@'] = implode(" \\\n ", array_map(fn ($f) => 'src/php/ext/grpc/' . basename($f), $c_files));
$config_m4 = str_replace(array_keys($replace), array_values($replace), $config_m4);
file_put_contents($this->source_dir . '/config.m4', $config_m4);
copy($this->source_dir . '/src/php/ext/grpc/php_grpc.h', $this->source_dir . '/php_grpc.h');
return true;
}
@@ -52,7 +79,6 @@ class grpc extends Extension
public function patchBeforeMake(): bool
{
parent::patchBeforeMake();
// add -Wno-strict-prototypes
GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes');
return true;
}

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\toolchain\ToolchainManager;
use SPC\toolchain\ZigToolchain;
use SPC\util\CustomExt;
#[CustomExt('imagick')]
@@ -19,7 +21,9 @@ class imagick extends Extension
protected function splitLibsIntoStaticAndShared(string $allLibs): array
{
[$static, $shared] = parent::splitLibsIntoStaticAndShared($allLibs);
if (str_contains(getenv('PATH'), 'rh/devtoolset') || str_contains(getenv('PATH'), 'rh/gcc-toolset')) {
if (ToolchainManager::getToolchainClass() !== ZigToolchain::class &&
(str_contains(getenv('PATH'), 'rh/devtoolset') || str_contains(getenv('PATH'), 'rh/gcc-toolset'))
) {
$static .= ' -l:libstdc++.a';
$shared = str_replace('-lstdc++', '', $shared);
}

View File

@@ -18,6 +18,9 @@ class memcache extends Extension
public function patchBeforeBuildconf(): bool
{
if (!$this->isBuildStatic()) {
return false;
}
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/ext/memcache/config9.m4',
'if test -d $abs_srcdir/src ; then',
@@ -43,4 +46,27 @@ EOF
);
return true;
}
public function patchBeforeSharedConfigure(): bool
{
if (!$this->isBuildShared()) {
return false;
}
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/ext/memcache/config9.m4',
'if test -d $abs_srcdir/main ; then',
'if test -d $abs_srcdir/src ; then',
);
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/ext/memcache/config9.m4',
'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"',
'export CPPFLAGS="$CPPFLAGS $INCLUDES"',
);
return true;
}
protected function getExtraEnv(): array
{
return ['CFLAGS' => '-std=c17'];
}
}

View File

@@ -24,4 +24,9 @@ class mongodb extends Extension
$arg .= $this->builder->getLib('zlib') ? ' --with-mongodb-zlib=yes ' : ' --with-mongodb-zlib=bundled ';
return clean_spaces($arg);
}
public function getExtraEnv(): array
{
return ['CFLAGS' => '-std=c17'];
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\util\CustomExt;
#[CustomExt('mysqlnd_ed25519')]
class mysqlnd_ed25519 extends Extension
{
public function getConfigureArg(bool $shared = false): string
{
return '--with-mysqlnd_ed25519' . ($shared ? '=shared' : '');
}
public function getUnixConfigureArg(bool $shared = false): string
{
return $this->getConfigureArg();
}
}

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\util\CustomExt;
#[CustomExt('mysqlnd_parsec')]
class mysqlnd_parsec extends Extension
{
public function getConfigureArg(bool $shared = false): string
{
return '--enable-mysqlnd_parsec' . ($shared ? '=shared' : '');
}
public function getUnixConfigureArg(bool $shared = false): string
{
return $this->getConfigureArg();
}
}

View File

@@ -24,25 +24,6 @@ class password_argon2 extends Extension
}
}
public function patchBeforeMake(): bool
{
$patched = parent::patchBeforeMake();
if ($this->builder->getLib('libsodium') !== null) {
$extraLibs = getenv('SPC_EXTRA_LIBS');
if ($extraLibs !== false) {
$extraLibs = str_replace(
[BUILD_LIB_PATH . '/libargon2.a', BUILD_LIB_PATH . '/libsodium.a'],
['', BUILD_LIB_PATH . '/libargon2.a ' . BUILD_LIB_PATH . '/libsodium.a'],
$extraLibs,
);
$extraLibs = trim(preg_replace('/\s+/', ' ', $extraLibs)); // normalize spacing
f_putenv('SPC_EXTRA_LIBS=' . $extraLibs);
return true;
}
}
return $patched;
}
public function getConfigureArg(bool $shared = false): string
{
if ($this->builder->getLib('openssl') !== null) {

View File

@@ -45,7 +45,7 @@ class pgsql extends Extension
protected function getExtraEnv(): array
{
return [
'CFLAGS' => '-Wno-int-conversion',
'CFLAGS' => '-std=c17 -Wno-int-conversion',
];
}
}

View File

@@ -45,4 +45,11 @@ class spx extends Extension
FileSystem::copy($this->source_dir . '/src/php_spx.h', $this->source_dir . '/php_spx.h');
return true;
}
public function getSharedExtensionEnv(): array
{
$env = parent::getSharedExtensionEnv();
$env['SPX_SHARED_LIBADD'] = $env['LIBS'];
return $env;
}
}

View File

@@ -17,6 +17,7 @@ class swoole extends Extension
public function patchBeforeMake(): bool
{
$patched = parent::patchBeforeMake();
FileSystem::replaceFileStr($this->source_dir . '/ext-src/php_swoole_private.h', 'PHP_VERSION_ID > 80500', 'PHP_VERSION_ID >= 80600');
if ($this->builder instanceof MacOSBuilder) {
// Fix swoole with event extension <util.h> conflict bug
$util_path = shell()->execWithResult('xcrun --show-sdk-path', false)[1][0] . '/usr/include/util.h';
@@ -49,19 +50,16 @@ class swoole extends Extension
// commonly used feature: coroutine-time
$arg .= ' --enable-swoole-coro-time --with-pic';
$arg .= ' --enable-swoole-ssh --enable-swoole-curl';
$arg .= $this->builder->getOption('enable-zts') ? ' --enable-swoole-thread --disable-thread-context' : ' --disable-swoole-thread --enable-thread-context';
// required features: curl, openssl (but curl hook is buggy for php 8.0)
$arg .= $this->builder->getPHPVersionID() >= 80100 ? ' --enable-swoole-curl' : ' --disable-swoole-curl';
$arg .= ' --enable-openssl';
// additional features that only require libraries
$arg .= $this->builder->getLib('libcares') ? ' --enable-cares' : '';
$arg .= $this->builder->getLib('brotli') ? (' --enable-brotli --with-brotli-dir=' . BUILD_ROOT_PATH) : '';
$arg .= $this->builder->getLib('nghttp2') ? (' --with-nghttp2-dir=' . BUILD_ROOT_PATH) : '';
$arg .= $this->builder->getLib('zstd') ? ' --enable-zstd' : '';
$arg .= $this->builder->getLib('liburing') ? ' --enable-iouring' : '';
$arg .= $this->builder->getLib('liburing') ? ' --enable-iouring --enable-uring-socket' : '';
$arg .= $this->builder->getExt('sockets') ? ' --enable-sockets' : '';
// enable additional features that require the pdo extension, but conflict with pdo_* extensions
@@ -73,6 +71,7 @@ class swoole extends Extension
$config = (new SPCConfigUtil($this->builder))->getLibraryConfig($this->builder->getLib('unixodbc'));
$arg .= ' --with-swoole-odbc=unixODBC,' . BUILD_ROOT_PATH . ' SWOOLE_ODBC_LIBS="' . $config['libs'] . '"';
}
$arg .= $this->builder->getExt('ftp')?->isBuildStatic() ? ' --disable-swoole-ftp' : ' --enable-swoole-ftp';
if ($this->getExtVersion() >= '6.1.0') {
$arg .= ' --enable-swoole-stdext';

View File

@@ -162,7 +162,7 @@ class LinuxBuilder extends UnixBuilderBase
throw new WrongUsageException(
"You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" .
'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" .
'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.'
'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc` or use SPC_MUSL_DYNAMIC=true on alpine.'
);
}
logger()->info('Building shared extensions...');
@@ -283,11 +283,14 @@ class LinuxBuilder extends UnixBuilderBase
// process libphp.so for shared embed
$libphpSo = BUILD_LIB_PATH . '/libphp.so';
$libphpSoDest = BUILD_LIB_PATH . '/libphp.so';
if (file_exists($libphpSo)) {
// post actions: rename libphp.so to libphp-<release>.so if -release is set in LDFLAGS
$this->processLibphpSoFile($libphpSo);
// deploy libphp.so
$this->deployBinary($libphpSo, $libphpSo, false);
preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches);
if (!empty($matches[1])) {
$libphpSoDest = str_replace('.so', '-' . $matches[1] . '.so', $libphpSo);
}
$this->deployBinary($libphpSo, $libphpSoDest, false);
}
// process shared extensions build-with-php
@@ -324,74 +327,6 @@ class LinuxBuilder extends UnixBuilderBase
]);
}
private function processLibphpSoFile(string $libphpSo): void
{
$ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: '';
$libDir = BUILD_LIB_PATH;
$modulesDir = BUILD_MODULES_PATH;
$realLibName = 'libphp.so';
$cwd = getcwd();
if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) {
$release = $matches[1];
$realLibName = "libphp-{$release}.so";
$libphpRelease = "{$libDir}/{$realLibName}";
if (!file_exists($libphpRelease) && file_exists($libphpSo)) {
rename($libphpSo, $libphpRelease);
}
if (file_exists($libphpRelease)) {
chdir($libDir);
if (file_exists($libphpSo)) {
unlink($libphpSo);
}
symlink($realLibName, 'libphp.so');
shell()->exec(sprintf(
'patchelf --set-soname %s %s',
escapeshellarg($realLibName),
escapeshellarg($libphpRelease)
));
}
if (is_dir($modulesDir)) {
chdir($modulesDir);
foreach ($this->getExts() as $ext) {
if (!$ext->isBuildShared()) {
continue;
}
$name = $ext->getName();
$versioned = "{$name}-{$release}.so";
$unversioned = "{$name}.so";
$src = "{$modulesDir}/{$versioned}";
$dst = "{$modulesDir}/{$unversioned}";
if (is_file($src)) {
rename($src, $dst);
shell()->exec(sprintf(
'patchelf --set-soname %s %s',
escapeshellarg($unversioned),
escapeshellarg($dst)
));
}
}
}
chdir($cwd);
}
$target = "{$libDir}/{$realLibName}";
if (file_exists($target)) {
[, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target));
$output = implode("\n", $output);
if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) {
$currentSoname = $sonameMatch[1];
if ($currentSoname !== basename($target)) {
shell()->exec(sprintf(
'patchelf --set-soname %s %s',
escapeshellarg(basename($target)),
escapeshellarg($target)
));
}
}
}
}
/**
* Patch micro.sfx after UPX compression.
* micro needs special section handling in LinuxBuilder.

View File

@@ -141,7 +141,7 @@ class SystemUtil
{
return [
// debian-like
'debian', 'ubuntu', 'Deepin', 'neon',
'debian', 'ubuntu', 'Deepin',
// rhel-like
'redhat',
// centos

View File

@@ -6,6 +6,8 @@ namespace SPC\builder\linux\library;
use SPC\builder\linux\SystemUtil;
use SPC\store\FileSystem;
use SPC\toolchain\GccNativeToolchain;
use SPC\toolchain\ToolchainManager;
use SPC\util\executor\UnixAutoconfExecutor;
use SPC\util\SPCTarget;
@@ -15,26 +17,19 @@ class liburing extends LinuxLibraryBase
public function patchBeforeBuild(): bool
{
if (!SystemUtil::isMuslDist()) {
return false;
if (SystemUtil::isMuslDist()) {
FileSystem::replaceFileStr($this->source_dir . '/configure', 'realpath -s', 'realpath');
return true;
}
FileSystem::replaceFileStr($this->source_dir . '/configure', 'realpath -s', 'realpath');
return true;
return false;
}
protected function build(): void
{
$use_libc = SPCTarget::getLibc() !== 'glibc' || version_compare(SPCTarget::getLibcVersion(), '2.30', '>=');
$use_libc = ToolchainManager::getToolchainClass() !== GccNativeToolchain::class || version_compare(SPCTarget::getLibcVersion(), '2.30', '>=');
$make = UnixAutoconfExecutor::create($this);
if (!$use_libc) {
$make->appendEnv([
'CC' => 'gcc', // libc-less version fails to compile with clang or zig
'CXX' => 'g++',
'AR' => 'ar',
'LD' => 'ld',
]);
} else {
if ($use_libc) {
$make->appendEnv([
'CFLAGS' => '-D_GNU_SOURCE',
]);
@@ -51,7 +46,7 @@ class liburing extends LinuxLibraryBase
$use_libc ? '--use-libc' : '',
)
->configure()
->make('library', 'install ENABLE_SHARED=0', with_clean: false);
->make('library ENABLE_SHARED=0', 'install ENABLE_SHARED=0', with_clean: false);
$this->patchPkgconfPrefix();
}

View File

@@ -21,6 +21,7 @@ declare(strict_types=1);
namespace SPC\builder\linux\library;
use SPC\builder\linux\SystemUtil;
use SPC\store\FileSystem;
class openssl extends LinuxLibraryBase
@@ -51,6 +52,9 @@ class openssl extends LinuxLibraryBase
$zlib_extra = '';
}
$openssl_dir = getenv('OPENSSLDIR') ?: null;
// TODO: in v3 use the following: $openssl_dir ??= SystemUtil::getOSRelease()['dist'] === 'redhat' ? '/etc/pki/tls' : '/etc/ssl';
$openssl_dir ??= '/etc/ssl';
$ex_lib = trim($ex_lib);
shell()->cd($this->source_dir)->initializeEnv($this)
@@ -58,10 +62,11 @@ class openssl extends LinuxLibraryBase
"{$env} ./Configure no-shared {$extra} " .
'--prefix=' . BUILD_ROOT_PATH . ' ' .
'--libdir=' . BUILD_LIB_PATH . ' ' .
'--openssldir=/etc/ssl ' .
"--openssldir={$openssl_dir} " .
"{$zlib_extra}" .
'enable-pie ' .
'no-legacy ' .
'no-tests ' .
"linux-{$arch}"
)
->exec('make clean')

View File

@@ -34,7 +34,7 @@ trait UnixLibraryTrait
$files = array_map(fn ($x) => "{$x}.pc", $conf_pc);
}
foreach ($files as $name) {
$realpath = realpath(BUILD_ROOT_PATH . '/lib/pkgconfig/' . $name);
$realpath = realpath(BUILD_LIB_PATH . '/pkgconfig/' . $name);
if ($realpath === false) {
throw new PatchException('pkg-config prefix patcher', 'Cannot find library [' . static::NAME . '] pkgconfig file [' . $name . '] in ' . BUILD_LIB_PATH . '/pkgconfig/ !');
}

View File

@@ -74,10 +74,10 @@ trait UnixSystemUtilTrait
}
// https://github.com/ziglang/zig/issues/24662
if (ToolchainManager::getToolchainClass() === ZigToolchain::class) {
return '-Wl,--export-dynamic';
return '-Wl,--export-dynamic'; // needs release 0.16, can be removed then
}
// macOS
if (SPCTarget::getTargetOS() !== 'Linux') {
// macOS/zig
if (SPCTarget::getTargetOS() !== 'Linux' || ToolchainManager::getToolchainClass() === ZigToolchain::class) {
return "-Wl,-exported_symbols_list,{$symbol_file}";
}
return "-Wl,--dynamic-list={$symbol_file}";

View File

@@ -145,11 +145,10 @@ abstract class UnixBuilderBase extends BuilderBase
throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}");
}
// extract debug info
$this->extractDebugInfo($dst);
// strip
if (!$this->getOption('no-strip')) {
// extract debug info
$this->extractDebugInfo($dst);
// extra strip
$this->stripBinary($dst);
}
@@ -236,8 +235,10 @@ abstract class UnixBuilderBase extends BuilderBase
$lens .= ' -static';
}
$dynamic_exports = '';
$embedType = 'static';
// if someone changed to EMBED_TYPE=shared, we need to add LD_LIBRARY_PATH
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') {
$embedType = 'shared';
if (PHP_OS_FAMILY === 'Darwin') {
$ext_path = 'DYLD_LIBRARY_PATH=' . BUILD_LIB_PATH . ':$DYLD_LIBRARY_PATH ';
} else {
@@ -256,18 +257,19 @@ abstract class UnixBuilderBase extends BuilderBase
}
}
$cc = getenv('CC');
[$ret, $out] = shell()->cd($sample_file_path)->execWithResult("{$cc} -o embed embed.c {$lens} {$dynamic_exports}");
if ($ret !== 0) {
throw new ValidationException(
'embed failed sanity check: build failed. Error message: ' . implode("\n", $out),
validation_module: 'static libphp.a sanity check'
'embed failed to build. Error message: ' . implode("\n", $out),
validation_module: $embedType . ' libphp embed build sanity check'
);
}
[$ret, $output] = shell()->cd($sample_file_path)->execWithResult($ext_path . './embed');
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new ValidationException(
'embed failed sanity check: run failed. Error message: ' . implode("\n", $output),
validation_module: 'static libphp.a sanity check'
'embed failed to run. Error message: ' . implode("\n", $output),
validation_module: $embedType . ' libphp embed run sanity check'
);
}
}
@@ -363,6 +365,7 @@ abstract class UnixBuilderBase extends BuilderBase
$frankenphpAppPath = $this->getOption('with-frankenphp-app');
if ($frankenphpAppPath) {
$frankenphpAppPath = trim($frankenphpAppPath, "\"'");
if (!is_dir($frankenphpAppPath)) {
throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}");
}
@@ -453,6 +456,8 @@ abstract class UnixBuilderBase extends BuilderBase
'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}",
'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' .
'-ldflags \"-linkmode=external ' . $extLdFlags . ' ' .
'-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' .
'-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp\' ' .
'-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' .
"v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " .
"-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}",

View File

@@ -13,8 +13,8 @@ trait freetype
{
$cmake = UnixCMakeExecutor::create($this)
->optionalLib('libpng', ...cmake_boolean_args('FT_DISABLE_PNG', true))
->optionalLib('bzip2', ...cmake_boolean_args('FT_DISABLE_BZIP2', true))
->optionalLib('brotli', ...cmake_boolean_args('FT_DISABLE_BROTLI', true))
->addConfigureArgs('-DFT_DISABLE_BZIP2=ON')
->addConfigureArgs('-DFT_DISABLE_BROTLI=ON')
->addConfigureArgs('-DFT_DISABLE_HARFBUZZ=ON');
// fix cmake 4.0 compatibility

View File

@@ -16,7 +16,11 @@ trait gettext
->addConfigureArgs(
'--disable-java',
'--disable-c++',
'--with-included-gettext',
'--disable-d',
'--disable-rpath',
'--disable-modula2',
'--disable-libasprintf',
'--with-included-libintl',
"--with-iconv-prefix={$this->getBuildRootPath()}",
);

View File

@@ -10,7 +10,14 @@ trait gmp
{
protected function build(): void
{
UnixAutoconfExecutor::create($this)->configure()->make();
UnixAutoconfExecutor::create($this)
->appendEnv([
'CFLAGS' => '-std=c17',
])
->configure(
'--enable-fat'
)
->make();
$this->patchPkgconfPrefix(['gmp.pc']);
}
}

View File

@@ -11,6 +11,11 @@ trait libavif
protected function build(): void
{
UnixCMakeExecutor::create($this)
->optionalLib('libaom', '-DAVIF_CODEC_AOM=SYSTEM', '-DAVIF_CODEC_AOM=OFF')
->optionalLib('libsharpyuv', '-DAVIF_LIBSHARPYUV=SYSTEM', '-DAVIF_LIBSHARPYUV=OFF')
->optionalLib('libjpeg', '-DAVIF_JPEG=SYSTEM', '-DAVIF_JPEG=OFF')
->optionalLib('libxml2', '-DAVIF_LIBXML2=SYSTEM', '-DAVIF_LIBXML2=OFF')
->optionalLib('libpng', '-DAVIF_LIBPNG=SYSTEM', '-DAVIF_LIBPNG=OFF')
->addConfigureArgs('-DAVIF_LIBYUV=OFF')
->build();
// patch pkgconfig

View File

@@ -11,7 +11,10 @@ trait libde265
protected function build(): void
{
UnixCMakeExecutor::create($this)
->addConfigureArgs('-DENABLE_SDL=OFF')
->addConfigureArgs(
'-DENABLE_SDL=OFF',
'-DENABLE_DECODER=OFF'
)
->build();
$this->patchPkgconfPrefix(['libde265.pc']);
}

View File

@@ -14,6 +14,7 @@ trait libjpeg
->addConfigureArgs(
'-DENABLE_STATIC=ON',
'-DENABLE_SHARED=OFF',
'-DWITH_SYSTEM_ZLIB=ON'
)
->build();
// patch pkgconfig

View File

@@ -29,13 +29,17 @@ trait libjxl
);
if (ToolchainManager::getToolchainClass() === ZigToolchain::class) {
$cmake->addConfigureArgs(
'-DCXX_MAVX512F_SUPPORTED:BOOL=FALSE',
'-DCXX_MAVX512DQ_SUPPORTED:BOOL=FALSE',
'-DCXX_MAVX512CD_SUPPORTED:BOOL=FALSE',
'-DCXX_MAVX512BW_SUPPORTED:BOOL=FALSE',
'-DCXX_MAVX512VL_SUPPORTED:BOOL=FALSE'
);
$cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: '';
$has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4');
if (!$has_avx512) {
$cmake->addConfigureArgs(
'-DCXX_MAVX512F_SUPPORTED:BOOL=FALSE',
'-DCXX_MAVX512DQ_SUPPORTED:BOOL=FALSE',
'-DCXX_MAVX512CD_SUPPORTED:BOOL=FALSE',
'-DCXX_MAVX512BW_SUPPORTED:BOOL=FALSE',
'-DCXX_MAVX512VL_SUPPORTED:BOOL=FALSE'
);
}
}
$cmake->build();

View File

@@ -10,8 +10,26 @@ trait libwebp
{
protected function build(): void
{
$code = '#include <immintrin.h>
int main() { return _mm256_cvtsi256_si32(_mm256_setzero_si256()); }';
$cc = getenv('CC') ?: 'gcc';
[$ret] = shell()->execWithResult("printf '%s' '{$code}' | {$cc} -x c -mavx2 -o /dev/null - 2>&1");
$disableAvx2 = $ret !== 0 && GNU_ARCH === 'x86_64' && PHP_OS_FAMILY === 'Linux';
UnixCMakeExecutor::create($this)
->addConfigureArgs('-DWEBP_BUILD_EXTRAS=ON')
->addConfigureArgs(
'-DWEBP_BUILD_EXTRAS=OFF',
'-DWEBP_BUILD_ANIM_UTILS=OFF',
'-DWEBP_BUILD_CWEBP=OFF',
'-DWEBP_BUILD_DWEBP=OFF',
'-DWEBP_BUILD_GIF2WEBP=OFF',
'-DWEBP_BUILD_IMG2WEBP=OFF',
'-DWEBP_BUILD_VWEBP=OFF',
'-DWEBP_BUILD_WEBPINFO=OFF',
'-DWEBP_BUILD_WEBPMUX=OFF',
'-DWEBP_BUILD_FUZZTEST=OFF',
$disableAvx2 ? '-DWEBP_ENABLE_SIMD=OFF' : ''
)
->build();
// patch pkgconfig
$this->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX | PKGCONF_PATCH_LIBDIR);

View File

@@ -16,6 +16,7 @@ trait ncurses
UnixAutoconfExecutor::create($this)
->appendEnv([
'CFLAGS' => '-std=c17',
'LDFLAGS' => SPCTarget::isStatic() ? '-static' : '',
])
->configure(
@@ -29,7 +30,7 @@ trait ncurses
'--without-tests',
'--without-dlsym',
'--without-debug',
'-enable-symlinks',
'--enable-symlinks',
"--bindir={$this->getBinDir()}",
"--includedir={$this->getIncludeDir()}",
"--libdir={$this->getLibDir()}",

View File

@@ -4,29 +4,14 @@ declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\exception\FileSystemException;
use SPC\store\FileSystem;
use SPC\util\PkgConfigUtil;
use SPC\util\SPCConfigUtil;
use SPC\util\SPCTarget;
trait postgresql
{
public function patchBeforeBuild(): bool
{
// fix aarch64 build on glibc 2.17 (e.g. CentOS 7)
if (SPCTarget::getLibcVersion() === '2.17' && GNU_ARCH === 'aarch64') {
try {
FileSystem::replaceFileStr("{$this->source_dir}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0');
FileSystem::replaceFileStr(
"{$this->source_dir}/src/port/pg_crc32c_armv8_choose.c",
'#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32)',
'#if defined(__linux__) && !defined(HWCAP_CRC32)'
);
} catch (FileSystemException) {
// allow file not-existence to make it compatible with old and new version
}
}
// skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries
FileSystem::replaceFileStr("{$this->source_dir}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';');
// disable shared libs build
@@ -50,7 +35,7 @@ trait postgresql
$config = $spc->config(libraries: $libs, include_suggest_lib: $this->builder->getOption('with-suggested-libs', false));
$env_vars = [
'CFLAGS' => $config['cflags'],
'CFLAGS' => $config['cflags'] . ' -std=c17',
'CPPFLAGS' => '-DPIC',
'LDFLAGS' => $config['ldflags'],
'LIBS' => $config['libs'],
@@ -108,8 +93,7 @@ trait postgresql
// remove dynamic libs
shell()->cd($this->source_dir . '/build')
->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so.*")
->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so")
->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so*")
->exec("rm -rf {$this->getBuildRootPath()}/lib/*.dylib");
FileSystem::replaceFileStr("{$this->getLibDir()}/pkgconfig/libpq.pc", '-lldap', '-lldap -llber');

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\exception\WrongUsageException;
use SPC\store\FileSystem;
use SPC\util\executor\UnixAutoconfExecutor;
trait unixodbc
@@ -30,7 +31,15 @@ trait unixodbc
'--enable-gui=no',
)
->make();
$this->patchPkgconfPrefix(['odbc.pc', 'odbccr.pc', 'odbcinst.pc']);
$pkgConfigs = ['odbc.pc', 'odbccr.pc', 'odbcinst.pc'];
$this->patchPkgconfPrefix($pkgConfigs);
foreach ($pkgConfigs as $file) {
FileSystem::replaceFileStr(
BUILD_LIB_PATH . "/pkgconfig/{$file}",
'$(top_build_prefix)libltdl/libltdlc.la',
''
);
}
$this->patchLaDependencyPrefix();
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace SPC\builder\windows\library;
use SPC\store\FileSystem;
class brotli extends WindowsLibraryBase
{
public const NAME = 'brotli';
protected function build(): void
{
// reset cmake
FileSystem::resetDir($this->source_dir . '\build');
// start build
cmd()->cd($this->source_dir)
->execWithWrapper(
$this->builder->makeSimpleWrapper('cmake'),
'-B build ' .
'-A x64 ' .
"-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " .
'-DCMAKE_BUILD_TYPE=Release ' .
'-DBUILD_SHARED_LIBS=OFF ' .
'-DBROTLI_BUILD_TOOLS=OFF ' .
'-DBROTLI_BUNDLED_MODE=OFF ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' '
)
->execWithWrapper(
$this->builder->makeSimpleWrapper('cmake'),
"--build build --config Release --target install -j{$this->builder->concurrency}"
);
}
}

View File

@@ -30,7 +30,6 @@ class curl extends WindowsLibraryBase
'-DCMAKE_BUILD_TYPE=Release ' .
'-DBUILD_SHARED_LIBS=OFF ' .
'-DBUILD_STATIC_LIBS=ON ' .
'-DCURL_STATICLIB=ON ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' .
'-DBUILD_CURL_EXE=OFF ' . // disable curl.exe
'-DBUILD_TESTING=OFF ' . // disable tests
@@ -42,9 +41,9 @@ class curl extends WindowsLibraryBase
'-DCURL_USE_OPENSSL=OFF ' . // disable openssl due to certificate issue
'-DCURL_ENABLE_SSL=ON ' .
'-DUSE_NGHTTP2=ON ' . // enable nghttp2
'-DSHARE_LIB_OBJECT=OFF ' . // disable shared lib object
'-DCURL_USE_LIBSSH2=ON ' . // enable libssh2
'-DENABLE_IPV6=ON ' . // enable ipv6
'-DNGHTTP2_CFLAGS="/DNGHTTP2_STATICLIB" ' .
$alt
)
->execWithWrapper(
@@ -53,5 +52,7 @@ class curl extends WindowsLibraryBase
);
// move libcurl.lib to libcurl_a.lib
rename(BUILD_LIB_PATH . '\libcurl.lib', BUILD_LIB_PATH . '\libcurl_a.lib');
FileSystem::replaceFileStr(BUILD_INCLUDE_PATH . '\curl\curl.h', '#ifdef CURL_STATICLIB', '#if 1');
}
}

View File

@@ -24,6 +24,8 @@ class freetype extends WindowsLibraryBase
"-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " .
'-DCMAKE_BUILD_TYPE=Release ' .
'-DBUILD_SHARED_LIBS=OFF ' .
'-DFT_DISABLE_BROTLI=TRUE ' .
'-DFT_DISABLE_BZIP2=TRUE ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' '
)
->execWithWrapper(

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace SPC\builder\windows\library;
use SPC\store\FileSystem;
class libaom extends WindowsLibraryBase
{
public const NAME = 'libaom';
protected function build(): void
{
// libaom source tree contains a build/cmake/ directory with its own
// cmake modules, so we must use a different name for the build dir.
FileSystem::resetDir($this->source_dir . '\builddir');
// start build
cmd()->cd($this->source_dir)
->execWithWrapper(
$this->builder->makeSimpleWrapper('cmake'),
'-S . -B builddir ' .
'-A x64 ' .
"-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " .
'-DCMAKE_BUILD_TYPE=Release ' .
'-DBUILD_SHARED_LIBS=OFF ' .
'-DAOM_TARGET_CPU=generic ' .
'-DENABLE_DOCS=OFF ' .
'-DENABLE_EXAMPLES=OFF ' .
'-DENABLE_TESTDATA=OFF ' .
'-DENABLE_TESTS=OFF ' .
'-DENABLE_TOOLS=OFF ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' '
)
->execWithWrapper(
$this->builder->makeSimpleWrapper('cmake'),
"--build builddir --config Release --target install -j{$this->builder->concurrency}"
);
}
}

View File

@@ -29,11 +29,16 @@ class nghttp2 extends WindowsLibraryBase
'-DBUILD_SHARED_LIBS=OFF ' .
'-DENABLE_STATIC_CRT=ON ' .
'-DENABLE_LIB_ONLY=ON ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' '
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' .
'-DENABLE_STATIC_CRT=ON ' .
'-DENABLE_DOC=OFF ' .
'-DBUILD_TESTING=OFF '
)
->execWithWrapper(
$this->builder->makeSimpleWrapper('cmake'),
"--build build --config Release --target install -j{$this->builder->concurrency}"
);
FileSystem::replaceFileStr(BUILD_INCLUDE_PATH . '\nghttp2\nghttp2.h', '#ifdef NGHTTP2_STATICLIB', '#if 1');
}
}

View File

@@ -31,8 +31,24 @@ class zlib extends WindowsLibraryBase
$this->builder->makeSimpleWrapper('cmake'),
"--build build --config Release --target install -j{$this->builder->concurrency}"
);
copy(BUILD_LIB_PATH . '\zlibstatic.lib', BUILD_LIB_PATH . '\zlib_a.lib');
unlink(BUILD_ROOT_PATH . '\bin\zlib.dll');
unlink(BUILD_LIB_PATH . '\zlib.lib');
$detect_list = [
'zlibstatic.lib',
'zs.lib',
'libzs.lib',
'libz.lib',
];
foreach ($detect_list as $item) {
if (file_exists(BUILD_LIB_PATH . '\\' . $item)) {
FileSystem::copy(BUILD_LIB_PATH . '\\' . $item, BUILD_LIB_PATH . '\zlib_a.lib');
FileSystem::copy(BUILD_LIB_PATH . '\\' . $item, BUILD_LIB_PATH . '\zlibstatic.lib');
break;
}
}
FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '\bin\zlib.dll');
FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\zlib.lib');
FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\libz.dll');
FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\libz.lib');
FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\z.lib');
FileSystem::removeFileIfExists(BUILD_LIB_PATH . '\z.dll');
}
}

View File

@@ -30,7 +30,7 @@ class DownloadCommand extends BaseCommand
$this->addArgument('sources', InputArgument::REQUIRED, 'The sources will be compiled, comma separated');
$this->addOption('shallow-clone', null, null, 'Clone shallow');
$this->addOption('with-openssl11', null, null, 'Use openssl 1.1');
$this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'version in major.minor format (default 8.4)', '8.4');
$this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'version in major.minor format (default 8.5)', '8.5');
$this->addOption('clean', null, null, 'Clean old download cache and source before fetch');
$this->addOption('all', 'A', null, 'Fetch all sources that static-php-cli needed');
$this->addOption('custom-url', 'U', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom source download url, e.g "php-src:https://downloads.php.net/~eric/php-8.3.0beta1.tar.gz"');

View File

@@ -20,9 +20,9 @@ class SwitchPhpVersionCommand extends BaseCommand
$this->addArgument(
'php-major-version',
InputArgument::REQUIRED,
'PHP major version (supported: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4)',
'PHP major version (supported: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, 8.5)',
null,
fn () => ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
fn () => ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']
);
$this->no_motd = true;
@@ -32,7 +32,7 @@ class SwitchPhpVersionCommand extends BaseCommand
public function handle(): int
{
$php_ver = $this->input->getArgument('php-major-version');
if (!in_array($php_ver, ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'])) {
if (!in_array($php_ver, ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5'])) {
// match x.y.z
preg_match('/^\d+\.\d+\.\d+$/', $php_ver, $matches);
if (!$matches) {

View File

@@ -22,7 +22,6 @@ class LinuxToolCheckList
'bzip2', 'cmake', 'gcc',
'g++', 'patch', 'binutils-gold',
'libtoolize', 'which',
'patchelf',
];
public const TOOLS_DEBIAN = [
@@ -31,7 +30,6 @@ class LinuxToolCheckList
'tar', 'unzip', 'gzip', 'gcc', 'g++',
'bzip2', 'cmake', 'patch',
'xz', 'libtoolize', 'which',
'patchelf',
];
public const TOOLS_RHEL = [
@@ -39,8 +37,7 @@ class LinuxToolCheckList
'git', 'autoconf', 'automake',
'tar', 'unzip', 'gzip', 'gcc', 'g++',
'bzip2', 'cmake', 'patch', 'which',
'xz', 'libtool', 'gettext-devel',
'patchelf', 'file',
'xz', 'libtool', 'gettext-devel', 'file',
];
public const TOOLS_ARCH = [
@@ -112,7 +109,7 @@ class LinuxToolCheckList
public function fixBuildTools(array $distro, array $missing): bool
{
$install_cmd = match ($distro['dist']) {
'ubuntu', 'debian', 'Deepin', 'neon' => 'apt-get install -y',
'ubuntu', 'debian', 'Deepin' => 'apt-get install -y',
'alpine' => 'apk add',
'redhat' => 'dnf install -y',
'centos' => 'yum install -y',
@@ -128,7 +125,7 @@ class LinuxToolCheckList
logger()->warning('Current user (' . $user . ') is not root, using sudo for running command (may require password input)');
}
$is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin', 'neon']);
$is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin']);
$to_install = $is_debian ? str_replace('xz', 'xz-utils', $missing) : $missing;
// debian, alpine libtool -> libtoolize
$to_install = str_replace('libtoolize', 'libtool', $to_install);

View File

@@ -97,8 +97,9 @@ class Downloader
public static function getLatestGithubTarball(string $name, array $source, string $type = 'releases'): array
{
logger()->debug("finding {$name} source from github {$type} tarball");
$source['query'] ??= '';
$data = json_decode(self::curlExec(
url: "https://api.github.com/repos/{$source['repo']}/{$type}",
url: "https://api.github.com/repos/{$source['repo']}/{$type}{$source['query']}",
hooks: [[CurlHook::class, 'setupGithubToken']],
retries: self::getRetryAttempts()
), true, 512, JSON_THROW_ON_ERROR);
@@ -108,6 +109,9 @@ class Downloader
if (($rel['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) {
continue;
}
if (($rel['draft'] ?? false) === true && (($source['prefer-stable'] ?? false) || !$rel['tarball_url'])) {
continue;
}
if (!($source['match'] ?? null)) {
$url = $rel['tarball_url'] ?? null;
break;

View File

@@ -408,13 +408,13 @@ class FileSystem
continue;
}
$sub_file = self::convertPath($dir . '/' . $v);
if (is_dir($sub_file)) {
# 如果是 目录 且 递推 , 则递推添加下级文件
if (!self::removeDir($sub_file)) {
if (is_link($sub_file) || is_file($sub_file)) {
if (!unlink($sub_file)) {
return false;
}
} elseif (is_link($sub_file) || is_file($sub_file)) {
if (!unlink($sub_file)) {
} elseif (is_dir($sub_file)) {
# 如果是 目录 且 递推 , 则递推添加下级文件
if (!self::removeDir($sub_file)) {
return false;
}
}
@@ -572,6 +572,44 @@ class FileSystem
return file_put_contents($file, implode('', $lines));
}
/**
* Move file or directory, handling cross-device scenarios
* Uses rename() if possible, falls back to copy+delete for cross-device moves
*
* @param string $source Source path
* @param string $dest Destination path
*/
public static function moveFileOrDir(string $source, string $dest): void
{
$source = self::convertPath($source);
$dest = self::convertPath($dest);
// Check if source and dest are on the same device to avoid cross-device rename errors
$source_stat = @stat($source);
$dest_parent = dirname($dest);
$dest_stat = @stat($dest_parent);
// Only use rename if on same device
if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) {
if (@rename($source, $dest)) {
return;
}
}
// Fall back to copy + delete for cross-device moves or if rename failed
if (is_dir($source)) {
self::copyDir($source, $dest);
self::removeDir($source);
} else {
if (!copy($source, $dest)) {
throw new FileSystemException("Failed to copy file from {$source} to {$dest}");
}
if (!unlink($source)) {
throw new FileSystemException("Failed to remove source file: {$source}");
}
}
}
private static function extractArchive(string $filename, string $target): void
{
// Create base dir
@@ -648,44 +686,6 @@ class FileSystem
};
}
/**
* Move file or directory, handling cross-device scenarios
* Uses rename() if possible, falls back to copy+delete for cross-device moves
*
* @param string $source Source path
* @param string $dest Destination path
*/
private static function moveFileOrDir(string $source, string $dest): void
{
$source = self::convertPath($source);
$dest = self::convertPath($dest);
// Check if source and dest are on the same device to avoid cross-device rename errors
$source_stat = @stat($source);
$dest_parent = dirname($dest);
$dest_stat = @stat($dest_parent);
// Only use rename if on same device
if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) {
if (@rename($source, $dest)) {
return;
}
}
// Fall back to copy + delete for cross-device moves or if rename failed
if (is_dir($source)) {
self::copyDir($source, $dest);
self::removeDir($source);
} else {
if (!copy($source, $dest)) {
throw new FileSystemException("Failed to copy file from {$source} to {$dest}");
}
if (!unlink($source)) {
throw new FileSystemException("Failed to remove source file: {$source}");
}
}
}
/**
* Unzip file with stripping top-level directory
*/

View File

@@ -22,9 +22,10 @@ class SourcePatcher
FileSystem::addSourceExtractHook('swoole', [__CLASS__, 'patchSwoole']);
FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchPhpLibxml212']);
FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchGDWin32']);
FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']);
// FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']);
FileSystem::addSourceExtractHook('sqlsrv', [__CLASS__, 'patchSQLSRVWin32']);
FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVWin32']);
FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVPhp85']);
FileSystem::addSourceExtractHook('yaml', [__CLASS__, 'patchYamlWin32']);
FileSystem::addSourceExtractHook('libyaml', [__CLASS__, 'patchLibYaml']);
FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchImapLicense']);
@@ -432,6 +433,23 @@ class SourcePatcher
return false;
}
/**
* Fix the compilation issue of pdo_sqlsrv with php 8.5
*/
public static function patchSQLSRVPhp85(): bool
{
$source_dir = SOURCE_PATH . '/php-src/ext/pdo_sqlsrv';
if (!file_exists($source_dir . '/config.m4') && is_dir($source_dir . '/source/pdo_sqlsrv')) {
FileSystem::moveFileOrDir($source_dir . '/LICENSE', $source_dir . '/source/pdo_sqlsrv/LICENSE');
FileSystem::moveFileOrDir($source_dir . '/source/shared', $source_dir . '/source/pdo_sqlsrv/shared');
FileSystem::moveFileOrDir($source_dir . '/source/pdo_sqlsrv', SOURCE_PATH . '/pdo_sqlsrv');
FileSystem::removeDir($source_dir);
FileSystem::moveFileOrDir(SOURCE_PATH . '/pdo_sqlsrv', $source_dir);
return true;
}
return false;
}
public static function patchYamlWin32(): bool
{
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/yaml/config.w32', "lib.substr(lib.length - 6, 6) == '_a.lib'", "lib.substr(lib.length - 6, 6) == '_a.lib' || 'yes' == 'yes'");
@@ -616,7 +634,13 @@ class SourcePatcher
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/gd/libgd/gdft.c', '#ifndef MSWIN32', '#ifndef _WIN32');
}
// custom config.w32, because official config.w32 is hard-coded many things
$origin = $ver_id >= 80100 ? file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_81.w32') : file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_80.w32');
if ($ver_id >= 80500) {
$origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_85.w32');
} elseif ($ver_id >= 80100) {
$origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_81.w32');
} else {
$origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_80.w32');
}
file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32.bak', file_get_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32'));
return file_put_contents(SOURCE_PATH . '/php-src/ext/gd/config.w32', $origin) !== false;
}

View File

@@ -30,8 +30,8 @@ class GoXcaddy extends CustomPackage
public function fetch(string $name, bool $force = false, ?array $config = null): void
{
$pkgroot = PKG_ROOT_PATH;
$go_exec = "{$pkgroot}/{$name}/bin/go";
$xcaddy_exec = "{$pkgroot}/{$name}/bin/xcaddy";
$go_exec = "{$pkgroot}/go-xcaddy/bin/go";
$xcaddy_exec = "{$pkgroot}/go-xcaddy/bin/xcaddy";
if ($force) {
FileSystem::removeDir("{$pkgroot}/{$name}");
}
@@ -48,10 +48,10 @@ class GoXcaddy extends CustomPackage
'macos' => 'darwin',
default => throw new \InvalidArgumentException('Unsupported OS: ' . $name),
};
$go_version = '1.25.0';
[$go_version] = explode("\n", Downloader::curlExec('https://go.dev/VERSION?m=text'));
$config = [
'type' => 'url',
'url' => "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz",
'url' => "https://go.dev/dl/{$go_version}.{$os}-{$arch}.tar.gz",
];
Downloader::downloadPackage($name, $config, $force);
}

View File

@@ -72,8 +72,11 @@ class Zig extends CustomPackage
$latest_version = null;
foreach ($index_json as $version => $data) {
$latest_version = $version;
break;
// Skip the master branch, get the latest stable release
if ($version !== 'master') {
$latest_version = $version;
break;
}
}
if (!$latest_version) {

View File

@@ -6,15 +6,21 @@ namespace SPC\store\source;
use JetBrains\PhpStorm\ArrayShape;
use SPC\exception\DownloaderException;
use SPC\exception\SPCException;
use SPC\store\Downloader;
class PhpSource extends CustomSourceBase
{
public const NAME = 'php-src';
public const string NAME = 'php-src';
public const array WEB_PHP_DOMAINS = [
'https://www.php.net',
'https://phpmirror.static-php.dev',
];
public function fetch(bool $force = false, ?array $config = null, int $lock_as = SPC_DOWNLOAD_SOURCE): void
{
$major = defined('SPC_BUILD_PHP_VERSION') ? SPC_BUILD_PHP_VERSION : '8.4';
$major = defined('SPC_BUILD_PHP_VERSION') ? SPC_BUILD_PHP_VERSION : '8.5';
if ($major === 'git') {
Downloader::downloadSource('php-src', ['type' => 'git', 'url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $force);
} else {
@@ -28,21 +34,26 @@ class PhpSource extends CustomSourceBase
#[ArrayShape(['type' => 'string', 'path' => 'string', 'rev' => 'string', 'url' => 'string'])]
public function getLatestPHPInfo(string $major_version): array
{
// 查找最新的小版本号
$info = json_decode(Downloader::curlExec(
url: "https://www.php.net/releases/index.php?json&version={$major_version}",
retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0
), true);
if (!isset($info['version'])) {
throw new DownloaderException("Version {$major_version} not found.");
foreach (self::WEB_PHP_DOMAINS as $domain) {
try {
$info = json_decode(Downloader::curlExec(
url: "{$domain}/releases/index.php?json&version={$major_version}",
retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0
), true);
if (!isset($info['version'])) {
throw new DownloaderException("Version {$major_version} not found.");
}
$version = $info['version'];
return [
'type' => 'url',
'url' => "{$domain}/distributions/php-{$version}.tar.xz",
];
} catch (SPCException) {
logger()->warning('Failed to fetch latest PHP version for major version {$major_version} from {$domain}, trying next mirror if available.');
continue;
}
}
$version = $info['version'];
// 从官网直接下载
return [
'type' => 'url',
'url' => "https://www.php.net/distributions/php-{$version}.tar.xz",
];
// exception if all mirrors failed
throw new DownloaderException("Failed to fetch latest PHP version for major version {$major_version} from all tried mirrors.");
}
}

View File

@@ -67,7 +67,8 @@ class ZigToolchain implements ToolchainInterface
$cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: '';
$has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4');
if (!$has_avx512) {
GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no');
$extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: '';
GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no {$extra_vars}");
}
}

View File

@@ -393,7 +393,7 @@ class ConfigValidator
}
// check php-version
if (isset($craft['php-version'])) {
// validdate version, accept 8.x, 7.x, 8.x.x, 7.x.x, 8, 7
// validate version, accept 8.x, 7.x, 8.x.x, 7.x.x, 8, 7
$version = strval($craft['php-version']);
if (!preg_match('/^(\d+)(\.\d+)?(\.\d+)?$/', $version, $matches)) {
throw new ValidationException('Craft file php-version is invalid');

View File

@@ -80,7 +80,6 @@ class SPCConfigUtil
$libs = $this->getLibsString($libraries, !$this->absolute_libs);
// additional OS-specific libraries (e.g. macOS -lresolv)
// embed
if ($extra_libs = SPCTarget::getRuntimeLibs()) {
$libs .= " {$extra_libs}";
}
@@ -226,9 +225,17 @@ class SPCConfigUtil
// parse pkg-configs
foreach ($libraries as $library) {
$pc = Config::getLib($library, 'pkg-configs', []);
$pkg_config_path = getenv('PKG_CONFIG_PATH') ?: '';
$search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path));
foreach ($pc as $file) {
if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$file}.pc")) {
throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$library}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first.");
$found = false;
foreach ($search_paths as $path) {
if (file_exists($path . "/{$file}.pc")) {
$found = true;
}
}
if (!$found) {
throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$library}] does not exist. Please build it first.");
}
}
$pc_cflags = implode(' ', $pc);
@@ -257,9 +264,17 @@ class SPCConfigUtil
foreach ($libraries as $library) {
// add pkg-configs libs
$pkg_configs = Config::getLib($library, 'pkg-configs', []);
foreach ($pkg_configs as $pkg_config) {
if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$pkg_config}.pc")) {
throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$library}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first.");
$pkg_config_path = getenv('PKG_CONFIG_PATH') ?: '';
$search_paths = array_filter(explode(is_unix() ? ':' : ';', $pkg_config_path));
foreach ($pkg_configs as $file) {
$found = false;
foreach ($search_paths as $path) {
if (file_exists($path . "/{$file}.pc")) {
$found = true;
}
}
if (!$found) {
throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$library}] does not exist. Please build it first.");
}
}
$pkg_configs = implode(' ', $pkg_configs);

View File

@@ -27,10 +27,10 @@ class SPCTarget
return true;
}
if (ToolchainManager::getToolchainClass() === GccNativeToolchain::class) {
return PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist();
return PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist() && !getenv('SPC_MUSL_DYNAMIC');
}
if (ToolchainManager::getToolchainClass() === ClangNativeToolchain::class) {
return PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist();
return PHP_OS_FAMILY === 'Linux' && SystemUtil::isMuslDist() && !getenv('SPC_MUSL_DYNAMIC');
}
// if SPC_LIBC is set, it means the target is static, remove it when 3.0 is released
if ($target = getenv('SPC_TARGET')) {

View File

@@ -16,12 +16,11 @@ class UnixAutoconfExecutor extends Executor
protected array $configure_args = [];
protected array $ignore_args = [];
public function __construct(protected BSDLibraryBase|LinuxLibraryBase|MacOSLibraryBase $library)
{
parent::__construct($library);
$this->initShell();
$this->configure_args = $this->getDefaultConfigureArgs();
}
/**
@@ -29,19 +28,12 @@ class UnixAutoconfExecutor extends Executor
*/
public function configure(...$args): static
{
// remove all the ignored args
$args = array_merge($args, $this->getDefaultConfigureArgs(), $this->configure_args);
$args = array_diff($args, $this->ignore_args);
$args = array_merge($args, $this->configure_args);
$configure_args = implode(' ', $args);
return $this->seekLogFileOnException(fn () => $this->shell->exec("./configure {$configure_args}"));
}
public function getConfigureArgsString(): string
{
return implode(' ', array_merge($this->getDefaultConfigureArgs(), $this->configure_args));
}
/**
* Run make
*
@@ -111,7 +103,7 @@ class UnixAutoconfExecutor extends Executor
*/
public function removeConfigureArgs(...$args): static
{
$this->ignore_args = [...$this->ignore_args, ...$args];
$this->configure_args = array_diff($this->configure_args, $args);
return $this;
}
@@ -133,8 +125,8 @@ class UnixAutoconfExecutor extends Executor
private function getDefaultConfigureArgs(): array
{
return [
'--disable-shared',
'--enable-static',
'--disable-shared',
"--prefix={$this->library->getBuildRootPath()}",
'--with-pic',
'--enable-pic',

View File

@@ -1,544 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Artifact;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\Config\ConfigValidator;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
class Artifact
{
public const int FETCH_PREFER_SOURCE = 0;
public const int FETCH_PREFER_BINARY = 1;
public const int FETCH_ONLY_SOURCE = 2;
public const int FETCH_ONLY_BINARY = 3;
protected ?array $config;
/** @var null|callable Bind custom source fetcher callback */
protected mixed $custom_source_callback = null;
/** @var array<string, callable> Bind custom binary fetcher callbacks */
protected mixed $custom_binary_callbacks = [];
/** @var null|callable Bind custom source extract callback (completely takes over extraction) */
protected mixed $source_extract_callback = null;
/** @var null|array{callback: callable, platforms: string[]} Bind custom binary extract callback (completely takes over extraction) */
protected ?array $binary_extract_callback = null;
/** @var array<callable> After source extract hooks */
protected array $after_source_extract_callbacks = [];
/** @var array<array{callback: callable, platforms: string[]}> After binary extract hooks */
protected array $after_binary_extract_callbacks = [];
public function __construct(protected readonly string $name, ?array $config = null)
{
$this->config = $config ?? ArtifactConfig::get($name);
if ($this->config === null) {
throw new WrongUsageException("Artifact '{$name}' not found.");
}
}
public function getName(): string
{
return $this->name;
}
/**
* Checks if the source of an artifact is already downloaded.
*
* @param bool $compare_hash Whether to compare hash of the downloaded source
*/
public function isSourceDownloaded(bool $compare_hash = false): bool
{
return ApplicationContext::get(ArtifactCache::class)->isSourceDownloaded($this->name, $compare_hash);
}
/**
* Checks if the binary of an artifact is already downloaded for the specified target OS.
*
* @param null|string $target_os Target OS platform string, null for current platform
* @param bool $compare_hash Whether to compare hash of the downloaded binary
*/
public function isBinaryDownloaded(?string $target_os = null, bool $compare_hash = false): bool
{
$target_os = $target_os ?? SystemTarget::getCurrentPlatformString();
return ApplicationContext::get(ArtifactCache::class)->isBinaryDownloaded($this->name, $target_os, $compare_hash);
}
public function shouldUseBinary(): bool
{
$platform = SystemTarget::getCurrentPlatformString();
return $this->isBinaryDownloaded($platform) && $this->hasPlatformBinary();
}
/**
* Checks if the source of an artifact is already extracted.
*
* @param bool $compare_hash Whether to compare hash of the extracted source
*/
public function isSourceExtracted(bool $compare_hash = false): bool
{
$target_path = $this->getSourceDir();
if (!is_dir($target_path)) {
return false;
}
if (!$compare_hash) {
return true;
}
// Get expected hash from cache
$cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name);
if ($cache_info === null) {
return false;
}
$expected_hash = $cache_info['hash'] ?? null;
// Local source: always consider extracted if directory exists
if ($expected_hash === null) {
return true;
}
// Check hash marker file
$hash_file = "{$target_path}/.spc-hash";
if (!file_exists($hash_file)) {
return false;
}
return FileSystem::readFile($hash_file) === $expected_hash;
}
/**
* Checks if the binary of an artifact is already extracted for the specified target OS.
*
* @param null|string $target_os Target OS platform string, null for current platform
* @param bool $compare_hash Whether to compare hash of the extracted binary
*/
public function isBinaryExtracted(?string $target_os = null, bool $compare_hash = false): bool
{
$target_os = $target_os ?? SystemTarget::getCurrentPlatformString();
$extract_config = $this->getBinaryExtractConfig();
$mode = $extract_config['mode'];
// For merge mode, check marker file
if ($mode === 'merge') {
$target_path = $extract_config['path'];
$marker_file = "{$target_path}/.spc-{$this->name}-installed";
if (!file_exists($marker_file)) {
return false;
}
if (!$compare_hash) {
return true;
}
// Get expected hash from cache
$cache_info = ApplicationContext::get(ArtifactCache::class)->getBinaryInfo($this->name, $target_os);
if ($cache_info === null) {
return false;
}
$expected_hash = $cache_info['hash'] ?? null;
if ($expected_hash === null) {
return true; // Local binary
}
$installed_hash = FileSystem::readFile($marker_file);
return $installed_hash === $expected_hash;
}
// For selective mode, cannot reliably check extraction status
if ($mode === 'selective') {
return false;
}
// For standalone mode, check directory and hash
$target_path = $extract_config['path'];
if (!is_dir($target_path)) {
return false;
}
if (!$compare_hash) {
return true;
}
// Get expected hash from cache
$cache_info = ApplicationContext::get(ArtifactCache::class)->getBinaryInfo($this->name, $target_os);
if ($cache_info === null) {
return false;
}
$expected_hash = $cache_info['hash'] ?? null;
// Local binary: always consider extracted if directory exists
if ($expected_hash === null) {
return true;
}
// Check hash marker file
$hash_file = "{$target_path}/.spc-hash";
if (!file_exists($hash_file)) {
return false;
}
return FileSystem::readFile($hash_file) === $expected_hash;
}
/**
* Checks if the artifact has a source defined.
*/
public function hasSource(): bool
{
return isset($this->config['source']) || $this->custom_source_callback !== null;
}
/**
* Checks if the artifact has a local binary defined for the current system target.
*/
public function hasPlatformBinary(): bool
{
$target = SystemTarget::getCurrentPlatformString();
return isset($this->config['binary'][$target]) || isset($this->custom_binary_callbacks[$target]);
}
public function getDownloadConfig(string $type): mixed
{
return $this->config[$type] ?? null;
}
/**
* Get source extraction directory.
*
* Rules:
* 1. If extract is not specified: SOURCE_PATH/{artifact_name}
* 2. If extract is relative path: SOURCE_PATH/{value}
* 3. If extract is absolute path: {value}
* 4. If extract is array (dict): handled by extractor (selective extraction)
*/
public function getSourceDir(): string
{
// defined in config
$extract = $this->config['source']['extract'] ?? null;
if ($extract === null) {
return FileSystem::convertPath(SOURCE_PATH . '/' . $this->name);
}
// Array (dict) mode - return default path, actual handling is in extractor
if (is_array($extract)) {
return FileSystem::convertPath(SOURCE_PATH . '/' . $this->name);
}
// String path
$path = $this->replaceExtractPathVariables($extract);
// Absolute path
if (!FileSystem::isRelativePath($path)) {
return FileSystem::convertPath($path);
}
// Relative path: based on SOURCE_PATH
return FileSystem::convertPath(SOURCE_PATH . '/' . $path);
}
/**
* Get binary extraction directory and mode.
*
* Rules:
* 1. If extract is not specified: PKG_ROOT_PATH (standard mode)
* 2. If extract is "hosted": BUILD_ROOT_PATH (standard mode, for pre-built libraries)
* 3. If extract is relative path: PKG_ROOT_PATH/{value} (standard mode)
* 4. If extract is absolute path: {value} (standard mode)
* 5. If extract is array (dict): selective extraction mode
*
* @return array{path: ?string, mode: 'merge'|'selective'|'standard', files?: array}
*/
public function getBinaryExtractConfig(array $cache_info = []): array
{
if (is_string($cache_info['extract'] ?? null)) {
return ['path' => $this->replaceExtractPathVariables($cache_info['extract']), 'mode' => 'standard'];
}
$platform = SystemTarget::getCurrentPlatformString();
$binary_config = $this->config['binary'][$platform] ?? null;
if ($binary_config === null) {
return ['path' => PKG_ROOT_PATH, 'mode' => 'standard'];
}
$extract = $binary_config['extract'] ?? null;
// Not specified: PKG_ROOT_PATH merge
if ($extract === null) {
return ['path' => PKG_ROOT_PATH, 'mode' => 'standard'];
}
// "hosted" mode: BUILD_ROOT_PATH merge (for pre-built libraries)
if ($extract === 'hosted' || ($binary_config['type'] ?? '') === 'hosted') {
return ['path' => BUILD_ROOT_PATH, 'mode' => 'standard'];
}
// Array (dict) mode: selective extraction
if (is_array($extract)) {
return [
'path' => null,
'mode' => 'selective',
'files' => $extract,
];
}
// String path
$path = $this->replaceExtractPathVariables($extract);
// Absolute path: standalone mode
if (!FileSystem::isRelativePath($path)) {
return ['path' => FileSystem::convertPath($path), 'mode' => 'standard'];
}
// Relative path: PKG_ROOT_PATH/{value} standalone mode
return ['path' => FileSystem::convertPath(PKG_ROOT_PATH . '/' . $path), 'mode' => 'standard'];
}
/**
* Get the binary extraction directory.
* For merge mode, returns the base path.
* For standalone mode, returns the specific directory.
*/
public function getBinaryDir(): string
{
$config = $this->getBinaryExtractConfig();
return $config['path'];
}
/**
* Set custom source fetcher callback.
*/
public function setCustomSourceCallback(callable $callback): void
{
$this->custom_source_callback = $callback;
}
public function getCustomSourceCallback(): ?callable
{
return $this->custom_source_callback ?? null;
}
public function getCustomBinaryCallback(): ?callable
{
$current_platform = SystemTarget::getCurrentPlatformString();
return $this->custom_binary_callbacks[$current_platform] ?? null;
}
public function emitCustomBinary(): void
{
$current_platform = SystemTarget::getCurrentPlatformString();
if (!isset($this->custom_binary_callbacks[$current_platform])) {
throw new SPCInternalException("No custom binary callback defined for artifact '{$this->name}' on target OS '{$current_platform}'.");
}
$callback = $this->custom_binary_callbacks[$current_platform];
ApplicationContext::invoke($callback, [Artifact::class => $this]);
}
/**
* Set custom binary fetcher callback for a specific target OS.
*
* @param string $target_os Target OS platform string (e.g. linux-x86_64)
* @param callable $callback Custom binary fetcher callback
*/
public function setCustomBinaryCallback(string $target_os, callable $callback): void
{
ConfigValidator::validatePlatformString($target_os);
$this->custom_binary_callbacks[$target_os] = $callback;
}
// ==================== Extraction Callbacks ====================
/**
* Set custom source extract callback.
* This callback completely takes over the source extraction process.
*
* Callback signature: function(Artifact $artifact, string $source_file, string $target_path): void
*/
public function setSourceExtractCallback(callable $callback): void
{
$this->source_extract_callback = $callback;
}
/**
* Get the source extract callback.
*/
public function getSourceExtractCallback(): ?callable
{
return $this->source_extract_callback;
}
/**
* Check if a custom source extract callback is set.
*/
public function hasSourceExtractCallback(): bool
{
return $this->source_extract_callback !== null;
}
/**
* Set custom binary extract callback.
* This callback completely takes over the binary extraction process.
*
* Callback signature: function(Artifact $artifact, string $source_file, string $target_path, string $platform): void
*
* @param callable $callback The callback function
* @param string[] $platforms Platform filters (empty = all platforms)
*/
public function setBinaryExtractCallback(callable $callback, array $platforms = []): void
{
$this->binary_extract_callback = [
'callback' => $callback,
'platforms' => $platforms,
];
}
/**
* Get the binary extract callback for current platform.
*
* @return null|callable The callback if set and matches current platform, null otherwise
*/
public function getBinaryExtractCallback(): ?callable
{
if ($this->binary_extract_callback === null) {
return null;
}
$platforms = $this->binary_extract_callback['platforms'];
$current_platform = SystemTarget::getCurrentPlatformString();
// Empty platforms array means all platforms
if (empty($platforms) || in_array($current_platform, $platforms, true)) {
return $this->binary_extract_callback['callback'];
}
return null;
}
/**
* Check if a custom binary extract callback is set for current platform.
*/
public function hasBinaryExtractCallback(): bool
{
return $this->getBinaryExtractCallback() !== null;
}
/**
* Add a callback to run after source extraction completes.
*
* Callback signature: function(string $target_path): void
*/
public function addAfterSourceExtractCallback(callable $callback): void
{
$this->after_source_extract_callbacks[] = $callback;
}
/**
* Add a callback to run after binary extraction completes.
*
* Callback signature: function(string $target_path, string $platform): void
*
* @param callable $callback The callback function
* @param string[] $platforms Platform filters (empty = all platforms)
*/
public function addAfterBinaryExtractCallback(callable $callback, array $platforms = []): void
{
$this->after_binary_extract_callbacks[] = [
'callback' => $callback,
'platforms' => $platforms,
];
}
/**
* Emit all after source extract callbacks.
*
* @param string $target_path The directory where source was extracted
*/
public function emitAfterSourceExtract(string $target_path): void
{
if (empty($this->after_source_extract_callbacks)) {
logger()->debug("No after-source-extract hooks registered for [{$this->name}]");
return;
}
logger()->debug('Executing ' . count($this->after_source_extract_callbacks) . " after-source-extract hook(s) for [{$this->name}]");
foreach ($this->after_source_extract_callbacks as $callback) {
$callback_name = is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure');
logger()->debug(" 🪝 Running hook: {$callback_name}");
ApplicationContext::invoke($callback, ['target_path' => $target_path, Artifact::class => $this]);
}
}
/**
* Emit all after binary extract callbacks for the specified platform.
*
* @param null|string $target_path The directory where binary was extracted
* @param string $platform The platform string (e.g., 'linux-x86_64')
*/
public function emitAfterBinaryExtract(?string $target_path, string $platform): void
{
if (empty($this->after_binary_extract_callbacks)) {
logger()->debug("No after-binary-extract hooks registered for [{$this->name}]");
return;
}
$executed = 0;
foreach ($this->after_binary_extract_callbacks as $item) {
$callback_platforms = $item['platforms'];
// Empty platforms array means all platforms
if (empty($callback_platforms) || in_array($platform, $callback_platforms, true)) {
$callback = $item['callback'];
$callback_name = is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure');
logger()->debug(" 🪝 Running hook: {$callback_name} (platform: {$platform})");
ApplicationContext::invoke($callback, [
'target_path' => $target_path,
'platform' => $platform,
Artifact::class => $this,
]);
++$executed;
}
}
logger()->debug("Executed {$executed} after-binary-extract hook(s) for [{$this->name}] on platform [{$platform}]");
}
/**
* Replaces variables in the extract path.
*
* @param string $extract the extract path with variables
*/
private function replaceExtractPathVariables(string $extract): string
{
$replacement = [
'{artifact_name}' => $this->name,
'{pkg_root_path}' => PKG_ROOT_PATH,
'{build_root_path}' => BUILD_ROOT_PATH,
'{php_sdk_path}' => getenv('PHP_SDK_PATH') ?: WORKING_DIR . '/php-sdk-binary-tools',
'{working_dir}' => WORKING_DIR,
'{download_path}' => DOWNLOAD_PATH,
'{source_path}' => SOURCE_PATH,
];
return str_replace(array_keys($replacement), array_values($replacement), $extract);
}
}

View File

@@ -1,305 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Artifact;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Util\FileSystem;
class ArtifactCache
{
/**
* @var array<string, array{
* source: null|array{
* lock_type: 'binary'|'source',
* cache_type: 'archive'|'file'|'git'|'local',
* filename?: string,
* dirname?: string,
* extract: null|'&custom'|string,
* hash: null|string
* },
* binary: array{
* windows-x86_64?: null|array{
* lock_type: 'binary'|'source',
* cache_type: 'archive'|'file'|'git'|'local',
* filename?: string,
* dirname?: string,
* extract: null|'&custom'|string,
* hash: null|string,
* version?: null|string
* }
* }
* }>
*/
protected array $cache = [];
/**
* @param string $cache_file Lock file position
*/
public function __construct(protected string $cache_file = DOWNLOAD_PATH . '/.cache.json')
{
if (!file_exists($this->cache_file)) {
logger()->debug("Cache file does not exist, creating new one at {$this->cache_file}");
FileSystem::createDir(dirname($this->cache_file));
file_put_contents($this->cache_file, json_encode([]));
} else {
$content = file_get_contents($this->cache_file);
$this->cache = json_decode($content ?: '{}', true) ?? [];
}
}
/**
* Checks if the source of an artifact is already downloaded.
*
* @param string $artifact_name Artifact name
* @param bool $compare_hash Whether to compare hash of the downloaded source
*/
public function isSourceDownloaded(string $artifact_name, bool $compare_hash = false): bool
{
$item = $this->cache[$artifact_name] ?? null;
if ($item === null) {
return false;
}
return $this->isObjectDownloaded($this->cache[$artifact_name]['source'] ?? null, $compare_hash);
}
/**
* Check if the binary of an artifact for target OS is already downloaded.
*
* @param string $artifact_name Artifact name
* @param string $target_os Target OS (accepts {windows|linux|macos}-{x86_64|aarch64})
* @param bool $compare_hash Whether to compare hash of the downloaded binary
*/
public function isBinaryDownloaded(string $artifact_name, string $target_os, bool $compare_hash = false): bool
{
$item = $this->cache[$artifact_name] ?? null;
if ($item === null) {
return false;
}
return $this->isObjectDownloaded($this->cache[$artifact_name]['binary'][$target_os] ?? null, $compare_hash);
}
/**
* Lock the downloaded artifact info into cache.
*
* @param Artifact|string $artifact Artifact instance
* @param 'binary'|'source' $lock_type Lock type ('source'|'binary')
* @param DownloadResult $download_result Download result object
* @param null|string $platform Target platform string for binary lock, null for source lock
*/
public function lock(Artifact|string $artifact, string $lock_type, DownloadResult $download_result, ?string $platform = null): void
{
$artifact_name = $artifact instanceof Artifact ? $artifact->getName() : $artifact;
if (!isset($this->cache[$artifact_name])) {
$this->cache[$artifact_name] = [
'source' => null,
'binary' => [],
];
}
$obj = null;
if ($download_result->cache_type === 'archive') {
$obj = [
'lock_type' => $lock_type,
'cache_type' => 'archive',
'filename' => $download_result->filename,
'extract' => $download_result->extract,
'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename),
'version' => $download_result->version,
'config' => $download_result->config,
];
} elseif ($download_result->cache_type === 'file') {
$obj = [
'lock_type' => $lock_type,
'cache_type' => 'file',
'filename' => $download_result->filename,
'extract' => $download_result->extract,
'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename),
'version' => $download_result->version,
'config' => $download_result->config,
];
} elseif ($download_result->cache_type === 'git') {
$obj = [
'lock_type' => $lock_type,
'cache_type' => 'git',
'dirname' => $download_result->dirname,
'extract' => $download_result->extract,
'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')),
'version' => $download_result->version,
'config' => $download_result->config,
];
} elseif ($download_result->cache_type === 'local') {
$obj = [
'lock_type' => $lock_type,
'cache_type' => 'local',
'dirname' => $download_result->dirname,
'extract' => $download_result->extract,
'hash' => null,
'version' => $download_result->version,
'config' => $download_result->config,
];
}
if ($obj === null) {
throw new SPCInternalException("Invalid download result for locking artifact {$artifact_name}");
}
if ($lock_type === 'binary') {
if ($platform === null) {
throw new SPCInternalException("Invalid download result for locking binary artifact {$artifact_name}: platform cannot be null");
}
$obj['platform'] = $platform;
}
if ($lock_type === 'source') {
$this->cache[$artifact_name]['source'] = $obj;
} elseif ($lock_type === 'binary') {
$this->cache[$artifact_name]['binary'][$platform] = $obj;
} else {
throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}");
}
// save cache to file
file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT));
}
/**
* Get source cache info for an artifact.
*
* @param string $artifact_name Artifact name
* @return null|array Cache info array or null if not found
*/
public function getSourceInfo(string $artifact_name): ?array
{
return $this->cache[$artifact_name]['source'] ?? null;
}
/**
* Get binary cache info for an artifact on specific platform.
*
* @param string $artifact_name Artifact name
* @param string $platform Platform string (e.g., 'linux-x86_64')
* @return null|array{
* lock_type: 'binary'|'source',
* cache_type: 'archive'|'git'|'local',
* filename?: string,
* extract: null|'&custom'|string,
* hash: null|string,
* dirname?: string,
* version?: null|string
* } Cache info array or null if not found
*/
public function getBinaryInfo(string $artifact_name, string $platform): ?array
{
return $this->cache[$artifact_name]['binary'][$platform] ?? null;
}
/**
* Get the full path to the cached file/directory.
*
* @param array $cache_info Cache info from getSourceInfo() or getBinaryInfo()
* @return string Full path to the cached file or directory
*/
public function getCacheFullPath(array $cache_info): string
{
return match ($cache_info['cache_type']) {
'archive', 'file' => DOWNLOAD_PATH . '/' . $cache_info['filename'],
'git' => DOWNLOAD_PATH . '/' . $cache_info['dirname'],
'local' => $cache_info['dirname'], // local dirname is absolute path
default => throw new SPCInternalException("Unknown cache type: {$cache_info['cache_type']}"),
};
}
/**
* Remove source cache entry for an artifact.
*
* @param string $artifact_name Artifact name
* @param bool $delete_file Whether to also delete the cached file/directory
*/
public function removeSource(string $artifact_name, bool $delete_file = false): void
{
$source_info = $this->getSourceInfo($artifact_name);
if ($source_info === null) {
return;
}
// Optionally delete the actual file/directory
if ($delete_file) {
$path = $this->getCacheFullPath($source_info);
if (in_array($source_info['cache_type'], ['archive', 'file']) && file_exists($path)) {
unlink($path);
logger()->debug("Deleted cached archive: {$path}");
} elseif ($source_info['cache_type'] === 'git' && is_dir($path)) {
FileSystem::removeDir($path);
logger()->debug("Deleted cached git repository: {$path}");
}
}
// Remove from cache
$this->cache[$artifact_name]['source'] = null;
$this->save();
logger()->debug("Removed source cache entry for [{$artifact_name}]");
}
/**
* Remove binary cache entry for an artifact on specific platform.
*
* @param string $artifact_name Artifact name
* @param string $platform Platform string (e.g., 'linux-x86_64')
* @param bool $delete_file Whether to also delete the cached file/directory
*/
public function removeBinary(string $artifact_name, string $platform, bool $delete_file = false): void
{
$binary_info = $this->getBinaryInfo($artifact_name, $platform);
if ($binary_info === null) {
return;
}
// Optionally delete the actual file/directory
if ($delete_file) {
$path = $this->getCacheFullPath($binary_info);
if (in_array($binary_info['cache_type'], ['archive', 'file']) && file_exists($path)) {
unlink($path);
logger()->debug("Deleted cached binary archive: {$path}");
} elseif ($binary_info['cache_type'] === 'git' && is_dir($path)) {
FileSystem::removeDir($path);
logger()->debug("Deleted cached binary git repository: {$path}");
}
}
// Remove from cache
unset($this->cache[$artifact_name]['binary'][$platform]);
$this->save();
logger()->debug("Removed binary cache entry for [{$artifact_name}] on platform [{$platform}]");
}
/**
* Save cache to file.
*/
public function save(): void
{
file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT));
}
private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool
{
if ($object === null) {
return false;
}
// check if source is cached and file/dir exists in downloads/ dir
return match ($object['cache_type'] ?? null) {
'archive', 'file' => isset($object['filename']) &&
file_exists(DOWNLOAD_PATH . '/' . $object['filename']) &&
(!$compare_hash || (
isset($object['hash']) &&
sha1_file(DOWNLOAD_PATH . '/' . $object['filename']) === $object['hash']
)),
'git' => isset($object['dirname']) &&
is_dir(DOWNLOAD_PATH . '/' . $object['dirname'] . '/.git') &&
(!$compare_hash || (
isset($object['hash']) &&
trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $object['dirname']) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')) === $object['hash']
)),
'local' => isset($object['dirname']) &&
is_dir($object['dirname']), // local dirname is absolute path
default => false,
};
}
}

View File

@@ -1,676 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Artifact;
use Psr\Log\LogLevel;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Artifact\Downloader\Type\BitBucketTag;
use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface;
use StaticPHP\Artifact\Downloader\Type\FileList;
use StaticPHP\Artifact\Downloader\Type\Git;
use StaticPHP\Artifact\Downloader\Type\GitHubRelease;
use StaticPHP\Artifact\Downloader\Type\GitHubTarball;
use StaticPHP\Artifact\Downloader\Type\HostedPackageBin;
use StaticPHP\Artifact\Downloader\Type\LocalDir;
use StaticPHP\Artifact\Downloader\Type\PhpRelease;
use StaticPHP\Artifact\Downloader\Type\PIE;
use StaticPHP\Artifact\Downloader\Type\Url;
use StaticPHP\Artifact\Downloader\Type\ValidatorInterface;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Exception\ExecutionException;
use StaticPHP\Exception\SPCException;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Logger\ConsoleColor;
/**
* Artifact Downloader class
*/
class ArtifactDownloader
{
/** @var array<string, class-string<DownloadTypeInterface>> */
public const array DOWNLOADERS = [
'bitbuckettag' => BitBucketTag::class,
'filelist' => FileList::class,
'git' => Git::class,
'ghrel' => GitHubRelease::class,
'ghtar' => GitHubTarball::class,
'ghtagtar' => GitHubTarball::class,
'local' => LocalDir::class,
'pie' => PIE::class,
'url' => Url::class,
'php-release' => PhpRelease::class,
'hosted' => HostedPackageBin::class,
];
/** @var array<string, Artifact> Artifact objects */
protected array $artifacts = [];
/** @var int Parallel process number (1 and 0 as single-threaded mode) */
protected int $parallel = 1;
protected int $retry = 0;
/** @var array<string, string> Override custom download urls from options */
protected array $custom_urls = [];
/** @var array<string, array{0: string, 1: string}> Override custom git options from options ([branch, git url]) */
protected array $custom_gits = [];
/** @var array<string, string> Override custom local paths from options */
protected array $custom_locals = [];
/** @var int Fetch type preference */
protected int $default_fetch_pref = Artifact::FETCH_PREFER_SOURCE;
/** @var array<string, int> Specific fetch preference */
protected array $fetch_prefs = [];
/** @var array<string>|bool Whether to ignore cache for specific artifacts or all */
protected array|bool $ignore_cache = false;
/** @var bool Whether to enable alternative mirror downloads */
protected bool $alt = true;
private array $_before_files;
/**
* @param array{
* parallel?: int,
* retry?: int,
* custom-url?: array<string>,
* custom-git?: array<string>,
* custom-local?: array<string>,
* prefer-source?: null|bool|string,
* prefer-pre-built?: null|bool|string,
* prefer-binary?: null|bool|string,
* source-only?: null|bool|string,
* binary-only?: null|bool|string,
* ignore-cache?: null|bool|string,
* ignore-cache-sources?: null|bool|string,
* no-alt?: bool,
* no-shallow-clone?: bool
* } $options Downloader options
*/
public function __construct(protected array $options = [])
{
// Allow setting concurrency via options
$this->parallel = max(1, (int) ($options['parallel'] ?? 1));
// Allow setting retry via options
$this->retry = max(0, (int) ($options['retry'] ?? 0));
// Prefer source (default)
if (array_key_exists('prefer-source', $options)) {
if (is_string($options['prefer-source'])) {
$ls = parse_comma_list($options['prefer-source']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_PREFER_SOURCE;
}
} elseif ($options['prefer-source'] === null || $options['prefer-source'] === true) {
$this->default_fetch_pref = Artifact::FETCH_PREFER_SOURCE;
}
}
// Prefer binary (originally prefer-pre-built)
if (array_key_exists('prefer-binary', $options)) {
if (is_string($options['prefer-binary'])) {
$ls = parse_comma_list($options['prefer-binary']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY;
}
} elseif ($options['prefer-binary'] === null || $options['prefer-binary'] === true) {
$this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY;
}
}
if (array_key_exists('prefer-pre-built', $options)) {
if (is_string($options['prefer-pre-built'])) {
$ls = parse_comma_list($options['prefer-pre-built']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY;
}
} elseif ($options['prefer-pre-built'] === null || $options['prefer-pre-built'] === true) {
$this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY;
}
}
// Source only
if (array_key_exists('source-only', $options)) {
if (is_string($options['source-only'])) {
$ls = parse_comma_list($options['source-only']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_ONLY_SOURCE;
}
} elseif ($options['source-only'] === null || $options['source-only'] === true) {
$this->default_fetch_pref = Artifact::FETCH_ONLY_SOURCE;
}
}
// Binary only
if (array_key_exists('binary-only', $options)) {
if (is_string($options['binary-only'])) {
$ls = parse_comma_list($options['binary-only']);
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_ONLY_BINARY;
}
} elseif ($options['binary-only'] === null || $options['binary-only'] === true) {
$this->default_fetch_pref = Artifact::FETCH_ONLY_BINARY;
}
}
// Ignore cache
if (array_key_exists('ignore-cache', $options)) {
if (is_string($options['ignore-cache'])) {
$this->ignore_cache = parse_comma_list($options['ignore-cache']);
} elseif ($options['ignore-cache'] === null || $options['ignore-cache'] === true) {
$this->ignore_cache = true;
}
}
// backward compatibility for ignore-cache-sources
if (array_key_exists('ignore-cache-sources', $options)) {
if (is_string($options['ignore-cache-sources'])) {
$this->ignore_cache = parse_comma_list($options['ignore-cache-sources']);
} elseif ($options['ignore-cache-sources'] === null || $options['ignore-cache-sources'] === true) {
$this->ignore_cache = true;
}
}
// Allow setting custom urls via options
foreach (($options['custom-url'] ?? []) as $value) {
[$artifact_name, $url] = explode(':', $value, 2);
$this->custom_urls[$artifact_name] = $url;
$this->ignore_cache = match ($this->ignore_cache) {
true => true,
false => [$artifact_name],
default => array_merge($this->ignore_cache, [$artifact_name]),
};
}
// Allow setting custom git options via options
foreach (($options['custom-git'] ?? []) as $value) {
[$artifact_name, $branch, $git_url] = explode(':', $value, 3) + [null, null, null];
$this->custom_gits[$artifact_name] = [$branch ?? 'main', $git_url];
$this->ignore_cache = match ($this->ignore_cache) {
true => true,
false => [$artifact_name],
default => array_merge($this->ignore_cache, [$artifact_name]),
};
}
// Allow setting custom local paths via options
foreach (($options['custom-local'] ?? []) as $value) {
[$artifact_name, $local_path] = explode(':', $value, 2);
$this->custom_locals[$artifact_name] = $local_path;
$this->ignore_cache = match ($this->ignore_cache) {
true => true,
false => [$artifact_name],
default => array_merge($this->ignore_cache, [$artifact_name]),
};
}
// no alt
if (array_key_exists('no-alt', $options) && $options['no-alt'] === true) {
$this->alt = false;
}
// read downloads dir
$this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
}
/**
* Add an artifact to the download list.
*
* @param Artifact|string $artifact Artifact instance or artifact name
*/
public function add(Artifact|string $artifact): static
{
if (is_string($artifact)) {
$artifact_instance = ArtifactLoader::getArtifactInstance($artifact);
} else {
$artifact_instance = $artifact;
}
if ($artifact_instance === null) {
$name = $artifact;
throw new WrongUsageException("Artifact '{$name}' not found, please check the name.");
}
// only add if not already added
if (!isset($this->artifacts[$artifact_instance->getName()])) {
$this->artifacts[$artifact_instance->getName()] = $artifact_instance;
}
return $this;
}
/**
* Add multiple artifacts to the download list.
*
* @param array<Artifact|string> $artifacts Multiple artifacts to add
*/
public function addArtifacts(array $artifacts): static
{
foreach ($artifacts as $artifact) {
$this->add($artifact);
}
return $this;
}
/**
* Set the concurrency limit for parallel downloads.
*
* @param int $parallel Number of concurrent downloads (default: 3)
*/
public function setParallel(int $parallel): static
{
$this->parallel = max(1, $parallel);
return $this;
}
/**
* Download all artifacts, with optional parallel processing.
*
* @param bool $interactive Enable interactive mode with Ctrl+C handling
*/
public function download(bool $interactive = true): void
{
if ($interactive) {
Shell::passthruCallback(function () {
InteractiveTerm::advance();
});
keyboard_interrupt_register(function () {
echo PHP_EOL;
InteractiveTerm::error('Download cancelled by user.');
// scan changed files
$after_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
$new_files = array_diff($after_files, $this->_before_files);
// remove new files
foreach ($new_files as $file) {
if ($file === '.cache.json') {
continue;
}
logger()->debug("Removing corrupted artifact: {$file}");
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $file;
if (is_dir($path)) {
FileSystem::removeDir($path);
} elseif (is_file($path)) {
FileSystem::removeFileIfExists($path);
}
}
exit(2);
});
}
$this->applyCustomDownloads();
$count = count($this->artifacts);
$artifacts_str = implode(',', array_map(fn ($x) => '' . ConsoleColor::yellow($x->getName()), $this->artifacts));
// mute the first line if not interactive
if ($interactive) {
InteractiveTerm::notice("Downloading {$count} artifacts: {$artifacts_str} ...");
}
try {
// Create dir
if (!is_dir(DOWNLOAD_PATH)) {
FileSystem::createDir(DOWNLOAD_PATH);
}
logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ...");
// Download artifacts parallely
if ($this->parallel > 1) {
$this->downloadWithConcurrency();
} else {
// normal sequential download
$current = 0;
$skipped = [];
foreach ($this->artifacts as $artifact) {
++$current;
if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) {
$skipped[] = $artifact->getName();
continue;
}
$this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: [];
}
if ($interactive) {
$skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : '';
InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}", true);
echo PHP_EOL;
}
}
} catch (SPCException $e) {
array_map(fn ($x) => InteractiveTerm::error($x), explode("\n", $e->getMessage()));
throw new WrongUsageException();
} finally {
if ($interactive) {
Shell::passthruCallback(null);
keyboard_interrupt_unregister();
}
}
}
public function getRetry(): int
{
return $this->retry;
}
public function getArtifacts(): array
{
return $this->artifacts;
}
public function getOption(string $name, mixed $default = null): mixed
{
return $this->options[$name] ?? $default;
}
private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int
{
$queue = $this->generateQueue($artifact);
// already downloaded
if ($queue === []) {
logger()->debug("Artifact '{$artifact->getName()}' is already downloaded, skipping.");
return SPC_DOWNLOAD_STATUS_SKIPPED;
}
$try = false;
foreach ($queue as $item) {
try {
$instance = null;
$call = self::DOWNLOADERS[$item['config']['type']] ?? null;
$type_display_name = match (true) {
$item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader',
$item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader',
default => SPC_DOWNLOAD_TYPE_DISPLAY_NAME[$item['config']['type']] ?? $item['config']['type'],
};
$try_h = $try ? 'Try downloading' : 'Downloading';
logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ...");
if ($parallel === false && $interactive) {
InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ...");
}
// is valid download type
if ($item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null) {
$lock = ApplicationContext::invoke($callback, [
Artifact::class => $artifact,
ArtifactDownloader::class => $this,
]);
} elseif ($item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null) {
$lock = ApplicationContext::invoke($callback, [
Artifact::class => $artifact,
ArtifactDownloader::class => $this,
]);
} elseif (is_a($call, DownloadTypeInterface::class, true)) {
$instance = new $call();
$lock = $instance->download($artifact->getName(), $item['config'], $this);
} else {
if ($item['config']['type'] === 'custom') {
$msg = "Artifact [{$artifact->getName()}] has no valid custom " . SystemTarget::getCurrentPlatformString() . ' download callback defined.';
} else {
$msg = "Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}.";
}
throw new ValidationException($msg);
}
if (!$lock instanceof DownloadResult) {
throw new ValidationException("Artifact {$artifact->getName()} has invalid custom return value. Must be instance of DownloadResult.");
}
// verifying hash if possible
$hash_validator = $instance ?? null;
$verified = $lock->verified;
if ($hash_validator instanceof ValidatorInterface) {
if (!$hash_validator->validate($artifact->getName(), $item['config'], $this, $lock)) {
throw new ValidationException("Hash validation failed for artifact '{$artifact->getName()}' {$item['display']}.");
}
$verified = true;
}
// process lock
ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString());
if ($parallel === false && $interactive) {
$ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : '';
InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} .");
}
return SPC_DOWNLOAD_STATUS_SUCCESS;
} catch (DownloaderException|ExecutionException $e) {
if ($parallel === false && $interactive) {
InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false);
InteractiveTerm::error("Failed message: {$e->getMessage()}", true);
}
$try = true;
continue;
} catch (ValidationException $e) {
if ($parallel === false) {
InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false);
InteractiveTerm::error("Validation failed: {$e->getMessage()}");
}
break;
}
}
$vvv = ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-vvv` to enable verbose mode, and disable parallel downloading for more details." : '';
throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}");
}
private function downloadWithConcurrency(): void
{
$skipped = [];
$fiber_pool = [];
$old_verbosity = null;
$old_debug = null;
try {
$count = count($this->artifacts);
// must mute
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
$old_verbosity = $output->getVerbosity();
$old_debug = ApplicationContext::isDebug();
logger()->warning('Parallel download is not supported in verbose mode, I will mute the output temporarily.');
$output->setVerbosity(OutputInterface::VERBOSITY_NORMAL);
ApplicationContext::setDebug(false);
logger()->setLevel(LogLevel::ERROR);
}
$pool_count = $this->parallel;
$downloaded = 0;
$total = count($this->artifacts);
Shell::passthruCallback(function () {
InteractiveTerm::advance();
\Fiber::suspend();
});
InteractiveTerm::indicateProgress("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ...");
$failed_downloads = [];
while (true) {
// fill pool
while (count($fiber_pool) < $pool_count && ($artifact = array_shift($this->artifacts)) !== null) {
$current = $count - count($this->artifacts);
$fiber = new \Fiber(function () use ($artifact, $current, $count) {
return [$artifact, $this->downloadWithType($artifact, $current, $count, true)];
});
$fiber->start();
$fiber_pool[] = $fiber;
}
// check pool
foreach ($fiber_pool as $index => $fiber) {
if ($fiber->isTerminated()) {
try {
[$artifact, $int] = $fiber->getReturn();
if ($int === SPC_DOWNLOAD_STATUS_SKIPPED) {
$skipped[] = $artifact->getName();
}
} catch (\Throwable $e) {
$artifact_name = 'unknown';
if (isset($artifact)) {
$artifact_name = $artifact->getName();
}
$failed_downloads[] = ['artifact' => $artifact_name, 'error' => $e];
InteractiveTerm::setMessage("[{$downloaded}/{$total}] Download failed: {$artifact_name}");
InteractiveTerm::advance();
}
// remove from pool
unset($fiber_pool[$index]);
++$downloaded;
InteractiveTerm::setMessage("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ...");
InteractiveTerm::advance();
} else {
$fiber->resume();
}
}
// all done
if (count($this->artifacts) === 0 && count($fiber_pool) === 0) {
if (!empty($failed_downloads)) {
InteractiveTerm::finish('Download completed with ' . count($failed_downloads) . ' failure(s).', false);
foreach ($failed_downloads as $failure) {
InteractiveTerm::error("Failed to download '{$failure['artifact']}': {$failure['error']->getMessage()}");
}
throw new DownloaderException('Failed to download ' . count($failed_downloads) . ' artifact(s). Please check your internet connection and try again.');
}
$skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : '';
InteractiveTerm::finish("Downloaded all {$total} artifacts.{$skip_msg}");
break;
}
}
} catch (\Throwable $e) {
// throw to all fibers to make them stop
foreach ($fiber_pool as $fiber) {
if (!$fiber->isTerminated()) {
try {
$fiber->throw($e);
} catch (\Throwable) {
// ignore errors when stopping fibers
}
}
}
InteractiveTerm::finish('Parallel download failed !', false);
throw $e;
} finally {
if ($old_verbosity !== null) {
ApplicationContext::get(OutputInterface::class)->setVerbosity($old_verbosity);
logger()->setLevel(match ($old_verbosity) {
OutputInterface::VERBOSITY_VERBOSE => LogLevel::INFO,
OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => LogLevel::DEBUG,
default => LogLevel::WARNING,
});
}
if ($old_debug !== null) {
ApplicationContext::setDebug($old_debug);
}
Shell::passthruCallback(null);
}
}
/**
* Generate download queue based on type preference.
*/
private function generateQueue(Artifact $artifact): array
{
/** @var array<array{display: string, lock: string, config: array}> $queue */
$queue = [];
$binary_downloaded = $artifact->isBinaryDownloaded(compare_hash: true);
$source_downloaded = $artifact->isSourceDownloaded(compare_hash: true);
$item_source = ['display' => 'source', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source')];
$item_source_mirror = ['display' => 'source (mirror)', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source-mirror')];
// For binary config, handle both array configs and custom callbacks
$binary_config = $artifact->getDownloadConfig('binary');
$has_custom_binary = $artifact->getCustomBinaryCallback() !== null;
$item_binary_config = null;
if (is_array($binary_config)) {
$item_binary_config = $binary_config[SystemTarget::getCurrentPlatformString()] ?? null;
} elseif ($has_custom_binary) {
// For custom binaries, create a dummy config to allow queue generation
$item_binary_config = ['type' => 'custom'];
}
$item_binary = ['display' => 'binary', 'lock' => 'binary', 'config' => $item_binary_config];
$binary_mirror_config = $artifact->getDownloadConfig('binary-mirror');
$item_binary_mirror_config = null;
if (is_array($binary_mirror_config)) {
$item_binary_mirror_config = $binary_mirror_config[SystemTarget::getCurrentPlatformString()] ?? null;
}
$item_binary_mirror = ['display' => 'binary (mirror)', 'lock' => 'binary', 'config' => $item_binary_mirror_config];
$pref = $this->fetch_prefs[$artifact->getName()] ?? $this->default_fetch_pref;
if ($pref === Artifact::FETCH_PREFER_SOURCE) {
$queue[] = $item_source['config'] !== null ? $item_source : null;
$queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null;
$queue[] = $item_binary['config'] !== null ? $item_binary : null;
$queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null;
} elseif ($pref === Artifact::FETCH_PREFER_BINARY) {
$queue[] = $item_binary['config'] !== null ? $item_binary : null;
$queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null;
$queue[] = $item_source['config'] !== null ? $item_source : null;
$queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null;
} elseif ($pref === Artifact::FETCH_ONLY_SOURCE) {
$queue[] = $item_source['config'] !== null ? $item_source : null;
$queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null;
} elseif ($pref === Artifact::FETCH_ONLY_BINARY) {
$queue[] = $item_binary['config'] !== null ? $item_binary : null;
$queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null;
}
// filter nulls
$queue = array_values(array_filter($queue));
// always download
if ($this->ignore_cache === true || is_array($this->ignore_cache) && in_array($artifact->getName(), $this->ignore_cache)) {
// validate: ensure at least one download source is available
if (empty($queue)) {
throw new ValidationException("Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').');
}
return $queue;
}
// check if already downloaded
$has_usable_download = false;
if ($pref === Artifact::FETCH_PREFER_SOURCE) {
// prefer source: check source first, if not available check binary
$has_usable_download = $source_downloaded || $binary_downloaded;
} elseif ($pref === Artifact::FETCH_PREFER_BINARY) {
// prefer binary: check binary first, if not available check source
$has_usable_download = $binary_downloaded || $source_downloaded;
} elseif ($pref === Artifact::FETCH_ONLY_SOURCE) {
// source-only: only check if source is downloaded
$has_usable_download = $source_downloaded;
} elseif ($pref === Artifact::FETCH_ONLY_BINARY) {
// binary-only: only check if binary for current platform is downloaded
$has_usable_download = $binary_downloaded;
}
// if already downloaded, skip
if ($has_usable_download) {
return [];
}
// validate: ensure at least one download source is available
if (empty($queue)) {
if ($pref === Artifact::FETCH_ONLY_SOURCE) {
throw new ValidationException("Artifact '{$artifact->getName()}' does not provide source download, cannot use --source-only mode.");
}
if ($pref === Artifact::FETCH_ONLY_BINARY) {
throw new ValidationException("Artifact '{$artifact->getName()}' does not provide binary download for current platform (" . SystemTarget::getCurrentPlatformString() . '), cannot use --binary-only mode.');
}
// prefer modes should also throw error if no download source available
throw new ValidationException("Validation failed: Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').');
}
return $queue;
}
private function applyCustomDownloads(): void
{
foreach ($this->custom_urls as $artifact_name => $custom_url) {
if (isset($this->artifacts[$artifact_name])) {
$this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $custom_url) {
return (new Url())->download($artifact_name, ['url' => $custom_url], $downloader);
});
}
}
foreach ($this->custom_gits as $artifact_name => [$branch, $git_url]) {
if (isset($this->artifacts[$artifact_name])) {
$this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $branch, $git_url) {
return (new Git())->download($artifact_name, ['rev' => $branch, 'url' => $git_url], $downloader);
});
}
}
foreach ($this->custom_locals as $artifact_name => $local_path) {
if (isset($this->artifacts[$artifact_name])) {
$this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $local_path) {
return (new LocalDir())->download($artifact_name, ['dirname' => $local_path], $downloader);
});
}
}
}
}

View File

@@ -1,635 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Artifact;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\FileSystemException;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\Package;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\V2CompatLayer;
/**
* ArtifactExtractor is responsible for extracting downloaded artifacts to their target locations.
*
* Extraction rules for source:
* 1. If extract is not specified: SOURCE_PATH/{artifact_name}
* 2. If extract is relative path: SOURCE_PATH/{value}
* 3. If extract is absolute path: {value}
* 4. If extract is array (dict): selective extraction (file mapping)
*
* Extraction rules for binary:
* 1. If extract is not specified: PKG_ROOT_PATH (standard mode)
* 2. If extract is "hosted": BUILD_ROOT_PATH (standard mode, for pre-built libraries)
* 3. If extract is relative path: PKG_ROOT_PATH/{value} (standard mode)
* 4. If extract is absolute path: {value} (standard mode)
* 5. If extract is array (dict): selective extraction mode
*/
class ArtifactExtractor
{
/** @var array<string, bool> Track extracted artifacts to avoid duplicate extraction */
protected array $extracted = [];
public function __construct(
protected ArtifactCache $cache,
protected bool $interactive = true
) {}
/**
* Extract all artifacts for a list of packages.
*
* @param array<Package> $packages Packages to extract artifacts for
* @param bool $force_source If true, always extract source (ignore binary)
*/
public function extractForPackages(array $packages, bool $force_source = false): void
{
// Collect all unique artifacts
$artifacts = [];
foreach ($packages as $package) {
$artifact = $package->getArtifact();
if ($artifact !== null && !isset($artifacts[$artifact->getName()])) {
$artifacts[$artifact->getName()] = $artifact;
}
}
// Sort: php-src should be extracted first (extensions depend on it)
uksort($artifacts, function (string $a, string $b): int {
if ($a === 'php-src') {
return -1;
}
if ($b === 'php-src') {
return 1;
}
return 0;
});
// Extract each artifact
foreach ($artifacts as $artifact) {
$this->extract($artifact, $force_source);
}
}
/**
* Extract a single artifact.
*
* @param Artifact|string $artifact The artifact to extract
* @param bool $force_source If true, always extract source (ignore binary)
*/
public function extract(Artifact|string $artifact, bool $force_source = false): int
{
if (is_string($artifact)) {
$name = $artifact;
$artifact = ArtifactLoader::getArtifactInstance($name);
} else {
$name = $artifact->getName();
}
// Already extracted in this session
if (isset($this->extracted[$name])) {
logger()->debug("Artifact [{$name}] already extracted in this session, skip.");
return SPC_STATUS_ALREADY_EXTRACTED;
}
// Determine: use binary or source?
$use_binary = !$force_source && $artifact->shouldUseBinary();
if ($this->interactive) {
Shell::passthruCallback(function () {
InteractiveTerm::advance();
});
}
try {
V2CompatLayer::beforeExtractHook($artifact);
if ($use_binary) {
$status = $this->extractBinary($artifact);
} else {
$status = $this->extractSource($artifact);
}
V2CompatLayer::afterExtractHook($artifact);
} finally {
if ($this->interactive) {
Shell::passthruCallback(null);
}
}
$this->extracted[$name] = true;
return $status;
}
/**
* Extract source artifact.
*/
protected function extractSource(Artifact $artifact): int
{
$name = $artifact->getName();
$cache_info = $this->cache->getSourceInfo($name);
if ($cache_info === null) {
throw new WrongUsageException("Artifact source [{$name}] not downloaded, please download it first!");
}
$source_file = $this->cache->getCacheFullPath($cache_info);
$target_path = $artifact->getSourceDir();
// Check for custom extract callback
if ($artifact->hasSourceExtractCallback()) {
logger()->info("Extracting source [{$name}] using custom callback...");
$callback = $artifact->getSourceExtractCallback();
ApplicationContext::invoke($callback, [
Artifact::class => $artifact,
'source_file' => $source_file,
'target_path' => $target_path,
]);
// Emit after hooks
$artifact->emitAfterSourceExtract($target_path);
logger()->debug("Emitted after-source-extract hooks for [{$name}]");
return SPC_STATUS_EXTRACTED;
}
// Check for selective extraction (dict mode)
$extract_config = $artifact->getDownloadConfig('source')['extract'] ?? null;
if (is_array($extract_config)) {
$this->doSelectiveExtract($name, $cache_info, $extract_config);
$artifact->emitAfterSourceExtract($target_path);
logger()->debug("Emitted after-source-extract hooks for [{$name}]");
return SPC_STATUS_EXTRACTED;
}
// Standard extraction
$hash = $cache_info['hash'] ?? null;
if ($this->isAlreadyExtracted($target_path, $hash)) {
logger()->debug("Source [{$name}] already extracted at {$target_path}, skip.");
return SPC_STATUS_ALREADY_EXTRACTED;
}
// Remove old directory if hash mismatch
if (is_dir($target_path)) {
logger()->notice("Source [{$name}] hash mismatch, re-extracting...");
FileSystem::removeDir($target_path);
}
logger()->info("Extracting source [{$name}] to {$target_path}...");
$this->doStandardExtract($name, $cache_info, $target_path);
// Emit after hooks
$artifact->emitAfterSourceExtract($target_path);
logger()->debug("Emitted after-source-extract hooks for [{$name}]");
// Write hash marker
if ($hash !== null) {
FileSystem::writeFile("{$target_path}/.spc-hash", $hash);
}
return SPC_STATUS_EXTRACTED;
}
/**
* Extract binary artifact.
*/
protected function extractBinary(Artifact $artifact): int
{
$name = $artifact->getName();
$platform = SystemTarget::getCurrentPlatformString();
$cache_info = $this->cache->getBinaryInfo($name, $platform);
if ($cache_info === null) {
throw new WrongUsageException("Artifact binary [{$name}] for platform [{$platform}] not downloaded!");
}
$source_file = $this->cache->getCacheFullPath($cache_info);
$extract_config = $artifact->getBinaryExtractConfig($cache_info);
$target_path = $extract_config['path'];
// Check for custom extract callback
if ($artifact->hasBinaryExtractCallback()) {
logger()->info("Extracting binary [{$name}] using custom callback...");
$callback = $artifact->getBinaryExtractCallback();
ApplicationContext::invoke($callback, [
Artifact::class => $artifact,
'source_file' => $source_file,
'target_path' => $target_path,
'platform' => $platform,
]);
// Emit after hooks
$artifact->emitAfterBinaryExtract($target_path, $platform);
logger()->debug("Emitted after-binary-extract hooks for [{$name}]");
return SPC_STATUS_EXTRACTED;
}
// Handle different extraction modes
$mode = $extract_config['mode'];
if ($mode === 'selective') {
$this->doSelectiveExtract($name, $cache_info, $extract_config['files']);
$artifact->emitAfterBinaryExtract($target_path, $platform);
logger()->debug("Emitted after-binary-extract hooks for [{$name}]");
return SPC_STATUS_EXTRACTED;
}
$hash = $cache_info['hash'] ?? null;
if ($this->isAlreadyExtracted($target_path, $hash)) {
logger()->debug("Binary [{$name}] already extracted at {$target_path}, skip.");
return SPC_STATUS_ALREADY_EXTRACTED;
}
logger()->info("Extracting binary [{$name}] to {$target_path}...");
$this->doStandardExtract($name, $cache_info, $target_path);
$artifact->emitAfterBinaryExtract($target_path, $platform);
logger()->debug("Emitted after-binary-extract hooks for [{$name}]");
if ($hash !== null && $cache_info['cache_type'] !== 'file') {
FileSystem::writeFile("{$target_path}/.spc-hash", $hash);
}
return SPC_STATUS_EXTRACTED;
}
/**
* Standard extraction: extract entire archive to target directory.
*/
protected function doStandardExtract(string $name, array $cache_info, string $target_path): void
{
$source_file = $this->cache->getCacheFullPath($cache_info);
$cache_type = $cache_info['cache_type'];
// Validate source file exists before extraction
$this->validateSourceFile($name, $source_file, $cache_type);
$this->extractWithType($cache_type, $source_file, $target_path);
}
/**
* Selective extraction: extract specific files to specific locations.
*
* @param string $name Artifact name
* @param array $cache_info Cache info
* @param array<string,string> $file_map Map of source path => destination path
*/
protected function doSelectiveExtract(string $name, array $cache_info, array $file_map): void
{
// Extract to temp directory first
$temp_path = sys_get_temp_dir() . '/spc_extract_' . $name . '_' . bin2hex(random_bytes(8));
try {
logger()->info("Extracting [{$name}] with selective file mapping...");
$source_file = $this->cache->getCacheFullPath($cache_info);
$cache_type = $cache_info['cache_type'];
// Validate source file exists before extraction
$this->validateSourceFile($name, $source_file, $cache_type);
$this->extractWithType($cache_type, $source_file, $temp_path);
// Process file mappings
foreach ($file_map as $src_pattern => $dst_path) {
$dst_path = $this->replacePathVariables($dst_path);
$src_full = "{$temp_path}/{$src_pattern}";
// Handle glob patterns
if (str_contains($src_pattern, '*')) {
$matches = glob($src_full);
if (empty($matches)) {
logger()->warning("No files matched pattern [{$src_pattern}] in [{$name}]");
continue;
}
foreach ($matches as $match) {
$filename = basename($match);
$target = rtrim($dst_path, '/') . '/' . $filename;
$this->copyFileOrDir($match, $target);
}
} else {
// Direct file/directory copy
if (!file_exists($src_full) && !is_dir($src_full)) {
logger()->warning("Source [{$src_pattern}] not found in [{$name}]");
continue;
}
$this->copyFileOrDir($src_full, $dst_path);
}
}
} finally {
// Cleanup temp directory
if (is_dir($temp_path)) {
FileSystem::removeDir($temp_path);
}
}
}
/**
* Check if artifact is already extracted with correct hash.
*/
protected function isAlreadyExtracted(string $path, ?string $expected_hash): bool
{
if (!is_dir($path)) {
return false;
}
// Local source: always re-extract
if ($expected_hash === null) {
return false;
}
$hash_file = "{$path}/.spc-hash";
if (!file_exists($hash_file)) {
return false;
}
return FileSystem::readFile($hash_file) === $expected_hash;
}
/**
* Validate that the source file/directory exists before extraction.
*
* @param string $name Artifact name (for error messages)
* @param string $source_file Path to the source file or directory
* @param string $cache_type Cache type: archive, git, local
*/
protected function validateSourceFile(string $name, string $source_file, string $cache_type): void
{
$converted_path = FileSystem::convertPath($source_file);
switch ($cache_type) {
case 'archive':
if (!file_exists($converted_path)) {
throw new WrongUsageException(
"Artifact [{$name}] source archive not found at: {$converted_path}\n" .
"The file may have been deleted or moved. Please run 'spc download {$name}' to re-download it."
);
}
if (!is_file($converted_path)) {
throw new WrongUsageException(
"Artifact [{$name}] source path exists but is not a file: {$converted_path}\n" .
'Expected an archive file. Please check your downloads directory.'
);
}
break;
case 'file':
if (!file_exists($converted_path)) {
throw new WrongUsageException(
"Artifact [{$name}] source file not found at: {$converted_path}\n" .
"The file may have been deleted or moved. Please run 'spc download {$name}' to re-download it."
);
}
if (!is_file($converted_path)) {
throw new WrongUsageException(
"Artifact [{$name}] source path exists but is not a file: {$converted_path}\n" .
'Expected a regular file. Please check your downloads directory.'
);
}
break;
case 'git':
if (!is_dir($converted_path)) {
throw new WrongUsageException(
"Artifact [{$name}] git repository not found at: {$converted_path}\n" .
"The directory may have been deleted. Please run 'spc download {$name}' to re-clone it."
);
}
// Optionally check for .git directory to ensure it's a valid git repo
if (!is_dir("{$converted_path}/.git")) {
logger()->warning("Artifact [{$name}] directory exists but may not be a valid git repository (missing .git)");
}
break;
case 'local':
if (!file_exists($converted_path) && !is_dir($converted_path)) {
throw new WrongUsageException(
"Artifact [{$name}] local source not found at: {$converted_path}\n" .
'Please ensure the local path is correct and accessible.'
);
}
break;
default:
throw new SPCInternalException("Unknown cache type: {$cache_type}");
}
logger()->debug("Validated source file for [{$name}]: {$converted_path} (type: {$cache_type})");
}
/**
* Copy file or directory to destination.
*/
protected function copyFileOrDir(string $src, string $dst): void
{
$dst_dir = dirname($dst);
if (!is_dir($dst_dir)) {
FileSystem::createDir($dst_dir);
}
if (is_dir($src)) {
FileSystem::copyDir($src, $dst);
} else {
copy($src, $dst);
}
logger()->debug("Copied {$src} -> {$dst}");
}
/**
* Extract source based on cache type.
*
* @param string $cache_type Cache type: archive, git, local
* @param string $source_file Path to source file or directory
* @param string $target_path Target extraction path
*/
protected function extractWithType(string $cache_type, string $source_file, string $target_path): void
{
match ($cache_type) {
'archive' => $this->extractArchive($source_file, $target_path),
'file' => $this->copyFile($source_file, $target_path),
'git' => FileSystem::copyDir(FileSystem::convertPath($source_file), $target_path),
'local' => symlink(FileSystem::convertPath($source_file), $target_path),
default => throw new SPCInternalException("Unknown cache type: {$cache_type}"),
};
}
/**
* Extract archive file to target directory.
*
* Supports: tar, tar.gz, tgz, tar.bz2, tar.xz, txz, zip, exe
*/
protected function extractArchive(string $filename, string $target): void
{
$target = FileSystem::convertPath($target);
$filename = FileSystem::convertPath($filename);
FileSystem::createDir($target);
if (PHP_OS_FAMILY === 'Windows') {
// Use 7za.exe for Windows
$is_txz = str_ends_with($filename, '.txz') || str_ends_with($filename, '.tar.xz');
default_shell()->execute7zExtract($filename, $target, $is_txz);
return;
}
// Unix-like systems: determine compression type
if (str_ends_with($filename, '.tar.gz') || str_ends_with($filename, '.tgz')) {
default_shell()->executeTarExtract($filename, $target, 'gz');
} elseif (str_ends_with($filename, '.tar.bz2')) {
default_shell()->executeTarExtract($filename, $target, 'bz2');
} elseif (str_ends_with($filename, '.tar.xz') || str_ends_with($filename, '.txz')) {
default_shell()->executeTarExtract($filename, $target, 'xz');
} elseif (str_ends_with($filename, '.tar')) {
default_shell()->executeTarExtract($filename, $target, 'none');
} elseif (str_ends_with($filename, '.zip')) {
// Zip requires special handling for strip-components
$this->unzipWithStrip($filename, $target);
} elseif (str_ends_with($filename, '.exe')) {
// exe just copy to target
$dest_file = FileSystem::convertPath("{$target}/" . basename($filename));
FileSystem::copy($filename, $dest_file);
} else {
throw new FileSystemException("Unknown archive format: {$filename}");
}
}
/**
* Unzip file with stripping top-level directory.
*/
protected function unzipWithStrip(string $zip_file, string $extract_path): void
{
$temp_dir = FileSystem::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16)));
$zip_file = FileSystem::convertPath($zip_file);
$extract_path = FileSystem::convertPath($extract_path);
// Extract to temp dir
FileSystem::createDir($temp_dir);
if (PHP_OS_FAMILY === 'Windows') {
default_shell()->execute7zExtract($zip_file, $temp_dir);
} else {
default_shell()->executeUnzip($zip_file, $temp_dir);
}
// Scan first level dirs (relative, not recursive, include dirs)
$contents = FileSystem::scanDirFiles($temp_dir, false, true, true);
if ($contents === false) {
throw new FileSystemException('Cannot scan unzip temp dir: ' . $temp_dir);
}
// If extract path already exists, remove it
if (is_dir($extract_path)) {
FileSystem::removeDir($extract_path);
}
// If only one dir, move its contents to extract_path
$subdir = FileSystem::convertPath("{$temp_dir}/{$contents[0]}");
if (count($contents) === 1 && is_dir($subdir)) {
$this->moveFileOrDir($subdir, $extract_path);
} else {
// Else, if it contains only one dir, strip dir and copy other files
$dircount = 0;
$dir = [];
$top_files = [];
foreach ($contents as $item) {
if (is_dir(FileSystem::convertPath("{$temp_dir}/{$item}"))) {
++$dircount;
$dir[] = $item;
} else {
$top_files[] = $item;
}
}
// Extract dir contents to extract_path
FileSystem::createDir($extract_path);
// Extract move dir
if ($dircount === 1) {
$sub_contents = FileSystem::scanDirFiles("{$temp_dir}/{$dir[0]}", false, true, true);
if ($sub_contents === false) {
throw new FileSystemException("Cannot scan unzip temp sub-dir: {$dir[0]}");
}
foreach ($sub_contents as $sub_item) {
$this->moveFileOrDir(
FileSystem::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"),
FileSystem::convertPath("{$extract_path}/{$sub_item}")
);
}
} else {
foreach ($dir as $item) {
$this->moveFileOrDir(
FileSystem::convertPath("{$temp_dir}/{$item}"),
FileSystem::convertPath("{$extract_path}/{$item}")
);
}
}
// Move top-level files to extract_path
foreach ($top_files as $top_file) {
$this->moveFileOrDir(
FileSystem::convertPath("{$temp_dir}/{$top_file}"),
FileSystem::convertPath("{$extract_path}/{$top_file}")
);
}
}
// Clean up temp directory
FileSystem::removeDir($temp_dir);
}
/**
* Replace path variables.
*/
protected function replacePathVariables(string $path): string
{
$replacement = [
'{pkg_root_path}' => PKG_ROOT_PATH,
'{build_root_path}' => BUILD_ROOT_PATH,
'{source_path}' => SOURCE_PATH,
'{download_path}' => DOWNLOAD_PATH,
'{working_dir}' => WORKING_DIR,
];
return str_replace(array_keys($replacement), array_values($replacement), $path);
}
/**
* Move file or directory, handling cross-device scenarios
* Uses rename() if possible, falls back to copy+delete for cross-device moves
*
* @param string $source Source path
* @param string $dest Destination path
*/
private static function moveFileOrDir(string $source, string $dest): void
{
$source = FileSystem::convertPath($source);
$dest = FileSystem::convertPath($dest);
// Check if source and dest are on the same device to avoid cross-device rename errors
$source_stat = @stat($source);
$dest_parent = dirname($dest);
$dest_stat = @stat($dest_parent);
// Only use rename if on same device
if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) {
if (@rename($source, $dest)) {
return;
}
}
// Fall back to copy + delete for cross-device moves or if rename failed
if (is_dir($source)) {
FileSystem::copyDir($source, $dest);
FileSystem::removeDir($source);
} else {
if (!copy($source, $dest)) {
throw new FileSystemException("Failed to copy file from {$source} to {$dest}");
}
if (!unlink($source)) {
throw new FileSystemException("Failed to remove source file: {$source}");
}
}
}
private function copyFile(string $source_file, string $target_path): void
{
FileSystem::createDir(dirname($target_path));
FileSystem::copy(FileSystem::convertPath($source_file), $target_path);
}
}

View File

@@ -1,146 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Artifact\Downloader;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Util\FileSystem;
class DownloadResult
{
/**
* @param string $cache_type Type of cache: 'archive', 'git', or 'local'
* @param null|string $filename Filename for archive type
* @param null|string $dirname Directory name for git/local type
* @param mixed $extract Extraction path or configuration
* @param bool $verified Whether the download has been verified (hash check)
* @param null|string $version Version of the downloaded artifact (e.g., "1.2.3", "v2.0.0")
* @param array $metadata Additional metadata (e.g., commit hash, release notes, etc.)
*/
private function __construct(
public readonly string $cache_type,
public readonly array $config,
public readonly ?string $filename = null,
public readonly ?string $dirname = null,
public mixed $extract = null,
public bool $verified = false,
public readonly ?string $version = null,
public readonly array $metadata = [],
) {
switch ($this->cache_type) {
case 'archive':
$this->filename !== null ?: throw new DownloaderException('Archive download result must have a filename.');
$fn = FileSystem::isRelativePath($this->filename) ? (DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $this->filename) : $this->filename;
file_exists($fn) ?: throw new DownloaderException("Downloaded archive file does not exist: {$fn}");
break;
case 'git':
case 'local':
$this->dirname !== null ?: throw new DownloaderException('Git/local download result must have a dirname.');
$dn = FileSystem::isRelativePath($this->dirname) ? (DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $this->dirname) : $this->dirname;
file_exists($dn) ?: throw new DownloaderException("Downloaded directory does not exist: {$dn}");
break;
}
}
/**
* Create a download result for an archive file.
*
* @param string $filename Filename of the downloaded archive
* @param mixed $extract Extraction path or configuration
* @param bool $verified Whether the archive has been hash-verified
* @param null|string $version Version string of the downloaded artifact
* @param array $metadata Additional metadata
*/
public static function archive(
string $filename,
array $config,
mixed $extract = null,
bool $verified = false,
?string $version = null,
array $metadata = []
): DownloadResult {
return new self('archive', config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata);
}
/**
* Create a download result for a git clone.
*
* @param string $dirname Directory name of the cloned repository
* @param mixed $extract Extraction path or configuration
* @param null|string $version Version string (tag, branch, or commit)
* @param array $metadata Additional metadata (e.g., commit hash)
*/
public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult
{
return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata);
}
/**
* Create a download result for a local directory.
*
* @param string $dirname Directory name
* @param mixed $extract Extraction path or configuration
* @param null|string $version Version string if known
* @param array $metadata Additional metadata
*/
public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult
{
return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata);
}
/**
* Check if version information is available.
*/
public function hasVersion(): bool
{
return $this->version !== null;
}
/**
* Get a metadata value by key.
*
* @param string $key Metadata key
* @param mixed $default Default value if key doesn't exist
*/
public function getMeta(string $key, mixed $default = null): mixed
{
return $this->metadata[$key] ?? $default;
}
/**
* Create a new DownloadResult with updated version.
* (Immutable pattern - returns a new instance)
*/
public function withVersion(string $version): self
{
return new self(
$this->cache_type,
$this->config,
$this->filename,
$this->dirname,
$this->extract,
$this->verified,
$version,
$this->metadata
);
}
/**
* Create a new DownloadResult with additional metadata.
* (Immutable pattern - returns a new instance)
*/
public function withMeta(string $key, mixed $value): self
{
return new self(
$this->cache_type,
$this->config,
$this->filename,
$this->dirname,
$this->extract,
$this->verified,
$this->version,
array_merge($this->metadata, [$key => $value])
);
}
}

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Artifact\Downloader\Type;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Exception\DownloaderException;
/** bitbuckettag */
class BitBucketTag implements DownloadTypeInterface
{
public const string BITBUCKET_API_URL = 'https://api.bitbucket.org/2.0/repositories/{repo}/refs/tags';
public const string BITBUCKET_DOWNLOAD_URL = 'https://bitbucket.org/{repo}/get/{version}.tar.gz';
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
{
logger()->debug("Fetching {$name} API info from bitbucket tag");
$data = default_shell()->executeCurl(str_replace('{repo}', $config['repo'], self::BITBUCKET_API_URL), retries: $downloader->getRetry());
$data = json_decode($data ?: '', true);
$ver = $data['values'][0]['name'] ?? null;
if (!$ver) {
throw new DownloaderException("Failed to get {$name} version from BitBucket API");
}
$download_url = str_replace(['{repo}', '{version}'], [$config['repo'], $ver], self::BITBUCKET_DOWNLOAD_URL);
$headers = default_shell()->executeCurl($download_url, method: 'HEAD', retries: $downloader->getRetry());
preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?<filename>.+\.tar\.gz)\1/im', $headers, $matches);
if ($matches) {
$filename = $matches['filename'];
} else {
$filename = "{$name}-{$data['tag_name']}.tar.gz";
}
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
logger()->debug("Downloading {$name} version {$ver} from BitBucket: {$download_url}");
default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry());
return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null);
}
}

View File

@@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Artifact\Downloader\Type;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
interface DownloadTypeInterface
{
/**
* @param string $name Download item name
* @param array $config Input configuration for the download
* @param ArtifactDownloader $downloader Downloader instance
*/
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult;
}

Some files were not shown because too many files have changed in this diff Show More