diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 0166bfa0..9a40960e 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -46,6 +46,10 @@ on: description: Prefer pre-built binaries (reduce build time) type: boolean default: true + with-suggested-libs: + description: Build with suggested libs + type: boolean + default: true debug: description: Show full build logs type: boolean @@ -86,6 +90,10 @@ on: 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 @@ -157,6 +165,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 @@ -202,6 +213,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" diff --git a/.github/workflows/build-windows-x86_64.yml b/.github/workflows/build-windows-x86_64.yml index 57a68184..a53d9e43 100644 --- a/.github/workflows/build-windows-x86_64.yml +++ b/.github/workflows/build-windows-x86_64.yml @@ -29,6 +29,10 @@ on: description: prefer pre-built binaries (reduce build time) type: boolean default: true + with-suggested-libs: + description: Build with suggested libs + type: boolean + default: true debug: description: enable debug logs type: boolean diff --git a/.gitignore b/.gitignore index d33eae53..21bae186 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ packlib_files.txt .php-cs-fixer.cache .phpunit.result.cache +# doctor cache fallback (when ~/.cache/spc/ is not writable) +.spc-doctor.lock + # exclude self-runtime /bin/* !/bin/spc* @@ -61,3 +64,6 @@ log/ # spc.phar spc.phar spc.exe + +# dumped files from StaticPHP v3 +/dump-*.json diff --git a/README-zh.md b/README-zh.md index d8d1b396..8dc8d0a3 100755 --- a/README-zh.md +++ b/README-zh.md @@ -1,4 +1,4 @@ -# static-php-cli +# StaticPHP [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) @@ -6,7 +6,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) -**static-php-cli** 是一个用于构建静态、独立 PHP 运行时的强大工具,支持众多流行扩展。 +**StaticPHP** 是一个用于构建静态编译可执行文件(包括 PHP、扩展等)的强大工具。 ## 特性 @@ -80,7 +80,7 @@ download-options: ### 3. 静态 PHP 使用 -现在您可以将 static-php-cli 构建的二进制文件复制到另一台机器上,无需依赖即可运行: +现在您可以将 StaticPHP 构建的二进制文件复制到另一台机器上,无需依赖即可运行: ``` # php-cli @@ -97,7 +97,7 @@ buildroot/bin/php-fpm -v ## 文档 -当前 README 包含基本用法。有关 static-php-cli 的所有功能, +当前 README 包含基本用法。有关 StaticPHP 的所有功能, 请访问 。 ## 直接下载 diff --git a/README.md b/README.md index 3f3bfbf1..1d355c46 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# static-php-cli +# StaticPHP [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) @@ -6,8 +6,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) -**static-php-cli** is a powerful tool designed for building static, standalone PHP runtime -with popular extensions. +**StaticPHP** is a powerful tool designed for building portable executables including PHP, extensions, and more. ## Features @@ -81,7 +80,7 @@ Run command: ### 3. Static PHP usage -Now you can copy binaries built by static-php-cli to another machine and run with no dependencies: +Now you can copy binaries built by StaticPHP to another machine and run with no dependencies: ``` # php-cli @@ -98,7 +97,7 @@ buildroot/bin/php-fpm -v ## Documentation -The current README contains basic usage. For all the features of static-php-cli, +The current README contains basic usage. For all the features of StaticPHP, see . ## Direct Download diff --git a/config/artifact/ncurses.yml b/config/artifact/ncurses.yml new file mode 100644 index 00000000..52c8f59f --- /dev/null +++ b/config/artifact/ncurses.yml @@ -0,0 +1,9 @@ +ncurses: + binary: hosted + metadata: + license-files: + - COPYING + source: + type: filelist + url: 'https://ftp.gnu.org/pub/gnu/ncurses/' + regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' diff --git a/config/artifact/php-src.yml b/config/artifact/php-src.yml index 32bcb6cf..e304db9d 100644 --- a/config/artifact/php-src.yml +++ b/config/artifact/php-src.yml @@ -5,3 +5,7 @@ php-src: license: PHP-3.01 source: type: php-release + domain: 'https://www.php.net' + source-mirror: + type: php-release + domain: 'https://phpmirror.static-php.dev' diff --git a/config/downloader.php b/config/downloader.php deleted file mode 100644 index 48710a88..00000000 --- a/config/downloader.php +++ /dev/null @@ -1,30 +0,0 @@ - */ -return [ - '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, -]; diff --git a/config/env.ini b/config/env.ini index 9e295d79..94797118 100644 --- a/config/env.ini +++ b/config/env.ini @@ -121,6 +121,8 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --disable 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 -fno-ident -fPIE ${SPC_DEFAULT_C_FLAGS}" +; EXTRA_CXXFLAGS for `configure` and `make` php +SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_CXX_FLAGS}" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" @@ -132,6 +134,8 @@ OPENSSLDIR="" ; 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 +; Whether to use brew version of llvm or system version (valid options: 'system', 'brew', default: 'system') +SPC_USE_LLVM=system ; compiler environments (default value is defined by selected toolchain) CC=${SPC_DEFAULT_CC} CXX=${SPC_DEFAULT_CXX} @@ -155,5 +159,9 @@ 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_CXXFLAGS for `configure` and `make` php +SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_CXX_FLAGS}" +; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.dylib +SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" ; minimum compatible macOS version (LLVM vars, availability not guaranteed) MACOSX_DEPLOYMENT_TARGET=12.0 diff --git a/config/lib.json b/config/lib.json index 3be97248..ebbf4b87 100644 --- a/config/lib.json +++ b/config/lib.json @@ -373,6 +373,15 @@ ], "static-libs-windows": [ "avif.lib" + ], + "lib-depends": [ + "libaom" + ], + "lib-suggests": [ + "libwebp", + "libjpeg", + "libxml2", + "libpng" ] }, "libcares": { diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index d12cd219..b938182c 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -1,5 +1,152 @@ ext-bcmath: type: php-extension +ext-bz2: + type: php-extension + depends: + - bzip2 + php-extension: + arg-type@unix: with-path + arg-type@windows: with +ext-calendar: + type: php-extension +ext-ctype: + type: php-extension +ext-curl: + type: php-extension + depends: + - curl + depends@windows: + - ext-zlib + - ext-openssl + php-extension: + arg-type: with +ext-dba: + type: php-extension + suggests: + - qdbm + php-extension: + arg-type: custom +ext-dom: + type: php-extension + depends: + - libxml2 + - ext-xml + php-extension: + arg-type: '--enable-dom@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with +ext-exif: + type: php-extension +ext-ffi: + type: php-extension + depends@unix: + - libffi + php-extension: + arg-type@unix: '--with-ffi=@shared_suffix@ --enable-zend-signals' + arg-type@windows: with +ext-fileinfo: + type: php-extension +ext-filter: + type: php-extension +ext-ftp: + type: php-extension + suggests: + - ext-openssl +ext-gd: + type: php-extension + depends: + - zlib + - libpng + - ext-zlib + suggests: + - libavif + - libwebp + - libjpeg + - freetype + php-extension: + arg-type: custom +ext-gettext: + type: php-extension + depends: + - gettext + php-extension: + arg-type: with-path +ext-gmp: + type: php-extension + depends: + - gmp + php-extension: + arg-type: with-path +ext-iconv: + type: php-extension + depends@unix: + - libiconv + php-extension: + arg-type@unix: with-path + arg-type@windows: with +ext-intl: + type: php-extension + depends@unix: + - icu +ext-ldap: + type: php-extension + depends: + - ldap + suggests: + - gmp + - libsodium + - ext-openssl + php-extension: + arg-type: with-path +ext-libxml: + type: php-extension + depends: + - ext-xml + php-extension: + build-with-php: true + build-shared: false + arg-type: none +ext-mbregex: + type: php-extension + depends: + - onig + - ext-mbstring + php-extension: + arg-type: custom + build-shared: false + build-static: true + display-name: mbstring +ext-mbstring: + type: php-extension + php-extension: + arg-type: custom +ext-mysqli: + type: php-extension + depends: + - ext-mysqlnd + php-extension: + arg-type: with + build-with-php: true +ext-mysqlnd: + type: php-extension + depends: + - zlib + php-extension: + arg-type@unix: enable + arg-type@windows: with + build-with-php: true +ext-odbc: + type: php-extension + depends@unix: + - unixodbc + php-extension: + arg-type@unix: '--with-unixODBC@shared_path_suffix@' +ext-opcache: + type: php-extension + php-extension: + arg-type@unix: custom + arg-type@windows: enable + zend-extension: true + display-name: 'Zend Opcache' ext-openssl: type: php-extension depends: @@ -10,6 +157,185 @@ ext-openssl: arg-type: custom arg-type@windows: with build-with-php: true +ext-password-argon2: + type: php-extension + depends: + - libargon2 + - ext-openssl + php-extension: + arg-type: custom + display-name: '' +ext-pcntl: + type: php-extension +ext-pdo: + type: php-extension +ext-pdo_mysql: + type: php-extension + depends: + - ext-pdo + - ext-mysqlnd + php-extension: + arg-type: with +ext-pdo_odbc: + type: php-extension + depends: + - ext-pdo + - ext-odbc + depends@unix: + - unixodbc + - ext-pdo + - ext-odbc + php-extension: + arg-type: custom +ext-pdo_pgsql: + type: php-extension + depends@unix: + - ext-pdo + - ext-pgsql + - postgresql + php-extension: + arg-type@unix: with-path + arg-type@windows: '--with-pdo-pgsql=yes' +ext-pdo_sqlite: + type: php-extension + depends: + - ext-pdo + - ext-sqlite3 + - sqlite + php-extension: + arg-type: with +ext-pgsql: + type: php-extension + depends@unix: + - postgresql + php-extension: + arg-type: custom +ext-phar: + type: php-extension + depends: + - zlib +ext-posix: + type: php-extension +ext-readline: + type: php-extension + depends: + - libedit + php-extension: + support: + Windows: wip + BSD: wip + arg-type: '--with-libedit --without-readline' + build-shared: false + build-static: true +ext-session: + type: php-extension +ext-shmop: + type: php-extension + php-extension: + build-with-php: true +ext-simplexml: + type: php-extension + depends: + - ext-xml + php-extension: + arg-type@unix: '--enable-simplexml@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with + build-with-php: true +ext-snmp: + type: php-extension + depends: + - net-snmp + php-extension: + arg-type: with +ext-soap: + type: php-extension + depends: + - ext-xml + - ext-session + php-extension: + arg-type@unix: '--enable-soap@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with + build-with-php: true +ext-sockets: + type: php-extension +ext-sodium: + type: php-extension + depends: + - libsodium + php-extension: + arg-type: with +ext-sqlite3: + type: php-extension + depends: + - sqlite + php-extension: + arg-type@unix: with-path + arg-type@windows: with + build-with-php: true +ext-sysvmsg: + type: php-extension + php-extension: + support: + Windows: 'no' + BSD: wip +ext-sysvsem: + type: php-extension + php-extension: + support: + Windows: 'no' + BSD: wip +ext-sysvshm: + type: php-extension + php-extension: + support: + BSD: wip +ext-tidy: + type: php-extension + depends: + - tidy + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path +ext-tokenizer: + type: php-extension + php-extension: + build-with-php: true +ext-xml: + type: php-extension + depends: + - libxml2 + depends@windows: + - libxml2 + - ext-iconv + php-extension: + arg-type: '--enable-xml@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with + build-with-php: true +ext-xmlreader: + type: php-extension + depends: + - libxml2 + php-extension: + arg-type: '--enable-xmlreader@shared_suffix@ --with-libxml=@build_root_path@' + build-with-php: true +ext-xmlwriter: + type: php-extension + depends: + - libxml2 + php-extension: + arg-type: '--enable-xmlwriter@shared_suffix@ --with-libxml=@build_root_path@' + build-with-php: true +ext-xsl: + type: php-extension + depends: + - libxslt + - ext-xml + - ext-dom + php-extension: + arg-type: with-path + build-with-php: true ext-zlib: type: php-extension depends: diff --git a/config/pkg/ext/ext-amqp.yml b/config/pkg/ext/ext-amqp.yml index 93756914..1c802360 100644 --- a/config/pkg/ext/ext-amqp.yml +++ b/config/pkg/ext/ext-amqp.yml @@ -2,10 +2,8 @@ ext-amqp: type: php-extension artifact: source: - type: url - url: 'https://pecl.php.net/get/amqp' - extract: php-src/ext/amqp - filename: amqp.tgz + type: pecl + name: amqp metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-apcu.yml b/config/pkg/ext/ext-apcu.yml new file mode 100644 index 00000000..331b04f7 --- /dev/null +++ b/config/pkg/ext/ext-apcu.yml @@ -0,0 +1,9 @@ +ext-apcu: + type: php-extension + artifact: + source: + type: pecl + name: APCu + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/config/pkg/ext/ext-ast.yml b/config/pkg/ext/ext-ast.yml new file mode 100644 index 00000000..776b82df --- /dev/null +++ b/config/pkg/ext/ext-ast.yml @@ -0,0 +1,9 @@ +ext-ast: + type: php-extension + artifact: + source: + type: pecl + name: ast + metadata: + license-files: [LICENSE] + license: BSD-3-Clause diff --git a/config/pkg/ext/ext-brotli.yml b/config/pkg/ext/ext-brotli.yml new file mode 100644 index 00000000..147ecb63 --- /dev/null +++ b/config/pkg/ext/ext-brotli.yml @@ -0,0 +1,13 @@ +ext-brotli: + type: php-extension + artifact: + source: + type: git + extract: php-src/ext/brotli + rev: master + url: 'https://github.com/kjdev/php-ext-brotli' + metadata: + license-files: [LICENSE] + license: MIT + depends: + - brotli diff --git a/config/pkg/ext/ext-dio.yml b/config/pkg/ext/ext-dio.yml new file mode 100644 index 00000000..b445940c --- /dev/null +++ b/config/pkg/ext/ext-dio.yml @@ -0,0 +1,9 @@ +ext-dio: + type: php-extension + artifact: + source: + type: pecl + name: dio + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/config/pkg/ext/ext-ds.yml b/config/pkg/ext/ext-ds.yml new file mode 100644 index 00000000..0c0a4b3c --- /dev/null +++ b/config/pkg/ext/ext-ds.yml @@ -0,0 +1,9 @@ +ext-ds: + type: php-extension + artifact: + source: + type: pecl + name: ds + metadata: + license-files: [LICENSE] + license: MIT diff --git a/config/pkg/ext/ext-ev.yml b/config/pkg/ext/ext-ev.yml new file mode 100644 index 00000000..174e5f84 --- /dev/null +++ b/config/pkg/ext/ext-ev.yml @@ -0,0 +1,13 @@ +ext-ev: + type: php-extension + artifact: + source: + type: pecl + name: ev + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - ext-sockets + php-extension: + arg-type@windows: with diff --git a/config/pkg/ext/ext-event.yml b/config/pkg/ext/ext-event.yml new file mode 100644 index 00000000..dd9c1c8e --- /dev/null +++ b/config/pkg/ext/ext-event.yml @@ -0,0 +1,19 @@ +ext-event: + type: php-extension + artifact: + source: + type: url + url: 'https://bitbucket.org/osmanov/pecl-event/get/3.1.4.tar.gz' + extract: php-src/ext/event + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libevent + - ext-openssl + suggests: + - ext-sockets + php-extension: + support: + Windows: wip + arg-type: custom diff --git a/config/pkg/ext/ext-excimer.yml b/config/pkg/ext/ext-excimer.yml new file mode 100644 index 00000000..3d085888 --- /dev/null +++ b/config/pkg/ext/ext-excimer.yml @@ -0,0 +1,9 @@ +ext-excimer: + type: php-extension + artifact: + source: + type: pecl + name: excimer + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/config/pkg/ext/ext-glfw.yml b/config/pkg/ext/ext-glfw.yml index dc8844a8..1be1e75f 100644 --- a/config/pkg/ext/ext-glfw.yml +++ b/config/pkg/ext/ext-glfw.yml @@ -3,3 +3,5 @@ ext-glfw: artifact: glfw depends: - glfw + php-extension: + arg-type@unix: '--enable-glfw --with-glfw-dir=@build_root_path@' diff --git a/config/pkg/ext/ext-gmssl.yml b/config/pkg/ext/ext-gmssl.yml new file mode 100644 index 00000000..7ed8981d --- /dev/null +++ b/config/pkg/ext/ext-gmssl.yml @@ -0,0 +1,12 @@ +ext-gmssl: + type: php-extension + artifact: + source: + type: ghtar + repo: gmssl/GmSSL-PHP + extract: php-src/ext/gmssl + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - gmssl diff --git a/config/pkg/ext/ext-grpc.yml b/config/pkg/ext/ext-grpc.yml new file mode 100644 index 00000000..ff5bae7b --- /dev/null +++ b/config/pkg/ext/ext-grpc.yml @@ -0,0 +1,14 @@ +ext-grpc: + type: php-extension + artifact: + source: + type: pecl + name: grpc + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - grpc + lang: cpp + php-extension: + arg-type@unix: enable-path diff --git a/config/pkg/ext/ext-igbinary.yml b/config/pkg/ext/ext-igbinary.yml new file mode 100644 index 00000000..1a80831b --- /dev/null +++ b/config/pkg/ext/ext-igbinary.yml @@ -0,0 +1,12 @@ +ext-igbinary: + type: php-extension + artifact: + source: + type: pecl + name: igbinary + metadata: + license-files: [COPYING] + license: BSD-3-Clause + suggests: + - ext-session + - ext-apcu diff --git a/config/pkg/ext/ext-imagick.yml b/config/pkg/ext/ext-imagick.yml new file mode 100644 index 00000000..e6f9843e --- /dev/null +++ b/config/pkg/ext/ext-imagick.yml @@ -0,0 +1,13 @@ +ext-imagick: + type: php-extension + artifact: + source: + type: pecl + name: imagick + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - imagemagick + php-extension: + arg-type: custom diff --git a/config/pkg/ext/ext-imap.yml b/config/pkg/ext/ext-imap.yml new file mode 100644 index 00000000..a6c18dac --- /dev/null +++ b/config/pkg/ext/ext-imap.yml @@ -0,0 +1,15 @@ +ext-imap: + type: php-extension + artifact: + source: + type: pecl + name: imap + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - imap + suggests: + - ext-openssl + php-extension: + arg-type: custom diff --git a/config/pkg/ext/ext-inotify.yml b/config/pkg/ext/ext-inotify.yml new file mode 100644 index 00000000..0956f9e4 --- /dev/null +++ b/config/pkg/ext/ext-inotify.yml @@ -0,0 +1,9 @@ +ext-inotify: + type: php-extension + artifact: + source: + type: pecl + name: inotify + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/config/pkg/ext/ext-lz4.yml b/config/pkg/ext/ext-lz4.yml new file mode 100644 index 00000000..8a3bb4db --- /dev/null +++ b/config/pkg/ext/ext-lz4.yml @@ -0,0 +1,15 @@ +ext-lz4: + type: php-extension + artifact: + source: + type: ghtagtar + repo: kjdev/php-ext-lz4 + extract: php-src/ext/lz4 + metadata: + license-files: [LICENSE] + license: MIT + depends: + - liblz4 + php-extension: + arg-type@unix: '--enable-lz4=@shared_suffix@ --with-lz4-includedir=@build_root_path@' + arg-type@windows: '--enable-lz4' diff --git a/config/pkg/ext/ext-maxminddb.yml b/config/pkg/ext/ext-maxminddb.yml new file mode 100644 index 00000000..59d7e4e3 --- /dev/null +++ b/config/pkg/ext/ext-maxminddb.yml @@ -0,0 +1,13 @@ +ext-maxminddb: + type: php-extension + artifact: + source: + type: pecl + name: maxminddb + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - libmaxminddb + php-extension: + arg-type: with diff --git a/config/pkg/ext/ext-mbregex.yml b/config/pkg/ext/ext-mbregex.yml deleted file mode 100644 index ae59f023..00000000 --- a/config/pkg/ext/ext-mbregex.yml +++ /dev/null @@ -1,10 +0,0 @@ -ext-mbregex: - type: php-extension - depends: - - onig - - ext-mbstring - php-extension: - arg-type: custom - build-shared: false - build-static: true - display-name: mbstring diff --git a/config/pkg/ext/ext-mbstring.yml b/config/pkg/ext/ext-mbstring.yml deleted file mode 100644 index 6583ca61..00000000 --- a/config/pkg/ext/ext-mbstring.yml +++ /dev/null @@ -1,4 +0,0 @@ -ext-mbstring: - type: php-extension - php-extension: - arg-type: custom diff --git a/config/pkg/ext/ext-memcache.yml b/config/pkg/ext/ext-memcache.yml new file mode 100644 index 00000000..9db51c05 --- /dev/null +++ b/config/pkg/ext/ext-memcache.yml @@ -0,0 +1,14 @@ +ext-memcache: + type: php-extension + artifact: + source: + type: pecl + name: memcache + metadata: + license-files: [LICENSE] + license: PHP-3.0 + depends: + - ext-zlib + - ext-session + php-extension: + arg-type: '--enable-memcache@shared_suffix@ --with-zlib-dir=@build_root_path@' diff --git a/config/pkg/ext/ext-memcached.yml b/config/pkg/ext/ext-memcached.yml new file mode 100644 index 00000000..329227f2 --- /dev/null +++ b/config/pkg/ext/ext-memcached.yml @@ -0,0 +1,23 @@ +ext-memcached: + type: php-extension + artifact: + source: + type: pecl + name: memcached + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libmemcached + depends@unix: + - libmemcached + - fastlz + - ext-session + - ext-zlib + suggests: + - zstd + - ext-igbinary + - ext-msgpack + - ext-session + php-extension: + arg-type: '--enable-memcached@shared_suffix@ --with-zlib-dir=@build_root_path@' diff --git a/config/pkg/ext/ext-mongodb.yml b/config/pkg/ext/ext-mongodb.yml new file mode 100644 index 00000000..7cbdbb14 --- /dev/null +++ b/config/pkg/ext/ext-mongodb.yml @@ -0,0 +1,21 @@ +ext-mongodb: + type: php-extension + artifact: + source: + type: ghrel + repo: mongodb/mongo-php-driver + match: mongodb.+\.tgz + extract: php-src/ext/mongodb + metadata: + license-files: [LICENSE] + license: PHP-3.01 + suggests: + - icu + - openssl + - zstd + - zlib + frameworks: + - CoreFoundation + - Security + php-extension: + arg-type: custom diff --git a/config/pkg/ext/ext-msgpack.yml b/config/pkg/ext/ext-msgpack.yml new file mode 100644 index 00000000..8b230c31 --- /dev/null +++ b/config/pkg/ext/ext-msgpack.yml @@ -0,0 +1,14 @@ +ext-msgpack: + type: php-extension + artifact: + source: + type: pecl + name: msgpack + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - ext-session + php-extension: + arg-type@unix: with + arg-type@windows: enable diff --git a/config/pkg/ext/ext-mysqlnd_ed25519.yml b/config/pkg/ext/ext-mysqlnd_ed25519.yml new file mode 100644 index 00000000..e7aa3de8 --- /dev/null +++ b/config/pkg/ext/ext-mysqlnd_ed25519.yml @@ -0,0 +1,18 @@ +ext-mysqlnd_ed25519: + type: php-extension + artifact: + source: + type: pie + repo: mariadb/mysqlnd_ed25519 + extract: php-src/ext/mysqlnd_ed25519 + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - ext-mysqlnd + - libsodium + suggests: + - openssl + php-extension: + arg-type: '--with-mysqlnd_ed25519=@shared_suffix@' + build-static: false diff --git a/config/pkg/ext/ext-mysqlnd_parsec.yml b/config/pkg/ext/ext-mysqlnd_parsec.yml new file mode 100644 index 00000000..903d65c4 --- /dev/null +++ b/config/pkg/ext/ext-mysqlnd_parsec.yml @@ -0,0 +1,17 @@ +ext-mysqlnd_parsec: + type: php-extension + artifact: + source: + type: pie + repo: mariadb/mysqlnd_parsec + extract: php-src/ext/mysqlnd_parsec + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - ext-mysqlnd + - libsodium + - openssl + php-extension: + arg-type: '--enable-mysqlnd_parsec' + build-static: false diff --git a/config/pkg/ext/ext-opentelemetry.yml b/config/pkg/ext/ext-opentelemetry.yml new file mode 100644 index 00000000..5caebef2 --- /dev/null +++ b/config/pkg/ext/ext-opentelemetry.yml @@ -0,0 +1,9 @@ +ext-opentelemetry: + type: php-extension + artifact: + source: + type: pecl + name: opentelemetry + metadata: + license-files: [LICENSE] + license: Apache-2.0 diff --git a/config/pkg/ext/ext-parallel.yml b/config/pkg/ext/ext-parallel.yml new file mode 100644 index 00000000..a3e91efe --- /dev/null +++ b/config/pkg/ext/ext-parallel.yml @@ -0,0 +1,9 @@ +ext-parallel: + type: php-extension + artifact: + source: + type: pecl + name: parallel + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/config/pkg/ext/ext-pcov.yml b/config/pkg/ext/ext-pcov.yml new file mode 100644 index 00000000..3fac61d0 --- /dev/null +++ b/config/pkg/ext/ext-pcov.yml @@ -0,0 +1,12 @@ +ext-pcov: + type: php-extension + artifact: + source: + type: pecl + name: pcov + metadata: + license-files: [LICENSE] + license: PHP-3.01 + php-extension: + build-static: false + build-shared: true diff --git a/config/pkg/ext/ext-pdo_sqlsrv.yml b/config/pkg/ext/ext-pdo_sqlsrv.yml new file mode 100644 index 00000000..6d57333b --- /dev/null +++ b/config/pkg/ext/ext-pdo_sqlsrv.yml @@ -0,0 +1,14 @@ +ext-pdo_sqlsrv: + type: php-extension + artifact: + source: + type: pecl + name: pdo_sqlsrv + metadata: + license-files: [LICENSE] + license: MIT + depends: + - ext-pdo + - ext-sqlsrv + php-extension: + arg-type: with diff --git a/config/pkg/ext/ext-phar.yml b/config/pkg/ext/ext-phar.yml deleted file mode 100644 index 3625d2c0..00000000 --- a/config/pkg/ext/ext-phar.yml +++ /dev/null @@ -1,4 +0,0 @@ -ext-phar: - type: php-extension - depends: - - zlib diff --git a/config/pkg/ext/ext-protobuf.yml b/config/pkg/ext/ext-protobuf.yml new file mode 100644 index 00000000..020059d3 --- /dev/null +++ b/config/pkg/ext/ext-protobuf.yml @@ -0,0 +1,9 @@ +ext-protobuf: + type: php-extension + artifact: + source: + type: pecl + name: protobuf + metadata: + license-files: [LICENSE] + license: BSD-3-Clause diff --git a/config/pkg/ext/ext-rar.yml b/config/pkg/ext/ext-rar.yml new file mode 100644 index 00000000..1770788a --- /dev/null +++ b/config/pkg/ext/ext-rar.yml @@ -0,0 +1,12 @@ +ext-rar: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/static-php/php-rar.git' + rev: issue-php82 + extract: php-src/ext/rar + metadata: + license-files: [LICENSE] + license: PHP-3.01 + lang: cpp diff --git a/config/pkg/ext/ext-rdkafka.yml b/config/pkg/ext/ext-rdkafka.yml new file mode 100644 index 00000000..1f26e49c --- /dev/null +++ b/config/pkg/ext/ext-rdkafka.yml @@ -0,0 +1,15 @@ +ext-rdkafka: + type: php-extension + artifact: + source: + type: ghtar + repo: arnaud-lb/php-rdkafka + extract: php-src/ext/rdkafka + metadata: + license-files: [LICENSE] + license: MIT + depends: + - librdkafka + lang: cpp + php-extension: + arg-type: custom diff --git a/config/pkg/ext/ext-readline.yml b/config/pkg/ext/ext-readline.yml deleted file mode 100644 index 19b1886c..00000000 --- a/config/pkg/ext/ext-readline.yml +++ /dev/null @@ -1,11 +0,0 @@ -ext-readline: - type: php-extension - depends: - - libedit - php-extension: - support: - Windows: wip - BSD: wip - arg-type: with-path - build-shared: false - build-static: true diff --git a/config/pkg/ext/ext-redis.yml b/config/pkg/ext/ext-redis.yml new file mode 100644 index 00000000..c05b4ee2 --- /dev/null +++ b/config/pkg/ext/ext-redis.yml @@ -0,0 +1,21 @@ +ext-redis: + type: php-extension + artifact: + source: + type: pecl + name: redis + metadata: + license-files: [LICENSE] + license: PHP-3.01 + suggests: + - ext-session + - ext-igbinary + - ext-msgpack + suggests@unix: + - ext-session + - ext-igbinary + - ext-msgpack + - zstd + - liblz4 + php-extension: + arg-type: custom diff --git a/config/pkg/ext/ext-simdjson.yml b/config/pkg/ext/ext-simdjson.yml new file mode 100644 index 00000000..37eeb5f1 --- /dev/null +++ b/config/pkg/ext/ext-simdjson.yml @@ -0,0 +1,10 @@ +ext-simdjson: + type: php-extension + artifact: + source: + type: pecl + name: simdjson + metadata: + license-files: [LICENSE] + license: Apache-2.0 + lang: cpp diff --git a/config/pkg/ext/ext-snappy.yml b/config/pkg/ext/ext-snappy.yml new file mode 100644 index 00000000..7ddec261 --- /dev/null +++ b/config/pkg/ext/ext-snappy.yml @@ -0,0 +1,18 @@ +ext-snappy: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/kjdev/php-ext-snappy' + rev: master + extract: php-src/ext/snappy + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - snappy + suggests: + - ext-apcu + lang: cpp + php-extension: + arg-type@unix: '--enable-snappy --with-snappy-includedir=@build_root_path@' diff --git a/config/pkg/ext/ext-spx.yml b/config/pkg/ext/ext-spx.yml new file mode 100644 index 00000000..a379cdd4 --- /dev/null +++ b/config/pkg/ext/ext-spx.yml @@ -0,0 +1,14 @@ +ext-spx: + type: php-extension + artifact: + source: + type: pie + repo: noisebynorthwest/php-spx + extract: php-src/ext/spx + metadata: + license-files: [LICENSE] + license: GPL-3.0-or-later + depends: + - ext-zlib + php-extension: + arg-type: '--enable-SPX@shared_suffix@' diff --git a/config/pkg/ext/ext-sqlsrv.yml b/config/pkg/ext/ext-sqlsrv.yml new file mode 100644 index 00000000..603d7a93 --- /dev/null +++ b/config/pkg/ext/ext-sqlsrv.yml @@ -0,0 +1,15 @@ +ext-sqlsrv: + type: php-extension + artifact: + source: + type: pecl + name: sqlsrv + metadata: + license-files: [LICENSE] + license: MIT + depends@linux: + - unixodbc + - ext-pcntl + depends@macos: + - unixodbc + lang: cpp diff --git a/config/pkg/ext/ext-ssh2.yml b/config/pkg/ext/ext-ssh2.yml new file mode 100644 index 00000000..14c9bf32 --- /dev/null +++ b/config/pkg/ext/ext-ssh2.yml @@ -0,0 +1,15 @@ +ext-ssh2: + type: php-extension + artifact: + source: + type: pecl + name: ssh2 + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libssh2 + - ext-openssl + - ext-zlib + php-extension: + arg-type: with-path diff --git a/config/pkg/ext/ext-swoole.yml b/config/pkg/ext/ext-swoole.yml new file mode 100644 index 00000000..b6499e85 --- /dev/null +++ b/config/pkg/ext/ext-swoole.yml @@ -0,0 +1,72 @@ +ext-swoole: + type: php-extension + artifact: + source: + type: ghtar + repo: swoole/swoole-src + extract: php-src/ext/swoole + match: v6\.+ + prefer-stable: true + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - libcares + - brotli + - nghttp2 + - zlib + - ext-openssl + - ext-curl + suggests: + - zstd + - ext-sockets + - ext-swoole-hook-pgsql + - ext-swoole-hook-mysql + - ext-swoole-hook-sqlite + - ext-swoole-hook-odbc + suggests@linux: + - zstd + - liburing + - ext-sockets + - ext-swoole-hook-pgsql + - ext-swoole-hook-mysql + - ext-swoole-hook-sqlite + - ext-swoole-hook-odbc + lang: cpp + php-extension: + arg-type: custom +ext-swoole-hook-mysql: + type: php-extension + depends: + - ext-mysqlnd + - ext-pdo + - ext-pdo_mysql + suggests: + - ext-mysqli + php-extension: + arg-type: none + display-name: swoole +ext-swoole-hook-odbc: + type: php-extension + depends: + - ext-pdo + - unixodbc + php-extension: + arg-type: none + display-name: swoole +ext-swoole-hook-pgsql: + type: php-extension + depends: + - ext-pgsql + - ext-pdo + php-extension: + arg-type: none + display-name: swoole +ext-swoole-hook-sqlite: + type: php-extension + depends: + - ext-sqlite3 + - ext-pdo + php-extension: + arg-type: none + display-name: swoole diff --git a/config/pkg/ext/ext-swow.yml b/config/pkg/ext/ext-swow.yml new file mode 100644 index 00000000..11592cd0 --- /dev/null +++ b/config/pkg/ext/ext-swow.yml @@ -0,0 +1,18 @@ +ext-swow: + type: php-extension + artifact: + source: + extract: php-src/ext/swow-src + type: ghtar + repo: swow/swow + prefer-stable: true + metadata: + license: Apache-2.0 + license-files: [LICENSE] + suggests: + - openssl + - curl + - ext-openssl + - ext-curl + php-extension: + arg-type: custom diff --git a/config/pkg/ext/ext-trader.yml b/config/pkg/ext/ext-trader.yml new file mode 100644 index 00000000..8e16afbb --- /dev/null +++ b/config/pkg/ext/ext-trader.yml @@ -0,0 +1,14 @@ +ext-trader: + type: php-extension + artifact: + source: + type: pecl + name: trader + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + php-extension: + support: + BSD: wip + Windows: wip + arg-type: enable diff --git a/config/pkg/ext/ext-uuid.yml b/config/pkg/ext/ext-uuid.yml new file mode 100644 index 00000000..68080531 --- /dev/null +++ b/config/pkg/ext/ext-uuid.yml @@ -0,0 +1,16 @@ +ext-uuid: + type: php-extension + artifact: + source: + type: pecl + name: uuid + metadata: + license-files: [LICENSE] + license: LGPL-2.1-only + depends: + - libuuid + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path diff --git a/config/pkg/ext/ext-uv.yml b/config/pkg/ext/ext-uv.yml new file mode 100644 index 00000000..f1a3031b --- /dev/null +++ b/config/pkg/ext/ext-uv.yml @@ -0,0 +1,18 @@ +ext-uv: + type: php-extension + artifact: + source: + type: pecl + name: uv + prefer-stable: false + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libuv + - ext-sockets + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path diff --git a/config/pkg/ext/ext-xdebug.yml b/config/pkg/ext/ext-xdebug.yml new file mode 100644 index 00000000..0374e573 --- /dev/null +++ b/config/pkg/ext/ext-xdebug.yml @@ -0,0 +1,14 @@ +ext-xdebug: + type: php-extension + artifact: + source: + type: pie + repo: xdebug/xdebug + metadata: + license-files: [LICENSE] + license: Xdebug-1.03 + php-extension: + zend-extension: true + build-static: false + build-shared: true + build-with-php: false diff --git a/config/pkg/ext/ext-xhprof.yml b/config/pkg/ext/ext-xhprof.yml new file mode 100644 index 00000000..b075f65b --- /dev/null +++ b/config/pkg/ext/ext-xhprof.yml @@ -0,0 +1,18 @@ +ext-xhprof: + type: php-extension + artifact: + source: + type: pecl + name: xhprof + extract: php-src/ext/xhprof-src + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - ext-ctype + php-extension: + support: + Windows: wip + BSD: wip + arg-type: enable + build-with-php: true diff --git a/config/pkg/ext/ext-xlswriter.yml b/config/pkg/ext/ext-xlswriter.yml new file mode 100644 index 00000000..24d2fa3c --- /dev/null +++ b/config/pkg/ext/ext-xlswriter.yml @@ -0,0 +1,18 @@ +ext-xlswriter: + type: php-extension + artifact: + source: + type: pecl + name: xlswriter + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + depends: + - ext-zlib + - ext-zip + suggests: + - openssl + php-extension: + support: + BSD: wip + arg-type: custom diff --git a/config/pkg/ext/ext-xz.yml b/config/pkg/ext/ext-xz.yml new file mode 100644 index 00000000..0d625ad2 --- /dev/null +++ b/config/pkg/ext/ext-xz.yml @@ -0,0 +1,15 @@ +ext-xz: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/codemasher/php-ext-xz' + rev: main + extract: php-src/ext/xz + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - xz + php-extension: + arg-type: with-path diff --git a/config/pkg/ext/ext-yac.yml b/config/pkg/ext/ext-yac.yml new file mode 100644 index 00000000..e10bea06 --- /dev/null +++ b/config/pkg/ext/ext-yac.yml @@ -0,0 +1,14 @@ +ext-yac: + type: php-extension + artifact: + source: + type: pecl + name: yac + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends@unix: + - fastlz + - ext-igbinary + php-extension: + arg-type@unix: '--enable-yac@shared_suffix@ --enable-igbinary --enable-json --with-system-fastlz' diff --git a/config/pkg/ext/ext-yaml.yml b/config/pkg/ext/ext-yaml.yml new file mode 100644 index 00000000..a60b6254 --- /dev/null +++ b/config/pkg/ext/ext-yaml.yml @@ -0,0 +1,16 @@ +ext-yaml: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/php/pecl-file_formats-yaml' + rev: php7 + extract: php-src/ext/yaml + metadata: + license-files: [LICENSE] + license: MIT + depends: + - libyaml + php-extension: + arg-type@unix: with-path + arg-type@windows: with diff --git a/config/pkg/ext/ext-zip.yml b/config/pkg/ext/ext-zip.yml new file mode 100644 index 00000000..a5a9e4b5 --- /dev/null +++ b/config/pkg/ext/ext-zip.yml @@ -0,0 +1,17 @@ +ext-zip: + type: php-extension + artifact: + source: + type: pecl + name: zip + extract: ext-zip + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends@unix: + - libzip + php-extension: + support: + BSD: wip + arg-type: custom + arg-type@windows: enable diff --git a/config/pkg/ext/ext-zstd.yml b/config/pkg/ext/ext-zstd.yml new file mode 100644 index 00000000..1f004f13 --- /dev/null +++ b/config/pkg/ext/ext-zstd.yml @@ -0,0 +1,15 @@ +ext-zstd: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/kjdev/php-ext-zstd' + rev: master + extract: php-src/ext/zstd + metadata: + license-files: [LICENSE] + license: MIT + depends: + - zstd + php-extension: + arg-type: '--enable-zstd --with-libzstd=@build_root_path@' diff --git a/config/pkg/lib/gmp.yml b/config/pkg/lib/gmp.yml index bdc13b55..c1469774 100644 --- a/config/pkg/lib/gmp.yml +++ b/config/pkg/lib/gmp.yml @@ -3,7 +3,7 @@ gmp: artifact: source: type: filelist - url: 'https://gmplib.org/download/gmp/' + url: 'https://ftp.gnu.org/gnu/gmp/' regex: '/href="(?gmp-(?[^"]+)\.tar\.xz)"/' source-mirror: type: url diff --git a/config/pkg/lib/libavif.yml b/config/pkg/lib/libavif.yml index 0d7ae151..c75b05c4 100644 --- a/config/pkg/lib/libavif.yml +++ b/config/pkg/lib/libavif.yml @@ -7,5 +7,12 @@ libavif: metadata: license-files: [LICENSE] license: BSD-2-Clause + depends: + - libaom + suggests: + - libwebp + - libjpeg + - libxml2 + - libpng static-libs@unix: - libavif.a diff --git a/config/pkg/lib/libxml2.yml b/config/pkg/lib/libxml2.yml index db88e8b1..7e86b5af 100644 --- a/config/pkg/lib/libxml2.yml +++ b/config/pkg/lib/libxml2.yml @@ -10,9 +10,8 @@ libxml2: license: MIT depends@unix: - libiconv - suggests@unix: - - xz - zlib + - xz headers: - libxml2 pkg-configs: diff --git a/config/pkg/lib/ncurses.yml b/config/pkg/lib/ncurses.yml index cbc1ba67..6ce20f91 100644 --- a/config/pkg/lib/ncurses.yml +++ b/config/pkg/lib/ncurses.yml @@ -1,12 +1,10 @@ ncurses: type: library - artifact: - source: - type: filelist - url: 'https://ftp.gnu.org/pub/gnu/ncurses/' - regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' - binary: hosted - metadata: - license-files: [COPYING] + artifact: ncurses static-libs@unix: - libncurses.a +ncursesw: + type: library + artifact: ncurses + static-libs@unix: + - libncursesw.a diff --git a/config/pkg/lib/curl.yml b/config/pkg/target/curl.yml similarity index 91% rename from config/pkg/lib/curl.yml rename to config/pkg/target/curl.yml index f183b21e..4daba8c1 100644 --- a/config/pkg/lib/curl.yml +++ b/config/pkg/target/curl.yml @@ -1,5 +1,5 @@ curl: - type: library + type: target artifact: source: type: ghrel @@ -29,5 +29,7 @@ curl: - SystemConfiguration headers: - curl + static-bins@unix: + - curl static-libs@unix: - libcurl.a diff --git a/config/pkg/target/go-xcaddy.yml b/config/pkg/target/go-xcaddy.yml index 89cb4cd0..deafb37d 100644 --- a/config/pkg/target/go-xcaddy.yml +++ b/config/pkg/target/go-xcaddy.yml @@ -2,5 +2,11 @@ go-xcaddy: type: target artifact: binary: custom + env: + GOROOT: '{pkg_root_path}/go-xcaddy' + GOBIN: '{pkg_root_path}/go-xcaddy/bin' + GOPATH: '{pkg_root_path}/go-xcaddy/go' + path@unix: + - '{pkg_root_path}/go-xcaddy/bin' static-bins: - xcaddy diff --git a/config/pkg/target/htop.yml b/config/pkg/target/htop.yml new file mode 100644 index 00000000..fcefa70a --- /dev/null +++ b/config/pkg/target/htop.yml @@ -0,0 +1,10 @@ +htop: + type: target + artifact: + source: + type: ghrel + repo: htop-dev/htop + match: htop.+\.tar\.xz + prefer-stable: true + depends: + - ncursesw diff --git a/config/pkg/target/protoc.yml b/config/pkg/target/protoc.yml new file mode 100644 index 00000000..b45fb335 --- /dev/null +++ b/config/pkg/target/protoc.yml @@ -0,0 +1,8 @@ +protoc: + type: target + artifact: + binary: + linux-x86_64: { type: ghrel, repo: protocolbuffers/protobuf, match: 'protoc-([0-9.]+)-linux-x86_64\.zip', extract: '{pkg_root_path}/protoc' } + linux-aarch64: { type: ghrel, repo: protocolbuffers/protobuf, match: 'protoc-([0-9.]+)-linux-aarch_64\.zip', extract: '{pkg_root_path}/protoc' } + path: + - '{pkg_root_path}/protoc/bin' diff --git a/config/pkg/target/rust.yml b/config/pkg/target/rust.yml new file mode 100644 index 00000000..f2e10738 --- /dev/null +++ b/config/pkg/target/rust.yml @@ -0,0 +1,6 @@ +rust: + type: target + artifact: + binary: custom + path: + - '{pkg_root_path}/rust/bin' diff --git a/config/source.json b/config/source.json index 03626015..040197cb 100644 --- a/config/source.json +++ b/config/source.json @@ -361,7 +361,7 @@ }, "gmp": { "type": "filelist", - "url": "https://gmplib.org/download/gmp/", + "url": "https://ftp.gnu.org/gnu/gmp/", "regex": "/href=\"(?gmp-(?[^\"]+)\\.tar\\.xz)\"/", "provide-pre-built": true, "alt": { @@ -526,7 +526,7 @@ "libavif": { "type": "ghtar", "repo": "AOMediaCodec/libavif", - "provide-pre-built": true, + "provide-pre-built": false, "license": { "type": "file", "path": "LICENSE" diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 5fa7327e..ca61bd46 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -6,8 +6,10 @@ namespace Package\Artifact; use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\GlobalEnvManager; @@ -65,6 +67,25 @@ class go_xcaddy return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version); } + #[CustomBinaryCheckUpdate('go-xcaddy', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [$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'); + } + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || $version !== $old_version, + ); + } + #[AfterBinaryExtract('go-xcaddy', [ 'linux-x86_64', 'linux-aarch64', diff --git a/src/Package/Artifact/php_src.php b/src/Package/Artifact/php_src.php index ae9488d6..119f9056 100644 --- a/src/Package/Artifact/php_src.php +++ b/src/Package/Artifact/php_src.php @@ -7,7 +7,6 @@ namespace Package\Artifact; use Package\Target\php; use StaticPHP\Attribute\Artifact\AfterSourceExtract; use StaticPHP\Attribute\PatchDescription; -use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SourcePatcher; @@ -52,16 +51,6 @@ class php_src } } - #[AfterSourceExtract('php-src')] - #[PatchDescription('Patch FFI extension on CentOS 7 with -O3 optimization (strncmp issue)')] - public function patchFfiCentos7FixO3strncmp(): void - { - spc_skip_if(!($ver = SystemTarget::getLibcVersion()) || version_compare($ver, '2.17', '>')); - $ver_id = php::getPHPVersionID(return_null_if_failed: true); - spc_skip_if($ver_id === null || $ver_id < 80316); - SourcePatcher::patchFile('ffi_centos7_fix_O3_strncmp.patch', SOURCE_PATH . '/php-src'); - } - #[AfterSourceExtract('php-src')] #[PatchDescription('Add LICENSE file to IMAP extension if missing')] public function patchImapLicense(): void diff --git a/src/Package/Artifact/rust.php b/src/Package/Artifact/rust.php new file mode 100644 index 00000000..e5c9f525 --- /dev/null +++ b/src/Package/Artifact/rust.php @@ -0,0 +1,85 @@ +executeCurl('https://static.rust-lang.org/dist/channel-rust-stable.toml', retries: $downloader->getRetry()); + // parse toml by regex since we want to avoid adding a toml parser dependency just for this + $cnt = preg_match_all('/^version = "([^"]+)"$/m', $toml_config ?: '', $matches); + if (!$cnt) { + throw new DownloaderException('Failed to parse Rust version from channel config'); + } + $versions = $matches[1]; + // strip version num \d.\d.\d (some version number is like "x.x.x (abcdefg 1970-01-01)" + $versions = array_filter(array_map(fn ($v) => preg_match('/^(\d+\.\d+\.\d+)/', $v, $m) ? $m[1] : null, $versions)); + usort($versions, 'version_compare'); + $latest_version = end($versions); + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Rust version'); + } + + // merge download link + $download_url = "https://static.rust-lang.org/dist/rust-{$latest_version}-{$arch}-unknown-linux-{$distro}.tar.xz"; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . basename($download_url); + default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive(basename($path), ['url' => $download_url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/rust-install', verified: false, version: $latest_version); + } + + #[CustomBinaryCheckUpdate('rust', [ + 'linux-x86_64', + 'linux-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $toml_config = default_shell()->executeCurl('https://static.rust-lang.org/dist/channel-rust-stable.toml', retries: $downloader->getRetry()); + $cnt = preg_match_all('/^version = "([^"]+)"$/m', $toml_config ?: '', $matches); + if (!$cnt) { + throw new DownloaderException('Failed to parse Rust version from channel config'); + } + $versions = array_filter(array_map(fn ($v) => preg_match('/^(\d+\.\d+\.\d+)/', $v, $m) ? $m[1] : null, $matches[1])); + usort($versions, 'version_compare'); + $latest_version = end($versions); + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Rust version'); + } + return new CheckUpdateResult( + old: $old_version, + new: $latest_version, + needUpdate: $old_version === null || $latest_version !== $old_version, + ); + } + + #[AfterBinaryExtract('rust', [ + 'linux-x86_64', + 'linux-aarch64', + ])] + public function postExtractRust(string $target_path): void + { + $prefix = PKG_ROOT_PATH . '/rust'; + shell()->exec("cd {$target_path} && ./install.sh --prefix={$prefix}"); + } +} diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 9a143063..95520aa4 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -6,8 +6,10 @@ namespace Package\Artifact; use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; @@ -24,6 +26,9 @@ class zig $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); $index_json = json_decode($index_json ?: '', true); $latest_version = null; + if ($index_json === null) { + throw new DownloaderException('Failed to fetch Zig version index'); + } foreach ($index_json as $version => $data) { if ($version !== 'master') { $latest_version = $version; @@ -59,6 +64,36 @@ class zig return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version); } + #[CustomBinaryCheckUpdate('zig', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); + $index_json = json_decode($index_json ?: '', true); + $latest_version = null; + if (!is_array($index_json)) { + throw new DownloaderException('Failed to fetch Zig version index for update check'); + } + foreach ($index_json as $version => $data) { + if ($version !== 'master') { + $latest_version = $version; + break; + } + } + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Zig version'); + } + return new CheckUpdateResult( + old: $old_version, + new: $latest_version, + needUpdate: $old_version === null || $latest_version !== $old_version, + ); + } + #[AfterBinaryExtract('zig', [ 'linux-x86_64', 'linux-aarch64', diff --git a/src/Package/Extension/dba.php b/src/Package/Extension/dba.php new file mode 100644 index 00000000..d16d979d --- /dev/null +++ b/src/Package/Extension/dba.php @@ -0,0 +1,28 @@ +getLibraryPackage('qdbm')) ? (" --with-qdbm={$qdbm->getBuildRootPath()}") : ''; + return '--enable-dba' . ($shared ? '=shared' : '') . $qdbm; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(PackageInstaller $installer): string + { + $qdbm = $installer->getLibraryPackage('qdbm') ? ' --with-qdbm' : ''; + return '--with-dba' . $qdbm; + } +} diff --git a/src/Package/Extension/dio.php b/src/Package/Extension/dio.php new file mode 100644 index 00000000..70ac387e --- /dev/null +++ b/src/Package/Extension/dio.php @@ -0,0 +1,23 @@ +getSourceDir()}/php_dio.h")) { + FileSystem::writeFile("{$this->getSourceDir()}/php_dio.h", FileSystem::readFile("{$this->getSourceDir()}/src/php_dio.h")); + } + } +} diff --git a/src/Package/Extension/event.php b/src/Package/Extension/event.php new file mode 100644 index 00000000..db119274 --- /dev/null +++ b/src/Package/Extension/event.php @@ -0,0 +1,46 @@ +getBuilder()->getBuildRootPath()}"; + if ($installer->getLibraryPackage('openssl')) { + $arg .= " --with-event-openssl={$this->getBuilder()->getBuildRootPath()}"; + } + if ($installer->getPhpExtensionPackage('ext-sockets')) { + $arg .= ' --enable-event-sockets'; + } else { + $arg .= ' --disable-event-sockets'; + } + return $arg; + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-event')] + #[PatchDescription('Prevent event extension compile error on macOS')] + public function patchBeforeMake(PackageInstaller $installer): void + { + // Prevent event extension compile error on macOS + if (SystemTarget::getTargetOS() === 'Darwin') { + $php_src = $installer->getTargetPackage('php')->getSourceDir(); + FileSystem::replaceFileRegex("{$php_src}/main/php_config.h", '/^#define HAVE_OPENPTY 1$/m', ''); + } + } +} diff --git a/src/Package/Extension/excimer.php b/src/Package/Extension/excimer.php new file mode 100644 index 00000000..9780a2ec --- /dev/null +++ b/src/Package/Extension/excimer.php @@ -0,0 +1,19 @@ +')); + $ver_id = php::getPHPVersionID(return_null_if_failed: true); + spc_skip_if($ver_id === null || $ver_id < 80316); + spc_skip_if(LinuxUtil::getOSRelease()['dist'] !== 'centos'); + SourcePatcher::patchFile('ffi_centos7_fix_O3_strncmp.patch', SOURCE_PATH . '/php-src'); + } +} diff --git a/src/Package/Extension/gd.php b/src/Package/Extension/gd.php new file mode 100644 index 00000000..5e815b5d --- /dev/null +++ b/src/Package/Extension/gd.php @@ -0,0 +1,26 @@ +getLibraryPackage('freetype') ? ' --with-freetype' : ''; + $arg .= $installer->getLibraryPackage('libjpeg') ? ' --with-jpeg' : ''; + $arg .= $installer->getLibraryPackage('libwebp') ? ' --with-webp' : ''; + $arg .= $installer->getLibraryPackage('libavif') ? ' --with-avif' : ''; + return $arg; + } +} diff --git a/src/Package/Extension/glfw.php b/src/Package/Extension/glfw.php index 61d722e1..2a9c7ee5 100644 --- a/src/Package/Extension/glfw.php +++ b/src/Package/Extension/glfw.php @@ -6,7 +6,6 @@ namespace Package\Extension; use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; -use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\PhpExtensionPackage; @@ -49,11 +48,4 @@ class glfw extends PhpExtensionPackage putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS=' . $extra_ldflags); } } - - #[CustomPhpConfigureArg('Darwin')] - #[CustomPhpConfigureArg('Linux')] - public function getUnixConfigureArg(bool $shared = false): string - { - return '--enable-glfw --with-glfw-dir=' . BUILD_ROOT_PATH; - } } diff --git a/src/Package/Extension/grpc.php b/src/Package/Extension/grpc.php new file mode 100644 index 00000000..c3b08f16 --- /dev/null +++ b/src/Package/Extension/grpc.php @@ -0,0 +1,70 @@ +getSourceDir()}/src/php/ext/grpc/call.c", + 'zend_exception_get_default(TSRMLS_C),', + 'zend_ce_exception,', + ); + + // custom config.m4 content for grpc extension, to prevent building libgrpc.a again + $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->getSourceDir()}/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->getSourceDir()}/config.m4", $config_m4); + + copy("{$this->getSourceDir()}/src/php/ext/grpc/php_grpc.h", "{$this->getSourceDir()}/php_grpc.h"); + } +} diff --git a/src/Package/Extension/imagick.php b/src/Package/Extension/imagick.php new file mode 100644 index 00000000..2d2aa0aa --- /dev/null +++ b/src/Package/Extension/imagick.php @@ -0,0 +1,21 @@ +getBuildRootPath() . $disable_omp; + } +} diff --git a/src/Package/Extension/imap.php b/src/Package/Extension/imap.php new file mode 100644 index 00000000..e9879b48 --- /dev/null +++ b/src/Package/Extension/imap.php @@ -0,0 +1,55 @@ +getOption('enable-zts')) { + throw new WrongUsageException('ext-imap is not thread safe, do not build it with ZTS builds'); + } + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-imap')] + public function patchBeforeBuildconf(PackageInstaller $installer): void + { + if ($installer->getLibraryPackage('openssl')) { + // sometimes imap with openssl does not contain zlib (required by openssl) + // we need to add it manually + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", 'TST_LIBS="$DLIBS $IMAP_SHARED_LIBADD"', 'TST_LIBS="$DLIBS $IMAP_SHARED_LIBADD -lz"'); + } + // c-client is built with PASSWDTYPE=nul so libcrypt is not referenced. + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.m4", + " PHP_CHECK_LIBRARY(crypt, crypt,\n [\n PHP_ADD_LIBRARY(crypt,, IMAP_SHARED_LIBADD)\n AC_DEFINE(HAVE_LIBCRYPT,1,[ ])\n ])", + ' dnl Skipped: crypt check not needed (c-client built with PASSWDTYPE=nul)' + ); + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(PackageInstaller $installer, PackageBuilder $builder): string + { + $arg = "--with-imap={$builder->getBuildRootPath()}"; + if (($ssl = $installer->getLibraryPackage('openssl')) !== null) { + $arg .= " --with-imap-ssl={$ssl->getBuildRootPath()}"; + } + return $arg; + } +} diff --git a/src/Package/Extension/maxminddb.php b/src/Package/Extension/maxminddb.php new file mode 100644 index 00000000..bda8d34c --- /dev/null +++ b/src/Package/Extension/maxminddb.php @@ -0,0 +1,30 @@ +getSourceDir()}/config.m4")) { + return; + } + // move ext/maxminddb/ext/* to ext/maxminddb/ + $files = FileSystem::scanDirFiles("{$this->getSourceDir()}/ext", false, true); + foreach ($files as $file) { + rename("{$this->getSourceDir()}/ext/{$file}", "{$this->getSourceDir()}/{$file}"); + } + } +} diff --git a/src/Package/Extension/memcache.php b/src/Package/Extension/memcache.php new file mode 100644 index 00000000..a9c58b76 --- /dev/null +++ b/src/Package/Extension/memcache.php @@ -0,0 +1,75 @@ +isBuildStatic()) { + return false; + } + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'if test -d $abs_srcdir/src ; then', + 'if test -d $abs_srcdir/main ; then' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', + 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"' + ); + // add for in-tree building + file_put_contents( + "{$this->getSourceDir()}/php_memcache.h", + <<<'EOF' +#ifndef PHP_MEMCACHE_H +#define PHP_MEMCACHE_H + +extern zend_module_entry memcache_module_entry; +#define phpext_memcache_ptr &memcache_module_entry + +#endif +EOF + ); + return true; + } + + #[BeforeStage('ext-memcache', [self::class, 'configureForUnix'])] + #[PatchDescription('Fix memcache extension compile error when building as shared')] + public function patchBeforeSharedConfigure(): bool + { + if (!$this->isBuildShared()) { + return false; + } + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'if test -d $abs_srcdir/main ; then', + 'if test -d $abs_srcdir/src ; then', + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"', + 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', + ); + return true; + } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=c17'; + return $parent; + } +} diff --git a/src/Package/Extension/memcached.php b/src/Package/Extension/memcached.php new file mode 100644 index 00000000..0453e8ec --- /dev/null +++ b/src/Package/Extension/memcached.php @@ -0,0 +1,30 @@ +getLibraryPackage('zlib')->getBuildRootPath() . ' ' . + '--with-libmemcached-dir=' . $installer->getLibraryPackage('libmemcached')->getBuildRootPath() . ' ' . + '--disable-memcached-sasl ' . + '--enable-memcached-json ' . + ($installer->getLibraryPackage('zstd') ? '--with-zstd ' : '') . + ($installer->getPhpExtensionPackage('ext-igbinary') ? '--enable-memcached-igbinary ' : '') . + ($installer->getPhpExtensionPackage('ext-session') ? '--enable-memcached-session ' : '') . + ($installer->getPhpExtensionPackage('ext-msgpack') ? '--enable-memcached-msgpack ' : '') . + '--with-system-fastlz'; + } +} diff --git a/src/Package/Extension/mongodb.php b/src/Package/Extension/mongodb.php new file mode 100644 index 00000000..3434491d --- /dev/null +++ b/src/Package/Extension/mongodb.php @@ -0,0 +1,38 @@ +getLibraryPackage('openssl')) { + $arg .= '--with-mongodb-ssl=openssl'; + } + $arg .= $installer->getLibraryPackage('icu') ? ' --with-mongodb-icu=yes ' : ' --with-mongodb-icu=no '; + $arg .= $installer->getLibraryPackage('zstd') ? ' --with-mongodb-zstd=yes ' : ' --with-mongodb-zstd=no '; + // $arg .= $installer->getLibraryPackage('snappy') ? ' --with-mongodb-snappy=yes ' : ' --with-mongodb-snappy=no '; + $arg .= $installer->getLibraryPackage('zlib') ? ' --with-mongodb-zlib=yes ' : ' --with-mongodb-zlib=bundled '; + return clean_spaces($arg); + } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=c17'; + return $parent; + } +} diff --git a/src/Package/Extension/opcache.php b/src/Package/Extension/opcache.php new file mode 100644 index 00000000..93cb0a9f --- /dev/null +++ b/src/Package/Extension/opcache.php @@ -0,0 +1,76 @@ += 8.0 !'); + } + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-opcache')] + #[PatchDescription('Fix static opcache build for PHP 8.2.0 to 8.4.x')] + public function patchBeforeBuildconf(PackageInstaller $installer): bool + { + $version = php::getPHPVersion(); + $php_src = $installer->getTargetPackage('php')->getSourceDir(); + if (file_exists("{$php_src}/.opcache_patched")) { + return false; + } + // if 8.2.0 <= PHP_VERSION < 8.2.23, we need to patch from legacy patch file + if (version_compare($version, '8.2.0', '>=') && version_compare($version, '8.2.23', '<')) { + SourcePatcher::patchFile('spc_fix_static_opcache_before_80222.patch', $php_src); + } + // if 8.3.0 <= PHP_VERSION < 8.3.11, we need to patch from legacy patch file + elseif (version_compare($version, '8.3.0', '>=') && version_compare($version, '8.3.11', '<')) { + SourcePatcher::patchFile('spc_fix_static_opcache_before_80310.patch', $php_src); + } + // if 8.3.12 <= PHP_VERSION < 8.5.0-dev, we need to patch from legacy patch file + elseif (version_compare($version, '8.5.0-dev', '<')) { + SourcePatcher::patchPhpSrc(items: ['static_opcache']); + } + // PHP 8.5.0-dev and later supports static opcache without patching + else { + return false; + } + return file_put_contents($php_src . '/.opcache_patched', '1') !== false; + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): string + { + $phpVersionID = php::getPHPVersionID(); + $opcache_jit = ' --enable-opcache-jit'; + if ((SystemTarget::getTargetOS() === 'Linux' && + SystemTarget::getLibc() === 'musl' && + $builder->getOption('enable-zts') && + SystemTarget::getTargetArch() === 'x86_64' && + $phpVersionID < 80500) || + $builder->getOption('disable-opcache-jit') + ) { + $opcache_jit = ' --disable-opcache-jit'; + } + return '--enable-opcache' . ($shared ? '=shared' : '') . $opcache_jit; + } +} diff --git a/src/Package/Extension/opentelemetry.php b/src/Package/Extension/opentelemetry.php new file mode 100644 index 00000000..632d1257 --- /dev/null +++ b/src/Package/Extension/opentelemetry.php @@ -0,0 +1,21 @@ +getOption('enable-zts')) { + throw new WrongUsageException('ext-parallel must be built with ZTS builds. Use "--enable-zts" option!'); + } + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-parallel')] + #[PatchDescription('Fix parallel m4 hardcoded PHP_VERSION check')] + public function patchBeforeBuildconf(): bool + { + FileSystem::replaceFileRegex("{$this->getSourceDir()}/config.m4", '/PHP_VERSION=.*/m', ''); + return true; + } +} diff --git a/src/Package/Extension/password_argon2.php b/src/Package/Extension/password_argon2.php new file mode 100644 index 00000000..77122405 --- /dev/null +++ b/src/Package/Extension/password_argon2.php @@ -0,0 +1,37 @@ +execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "assert(defined(\'PASSWORD_ARGON2I\'));"'); + if ($ret !== 0) { + throw new ValidationException('extension ' . $this->getName() . ' failed sanity check', validation_module: 'password_argon2 function check'); + } + } + + #[CustomPhpConfigureArg('Linux')] + #[CustomPhpConfigureArg('Darwin')] + public function getConfigureArg(PackageInstaller $installer, PackageBuilder $builder): string + { + if ($installer->getLibraryPackage('openssl') !== null) { + if (php::getPHPVersionID() >= 80500 || (php::getPHPVersionID() >= 80400 && !$builder->getOption('enable-zts'))) { + return '--without-password-argon2'; // use --with-openssl-argon2 in openssl extension instead + } + } + return '--with-password-argon2'; + } +} diff --git a/src/Package/Extension/pdo_odbc.php b/src/Package/Extension/pdo_odbc.php new file mode 100644 index 00000000..f8835d13 --- /dev/null +++ b/src/Package/Extension/pdo_odbc.php @@ -0,0 +1,35 @@ +getSourceDir()}/config.m4", 'PDO_ODBC_LDFLAGS="$pdo_odbc_def_ldflags', 'PDO_ODBC_LDFLAGS="-liconv $pdo_odbc_def_ldflags'); + } + + #[CustomPhpConfigureArg('Linux')] + #[CustomPhpConfigureArg('Darwin')] + public function getUnixConfigureArg(bool $shared): string + { + return '--with-pdo-odbc=' . ($shared ? 'shared,' : '') . 'unixODBC,' . BUILD_ROOT_PATH; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(bool $shared): string + { + return '--with-pdo-odbc'; + } +} diff --git a/src/Package/Extension/pdo_sqlite.php b/src/Package/Extension/pdo_sqlite.php new file mode 100644 index 00000000..b0429f62 --- /dev/null +++ b/src/Package/Extension/pdo_sqlite.php @@ -0,0 +1,25 @@ +getTargetPackage('php')->getSourceDir()}/configure", + '/sqlite3_column_table_name=yes/', + 'sqlite3_column_table_name=no' + ); + } +} diff --git a/src/Package/Extension/pgsql.php b/src/Package/Extension/pgsql.php new file mode 100644 index 00000000..6e2b8f0b --- /dev/null +++ b/src/Package/Extension/pgsql.php @@ -0,0 +1,48 @@ += 80400) { + $libfiles = new SPCConfigUtil(['libs_only_deps' => true, 'absolute_libs' => true])->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()), $builder->getOption('with-suggests'))['libs']; + $libfiles = str_replace("{$builder->getLibDir()}/lib", '-l', $libfiles); + $libfiles = str_replace('.a', '', $libfiles); + return '--with-pgsql' . ($shared ? '=shared' : '') . + ' PGSQL_CFLAGS=-I' . $builder->getIncludeDir() . + ' PGSQL_LIBS="-L' . $builder->getLibDir() . ' ' . $libfiles . '"'; + } + return '--with-pgsql=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath(); + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(bool $shared, PackageBuilder $builder): string + { + if (php::getPHPVersionID() >= 80400) { + return '--with-pgsql'; + } + return "--with-pgsql={$builder->getBuildRootPath()}"; + } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=c17 -Wno-int-conversion'; + return $parent; + } +} diff --git a/src/Package/Extension/phar.php b/src/Package/Extension/phar.php index dd9b1dff..2220f727 100644 --- a/src/Package/Extension/phar.php +++ b/src/Package/Extension/phar.php @@ -9,6 +9,8 @@ use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Util\FileSystem; use StaticPHP\Util\SourcePatcher; #[Extension('phar')] @@ -26,4 +28,24 @@ class phar { SourcePatcher::unpatchMicroPhar(); } + + #[BeforeStage('ext-phar', 'build')] + public function beforeBuildShared(PhpExtensionPackage $pkg): void + { + FileSystem::replaceFileStr( + "{$pkg->getSourceDir()}/config.m4", + ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'], + ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'] + ); + } + + #[AfterStage('ext-phar', 'build')] + public function afterBuildShared(PhpExtensionPackage $pkg): void + { + FileSystem::replaceFileStr( + "{$pkg->getSourceDir()}/config.m4", + ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'], + ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'] + ); + } } diff --git a/src/Package/Extension/protobuf.php b/src/Package/Extension/protobuf.php new file mode 100644 index 00000000..2c3dd036 --- /dev/null +++ b/src/Package/Extension/protobuf.php @@ -0,0 +1,28 @@ +getPhpExtensionPackage('ext-grpc'); + // protobuf conflicts with grpc + if ($grpc?->isBuildStatic()) { + throw new ValidationException('protobuf conflicts with grpc, please remove grpc or protobuf extension'); + } + } +} diff --git a/src/Package/Extension/rar.php b/src/Package/Extension/rar.php new file mode 100644 index 00000000..2fc20ed1 --- /dev/null +++ b/src/Package/Extension/rar.php @@ -0,0 +1,27 @@ += 15.0)')] + public function patchBeforeBuildconf(): void + { + // workaround for newer Xcode clang (>= 15.0) + if (SystemTarget::getTargetOS() === 'Darwin') { + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", '-Wall -fvisibility=hidden', '-Wall -Wno-incompatible-function-pointer-types -fvisibility=hidden'); + } + } +} diff --git a/src/Package/Extension/rdkafka.php b/src/Package/Extension/rdkafka.php new file mode 100644 index 00000000..4bb28ee5 --- /dev/null +++ b/src/Package/Extension/rdkafka.php @@ -0,0 +1,55 @@ +getSourceDir()}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\n", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm \$RDKAFKA_LIBS\n"); + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\"\n", '-L$RDKAFKA_DIR/$PHP_LIBDIR -lm $RDKAFKA_LIBS"'); + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", 'PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,', 'AC_CHECK_LIB([$LIBNAME], [$LIBSYMBOL],'); + return true; + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-rdkafka')] + #[PatchDescription('Patch rdkafka extension source code to fix build errors with inline builds')] + public function patchBeforeMake(): bool + { + // when compiling rdkafka with inline builds, it shows some errors, I don't know why. + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/rdkafka.c", + "#ifdef HAS_RD_KAFKA_TRANSACTIONS\n#include \"kafka_error_exception.h\"\n#endif", + '#include "kafka_error_exception.h"' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/kafka_error_exception.h", + ['#ifdef HAS_RD_KAFKA_TRANSACTIONS', '#endif'], + '' + ); + return true; + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): string + { + $pkgconf_libs = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->getExtensionConfig($this); + return '--with-rdkafka=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath() . " RDKAFKA_LIBS=\"{$pkgconf_libs['libs']}\""; + } +} diff --git a/src/Package/Extension/redis.php b/src/Package/Extension/redis.php new file mode 100644 index 00000000..bfc5cc5e --- /dev/null +++ b/src/Package/Extension/redis.php @@ -0,0 +1,47 @@ +isBuildStatic()) { + $arg .= $installer->getPhpExtensionPackage('session')?->isBuildStatic() ? ' --enable-redis-session' : ' --disable-redis-session'; + $arg .= $installer->getPhpExtensionPackage('igbinary')?->isBuildStatic() ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; + $arg .= $installer->getPhpExtensionPackage('msgpack')?->isBuildStatic() ? ' --enable-redis-msgpack' : ' --disable-redis-msgpack'; + } else { + $arg .= $installer->getPhpExtensionPackage('session') ? ' --enable-redis-session' : ' --disable-redis-session'; + $arg .= $installer->getPhpExtensionPackage('igbinary') ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; + $arg .= $installer->getPhpExtensionPackage('msgpack') ? ' --enable-redis-msgpack' : ' --disable-redis-msgpack'; + } + if ($zstd = $installer->getLibraryPackage('zstd')) { + $arg .= ' --enable-redis-zstd --with-libzstd="' . $zstd->getBuildRootPath() . '"'; + } + if ($liblz4 = $installer->getLibraryPackage('liblz4')) { + $arg .= ' --enable-redis-lz4 --with-liblz4="' . $liblz4->getBuildRootPath() . '"'; + } + return $arg; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(bool $shared, PackageInstaller $installer): string + { + $arg = '--enable-redis'; + $arg .= $installer->getPhpExtensionPackage('session') ? ' --enable-redis-session' : ' --disable-redis-session'; + $arg .= $installer->getPhpExtensionPackage('igbinary') ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; + return $arg; + } +} diff --git a/src/Package/Extension/simdjson.php b/src/Package/Extension/simdjson.php new file mode 100644 index 00000000..e04c415a --- /dev/null +++ b/src/Package/Extension/simdjson.php @@ -0,0 +1,70 @@ +getTargetPackage('php'); + $php_ver = php::getPHPVersionID(); + FileSystem::replaceFileRegex( + "{$this->getSourceDir()}/config.m4", + '/php_version=(`.*`)$/m', + "php_version={$php_ver}" + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.m4", + 'if test -z "$PHP_CONFIG"; then', + 'if false; then' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.w32", + "'yes',", + 'PHP_SIMDJSON_SHARED,' + ); + return true; + } + + public function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + if (ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + $extra = getenv('SPC_COMPILER_EXTRA'); + if (!str_contains((string) $extra, '-lstdc++')) { + f_putenv('SPC_COMPILER_EXTRA=' . clean_spaces($extra . ' -lstdc++')); + } + $env['CFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + $env['CXXFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + } + return $env; + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-simdjson')] + public function patchBeforeMake(): void + { + if (!ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + return; + } + $extra_cflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: ''; + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . trim($extra_cflags . ' -Xclang -target-feature -Xclang +evex512')); + $extra_cxxflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS') ?: ''; + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS=' . trim($extra_cxxflags . ' -Xclang -target-feature -Xclang +evex512')); + } +} diff --git a/src/Package/Extension/snmp.php b/src/Package/Extension/snmp.php new file mode 100644 index 00000000..d161c602 --- /dev/null +++ b/src/Package/Extension/snmp.php @@ -0,0 +1,34 @@ +getSourceDir()}/config.m4"); + } + $libs = implode(' ', PkgConfigUtil::getLibsArray('netsnmp')); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.m4", + 'PHP_EVAL_LIBLINE([$SNMP_LIBS], [SNMP_SHARED_LIBADD])', + "SNMP_LIBS=\"{$libs}\"\nPHP_EVAL_LIBLINE([\$SNMP_LIBS], [SNMP_SHARED_LIBADD])" + ); + return true; + } +} diff --git a/src/Package/Extension/spx.php b/src/Package/Extension/spx.php new file mode 100644 index 00000000..bb230ec9 --- /dev/null +++ b/src/Package/Extension/spx.php @@ -0,0 +1,52 @@ +getSourceDir()}/config.m4", + 'CFLAGS="$CFLAGS -Werror -Wall -O3 -pthread -std=gnu90"', + 'CFLAGS="$CFLAGS -pthread"' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/src/php_spx.h", + "extern zend_module_entry spx_module_entry;\n", + "extern zend_module_entry spx_module_entry;;\n#define phpext_spx_ptr &spx_module_entry\n" + ); + FileSystem::copy("{$this->getSourceDir()}/src/php_spx.h", "{$this->getSourceDir()}/php_spx.h"); + return true; + } + + #[BeforeStage('php', [php::class, 'configureForUnix'], 'ext-spx')] + #[PatchDescription('Fix spx extension compile error when configuring')] + public function patchBeforeConfigure(): void + { + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/Makefile.frag", + '@cp -r assets/web-ui/*', + "@cp -r {$this->getSourceDir()}/assets/web-ui/*", + ); + } + + public function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + $env['SPX_SHARED_LIBADD'] = $env['LIBS']; + return $env; + } +} diff --git a/src/Package/Extension/swoole.php b/src/Package/Extension/swoole.php new file mode 100644 index 00000000..c269ba54 --- /dev/null +++ b/src/Package/Extension/swoole.php @@ -0,0 +1,150 @@ +getPhpExtensionPackage('swoole-hook-odbc') && $installer->getPhpExtensionPackage('pdo_odbc')?->isBuildStatic()) { + throw new WrongUsageException('swoole-hook-odbc provides pdo_odbc, if you enable odbc hook for swoole, you must remove pdo_odbc extension.'); + } + // swoole-hook-pgsql conflicts with pdo_pgsql + if ($installer->getPhpExtensionPackage('swoole-hook-pgsql') && $installer->getPhpExtensionPackage('pdo_pgsql')?->isBuildStatic()) { + throw new WrongUsageException('swoole-hook-pgsql provides pdo_pgsql, if you enable pgsql hook for swoole, you must remove pdo_pgsql extension.'); + } + // swoole-hook-sqlite conflicts with pdo_sqlite + if ($installer->getPhpExtensionPackage('swoole-hook-sqlite') && $installer->getPhpExtensionPackage('pdo_sqlite')?->isBuildStatic()) { + throw new WrongUsageException('swoole-hook-sqlite provides pdo_sqlite, if you enable sqlite hook for swoole, you must remove pdo_sqlite extension.'); + } + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-swoole')] + #[PatchDescription('Fix maximum version check for Swoole 6.2')] + public function patchBeforeMake(): void + { + FileSystem::replaceFileStr($this->getSourceDir() . '/ext-src/php_swoole_private.h', 'PHP_VERSION_ID > 80500', 'PHP_VERSION_ID >= 80600'); + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-swoole')] + #[PatchDescription('Fix swoole with event extension conflict bug on macOS')] + public function patchBeforeMake2(): void + { + if (SystemTarget::getTargetOS() === 'Darwin') { + // Fix swoole with event extension conflict bug + $util_path = shell()->execWithResult('xcrun --show-sdk-path', false)[1][0] . '/usr/include/util.h'; + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/thirdparty/php/standard/proc_open.cc", + 'include ', + "include \"{$util_path}\"", + ); + } + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, PackageInstaller $installer): string + { + // enable swoole + $arg = '--enable-swoole' . ($shared ? '=shared' : ''); + + // commonly used feature: coroutine-time + $arg .= ' --enable-swoole-coro-time --with-pic'; + + $arg .= $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 .= php::getPHPVersionID() >= 80100 ? ' --enable-swoole-curl' : ' --disable-swoole-curl'; + $arg .= ' --enable-openssl'; + + // additional features that only require libraries + $arg .= $installer->getLibraryPackage('libcares') ? ' --enable-cares' : ''; + $arg .= $installer->getLibraryPackage('brotli') ? (' --enable-brotli --with-brotli-dir=' . BUILD_ROOT_PATH) : ''; + $arg .= $installer->getLibraryPackage('nghttp2') ? (' --with-nghttp2-dir=' . BUILD_ROOT_PATH) : ''; + $arg .= $installer->getLibraryPackage('zstd') ? ' --enable-zstd' : ''; + $arg .= $installer->getLibraryPackage('liburing') ? ' --enable-iouring' : ''; + $arg .= $installer->getPhpExtensionPackage('sockets') ? ' --enable-sockets' : ''; + + // enable additional features that require the pdo extension, but conflict with pdo_* extensions + // to make sure everything works as it should, this is done in fake addon extensions + $arg .= $installer->getPhpExtensionPackage('swoole-hook-pgsql') ? ' --enable-swoole-pgsql' : ' --disable-swoole-pgsql'; + $arg .= $installer->getPhpExtensionPackage('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd'; + $arg .= $installer->getPhpExtensionPackage('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite'; + if ($installer->getPhpExtensionPackage('swoole-hook-odbc')) { + $config = new SPCConfigUtil()->getLibraryConfig($installer->getLibraryPackage('unixodbc')); + $arg .= " --with-swoole-odbc=unixODBC,{$builder->getBuildRootPath()} SWOOLE_ODBC_LIBS=\"{$config['libs']}\""; + } + + // Get version from source directory + $ver = null; + $file = SOURCE_PATH . '/php-src/ext/swoole/include/swoole_version.h'; + // Match #define SWOOLE_VERSION "5.1.3" + $pattern = '/#define SWOOLE_VERSION "(.+)"/'; + if (preg_match($pattern, file_get_contents($file), $matches)) { + $ver = $matches[1]; + } + + if ($ver && $ver >= '6.1.0') { + $arg .= ' --enable-swoole-stdext'; + } + + if (SystemTarget::getTargetOS() === 'Darwin') { + $arg .= ' ac_cv_lib_pthread_pthread_barrier_init=no'; + } + + return $arg; + } + + #[AfterStage('php', [php::class, 'smokeTestCliForUnix'], 'ext-swoole-hook-mysql')] + public function mysqlTest(PackageInstaller $installer): void + { + [$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n' . $this->getSharedExtensionLoadString() . ' --ri "swoole"', false); + $out = implode('', $out); + if ($ret !== 0) { + throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: 'extension swoole_hook_mysql sanity check'); + } + // mysqlnd + if ($installer->getPhpExtensionPackage('swoole-hook-mysql') && !str_contains($out, 'mysqlnd')) { + throw new ValidationException('swoole mysql hook is not enabled correctly.', validation_module: 'Extension swoole mysql hook availability check'); + } + // coroutine_odbc + if ($installer->getPhpExtensionPackage('swoole-hook-odbc') && !str_contains($out, 'coroutine_odbc')) { + throw new ValidationException('swoole odbc hook is not enabled correctly.', validation_module: 'Extension swoole odbc hook availability check'); + } + // coroutine_pgsql + if ($installer->getPhpExtensionPackage('swoole-hook-pgsql') && !str_contains($out, 'coroutine_pgsql')) { + throw new ValidationException( + 'swoole pgsql hook is not enabled correctly.', + validation_module: 'Extension swoole pgsql hook availability check' + ); + } + // coroutine_sqlite + if ($installer->getPhpExtensionPackage('swoole-hook-sqlite') && !str_contains($out, 'coroutine_sqlite')) { + throw new ValidationException( + 'swoole sqlite hook is not enabled correctly.', + validation_module: 'Extension swoole sqlite hook availability check' + ); + } + } +} diff --git a/src/Package/Extension/swow.php b/src/Package/Extension/swow.php new file mode 100644 index 00000000..333a3ed7 --- /dev/null +++ b/src/Package/Extension/swow.php @@ -0,0 +1,44 @@ +getLibraryPackage('openssl') ? ' --enable-swow-ssl' : ' --disable-swow-ssl'; + $arg .= $installer->getLibraryPackage('curl') ? ' --enable-swow-curl' : ' --disable-swow-curl'; + return $arg; + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-swow')] + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-swow')] + public function patchBeforeBuildconf(PackageInstaller $installer): bool + { + $php_src = $installer->getTargetPackage('php')->getSourceDir(); + if (php::getPHPVersionID() >= 80000 && !is_link("{$php_src}/ext/swow")) { + if (PHP_OS_FAMILY === 'Windows') { + f_passthru("cd {$php_src}/ext && mklink /D swow swow-src\\ext"); + } else { + f_passthru("cd {$php_src}/ext && ln -s swow-src/ext swow"); + } + } + // replace AC_DEFUN([SWOW_PKG_CHECK_MODULES] to AC_DEFUN([SWOW_PKG_CHECK_MODULES_STATIC] + FileSystem::replaceFileStr($this->getSourceDir() . '/ext/config.m4', 'AC_DEFUN([SWOW_PKG_CHECK_MODULES]', 'AC_DEFUN([SWOW_PKG_CHECK_MODULES_STATIC]'); + return false; + } +} diff --git a/src/Package/Extension/trader.php b/src/Package/Extension/trader.php new file mode 100644 index 00000000..546b073a --- /dev/null +++ b/src/Package/Extension/trader.php @@ -0,0 +1,23 @@ +getSourceDir()}/config.m4", 'PHP_TA', 'PHP_TRADER'); + return true; + } +} diff --git a/src/Package/Extension/uv.php b/src/Package/Extension/uv.php new file mode 100644 index 00000000..869f4ad9 --- /dev/null +++ b/src/Package/Extension/uv.php @@ -0,0 +1,36 @@ +getSourceDir()}/Makefile", '/^(LDFLAGS =.*)$/m', '$1 -luv -ldl -lrt -pthread'); + return true; + } +} diff --git a/src/Package/Extension/xhprof.php b/src/Package/Extension/xhprof.php new file mode 100644 index 00000000..91c23fac --- /dev/null +++ b/src/Package/Extension/xhprof.php @@ -0,0 +1,35 @@ +getTargetPackage('php')->getSourceDir(); + $link = "{$php_src}/ext/xhprof"; + if (!is_link($link)) { + shell()->cd("{$php_src}/ext")->exec('ln -s xhprof-src/extension xhprof'); + + // patch config.m4 + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/extension/config.m4", + 'if test -f $phpincludedir/ext/pcre/php_pcre.h; then', + 'if test -f $abs_srcdir/ext/pcre/php_pcre.h; then' + ); + return true; + } + return false; + } +} diff --git a/src/Package/Extension/xlswriter.php b/src/Package/Extension/xlswriter.php new file mode 100644 index 00000000..b2f25716 --- /dev/null +++ b/src/Package/Extension/xlswriter.php @@ -0,0 +1,25 @@ +getLibraryPackage('openssl')) { + $arg .= ' --with-openssl=' . $installer->getLibraryPackage('openssl')->getBuildRootPath(); + } + return $arg; + } +} diff --git a/src/Package/Extension/yac.php b/src/Package/Extension/yac.php new file mode 100644 index 00000000..4bf2cf66 --- /dev/null +++ b/src/Package/Extension/yac.php @@ -0,0 +1,25 @@ +getSourceDir()}/storage/allocator/yac_allocator.h", 'defined(HAVE_SHM_MMAP_ANON)', 'defined(YAC_ALLOCATOR_H)'); + FileSystem::replaceFileStr("{$this->getSourceDir()}/serializer/igbinary.c", '#ifdef YAC_ENABLE_IGBINARY', '#if 1'); + FileSystem::replaceFileStr("{$this->getSourceDir()}/serializer/json.c", '#if YAC_ENABLE_JSON', '#if 1'); + return true; + } +} diff --git a/src/Package/Extension/zip.php b/src/Package/Extension/zip.php new file mode 100644 index 00000000..dc2d29c5 --- /dev/null +++ b/src/Package/Extension/zip.php @@ -0,0 +1,20 @@ += 80400 ? '' : ' --with-zlib-dir=' . $builder->getBuildRootPath(); + return '--with-zlib' . $zlib_dir; + } +} diff --git a/src/Package/Library/bzip2.php b/src/Package/Library/bzip2.php index 7f554ab4..403773da 100644 --- a/src/Package/Library/bzip2.php +++ b/src/Package/Library/bzip2.php @@ -6,19 +6,30 @@ namespace Package\Library; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; +use StaticPHP\Util\FileSystem; #[Library('bzip2')] class bzip2 { + #[PatchBeforeBuild] + public function patchBeforeBuild(LibraryPackage $lib): void + { + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall'); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib, PackageBuilder $builder): void { - shell()->cd($lib->getSourceDir())->initializeEnv($lib) - ->exec("make PREFIX='{$lib->getBuildRootPath()}' clean") - ->exec("make -j{$builder->concurrency} PREFIX='{$lib->getBuildRootPath()}' libbz2.a") + $shell = shell()->cd($lib->getSourceDir())->initializeEnv($lib); + $env = $shell->getEnvString(); + $cc_env = 'CC=' . escapeshellarg(getenv('CC') ?: 'cc') . ' AR=' . escapeshellarg(getenv('AR') ?: 'ar'); + + $shell->exec("make PREFIX='{$lib->getBuildRootPath()}' clean") + ->exec("make -j{$builder->concurrency} {$cc_env} {$env} PREFIX='{$lib->getBuildRootPath()}' libbz2.a") ->exec('cp libbz2.a ' . $lib->getLibDir()) ->exec('cp bzlib.h ' . $lib->getIncludeDir()); } diff --git a/src/Package/Library/grpc.php b/src/Package/Library/grpc.php index 86cddcc0..0e2d191a 100644 --- a/src/Package/Library/grpc.php +++ b/src/Package/Library/grpc.php @@ -48,6 +48,7 @@ class grpc '-DgRPC_ZLIB_PROVIDER=package', '-DgRPC_CARES_PROVIDER=package', '-DgRPC_SSL_PROVIDER=package', + '-DCMAKE_SKIP_INSTALL_RPATH=ON', ); if (PHP_OS_FAMILY === 'Linux' && $toolchain->isStatic() && !LinuxUtil::isMuslDist()) { diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php index 607d78ee..3c9f261c 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace Package\Library; -use Package\Target\php; -use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; @@ -19,15 +17,6 @@ use StaticPHP\Util\SourcePatcher; #[Library('imap')] class imap { - #[AfterStage('php', [php::class, 'patchUnixEmbedScripts'], '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"'); - } - } - #[PatchBeforeBuild] #[PatchDescription('Patch imap build system for Linux and macOS compatibility')] public function patchBeforeBuild(LibraryPackage $lib): void @@ -66,14 +55,24 @@ class imap } $libcVer = SystemTarget::getLibcVersion(); $extraLibs = $libcVer && version_compare($libcVer, '2.17', '<=') ? 'EXTRALDFLAGS="-ldl -lrt -lpthread"' : ''; - shell()->cd($lib->getSourceDir()) - ->exec('make clean') - ->exec('touch ip6') - ->exec('chmod +x tools/an') - ->exec('chmod +x tools/ua') - ->exec('chmod +x src/osdep/unix/drivers') - ->exec('chmod +x src/osdep/unix/mkauths') - ->exec("yes | make slx {$ssl_options} EXTRACFLAGS='-fPIC -Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types' {$extraLibs}"); + try { + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + // PASSWDTYPE=nul avoids any crypt() symbol reference in c-client.a; + // zig-cc 0.15+ uses paths_first strategy and cannot find libcrypt outside of buildroot. + ->exec("yes | make slx {$ssl_options} PASSWDTYPE=nul EXTRACFLAGS='-fPIC -Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types' {$extraLibs}"); + } catch (\Throwable $e) { + // slx target also builds bundled tools (mtest, etc.) which may fail to link -lcrypt dynamically + // (e.g. with zig-cc). We only need c-client.a, so tolerate the failure if it was built. + if (!file_exists("{$lib->getSourceDir()}/c-client/c-client.a")) { + throw $e; + } + } try { shell() ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") @@ -94,16 +93,24 @@ class imap $ssl_options = 'SSLTYPE=none'; } $out = shell()->execWithResult('echo "-include $(xcrun --show-sdk-path)/usr/include/poll.h -include $(xcrun --show-sdk-path)/usr/include/time.h -include $(xcrun --show-sdk-path)/usr/include/utime.h"')[1][0]; - shell()->cd($lib->getSourceDir()) - ->exec('make clean') - ->exec('touch ip6') - ->exec('chmod +x tools/an') - ->exec('chmod +x tools/ua') - ->exec('chmod +x src/osdep/unix/drivers') - ->exec('chmod +x src/osdep/unix/mkauths') - ->exec( - "echo y | make osx {$ssl_options} EXTRACFLAGS='-Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types {$out}'" - ); + try { + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + ->exec( + "echo y | make osx {$ssl_options} EXTRACFLAGS='-Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types {$out}'" + ); + } catch (\Throwable $e) { + // osx target also builds bundled tools (mtest, etc.) which may fail to link. + // We only need c-client.a, so tolerate the failure if it was built. + if (!file_exists("{$lib->getSourceDir()}/c-client/c-client.a")) { + throw $e; + } + } try { shell() ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") diff --git a/src/Package/Library/libacl.php b/src/Package/Library/libacl.php index a74cb2d4..97c57d39 100644 --- a/src/Package/Library/libacl.php +++ b/src/Package/Library/libacl.php @@ -8,6 +8,7 @@ use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Util\FileSystem; @@ -16,6 +17,7 @@ use StaticPHP\Util\FileSystem; class libacl { #[BeforeStage('php', [php::class, 'makeForUnix'], 'libacl')] + #[PatchDescription('Fix FPM_EXTRA_LIBS to avoid linking with acl on Unix')] public function patchBeforeMakePhpUnix(LibraryPackage $lib): void { $file_path = SOURCE_PATH . '/php-src/Makefile'; diff --git a/src/Package/Library/libavif.php b/src/Package/Library/libavif.php index 87e6c650..6db235e1 100644 --- a/src/Package/Library/libavif.php +++ b/src/Package/Library/libavif.php @@ -17,6 +17,11 @@ class libavif public function buildUnix(LibraryPackage $lib): void { UnixCMakeExecutor::create($lib) + ->optionalPackage('libaom', '-DAVIF_CODEC_AOM=SYSTEM', '-DAVIF_CODEC_AOM=OFF') + ->optionalPackage('libsharpyuv', '-DAVIF_LIBSHARPYUV=SYSTEM', '-DAVIF_LIBSHARPYUV=OFF') + ->optionalPackage('libjpeg', '-DAVIF_JPEG=SYSTEM', '-DAVIF_JPEG=OFF') + ->optionalPackage('libxml2', '-DAVIF_LIBXML2=SYSTEM', '-DAVIF_LIBXML2=OFF') + ->optionalPackage('libpng', '-DAVIF_LIBPNG=SYSTEM', '-DAVIF_LIBPNG=OFF') ->addConfigureArgs('-DAVIF_LIBYUV=OFF') ->build(); // patch pkgconfig diff --git a/src/Package/Library/libxml2.php b/src/Package/Library/libxml2.php index 7c35d685..3f8b3e71 100644 --- a/src/Package/Library/libxml2.php +++ b/src/Package/Library/libxml2.php @@ -17,17 +17,13 @@ class libxml2 public function buildForLinux(LibraryPackage $lib): void { UnixCMakeExecutor::create($lib) - ->optionalPackage( - 'zlib', - '-DLIBXML2_WITH_ZLIB=ON ' . - "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a " . - "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", - '-DLIBXML2_WITH_ZLIB=OFF', - ) - ->optionalPackage('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) ->addConfigureArgs( '-DLIBXML2_WITH_ICONV=ON', '-DIconv_IS_BUILT_IN=OFF', + '-DLIBXML2_WITH_ZLIB=ON', + "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a", + "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", + '-DLIBXML2_WITH_LZMA=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', diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php index c7c39dc1..dd591a6f 100644 --- a/src/Package/Library/ncurses.php +++ b/src/Package/Library/ncurses.php @@ -13,6 +13,7 @@ use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; #[Library('ncurses')] +#[Library('ncursesw')] class ncurses { #[BuildFor('Darwin')] @@ -21,37 +22,48 @@ class ncurses { $dirdiff = new DirDiff(BUILD_BIN_PATH); - UnixAutoconfExecutor::create($package) + $ac = 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()}", - ) + ]); + $wide = $package->getName() === 'ncurses' ? ['--disable-widec'] : []; + // Include standard system terminfo paths as fallback so binaries linking this ncurses + // (e.g. htop) can find terminfo on any target system without needing TERMINFO_DIRS set. + $terminfo_dirs = implode(':', [ + "{$package->getBuildRootPath()}/share/terminfo", + '/etc/terminfo', + '/lib/terminfo', + '/usr/share/terminfo', + ]); + $ac->configure( + '--enable-overwrite', + '--with-curses-h', + '--enable-pc-files', + '--enable-echo', + '--with-normal', + '--with-ticlib', + '--without-tests', + '--without-dlsym', + '--without-debug', + '--enable-symlinks', + "--with-terminfo-dirs={$terminfo_dirs}", + "--bindir={$package->getBinDir()}", + "--includedir={$package->getIncludeDir()}", + "--libdir={$package->getLibDir()}", + "--prefix={$package->getBuildRootPath()}", + ...$wide, + ) ->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'); + // 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']; + $suffix = $package->getName() === 'ncursesw' ? 'w' : ''; + $pkgconf_list = ["form{$suffix}.pc", "menu{$suffix}.pc", "ncurses++{$suffix}.pc", "ncurses{$suffix}.pc", "panel{$suffix}.pc", "tic{$suffix}.pc"]; $package->patchPkgconfPrefix($pkgconf_list); foreach ($pkgconf_list as $pkgconf) { diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 84b4657e..682b79e2 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -10,7 +10,6 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\PatchDescription; -use StaticPHP\Exception\FileSystemException; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -27,28 +26,17 @@ class postgresql extends LibraryPackage #[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'); + if (SystemTarget::getTargetOS() === 'Darwin') { + // on macOS, explicit_bzero is available but causes build failure due to detection issues, so we fake it as unavailable + shell()->cd($package->getSourceDir()) + ->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure'); + } } #[PatchBeforeBuild] #[PatchDescription('Various patches before building PostgreSQL')] public function patchBeforeBuild(): bool { - // fix aarch64 build on glibc 2.17 (e.g. CentOS 7) - if (SystemTarget::getLibcVersion() === '2.17' && SystemTarget::getTargetArch() === 'aarch64') { - try { - FileSystem::replaceFileStr("{$this->getSourceDir()}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0'); - FileSystem::replaceFileStr( - "{$this->getSourceDir()}/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->getSourceDir()}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); // disable shared libs build @@ -131,8 +119,7 @@ class postgresql extends LibraryPackage // remove dynamic libs shell()->cd($this->getSourceDir() . '/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'); diff --git a/src/Package/Library/curl.php b/src/Package/Target/curl.php similarity index 81% rename from src/Package/Library/curl.php rename to src/Package/Target/curl.php index 283c765a..dbfa8f7a 100644 --- a/src/Package/Library/curl.php +++ b/src/Package/Target/curl.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace Package\Library; +namespace Package\Target; use StaticPHP\Attribute\Package\BuildFor; -use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; +use StaticPHP\Attribute\Package\Target; use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; -#[Library('curl')] +#[Target('curl')] class curl { #[PatchBeforeBuild] @@ -48,14 +48,22 @@ class curl ->optionalPackage('idn2', ...cmake_boolean_args('CURL_USE_IDN2')) ->optionalPackage('libcares', '-DENABLE_ARES=ON') ->addConfigureArgs( - '-DBUILD_CURL_EXE=OFF', + '-DBUILD_CURL_EXE=ON', '-DBUILD_LIBCURL_DOCS=OFF', ) ->build(); // patch pkgconf $lib->patchPkgconfPrefix(['libcurl.pc']); + // curl's CMake embeds krb5 link flags directly without following Requires.private chain, + // so -lkrb5support (from mit-krb5.pc Libs.private) is missing from libcurl.pc. + $pc_path = "{$lib->getLibDir()}/pkgconfig/libcurl.pc"; + if (str_contains(FileSystem::readFile($pc_path), '-lgssapi_krb5')) { + FileSystem::replaceFileRegex($pc_path, '/-lcom_err$/m', '-lcom_err -lkrb5support'); + } shell()->cd("{$lib->getLibDir()}/cmake/CURL/") ->exec("sed -ie 's|\"/lib/libcurl.a\"|\"{$lib->getLibDir()}/libcurl.a\"|g' CURLTargets-release.cmake"); + + $lib->setOutput('Static curl executable path', BUILD_BIN_PATH . '/curl'); } } diff --git a/src/Package/Target/go_xcaddy.php b/src/Package/Target/go_xcaddy.php deleted file mode 100644 index 0f8c7553..00000000 --- a/src/Package/Target/go_xcaddy.php +++ /dev/null @@ -1,26 +0,0 @@ -removeConfigureArgs('--disable-shared', '--enable-static') + ->exec('./autogen.sh') + ->addConfigureArgs($toolchain->isStatic() ? '--enable-static' : '--disable-static') + ->configure() + ->make(with_clean: false); + } +} diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index c21ae590..38e2ad91 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -4,8 +4,10 @@ declare(strict_types=1); namespace Package\Target; +use Package\Target\php\frankenphp; use Package\Target\php\unix; use Package\Target\php\windows; +use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Info; use StaticPHP\Attribute\Package\InitPackage; @@ -25,6 +27,7 @@ use StaticPHP\Registry\PackageLoader; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\ToolchainManager; +use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\V2CompatLayer; @@ -42,6 +45,7 @@ class php extends TargetPackage { use unix; use windows; + use frankenphp; /** @var string[] Supported major PHP versions */ public const array SUPPORTED_MAJOR_VERSIONS = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']; @@ -102,6 +106,24 @@ class php extends TargetPackage throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); } + /** + * Get PHP version from source archive filename + * + * @return null|string PHP version (e.g., "8.4.0") + */ + public static function getPHPVersionFromArchive(bool $return_null_if_failed = false): ?string + { + $archives = ApplicationContext::get(ArtifactCache::class)->getSourceInfo('php-src'); + $filename = $archives['filename'] ?? ''; + if (!preg_match('/php-(\d+\.\d+\.\d+(?:RC\d+|alpha\d+|beta\d+)?)\.tar\.(?:gz|bz2|xz)/', $filename, $match)) { + if ($return_null_if_failed) { + return null; + } + throw new WrongUsageException('PHP source archive filename format is malformed (got: ' . $filename . ')'); + } + return $match[1]; + } + #[InitPackage] public function init(TargetPackage $package): void { @@ -119,6 +141,7 @@ class php extends TargetPackage $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'); + $package->addBuildOption('no-smoke-test', null, InputOption::VALUE_OPTIONAL, 'Disable smoke test for specific SAPIs, or all if no value provided', false); // phpmicro build options if ($package->getName() === 'php' || $package->getName() === 'php-micro') { @@ -193,11 +216,42 @@ class php extends TargetPackage } } + // Mark transitive PHP extension dependencies of static/shared extensions as static too. + // For static extensions: their ext deps must also be static. + // For shared extensions: their ext deps that are not themselves shared must be compiled + // into the static PHP build so their headers and symbols are available when linking the .so. + $all_input_ext_pkgs = array_map(fn ($x) => "ext-{$x}", array_values(array_unique([...$static_extensions, ...$shared_extensions]))); + if (!empty($all_input_ext_pkgs)) { + $transitive_deps = DependencyResolver::resolve($all_input_ext_pkgs, include_suggests: (bool) $package->getBuildOption('with-suggests', false)); + foreach ($transitive_deps as $dep_name) { + if (!str_starts_with($dep_name, 'ext-') || !PackageLoader::hasPackage($dep_name)) { + continue; + } + $dep_extname = substr($dep_name, 4); + if (in_array($dep_extname, $shared_extensions)) { + continue; // already designated as shared + } + $dep_instance = PackageLoader::getPackage($dep_name); + if (!$dep_instance instanceof PhpExtensionPackage || $dep_instance->isBuildStatic() || $dep_instance->isBuildShared()) { + continue; + } + $dep_config = PackageConfig::get($dep_name, 'php-extension', []); + if (($dep_config['build-static'] ?? true) !== false) { + $dep_instance->setBuildStatic(); + } + } + } + // building shared extensions need embed SAPI if (!empty($shared_extensions) && !$package->getBuildOption('build-embed', false) && $package->getName() === 'php') { $installer->addBuildPackage('php-embed'); } + // frankenphp depends on embed SAPI (libphp.a) + if ($package->getName() === 'frankenphp') { + $installer->addBuildPackage('php-embed'); + } + return [...$extensions_pkg, ...$additional_packages]; } @@ -209,7 +263,7 @@ class php extends 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 + // frankenphp doesn't support windows, BSD is currently not supported by StaticPHP if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); } @@ -239,7 +293,8 @@ class php extends TargetPackage $installer->isPackageResolved('php-embed') ? 'embed' : null, $installer->isPackageResolved('frankenphp') ? 'frankenphp' : null, ]); - $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildStatic()); + $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 [ @@ -247,6 +302,7 @@ class php extends TargetPackage 'Build Target' => getenv('SPC_TARGET') ?: '', 'Build Toolchain' => ToolchainManager::getToolchainClass(), 'Build SAPI' => implode(', ', $sapis), + 'PHP Version' => self::getPHPVersion(return_null_if_failed: true) ?? self::getPHPVersionFromArchive(return_null_if_failed: true) ?? 'Unknown', '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)), @@ -272,10 +328,10 @@ class php extends TargetPackage // 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')) { + if (!str_contains($file, 'StaticPHP.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); + logger()->debug('Inserting StaticPHP.version to php-src'); + $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"StaticPHP.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); FileSystem::writeFile("{$package->getSourceDir()}/main/main.c", $file); } diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 06687090..f513242b 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -7,6 +7,7 @@ namespace Package\Target\php; use Package\Target\php; use StaticPHP\Attribute\Package\Stage; use StaticPHP\Exception\SPCInternalException; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -22,13 +23,14 @@ use ZM\Logger\ConsoleColor; trait frankenphp { #[Stage] - public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void + public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void { if (getenv('GOROOT') === false) { throw new SPCInternalException('go-xcaddy is not initialized properly. GOROOT is not set.'); } // process --with-frankenphp-app option + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('processing --with-frankenphp-app option')); $package->runStage([$this, 'processFrankenphpApp']); // modules @@ -88,6 +90,8 @@ trait frankenphp 'CGO_LDFLAGS' => "{$package->getLibExtraLdFlags()} {$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{$frankenphp_version} PHP {$libphp_version} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", @@ -102,6 +106,40 @@ trait frankenphp $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '/frankenphp'); } + #[Stage] + public function smokeTestFrankenphpForUnix(PackageBuilder $builder): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test', false); + $option = match ($no_smoke_test) { + false => false, // default value, run all smoke tests + null => 'all', // --no-smoke-test without value, skip all smoke tests + default => parse_comma_list($no_smoke_test), // --no-smoke-test=frankenphp,... + }; + if ($option === 'all' || (is_array($option) && in_array('frankenphp', $option, true))) { + return; + } + + InteractiveTerm::setMessage('Running FrankenPHP smoke test'); + $frankenphp = BUILD_BIN_PATH . '/frankenphp'; + if (!file_exists($frankenphp)) { + throw new ValidationException( + "FrankenPHP binary not found: {$frankenphp}", + validation_module: 'FrankenPHP smoke test' + ); + } + $prefix = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_' : 'LD_'; + [$ret, $output] = shell() + ->setEnv(["{$prefix}LIBRARY_PATH" => BUILD_LIB_PATH]) + ->execWithResult("{$frankenphp} version"); + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { + throw new ValidationException( + 'FrankenPHP failed smoke test: ret[' . $ret . ']. out[' . implode('', $output) . ']', + validation_module: 'FrankenPHP smoke test' + ); + } + } + /** * Process the --with-frankenphp-app option * Creates app.tar and app.checksum in source/frankenphp directory @@ -114,7 +152,7 @@ trait frankenphp $frankenphpAppPath = $package->getBuildOption('with-frankenphp-app'); if ($frankenphpAppPath) { - InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('processing --with-frankenphp-app option')); + $frankenphpAppPath = trim($frankenphpAppPath, "\"'"); if (!is_dir($frankenphpAppPath)) { throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); } diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 13c89780..f16d879f 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -12,6 +12,7 @@ use StaticPHP\Attribute\PatchDescription; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\PatchException; use StaticPHP\Exception\SPCException; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -22,6 +23,7 @@ use StaticPHP\Toolchain\Interface\ToolchainInterface; 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; @@ -29,13 +31,15 @@ use ZM\Logger\ConsoleColor; trait unix { - use frankenphp; - #[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')] + #[PatchDescription('Patch SPC_MICRO_PATCHES defined patches (e.g. cli_checks, disable_huge_page)')] #[PatchDescription('Patch configure.ac for musl and musl-toolchain')] #[PatchDescription('Let php m4 tools use static pkg-config')] public function patchBeforeBuildconf(TargetPackage $package): void { + // php-src patches from micro (reads SPC_MICRO_PATCHES env var) + SourcePatcher::patchPhpSrc(); + // patch configure.ac for musl and musl-toolchain $musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl'; FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac'); @@ -47,40 +51,10 @@ trait unix // let php m4 tools use static pkg-config FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); - } - #[BeforeStage('php', [php::class, 'makeForUnix'], 'php')] - #[PatchDescription('Patch TSRM for musl TLS symbol visibility issue')] - #[PatchDescription('Patch ext/standard/info.c for configure command info')] - public function patchTSRMBeforeUnixMake(ToolchainInterface $toolchain): void - { - if (!$toolchain->isStatic() && SystemTarget::getLibc() === 'musl') { - // we need to patch the symbol to global visibility, otherwise extensions with `initial-exec` TLS model will fail to load - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } else { - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } - - if (str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), '-release')) { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef CONFIGURE_COMMAND', - '#ifdef NO_CONFIGURE_COMMAND', - ); - } else { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef NO_CONFIGURE_COMMAND', - '#ifdef CONFIGURE_COMMAND', - ); + // also patch extension config.m4 files (they call PKG_CHECK_MODULES directly, not via php.m4) + foreach (glob("{$package->getSourceDir()}/ext/*/*.m4") as $m4file) { + FileSystem::replaceFileStr($m4file, 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); } } @@ -89,7 +63,6 @@ trait unix { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); V2CompatLayer::emitPatchPoint('before-php-buildconf'); - // run ./buildconf shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); } @@ -102,6 +75,13 @@ trait unix $args = []; $version_id = self::getPHPVersionID(); + + // disable undefined behavior sanitizer when opcache JIT is enabled (Linux only) + if (SystemTarget::getTargetOS() === 'Linux' && !$package->getBuildOption('disable-opcache-jit', false)) { + if ($version_id >= 80500 || $installer->isPackageResolved('ext-opcache')) { + f_putenv('SPC_COMPILER_EXTRA=-fno-sanitize=undefined'); + } + } // PHP JSON extension is built-in since PHP 8.0 if ($version_id < 80000) { $args[] = '--enable-json'; @@ -122,7 +102,9 @@ trait unix } // 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-fpm') + ? '--enable-fpm' . ($installer->isPackageResolved('libacl') ? ' --with-fpm-acl' : '') + : '--disable-fpm'; $args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) { 'Linux' => '--enable-micro=all-static', default => '--enable-micro', @@ -135,14 +117,69 @@ trait unix $static_extension_str = $this->makeStaticExtensionString($installer); + // reuse the same make vars so configure conftest links use the same LIBS (incl. -framework flags) + $vars = $this->makeVars($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'), + 'LIBS' => $vars['EXTRA_LIBS'] ?? '', ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); } + #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch TSRM.h to fix musl TLS symbol visibility for non-static builds')] + public function beforeMakeUnix(ToolchainInterface $toolchain): void + { + if (!$toolchain->isStatic() && SystemTarget::getLibc() === 'musl') { + // we need to patch the symbol to global visibility, otherwise extensions with `initial-exec` TLS model will fail to load + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/TSRM/TSRM.h', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + ); + } else { + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/TSRM/TSRM.h', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + ); + } + } + + #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch Makefile to fix //lib path for Linux builds')] + public function tryPatchMakefileUnix(): void + { + if (SystemTarget::getTargetOS() !== 'Linux') { + return; + } + + // replace //lib with /lib in Makefile + shell()->cd(SOURCE_PATH . '/php-src')->exec('sed -i "s|//lib|/lib|g" Makefile'); + } + + #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch info.c to hide configure command in release builds')] + public function patchInfoCForRelease(): void + { + if (str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), '-release')) { + FileSystem::replaceFileLineContainsString( + SOURCE_PATH . '/php-src/ext/standard/info.c', + '#ifdef CONFIGURE_COMMAND', + '#ifdef NO_CONFIGURE_COMMAND', + ); + } else { + FileSystem::replaceFileLineContainsString( + SOURCE_PATH . '/php-src/ext/standard/info.c', + '#ifdef NO_CONFIGURE_COMMAND', + '#ifdef CONFIGURE_COMMAND', + ); + } + } + #[Stage] public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void { @@ -151,23 +188,18 @@ trait unix logger()->info('cleaning up php-src build files'); shell()->cd($package->getSourceDir())->exec('make clean'); - // cli if ($installer->isPackageResolved('php-cli')) { $package->runStage([self::class, 'makeCliForUnix']); } - // cgi if ($installer->isPackageResolved('php-cgi')) { $package->runStage([self::class, 'makeCgiForUnix']); } - // fpm if ($installer->isPackageResolved('php-fpm')) { $package->runStage([self::class, 'makeFpmForUnix']); } - // micro if ($installer->isPackageResolved('php-micro')) { $package->runStage([self::class, 'makeMicroForUnix']); } - // embed if ($installer->isPackageResolved('php-embed')) { $package->runStage([self::class, 'makeEmbedForUnix']); } @@ -219,44 +251,46 @@ trait unix } #[Stage] - #[PatchDescription('Patch micro.sfx after UPX compression')] + #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { - 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' : ''; - $makeArgs = $this->makeVarsToArgs($vars); - // build - shell()->cd($package->getSourceDir()) - ->setEnv($vars) - ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); - - $dst = BUILD_BIN_PATH . '/micro.sfx'; - $builder->deployBinary("{$package->getSourceDir()}/sapi/micro/micro.sfx", $dst); - - /* - * Patch micro.sfx after UPX compression. - * micro needs special section handling in LinuxBuilder. - * The micro.sfx does not support UPX directly, but we can remove UPX - * info segment to adapt. - * This will also make micro.sfx with upx-packed more like a malware fore antivirus - */ - if ($package->getBuildOption('with-upx-pack') && SystemTarget::getTargetOS() === 'Linux') { - // strip first - // cut binary with readelf - [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \$1, \$2, \$3, \$4, \$6, \$7}'"); - $out[1] = explode(' ', $out[1]); - $offset = $out[1][0]; - if ($ret !== 0 || !str_starts_with($offset, '0x')) { - throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + $phar_patched = false; + try { + if ($installer->isPackageResolved('ext-phar')) { + $phar_patched = true; + SourcePatcher::patchMicroPhar(self::getPHPVersionID()); } - $offset = hexdec($offset); - // remove upx extra wastes - file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); - } + 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' : ''; + $makeArgs = $this->makeVarsToArgs($vars); + // build + shell()->cd($package->getSourceDir()) + ->setEnv($vars) + ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); - $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx'); + $dst = BUILD_BIN_PATH . '/micro.sfx'; + $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst); + // patch after UPX-ed micro.sfx (Linux only) + if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) { + // cut binary with readelf to remove UPX extra segment + [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \\$1, \\$2, \\$3, \\$4, \\$6, \\$7}'"); + $out[1] = explode(' ', $out[1]); + $offset = $out[1][0]; + if ($ret !== 0 || !str_starts_with($offset, '0x')) { + throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + } + $offset = hexdec($offset); + // remove upx extra wastes + file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); + } + $package->setOutput('Binary path for micro SAPI', $dst); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); + } + } } #[Stage] @@ -285,18 +319,13 @@ trait unix // process libphp.so for shared embed $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; $libphp_so = "{$package->getLibDir()}/libphp.{$suffix}"; - $libphp_so_dst = $libphp_so; if (file_exists($libphp_so)) { // rename libphp.so if -release is set if (SystemTarget::getTargetOS() === 'Linux') { - // deploy libphp.so - preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches); - if (!empty($matches[1])) { - $libphp_so_dst = str_replace('.so', '-' . $matches[1] . '.so', $libphp_so); - } + $this->processLibphpSoFile($libphp_so, $installer); } // deploy - $builder->deployBinary($libphp_so, $libphp_so_dst, false); + $builder->deployBinary($libphp_so, $libphp_so, false); $package->setOutput('Library path for embed SAPI', $libphp_so); } @@ -368,16 +397,68 @@ trait unix } } + #[Stage] + public function smokeTestForUnix(PackageBuilder $builder, TargetPackage $package, PackageInstaller $installer): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test'); + // validate option + $option = match ($no_smoke_test) { + false => false, // default value, run all smoke tests + null => 'all', // --no-smoke-test without value, skip all smoke tests + default => parse_comma_list($no_smoke_test), // --no-smoke-test=cli,fpm, skip specified smoke tests + }; + $valid_tests = ['cli', 'cgi', 'micro', 'micro-exts', 'embed', 'frankenphp']; + // compat: --without-micro-ext-test is equivalent to --no-smoke-test=micro-exts + if ($builder->getOption('without-micro-ext-test', false)) { + $valid_tests = array_diff($valid_tests, ['micro-exts']); + } + if (is_array($option)) { + /* + 1. if option is not in valid tests, throw WrongUsageException + 2. if all passed options are valid, remove them from $valid_tests, and run the remaining tests + */ + foreach ($option as $test) { + if (!in_array($test, $valid_tests, true)) { + throw new WrongUsageException("Invalid value for --no-smoke-test: {$test}. Valid values are: " . implode(', ', $valid_tests)); + } + $valid_tests = array_diff($valid_tests, [$test]); + } + } elseif ($option === 'all') { + $valid_tests = []; + } + // run cli tests + if (in_array('cli', $valid_tests, true) && $installer->isPackageResolved('php-cli')) { + $package->runStage([$this, 'smokeTestCliForUnix']); + } + // run cgi tests + if (in_array('cgi', $valid_tests, true) && $installer->isPackageResolved('php-cgi')) { + $package->runStage([$this, 'smokeTestCgiForUnix']); + } + // run micro tests + if (in_array('micro', $valid_tests, true) && $installer->isPackageResolved('php-micro')) { + $skipExtTest = !in_array('micro-exts', $valid_tests, true); + $package->runStage([$this, 'smokeTestMicroForUnix'], ['skipExtTest' => $skipExtTest]); + } + // run embed tests + if (in_array('embed', $valid_tests, true) && $installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'smokeTestEmbedForUnix']); + } + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function build(TargetPackage $package): void { - // virtual target, do nothing - if (in_array($package->getName(), ['php-cli', 'php-fpm', 'php-cgi', 'php-micro', 'php-embed'], true)) { + // frankenphp is not a php sapi, it's a standalone Go binary that depends on libphp.a (embed) + if ($package->getName() === 'frankenphp') { + /* @var php $this */ + $package->runStage([$this, 'buildFrankenphpForUnix']); + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); return; } - if ($package->getName() === 'frankenphp') { - $package->runStage([$this, 'buildFrankenphpUnix']); + // virtual target, do nothing + if ($package->getName() !== 'php') { return; } @@ -388,6 +469,27 @@ trait unix $package->runStage([$this, 'unixBuildSharedExt']); } + #[Stage('postInstall')] + public function postInstall(TargetPackage $package, PackageInstaller $installer): void + { + if ($package->getName() === 'frankenphp') { + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); + return; + } + if ($package->getName() !== 'php') { + return; + } + if (SystemTarget::isUnix()) { + if ($installer->interactive) { + InteractiveTerm::indicateProgress('Running PHP smoke tests'); + } + $package->runStage([$this, 'smokeTestForUnix']); + if ($installer->interactive) { + InteractiveTerm::finish('PHP smoke tests passed'); + } + } + } + /** * Patch phpize and php-config if needed */ @@ -415,6 +517,132 @@ trait unix } } + #[Stage] + public function smokeTestCliForUnix(PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Running basic php-cli smoke test'); + [$ret, $output] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n -r "echo \"hello\";"'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); + } + + $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); + foreach ($exts as $ext) { + InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); + $ext->runSmokeTestCliUnix(); + } + } + + #[Stage] + public function smokeTestCgiForUnix(): void + { + InteractiveTerm::setMessage('Running basic php-cgi smoke test'); + [$ret, $output] = shell()->execWithResult("echo 'Hello, World!\";' | " . BUILD_BIN_PATH . '/php-cgi -n'); + $raw_output = implode('', $output); + if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!') || !str_contains($raw_output, 'text/html')) { + throw new ValidationException("cgi failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi smoke test'); + } + } + + #[Stage] + public function smokeTestMicroForUnix(PackageInstaller $installer, bool $skipExtTest = false): void + { + $micro_sfx = BUILD_BIN_PATH . '/micro.sfx'; + + // micro_ext_test + InteractiveTerm::setMessage('Running php-micro ext smoke test'); + $content = $skipExtTest + ? 'generateMicroExtTests($installer); + $test_file = SOURCE_PATH . '/micro_ext_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + chmod($test_file, 0755); + [$ret, $out] = shell()->execWithResult($test_file); + $raw_out = trim(implode('', $out)); + if ($ret !== 0 || !str_starts_with($raw_out, '[micro-test-start]') || !str_ends_with($raw_out, '[micro-test-end]')) { + throw new ValidationException( + "micro_ext_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_ext_test]' + ); + } + + // micro_zend_bug_test + InteractiveTerm::setMessage('Running php-micro zend bug smoke test'); + $content = file_get_contents(ROOT_DIR . '/src/globals/common-tests/micro_zend_mm_heap_corrupted.txt'); + $test_file = SOURCE_PATH . '/micro_zend_bug_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + chmod($test_file, 0755); + [$ret, $out] = shell()->execWithResult($test_file); + if ($ret !== 0) { + $raw_out = trim(implode('', $out)); + throw new ValidationException( + "micro_zend_bug_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_zend_bug_test]' + ); + } + } + + #[Stage] + public function smokeTestEmbedForUnix(PackageInstaller $installer, ToolchainInterface $toolchain): void + { + $sample_file_path = SOURCE_PATH . '/embed-test'; + FileSystem::createDir($sample_file_path); + // copy embed test files + copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c'); + copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php'); + + $config = new SPCConfigUtil()->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + $lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}"; + if ($toolchain->isStatic()) { + $lens .= ' -static'; + } + + $dynamic_exports = ''; + $envVars = []; + $embedType = 'static'; + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { + $embedType = 'shared'; + $libPathKey = SystemTarget::getTargetOS() === 'Darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH'; + $envVars[$libPathKey] = BUILD_LIB_PATH . (($existing = getenv($libPathKey)) ? ':' . $existing : ''); + FileSystem::removeFileIfExists(BUILD_LIB_PATH . '/libphp.a'); + } else { + $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; + foreach (glob(BUILD_LIB_PATH . "/libphp*.{$suffix}") as $file) { + unlink($file); + } + // calling getDynamicExportedSymbols on non-Linux is okay + if ($dynamic_exports = UnixUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) { + $dynamic_exports = ' ' . $dynamic_exports; + } + } + + $cc = getenv('CC'); + InteractiveTerm::setMessage('Running php-embed build smoke test'); + [$ret, $out] = shell()->cd($sample_file_path)->execWithResult("{$cc} -o embed embed.c {$lens}{$dynamic_exports}"); + if ($ret !== 0) { + throw new ValidationException( + 'embed failed to build. Error message: ' . implode("\n", $out), + validation_module: $embedType . ' libphp embed build smoke test' + ); + } + + InteractiveTerm::setMessage('Running php-embed run smoke test'); + [$ret, $output] = shell()->cd($sample_file_path)->setEnv($envVars)->execWithResult('./embed'); + if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { + throw new ValidationException( + 'embed failed to run. Error message: ' . implode("\n", $output), + validation_module: $embedType . ' libphp embed run smoke test' + ); + } + } + /** * Seek php-src/config.log when building PHP, add it to exception. */ @@ -431,6 +659,26 @@ trait unix } } + /** + * Generate micro extension test php code. + */ + private function generateMicroExtTests(PackageInstaller $installer): string + { + $php = "getResolvedPackages(PhpExtensionPackage::class) as $ext) { + if (!$ext->isBuildStatic()) { + continue; + } + $ext_name = $ext->getDistName(); + if (!empty($ext_name)) { + $php .= "echo 'Running micro with {$ext_name} test' . PHP_EOL;\n"; + $php .= "assert(extension_loaded('{$ext_name}'));\n\n"; + } + } + $php .= "echo '[micro-test-end]';\n"; + return $php; + } + /** * Rename libphp.so to libphp-.so if -release is set in LDFLAGS. */ @@ -518,6 +766,7 @@ trait unix return array_filter([ 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), + 'EXTRA_CXXFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS'), 'EXTRA_LDFLAGS_PROGRAM' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . "{$config['ldflags']} {$static} {$pie}", 'EXTRA_LDFLAGS' => $config['ldflags'], 'EXTRA_LIBS' => $libs, diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index e77b88cd..74e746e2 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -22,6 +22,13 @@ use ZM\Logger\ConsoleColor; trait windows { + #[BeforeStage('php', [self::class, 'buildconfForWindows'])] + #[PatchDescription('Patch for fixing win32 xml related extensions builds')] + public function beforeBuildconfWin(TargetPackage $package): void + { + FileSystem::replaceFileStr("{$package->getSourceDir()}/win32/build/config.w32", 'dllmain.c ', ''); + } + #[Stage] public function buildconfForWindows(TargetPackage $package): void { diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 19fdd41d..750c49e4 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ use Symfony\Component\Console\Application; */ final class ConsoleApplication extends Application { - public const string VERSION = '2.8.0'; + public const string VERSION = '2.8.3'; public function __construct() { diff --git a/src/SPC/builder/extension/ffi.php b/src/SPC/builder/extension/ffi.php index 98547723..8e192bea 100644 --- a/src/SPC/builder/extension/ffi.php +++ b/src/SPC/builder/extension/ffi.php @@ -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'; diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 464c9b2f..1b532bbc 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -365,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}"); } @@ -455,6 +456,7 @@ 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.CustomVersion=FrankenPHP ' . "v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", diff --git a/src/SPC/builder/unix/library/libavif.php b/src/SPC/builder/unix/library/libavif.php index fbd4fa18..a5b57aef 100644 --- a/src/SPC/builder/unix/library/libavif.php +++ b/src/SPC/builder/unix/library/libavif.php @@ -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 diff --git a/src/SPC/builder/unix/library/postgresql.php b/src/SPC/builder/unix/library/postgresql.php index a72f3a1a..2ad4f51b 100644 --- a/src/SPC/builder/unix/library/postgresql.php +++ b/src/SPC/builder/unix/library/postgresql.php @@ -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 diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index 0a1e0299..7317c4f9 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -22,7 +22,7 @@ class SourcePatcher FileSystem::addSourceExtractHook('swoole', [__CLASS__, 'patchSwoole']); FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchPhpLibxml212']); // migrated FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchGDWin32']); // migrated - FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']); // migrated + // FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']); // migrated FileSystem::addSourceExtractHook('sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVPhp85']); diff --git a/src/SPC/store/pkg/GoXcaddy.php b/src/SPC/store/pkg/GoXcaddy.php index 93821aaa..462342db 100644 --- a/src/SPC/store/pkg/GoXcaddy.php +++ b/src/SPC/store/pkg/GoXcaddy.php @@ -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}"); } diff --git a/src/SPC/store/source/PhpSource.php b/src/SPC/store/source/PhpSource.php index 27e8bb89..02ba8755 100644 --- a/src/SPC/store/source/PhpSource.php +++ b/src/SPC/store/source/PhpSource.php @@ -6,11 +6,17 @@ 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 { @@ -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."); } } diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index dc602538..0b5d8a6d 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -27,9 +27,15 @@ class Artifact /** @var null|callable Bind custom source fetcher callback */ protected mixed $custom_source_callback = null; + /** @var null|callable Bind custom source check-update callback */ + protected mixed $custom_source_check_update_callback = null; + /** @var array Bind custom binary fetcher callbacks */ protected mixed $custom_binary_callbacks = []; + /** @var array Bind custom binary check-update callbacks */ + protected array $custom_binary_check_update_callbacks = []; + /** @var null|callable Bind custom source extract callback (completely takes over extraction) */ protected mixed $source_extract_callback = null; @@ -237,6 +243,39 @@ class Artifact return isset($this->config['binary'][$target]) || isset($this->custom_binary_callbacks[$target]); } + /** + * Get all platform strings for which a binary is declared (config or custom callback). + * + * For platforms where the binary type is "custom", a registered custom_binary_callback + * is required to consider it truly installable. + * + * @return string[] e.g. ['linux-x86_64', 'linux-aarch64', 'macos-aarch64'] + */ + public function getBinaryPlatforms(): array + { + $platforms = []; + if (isset($this->config['binary']) && is_array($this->config['binary'])) { + foreach ($this->config['binary'] as $platform => $platformConfig) { + $type = is_array($platformConfig) ? ($platformConfig['type'] ?? '') : ''; + if ($type === 'custom') { + // Only installable if a custom callback has been registered + if (isset($this->custom_binary_callbacks[$platform])) { + $platforms[] = $platform; + } + } else { + $platforms[] = $platform; + } + } + } + // Include custom callbacks for platforms not listed in config at all + foreach (array_keys($this->custom_binary_callbacks) as $platform) { + if (!in_array($platform, $platforms, true)) { + $platforms[] = $platform; + } + } + return $platforms; + } + public function getDownloadConfig(string $type): mixed { return $this->config[$type] ?? null; @@ -253,8 +292,11 @@ class Artifact */ public function getSourceDir(): string { - // defined in config - $extract = $this->config['source']['extract'] ?? null; + // Prefer cache extract path, fall back to config + $cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name); + $extract = is_string($cache_info['extract'] ?? null) + ? $cache_info['extract'] + : ($this->config['source']['extract'] ?? null); if ($extract === null) { return FileSystem::convertPath(SOURCE_PATH . '/' . $this->name); @@ -372,6 +414,19 @@ class Artifact return $this->custom_source_callback ?? null; } + /** + * Set custom source check-update callback. + */ + public function setCustomSourceCheckUpdateCallback(callable $callback): void + { + $this->custom_source_check_update_callback = $callback; + } + + public function getCustomSourceCheckUpdateCallback(): ?callable + { + return $this->custom_source_check_update_callback ?? null; + } + public function getCustomBinaryCallback(): ?callable { $current_platform = SystemTarget::getCurrentPlatformString(); @@ -400,6 +455,24 @@ class Artifact $this->custom_binary_callbacks[$target_os] = $callback; } + /** + * Set custom binary check-update callback for a specific target OS. + * + * @param string $target_os Target OS platform string (e.g. linux-x86_64) + * @param callable $callback Custom binary check-update callback + */ + public function setCustomBinaryCheckUpdateCallback(string $target_os, callable $callback): void + { + ConfigValidator::validatePlatformString($target_os); + $this->custom_binary_check_update_callbacks[$target_os] = $callback; + } + + public function getCustomBinaryCheckUpdateCallback(): ?callable + { + $current_platform = SystemTarget::getCurrentPlatformString(); + return $this->custom_binary_check_update_callbacks[$current_platform] ?? null; + } + // ==================== Extraction Callbacks ==================== /** diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index 3302a37b..5a2c8bac 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -18,7 +18,9 @@ class ArtifactCache * filename?: string, * dirname?: string, * extract: null|'&custom'|string, - * hash: null|string + * hash: null|string, + * time: int, + * downloader: null|string * }, * binary: array{ * windows-x86_64?: null|array{ @@ -28,7 +30,9 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, - * version?: null|string + * time: int, + * version?: null|string, + * downloader: null|string * } * } * }> @@ -106,8 +110,10 @@ class ArtifactCache 'filename' => $download_result->filename, 'extract' => $download_result->extract, 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'file') { $obj = [ @@ -116,8 +122,10 @@ class ArtifactCache 'filename' => $download_result->filename, 'extract' => $download_result->extract, 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'git') { $obj = [ @@ -126,8 +134,10 @@ class ArtifactCache 'dirname' => $download_result->dirname, 'extract' => $download_result->extract, 'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'local') { $obj = [ @@ -136,8 +146,10 @@ class ArtifactCache 'dirname' => $download_result->dirname, 'extract' => $download_result->extract, 'hash' => null, + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } if ($obj === null) { @@ -157,7 +169,7 @@ class ArtifactCache 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)); + file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } /** @@ -191,6 +203,17 @@ class ArtifactCache return $this->cache[$artifact_name]['binary'][$platform] ?? null; } + /** + * Get all binary cache entries for an artifact, keyed by platform string. + * + * @param string $artifact_name Artifact name + * @return array Map of platform → cache info (may be empty) + */ + public function getAllBinaryInfo(string $artifact_name): array + { + return $this->cache[$artifact_name]['binary'] ?? []; + } + /** * Get the full path to the cached file/directory. * @@ -270,12 +293,22 @@ class ArtifactCache logger()->debug("Removed binary cache entry for [{$artifact_name}] on platform [{$platform}]"); } + /** + * Get the names of all artifacts that have at least one downloaded entry (source or binary). + * + * @return array Artifact names + */ + public function getCachedArtifactNames(): array + { + return array_keys($this->cache); + } + /** * Save cache to file. */ public function save(): void { - file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index fd3caeaf..6cc57439 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -6,9 +6,19 @@ namespace StaticPHP\Artifact; use Psr\Log\LogLevel; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\BitBucketTag; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateInterface; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; 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\PECL; +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; @@ -29,6 +39,21 @@ use ZM\Logger\ConsoleColor; */ class ArtifactDownloader { + 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, + 'pecl' => PECL::class, + 'url' => Url::class, + 'php-release' => PhpRelease::class, + 'hosted' => HostedPackageBin::class, + ]; + /** @var array> */ protected array $downloaders = []; @@ -81,7 +106,7 @@ class ArtifactDownloader * no-shallow-clone?: bool * } $options Downloader options */ - public function __construct(protected array $options = []) + public function __construct(protected array $options = [], public readonly bool $interactive = true) { // Allow setting concurrency via options $this->parallel = max(1, (int) ($options['parallel'] ?? 1)); @@ -196,7 +221,7 @@ class ArtifactDownloader $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; // load downloaders - $this->downloaders = require ROOT_DIR . '/config/downloader.php'; + $this->downloaders = self::DOWNLOADERS; } /** @@ -248,12 +273,10 @@ class ArtifactDownloader /** * Download all artifacts, with optional parallel processing. - * - * @param bool $interactive Enable interactive mode with Ctrl+C handling */ - public function download(bool $interactive = true): void + public function download(): void { - if ($interactive) { + if ($this->interactive) { Shell::passthruCallback(function () { InteractiveTerm::advance(); }); @@ -286,7 +309,7 @@ class ArtifactDownloader $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) { + if ($this->interactive) { InteractiveTerm::notice("Downloading {$count} artifacts: {$artifacts_str} ..."); } try { @@ -304,25 +327,100 @@ class ArtifactDownloader $skipped = []; foreach ($this->artifacts as $artifact) { ++$current; - if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) { + if ($this->downloadWithType($artifact, $current, $count) === SPC_DOWNLOAD_STATUS_SKIPPED) { $skipped[] = $artifact->getName(); continue; } $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; } - if ($interactive) { + if ($this->interactive) { $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}\n", true); } } } finally { - if ($interactive) { + if ($this->interactive) { Shell::passthruCallback(null); keyboard_interrupt_unregister(); } } } + public function checkUpdate(string $artifact_name, bool $prefer_source = false, bool $bare = false): CheckUpdateResult + { + $artifact = ArtifactLoader::getArtifactInstance($artifact_name); + if ($artifact === null) { + throw new WrongUsageException("Artifact '{$artifact_name}' not found, please check the name."); + } + if ($bare) { + [$first, $second] = $prefer_source + ? [fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name), fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name)] + : [fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name), fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name)]; + $result = $first() ?? $second(); + if ($result !== null) { + return $result; + } + // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping."); + return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true); + } + $cache = ApplicationContext::get(ArtifactCache::class); + if ($prefer_source) { + $info = $cache->getSourceInfo($artifact_name) ?? $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString()); + } else { + $info = $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString()) ?? $cache->getSourceInfo($artifact_name); + } + if ($info === null) { + throw new WrongUsageException("Artifact '{$artifact_name}' is not downloaded yet, cannot check update."); + } + if (is_a($info['downloader'] ?? null, CheckUpdateInterface::class, true)) { + $cls = $info['downloader']; + /** @var CheckUpdateInterface $downloader */ + $downloader = new $cls(); + return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); + } + + if (($info['lock_type'] ?? null) === 'source' && ($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => $info['version'], + ]); + } + + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => $info['version'], + ]); + } + // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping."); + return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true); + } + + /** + * Check updates for multiple artifacts, with optional parallel processing. + * + * @param array $artifact_names Artifact names to check + * @param bool $prefer_source Whether to prefer source over binary + * @param bool $bare Check without requiring artifact to be downloaded first + * @param null|callable $onResult Called immediately with (string $name, CheckUpdateResult) as each result arrives + * @return array Results keyed by artifact name + */ + public function checkUpdates(array $artifact_names, bool $prefer_source = false, bool $bare = false, ?callable $onResult = null): array + { + if ($this->parallel > 1 && count($artifact_names) > 1) { + return $this->checkUpdatesWithConcurrency($artifact_names, $prefer_source, $bare, $onResult); + } + $results = []; + foreach ($artifact_names as $name) { + $result = $this->checkUpdate($name, $prefer_source, $bare); + $results[$name] = $result; + if ($onResult !== null) { + ($onResult)($name, $result); + } + } + return $results; + } + public function getRetry(): int { return $this->retry; @@ -338,7 +436,106 @@ class ArtifactDownloader return $this->options[$name] ?? $default; } - private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int + private function checkUpdatesWithConcurrency(array $artifact_names, bool $prefer_source, bool $bare, ?callable $onResult): array + { + $results = []; + $fiber_pool = []; + $remaining = $artifact_names; + + Shell::passthruCallback(function () { + \Fiber::suspend(); + }); + + try { + while (!empty($remaining) || !empty($fiber_pool)) { + // fill pool + while (count($fiber_pool) < $this->parallel && !empty($remaining)) { + $name = array_shift($remaining); + $fiber = new \Fiber(function () use ($name, $prefer_source, $bare) { + return [$name, $this->checkUpdate($name, $prefer_source, $bare)]; + }); + $fiber->start(); + $fiber_pool[$name] = $fiber; + } + // check pool + foreach ($fiber_pool as $fiber_name => $fiber) { + if ($fiber->isTerminated()) { + // getReturn() re-throws if the fiber threw — propagates immediately + [$artifact_name, $result] = $fiber->getReturn(); + $results[$artifact_name] = $result; + if ($onResult !== null) { + ($onResult)($artifact_name, $result); + } + unset($fiber_pool[$fiber_name]); + } else { + $fiber->resume(); + } + } + } + } catch (\Throwable $e) { + // terminate all still-suspended fibers so their curl processes don't hang + foreach ($fiber_pool as $fiber) { + if (!$fiber->isTerminated()) { + try { + $fiber->throw($e); + } catch (\Throwable) { + // ignore — we only care about stopping them + } + } + } + throw $e; + } finally { + Shell::passthruCallback(null); + } + + return $results; + } + + private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + { + if (($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => null, + ]); + } + $config = $artifact->getDownloadConfig('source'); + if (!is_array($config)) { + return null; + } + $cls = $this->downloaders[$config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + return null; + } + /** @var CheckUpdateInterface $dl */ + $dl = new $cls(); + return $dl->checkUpdate($artifact_name, $config, null, $this); + } + + private function probeBinaryCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + { + // custom binary callback takes precedence over config-based binary + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => null, + ]); + } + $binary_config = $artifact->getDownloadConfig('binary'); + $platform_config = is_array($binary_config) ? ($binary_config[SystemTarget::getCurrentPlatformString()] ?? null) : null; + if (!is_array($platform_config)) { + return null; + } + $cls = $this->downloaders[$platform_config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + return null; + } + /** @var CheckUpdateInterface $dl */ + $dl = new $cls(); + return $dl->checkUpdate($artifact_name, $platform_config, null, $this); + } + + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int { $queue = $this->generateQueue($artifact); // already downloaded @@ -359,7 +556,7 @@ class ArtifactDownloader }; $try_h = $try ? 'Try downloading' : 'Downloading'; logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ..."); - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ..."); } // is valid download type @@ -398,13 +595,13 @@ class ArtifactDownloader } // process lock ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString()); - if ($parallel === false && $interactive) { + if ($parallel === false && $this->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) { + if ($parallel === false && $this->interactive) { InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); InteractiveTerm::error("Failed message: {$e->getMessage()}", true); } diff --git a/src/StaticPHP/Artifact/Downloader/DownloadResult.php b/src/StaticPHP/Artifact/Downloader/DownloadResult.php index 6fa40bed..2efe6945 100644 --- a/src/StaticPHP/Artifact/Downloader/DownloadResult.php +++ b/src/StaticPHP/Artifact/Downloader/DownloadResult.php @@ -17,6 +17,7 @@ class DownloadResult * @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.) + * @param null|string $downloader Class name of the downloader that performed this download */ private function __construct( public readonly string $cache_type, @@ -27,6 +28,7 @@ class DownloadResult public bool $verified = false, public readonly ?string $version = null, public readonly array $metadata = [], + public readonly ?string $downloader = null, ) { switch ($this->cache_type) { case 'archive': @@ -59,11 +61,12 @@ class DownloadResult mixed $extract = null, bool $verified = false, ?string $version = null, - array $metadata = [] + array $metadata = [], + ?string $downloader = null, ): DownloadResult { // judge if it is archive or just a pure file $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; - return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata); + return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); } public static function file( @@ -71,10 +74,11 @@ class DownloadResult array $config, bool $verified = false, ?string $version = null, - array $metadata = [] + array $metadata = [], + ?string $downloader = null, ): DownloadResult { $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; - return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata); + return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -85,9 +89,9 @@ class DownloadResult * @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 + public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = [], ?string $downloader = null): DownloadResult { - return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -98,9 +102,9 @@ class DownloadResult * @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 + public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = [], ?string $downloader = null): DownloadResult { - return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -136,7 +140,8 @@ class DownloadResult $this->extract, $this->verified, $version, - $this->metadata + $this->metadata, + $this->downloader, ); } @@ -154,7 +159,8 @@ class DownloadResult $this->extract, $this->verified, $this->version, - array_merge($this->metadata, [$key => $value]) + array_merge($this->metadata, [$key => $value]), + $this->downloader, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php index 30942fe1..2ecc48df 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php +++ b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php @@ -36,6 +36,6 @@ class BitBucketTag implements DownloadTypeInterface $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); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, downloader: static::class); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php new file mode 100644 index 00000000..1adcdfea --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php @@ -0,0 +1,20 @@ +fetchFileList($name, $config, $downloader); + if (isset($config['download-url'])) { + $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); + } else { + $url = $config['url'] . $filename; + } + $filename = end($versions); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [, $version] = $this->fetchFileList($name, $config, $downloader); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || $version !== $old_version, + ); + } + + protected function fetchFileList(string $name, array $config, ArtifactDownloader $downloader): array { logger()->debug("Fetching file list from {$config['url']}"); $page = default_shell()->executeCurl($config['url'], retries: $downloader->getRetry()); @@ -33,15 +58,6 @@ class FileList implements DownloadTypeInterface uksort($versions, 'version_compare'); $filename = end($versions); $version = array_key_last($versions); - if (isset($config['download-url'])) { - $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); - } else { - $url = $config['url'] . $filename; - } - $filename = end($versions); - $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; - logger()->debug("Downloading {$name} from URL: {$url}"); - default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + return [$filename, $version, $versions]; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 83c236eb..d5822e69 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -10,7 +10,7 @@ use StaticPHP\Exception\DownloaderException; use StaticPHP\Util\FileSystem; /** git */ -class Git implements DownloadTypeInterface +class Git implements DownloadTypeInterface, CheckUpdateInterface { public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { @@ -21,8 +21,11 @@ class Git implements DownloadTypeInterface // direct branch clone if (isset($config['rev'])) { default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); - $version = "dev-{$config['rev']}"; - return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse --short HEAD'); + $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; + $version = $hash !== '' ? "dev-{$config['rev']}+{$hash}" : "dev-{$config['rev']}"; + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } if (!isset($config['regex'])) { throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); @@ -64,8 +67,62 @@ class Git implements DownloadTypeInterface $branch = $matched_version_branch[$version]; logger()->info("Matched version {$version} from branch {$branch} for {$name}"); default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null); - return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches)."); } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + if (isset($config['rev'])) { + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->execWithResult(SPC_GIT_EXEC . ' ls-remote ' . escapeshellarg($config['url']) . ' ' . escapeshellarg('refs/heads/' . $config['rev'])); + if ($result[0] !== 0 || empty($result[1])) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $new_hash = substr($result[1][0], 0, 7); + $new_version = "dev-{$config['rev']}+{$new_hash}"; + // Extract stored hash from "dev-{rev}+{hash}", null if bare mode or old format without hash + $old_hash = ($old_version !== null && str_contains($old_version, '+')) ? substr(strrchr($old_version, '+'), 1) : null; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_hash === null || $new_hash !== $old_hash, + ); + } + if (!isset($config['regex'])) { + throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); + } + + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->execWithResult(SPC_GIT_EXEC . ' ls-remote ' . escapeshellarg($config['url'])); + if ($result[0] !== 0) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $refs = $result[1]; + $matched_version_branch = []; + + $regex = '/^' . $config['regex'] . '$/'; + foreach ($refs as $ref) { + $matches = null; + if (preg_match('/^[0-9a-f]{40}\s+refs\/heads\/(.+)$/', $ref, $matches)) { + $branch = $matches[1]; + if (preg_match($regex, $branch, $vermatch) && isset($vermatch['version'])) { + $matched_version_branch[$vermatch['version']] = $vermatch[0]; + } + } + } + uksort($matched_version_branch, function ($a, $b) { + return version_compare($b, $a); + }); + if (!empty($matched_version_branch)) { + $version = array_key_first($matched_version_branch); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || $version !== $old_version, + ); + } + throw new DownloaderException("No matching branch found for regex {$config['regex']}."); + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 7b041288..15626089 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -9,7 +9,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; /** ghrel */ -class GitHubRelease implements DownloadTypeInterface, ValidatorInterface +class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -48,6 +48,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface */ public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null): array { + logger()->debug("Fetching {$name} GitHub release from {$repo}"); $url = str_replace('{repo}', $repo, self::API_URL); $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); @@ -95,7 +96,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; logger()->debug("Downloading {$name} asset from URL: {$asset_url}"); default_shell()->executeCurlDownload($asset_url, $path, headers: $headers, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version, downloader: static::class); } public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool @@ -117,4 +118,18 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface logger()->debug("No sha256 digest found for GitHub release asset of {$name}, skipping hash validation"); return true; } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + if (!isset($config['match'])) { + throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); + } + $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); + $new_version = $this->version ?? $old_version ?? ''; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index 8aa1ac69..e473c0ca 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -10,7 +10,7 @@ use StaticPHP\Exception\DownloaderException; /** ghtar */ /** ghtagtar */ -class GitHubTarball implements DownloadTypeInterface +class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -42,12 +42,12 @@ class GitHubTarball implements DownloadTypeInterface } if ($match_url === null) { $url = $rel['tarball_url'] ?? null; - $version = $rel['tag_name'] ?? null; + $version = $rel['tag_name'] ?? $rel['name'] ?? null; break; } if (preg_match("|{$match_url}|", $rel['tarball_url'] ?? '')) { $url = $rel['tarball_url']; - $version = $rel['tag_name'] ?? null; + $version = $rel['tag_name'] ?? $rel['name'] ?? null; break; } } @@ -61,7 +61,7 @@ class GitHubTarball implements DownloadTypeInterface $filename = $matches['filename']; } else { $basename = $basename ?? basename($repo); - $filename = "{$basename}-" . ($rel_type === 'releases' ? $data['tag_name'] : $data['name']) . '.tar.gz'; + $filename = "{$basename}-" . ($rel_type === 'releases' ? ($data['tag_name'] ?? $data['name']) : $data['name']) . '.tar.gz'; } return [$url, $filename]; } @@ -77,6 +77,22 @@ class GitHubTarball implements DownloadTypeInterface [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $rel_type = match ($config['type']) { + 'ghtar' => 'releases', + 'ghtagtar' => 'tags', + default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), + }; + $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); + $new_version = $this->version ?? $old_version ?? ''; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php index 90c42507..34e350d4 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php @@ -16,10 +16,12 @@ trait GitHubTokenSetupTrait // GITHUB_TOKEN support if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) { logger()->debug("Using 'GITHUB_TOKEN' with user {$user} for authentication"); + spc_add_log_filter([$user, $token]); return ['Authorization: Basic ' . base64_encode("{$user}:{$token}")]; } if (($token = getenv('GITHUB_TOKEN')) !== false) { logger()->debug("Using 'GITHUB_TOKEN' for authentication"); + spc_add_log_filter($token); return ["Authorization: Bearer {$token}"]; } return []; diff --git a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php index c5cbb3b5..11caa19d 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php +++ b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php @@ -26,7 +26,7 @@ class HostedPackageBin implements DownloadTypeInterface public static function getReleaseInfo(): array { if (empty(self::$release_info)) { - $rel = (new GitHubRelease())->getGitHubReleases('hosted', self::BASE_REPO); + $rel = new GitHubRelease()->getGitHubReleases('hosted', self::BASE_REPO); if (empty($rel)) { throw new DownloaderException('No releases found for hosted package-bin'); } @@ -55,7 +55,7 @@ class HostedPackageBin implements DownloadTypeInterface $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; $headers = $this->getGitHubTokenHeaders(); default_shell()->executeCurlDownload($download_url, $path, headers: $headers, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } } throw new DownloaderException("No matching asset found for hosted package-bin {$name}: {$find_str}"); diff --git a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php index 93315ce3..77ac3d09 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php +++ b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php @@ -13,6 +13,6 @@ class LocalDir implements DownloadTypeInterface public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { logger()->debug("Using local source directory for {$name} from {$config['dirname']}"); - return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null); + return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null, downloader: static::class); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php new file mode 100644 index 00000000..df2da341 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php @@ -0,0 +1,79 @@ +VERSIONSTATE per release */ + private const string PECL_REST_URL = 'https://pecl.php.net/rest/r/%s/allreleases.xml'; + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [, $version] = $this->fetchPECLInfo($name, $config, $downloader); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || $version !== $old_version, + ); + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + [$filename, $version] = $this->fetchPECLInfo($name, $config, $downloader); + $url = self::PECL_BASE_URL . '/get/' . $filename; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + $extract = $config['extract'] ?? ('php-src/ext/' . $this->getExtractName($name)); + return DownloadResult::archive($filename, $config, $extract, version: $version, downloader: static::class); + } + + protected function fetchPECLInfo(string $name, array $config, ArtifactDownloader $downloader): array + { + $peclName = strtolower($config['name'] ?? $this->getExtractName($name)); + $url = sprintf(self::PECL_REST_URL, $peclName); + logger()->debug("Fetching PECL release list for {$name} from REST API"); + $xml = default_shell()->executeCurl($url, retries: $downloader->getRetry()); + if ($xml === false) { + throw new DownloaderException("Failed to fetch PECL release list for {$name}"); + } + // Match VERSIONSTATE + preg_match_all('/(?P[^<]+)<\/v>(?P[^<]+)<\/s><\/r>/', $xml, $matches); + if (empty($matches['version'])) { + throw new DownloaderException("Failed to parse PECL release list for {$name}"); + } + $versions = []; + logger()->debug('Matched ' . count($matches['version']) . " releases for {$name} from PECL"); + foreach ($matches['version'] as $i => $version) { + if ($matches['state'][$i] !== 'stable' && ($config['prefer-stable'] ?? true) === true) { + continue; + } + $versions[$version] = $peclName . '-' . $version . '.tgz'; + } + if (empty($versions)) { + throw new DownloaderException("No stable releases found for {$name} on PECL"); + } + uksort($versions, 'version_compare'); + $filename = end($versions); + $version = array_key_last($versions); + return [$filename, $version, $versions]; + } + + /** + * Derive the lowercase PECL package / extract name from the artifact name. + * e.g. "ext-apcu" -> "apcu", "ext-ast" -> "ast" + */ + private function getExtractName(string $name): string + { + return strtolower(preg_replace('/^ext-/i', '', $name)); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index e4f1a117..14996c5a 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -9,28 +9,13 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; /** pie */ -class PIE implements DownloadTypeInterface +class PIE implements DownloadTypeInterface, CheckUpdateInterface { public const string PACKAGIST_URL = 'https://repo.packagist.org/p2/'; public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { - $packagist_url = self::PACKAGIST_URL . "{$config['repo']}.json"; - logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); - $data = default_shell()->executeCurl($packagist_url, retries: $downloader->getRetry()); - if ($data === false) { - throw new DownloaderException("Failed to fetch packagist index for {$name} from {$packagist_url}"); - } - $data = json_decode($data, true); - if (!isset($data['packages'][$config['repo']]) || !is_array($data['packages'][$config['repo']])) { - throw new DownloaderException("failed to find {$name} repo info from packagist"); - } - // get the first version - $first = $data['packages'][$config['repo']][0] ?? []; - // check 'type' => 'php-ext' or contains 'php-ext' key - if (!isset($first['php-ext'])) { - throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); - } + $first = $this->fetchPackagistInfo($name, $config, $downloader); // get download link from dist $dist_url = $first['dist']['url'] ?? null; $dist_type = $first['dist']['type'] ?? null; @@ -42,6 +27,39 @@ class PIE implements DownloadTypeInterface $filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz'); $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $first = $this->fetchPackagistInfo($name, $config, $downloader); + $new_version = $first['version'] ?? null; + if ($new_version === null) { + throw new DownloaderException("failed to find version info for {$name} from packagist"); + } + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } + + protected function fetchPackagistInfo(string $name, array $config, ArtifactDownloader $downloader): array + { + $packagist_url = self::PACKAGIST_URL . "{$config['repo']}.json"; + logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); + $data = default_shell()->executeCurl($packagist_url, retries: $downloader->getRetry()); + if ($data === false) { + throw new DownloaderException("Failed to fetch packagist index for {$name} from {$packagist_url}"); + } + $data = json_decode($data, true); + if (!isset($data['packages'][$config['repo']]) || !is_array($data['packages'][$config['repo']])) { + throw new DownloaderException("failed to find {$name} repo info from packagist"); + } + $first = $data['packages'][$config['repo']][0] ?? []; + if (!isset($first['php-ext'])) { + throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); + } + return $first; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index ec6c33fa..b1fad70e 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -8,11 +8,17 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; -class PhpRelease implements DownloadTypeInterface, ValidatorInterface +class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { - public const string PHP_API = 'https://www.php.net/releases/index.php?json&version={version}'; + public const string DEFAULT_PHP_DOMAIN = 'https://www.php.net'; - public const string DOWNLOAD_URL = 'https://www.php.net/distributions/php-{version}.tar.xz'; + public const string API_URL = '/releases/index.php?json&version={version}'; + + public const string DOWNLOAD_URL = '/distributions/php-{version}.tar.xz'; + + public const string GIT_URL = 'https://github.com/php/php-src.git'; + + public const string GIT_REV = 'master'; private ?string $sha256 = ''; @@ -22,18 +28,9 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface // Handle 'git' version to clone from php-src repository if ($phpver === 'git') { $this->sha256 = null; - return (new Git())->download($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $downloader); - } - - // Fetch PHP release info first - $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); - if ($info === false) { - throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); - } - $info = json_decode($info, true); - if (!is_array($info) || !isset($info['version'])) { - throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); + return (new Git())->download($name, ['url' => self::GIT_URL, 'rev' => self::GIT_REV], $downloader); } + $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); $version = $info['version']; foreach ($info['source'] as $source) { if (str_ends_with($source['filename'], '.tar.xz')) { @@ -45,11 +42,12 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface if (!isset($filename)) { throw new DownloaderException("No suitable source tarball found for PHP version {$version}"); } - $url = str_replace('{version}', $version, self::DOWNLOAD_URL); + $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; + $url .= str_replace('{version}', $version, self::DOWNLOAD_URL); logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool @@ -73,4 +71,46 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface logger()->debug("SHA256 checksum validated successfully for {$name}."); return true; } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $phpver = $downloader->getOption('with-php', '8.4'); + if ($phpver === 'git') { + // git version: delegate to Git checkUpdate with master branch + return (new Git())->checkUpdate($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $old_version, $downloader); + } + $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); + $new_version = $info['version']; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } + + protected function fetchPhpReleaseInfo(string $name, array $config, ArtifactDownloader $downloader): array + { + $phpver = $downloader->getOption('with-php', '8.4'); + // Handle 'git' version to clone from php-src repository + if ($phpver === 'git') { + // cannot fetch release info for git version, return empty info to skip validation + throw new DownloaderException("Cannot fetch PHP release info for 'git' version."); + } + + $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; + $url .= self::API_URL; + $url = str_replace('{version}', $phpver, $url); + logger()->debug("Fetching PHP release info for version {$phpver} from {$url}"); + + // Fetch PHP release info first + $info = default_shell()->executeCurl($url, retries: $downloader->getRetry()); + if ($info === false) { + throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); + } + $info = json_decode($info, true); + if (!is_array($info) || !isset($info['version'])) { + throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); + } + return $info; + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Url.php b/src/StaticPHP/Artifact/Downloader/Type/Url.php index a56f4dc7..02425fe5 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Url.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Url.php @@ -18,6 +18,6 @@ class Url implements DownloadTypeInterface logger()->debug("Downloading {$name} from URL: {$url}"); $version = $config['version'] ?? null; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } } diff --git a/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php b/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php new file mode 100644 index 00000000..aa59af1a --- /dev/null +++ b/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php @@ -0,0 +1,11 @@ + 'debug', default => 'warning', }; + $isDebug = false; + // if '--debug' is set, override log level to debug + if ($this->input->getOption('debug')) { + $level = 'debug'; + logger()->warning('The --debug option is deprecated and will be removed in future versions. Please use -vv or -vvv to enable debug mode.'); + $this->output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + $isDebug = true; + } logger()->setLevel($level); // ansi @@ -95,7 +104,7 @@ abstract class BaseCommand extends Command } // Set debug mode in ApplicationContext - $isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; + $isDebug = $isDebug ?: $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; ApplicationContext::setDebug($isDebug); // show raw argv list for logger()->debug @@ -110,6 +119,21 @@ abstract class BaseCommand extends Command } } + /** + * Warn the user if doctor has not been run (or is outdated). + * Set SPC_SKIP_DOCTOR_CHECK=1 to suppress. + */ + protected function checkDoctorCache(): void + { + if (getenv('SPC_SKIP_DOCTOR_CHECK') || Doctor::isHealthy()) { + return; + } + $this->output->writeln(''); + $this->output->writeln('[WARNING] Please run `spc doctor` first to verify your build environment.'); + $this->output->writeln(''); + sleep(2); + } + protected function getOption(string $name): mixed { return $this->input->getOption($name); diff --git a/src/StaticPHP/Command/BuildLibsCommand.php b/src/StaticPHP/Command/BuildLibsCommand.php index 63a3ad0f..c18acb0f 100644 --- a/src/StaticPHP/Command/BuildLibsCommand.php +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -44,6 +44,8 @@ class BuildLibsCommand extends BaseCommand public function handle(): int { + $this->checkDoctorCache(); + $libs = parse_comma_list($this->input->getArgument('libraries')); $installer = new PackageInstaller($this->input->getOptions()); diff --git a/src/StaticPHP/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php index 2756070b..8e1ed632 100644 --- a/src/StaticPHP/Command/BuildTargetCommand.php +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -37,6 +37,8 @@ class BuildTargetCommand extends BaseCommand public function handle(): int { + $this->checkDoctorCache(); + // resolve legacy options to new options V2CompatLayer::convertOptions($this->input); diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php new file mode 100644 index 00000000..1663337c --- /dev/null +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -0,0 +1,79 @@ +addArgument('artifact', InputArgument::OPTIONAL, 'The name of the artifact(s) to check for updates, comma-separated (default: all downloaded artifacts)'); + $this->addOption('json', null, null, 'Output result in JSON format'); + $this->addOption('bare', null, null, 'Check update without requiring the artifact to be downloaded first (old version will be null)'); + $this->addOption('parallel', 'p', InputOption::VALUE_REQUIRED, 'Number of parallel update checks (default: 10)', 10); + + // --with-php option for checking updates with a specific PHP version context + $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format (default 8.4)', '8.4'); + } + + public function handle(): int + { + $artifact_arg = $this->input->getArgument('artifact'); + if ($artifact_arg === null) { + $artifacts = ApplicationContext::get(ArtifactCache::class)->getCachedArtifactNames(); + if (empty($artifacts)) { + $this->output->writeln('No downloaded artifacts found.'); + return static::OK; + } + } else { + $artifacts = parse_comma_list($artifact_arg); + } + + try { + $downloader = new ArtifactDownloader($this->input->getOptions()); + $bare = (bool) $this->getOption('bare'); + if ($this->getOption('json')) { + $results = $downloader->checkUpdates($artifacts, bare: $bare); + $outputs = []; + foreach ($results as $artifact => $result) { + $outputs[$artifact] = [ + 'need-update' => $result->needUpdate, + 'unsupported' => $result->unsupported, + 'old' => $result->old, + 'new' => $result->new, + ]; + } + $this->output->writeln(json_encode($outputs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return static::OK; + } + $downloader->checkUpdates($artifacts, bare: $bare, onResult: function (string $artifact, CheckUpdateResult $result) { + if ($result->unsupported) { + $this->output->writeln("Artifact {$artifact} does not support update checking, skipped"); + } elseif (!$result->needUpdate) { + $ver = $result->new ? "({$result->new})" : ''; + $this->output->writeln("Artifact {$artifact} is already up to date {$ver}"); + } else { + [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; + $this->output->writeln("Update available for {$artifact}: {$old} -> {$new}"); + } + }); + return static::OK; + } catch (SPCException $e) { + $e->setSimpleOutput(); + throw $e; + } + } +} diff --git a/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php b/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php new file mode 100644 index 00000000..e2f3dba9 --- /dev/null +++ b/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php @@ -0,0 +1,111 @@ +addArgument('output', InputArgument::OPTIONAL, 'Output file path (JSON). Defaults to /dump-capabilities.json', ROOT_DIR . '/dump-capabilities.json'); + $this->addOption('print', null, InputOption::VALUE_NONE, 'Print capabilities as a table to the terminal instead of writing to a file'); + } + + public function handle(): int + { + $result = $this->buildCapabilities(); + + if ($this->getOption('print')) { + $this->printTable($result); + } else { + $outputFile = $this->getArgument('output'); + $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + file_put_contents($outputFile, $json . PHP_EOL); + $this->output->writeln('Dumped capabilities for ' . count($result) . " package(s) to: {$outputFile}"); + } + + return static::SUCCESS; + } + + /** + * Build the capabilities map for all relevant packages. + * + * For library/target/virtual-target: + * buildable: string[] - OS families with a registered #[BuildFor] function + * installable: string[] - arch-os platforms with a declared binary + * + * For php-extension: + * buildable: array - {OS: 'yes'|'wip'|'partial'|'no'} (v2 support semantics) + * installable: (not applicable, omitted) + */ + private function buildCapabilities(): array + { + $result = []; + + // library / target / virtual-target + foreach (PackageLoader::getPackages(['library', 'target', 'virtual-target']) as $name => $pkg) { + $installable = []; + $artifact = $pkg->getArtifact(); + if ($artifact !== null) { + $installable = $artifact->getBinaryPlatforms(); + } + + $result[$name] = [ + 'type' => $pkg->getType(), + 'buildable' => $pkg->getBuildForOSList(), + 'installable' => $installable, + ]; + } + + // php-extension: buildable uses v2 support-field semantics + foreach (PackageLoader::getPackages('php-extension') as $name => $pkg) { + /* @var PhpExtensionPackage $pkg */ + $result[$name] = [ + 'type' => $pkg->getType(), + 'buildable' => $pkg->getBuildSupportStatus(), + ]; + } + + return $result; + } + + private function printTable(array $result): void + { + $table = new Table($this->output); + $table->setHeaders(['Package', 'Type', 'Buildable (OS)', 'Installable (arch-os)']); + + foreach ($result as $name => $info) { + // For php-extension, buildable is a map {OS => status} + if (is_array($info['buildable']) && array_is_list($info['buildable']) === false) { + $buildableStr = implode("\n", array_map( + static fn (string $os, string $status) => $status === 'yes' ? $os : "{$os} ({$status})", + array_keys($info['buildable']), + array_values($info['buildable']) + )); + } else { + $buildableStr = implode("\n", $info['buildable']) ?: ''; + } + + $table->addRow([ + $name, + $info['type'], + $buildableStr, + implode("\n", $info['installable'] ?? []) ?: '', + ]); + } + + $table->render(); + } +} diff --git a/src/StaticPHP/Command/Dev/DumpStagesCommand.php b/src/StaticPHP/Command/Dev/DumpStagesCommand.php new file mode 100644 index 00000000..c757ab86 --- /dev/null +++ b/src/StaticPHP/Command/Dev/DumpStagesCommand.php @@ -0,0 +1,158 @@ +addArgument('packages', InputArgument::OPTIONAL, 'Comma-separated list of packages to dump, e.g. "openssl,zlib,curl". Dumps all packages if omitted.'); + $this->addArgument('output', InputArgument::OPTIONAL, 'Output file path', ROOT_DIR . '/dump-stages.json'); + $this->addOption('relative', 'r', InputOption::VALUE_NONE, 'Output file paths relative to ROOT_DIR'); + } + + public function handle(): int + { + $outputFile = $this->getArgument('output'); + $useRelative = (bool) $this->getOption('relative'); + + $filterPackages = null; + if ($packagesArg = $this->getArgument('packages')) { + $filterPackages = array_flip(parse_comma_list($packagesArg)); + } + + $result = []; + + foreach (PackageLoader::getPackages() as $name => $pkg) { + if ($filterPackages !== null && !isset($filterPackages[$name])) { + continue; + } + $entry = [ + 'type' => $pkg->getType(), + 'stages' => [], + 'before_stages' => [], + 'after_stages' => [], + ]; + + // Resolve main stages + foreach ($pkg->getStages() as $stageName => $callable) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry['stages'][$stageName] = $location; + } + + $result[$name] = $entry; + } + + // Resolve before/after stage external callbacks + foreach (PackageLoader::getAllBeforeStages() as $pkgName => $stages) { + if ($filterPackages !== null && !isset($filterPackages[$pkgName])) { + continue; + } + foreach ($stages as $stageName => $callbacks) { + foreach ($callbacks as [$callable, $onlyWhen]) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry_data = $location ?? []; + if ($onlyWhen !== null) { + $entry_data['only_when_package_resolved'] = $onlyWhen; + } + $result[$pkgName]['before_stages'][$stageName][] = $entry_data; + } + } + } + + foreach (PackageLoader::getAllAfterStages() as $pkgName => $stages) { + if ($filterPackages !== null && !isset($filterPackages[$pkgName])) { + continue; + } + foreach ($stages as $stageName => $callbacks) { + foreach ($callbacks as [$callable, $onlyWhen]) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry_data = $location ?? []; + if ($onlyWhen !== null) { + $entry_data['only_when_package_resolved'] = $onlyWhen; + } + $result[$pkgName]['after_stages'][$stageName][] = $entry_data; + } + } + } + + $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + file_put_contents($outputFile, $json . PHP_EOL); + + $this->output->writeln('Dumped stages for ' . count($result) . " package(s) to: {$outputFile}"); + return static::SUCCESS; + } + + /** + * Resolve the file, start line, class and method name of a callable using reflection. + * + * @return null|array{file: string, line: false|int, class: string, method: string} + */ + private function resolveCallableLocation(mixed $callable): ?array + { + try { + if (is_array($callable) && count($callable) === 2) { + $ref = new \ReflectionMethod($callable[0], $callable[1]); + return [ + 'class' => $ref->getDeclaringClass()->getName(), + 'method' => $ref->getName(), + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + if ($callable instanceof \Closure) { + $ref = new \ReflectionFunction($callable); + $scopeClass = $ref->getClosureScopeClass(); + return [ + 'class' => $scopeClass !== null ? $scopeClass->getName() : '{closure}', + 'method' => '{closure}', + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + if (is_string($callable) && str_contains($callable, '::')) { + [$class, $method] = explode('::', $callable, 2); + $ref = new \ReflectionMethod($class, $method); + return [ + 'class' => $ref->getDeclaringClass()->getName(), + 'method' => $ref->getName(), + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + } catch (\ReflectionException) { + // ignore + } + return null; + } + + private function toRelativePath(string $absolutePath): string + { + $normalized = realpath($absolutePath) ?: $absolutePath; + $root = rtrim(ROOT_DIR, '/') . '/'; + if (str_starts_with($normalized, $root)) { + return substr($normalized, strlen($root)); + } + return $normalized; + } +} diff --git a/src/StaticPHP/Command/Dev/LintConfigCommand.php b/src/StaticPHP/Command/Dev/LintConfigCommand.php index 1efba4d5..ad1efb51 100644 --- a/src/StaticPHP/Command/Dev/LintConfigCommand.php +++ b/src/StaticPHP/Command/Dev/LintConfigCommand.php @@ -13,6 +13,8 @@ use Symfony\Component\Yaml\Yaml; #[AsCommand('dev:lint-config', 'Lint configuration file format', ['dev:sort-config'])] class LintConfigCommand extends BaseCommand { + protected bool $no_motd = true; + public function handle(): int { $checkOnly = $this->input->getOption('check'); @@ -37,6 +39,9 @@ class LintConfigCommand extends BaseCommand return static::VALIDATION_ERROR; } + if (!$hasChanges) { + $this->output->writeln('No changes.'); + } return static::SUCCESS; } diff --git a/src/StaticPHP/Command/Dev/PackageInfoCommand.php b/src/StaticPHP/Command/Dev/PackageInfoCommand.php new file mode 100644 index 00000000..ba621c5e --- /dev/null +++ b/src/StaticPHP/Command/Dev/PackageInfoCommand.php @@ -0,0 +1,417 @@ +addArgument('package', InputArgument::REQUIRED, 'Package name to inspect'); + $this->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON instead of colored terminal display'); + } + + public function handle(): int + { + $packageName = $this->getArgument('package'); + + if (!PackageConfig::isPackageExists($packageName)) { + $this->output->writeln("Package '{$packageName}' not found."); + return static::USER_ERROR; + } + + $pkgConfig = PackageConfig::get($packageName); + // Resolve the actual artifact name: + // - string field → named reference (e.g. php → php-src) + // - array field → inline artifact, key is package name + // - null → no artifact, or may match by package name + $artifactField = $pkgConfig['artifact'] ?? null; + $artifactName = is_string($artifactField) ? $artifactField : $packageName; + $artifactConfig = ArtifactConfig::get($artifactName); + $pkgInfo = Registry::getPackageConfigInfo($packageName); + $artifactInfo = Registry::getArtifactConfigInfo($artifactName); + $annotationInfo = PackageLoader::getPackageAnnotationInfo($packageName); + $cacheInfo = $this->resolveCacheInfo($artifactName, $artifactConfig); + + if ($this->getOption('json')) { + return $this->outputJson($packageName, $artifactName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo, $annotationInfo, $cacheInfo); + } + + return $this->outputTerminal($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo, $annotationInfo, $cacheInfo); + } + + private function outputJson(string $name, string $artifactName, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo, ?array $annotationInfo, ?array $cacheInfo): int + { + $data = [ + 'name' => $name, + 'registry' => $pkgInfo['registry'] ?? null, + 'package_config_file' => $pkgInfo ? $this->toRelativePath($pkgInfo['config']) : null, + 'package' => $pkgConfig, + ]; + + if ($artifactConfig !== null) { + $data['artifact_name'] = $artifactName !== $name ? $artifactName : null; + $data['artifact_config_file'] = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : null; + $data['artifact'] = $this->splitArtifactConfig($artifactConfig); + } + + if ($annotationInfo !== null) { + $data['annotations'] = $annotationInfo; + } + + if ($cacheInfo !== null) { + $data['cache'] = $cacheInfo; + } + + $this->output->writeln(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return static::SUCCESS; + } + + private function outputTerminal(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo, ?array $annotationInfo, ?array $cacheInfo): int + { + $type = $pkgConfig['type'] ?? 'unknown'; + $registry = $pkgInfo['registry'] ?? 'unknown'; + $pkgFile = $pkgInfo ? $this->toRelativePath($pkgInfo['config']) : 'unknown'; + + // Header + $this->output->writeln(''); + $this->output->writeln("Package: {$name} Type: {$type} Registry: {$registry}"); + $this->output->writeln("Config file: {$pkgFile}"); + $this->output->writeln(''); + + // Package config fields (excluding type and artifact which are shown separately) + $pkgFields = array_diff_key($pkgConfig, array_flip(['type', 'artifact'])); + if (!empty($pkgFields)) { + $this->output->writeln('── Package Config ──'); + $this->printYamlBlock($pkgFields, 0); + $this->output->writeln(''); + } + + // Artifact config + if ($artifactConfig !== null) { + $artifactFile = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : 'unknown'; + $artifactField = $pkgConfig['artifact'] ?? null; + if (is_string($artifactField)) { + // Named reference: show the artifact name it points to + $this->output->writeln("── Artifact Config ── artifact: {$artifactField} file: {$artifactFile}"); + } elseif (is_array($artifactField)) { + $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); + $this->output->writeln(' (inline in package config)'); + } else { + $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); + } + + $split = $this->splitArtifactConfig($artifactConfig); + + foreach ($split as $section => $value) { + $this->output->writeln(''); + $this->output->writeln(" [{$section}]"); + $this->printYamlBlock($value, 4); + } + $this->output->writeln(''); + } else { + $this->output->writeln('── Artifact Config ── (none)'); + $this->output->writeln(''); + } + + // Annotation section + $this->outputAnnotationSection($name, $annotationInfo); + + // Cache status section + $this->outputCacheSection($cacheInfo); + + return static::SUCCESS; + } + + private function outputAnnotationSection(string $packageName, ?array $annotationInfo): void + { + if ($annotationInfo === null) { + $this->output->writeln('── Annotations ── (no annotation class registered)'); + $this->output->writeln(''); + return; + } + + $shortClass = $this->classBaseName($annotationInfo['class']); + $this->output->writeln("── Annotations ── class: {$shortClass}"); + $this->output->writeln(" {$annotationInfo['class']}"); + + // Method-level hooks + $methods = $annotationInfo['methods']; + if (!empty($methods)) { + $this->output->writeln(''); + $this->output->writeln(' Method hooks:'); + foreach ($methods as $methodName => $attrs) { + $attrList = implode(' ', array_map(fn ($a) => $this->formatAttr($a), $attrs)); + $this->output->writeln(" {$methodName}() {$attrList}"); + } + } + + // Before-stage hooks targeting this package (inbound) + $beforeStages = $annotationInfo['before_stages']; + if (!empty($beforeStages)) { + $this->output->writeln(''); + $this->output->writeln(' Before-stage hooks (inbound):'); + foreach ($beforeStages as $stage => $hooks) { + foreach ($hooks as $hook) { + $source = $this->classBaseName($hook['class']) . '::' . $hook['method'] . '()'; + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" {$stage} ← {$source}{$cond}"); + } + } + } + + // After-stage hooks targeting this package (inbound) + $afterStages = $annotationInfo['after_stages']; + if (!empty($afterStages)) { + $this->output->writeln(''); + $this->output->writeln(' After-stage hooks (inbound):'); + foreach ($afterStages as $stage => $hooks) { + foreach ($hooks as $hook) { + $source = $this->classBaseName($hook['class']) . '::' . $hook['method'] . '()'; + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" {$stage} ← {$source}{$cond}"); + } + } + } + + // Outbound hooks: stages this package's class registers on other packages (exclude self-hooks) + $outboundBefore = $annotationInfo['outbound_before_stages'] ?? []; + $outboundAfter = $annotationInfo['outbound_after_stages'] ?? []; + // Filter out entries targeting the same package — those are already shown inbound + $outboundBefore = array_filter($outboundBefore, fn ($pkg) => $pkg !== $packageName, ARRAY_FILTER_USE_KEY); + $outboundAfter = array_filter($outboundAfter, fn ($pkg) => $pkg !== $packageName, ARRAY_FILTER_USE_KEY); + if (!empty($outboundBefore) || !empty($outboundAfter)) { + $this->output->writeln(''); + $this->output->writeln(' Hooks on other packages (outbound):'); + foreach ($outboundBefore as $targetPkg => $stages) { + foreach ($stages as $stage => $hooks) { + foreach ($hooks as $hook) { + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" #[BeforeStage] → {$targetPkg} {$stage} {$hook['method']}(){$cond}"); + } + } + } + foreach ($outboundAfter as $targetPkg => $stages) { + foreach ($stages as $stage => $hooks) { + foreach ($hooks as $hook) { + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" #[AfterStage] → {$targetPkg} {$stage} {$hook['method']}(){$cond}"); + } + } + } + } + + $this->output->writeln(''); + } + + /** + * Format a single attribute entry (from annotation_map) as a colored inline string. + * + * @param array{attr: string, args: array} $attr + */ + private function formatAttr(array $attr): string + { + $name = $attr['attr']; + $args = $attr['args']; + if (empty($args)) { + return "#[{$name}]"; + } + $argStr = implode(', ', array_map( + fn ($v) => is_string($v) ? "'{$v}'" : (string) $v, + array_values($args) + )); + return "#[{$name}({$argStr})]"; + } + + /** Return the trailing class name component without the namespace. */ + private function classBaseName(string $fqcn): string + { + $parts = explode('\\', $fqcn); + return end($parts); + } + + /** + * Split artifact config into logical sections for cleaner display. + * + * @return array + */ + private function splitArtifactConfig(array $config): array + { + $sections = []; + $sectionOrder = ['source', 'source-mirror', 'binary', 'binary-mirror', 'metadata']; + foreach ($sectionOrder as $key) { + if (array_key_exists($key, $config)) { + $sections[$key] = $config[$key]; + } + } + // Any remaining unknown keys + foreach ($config as $k => $v) { + if (!array_key_exists($k, $sections)) { + $sections[$k] = $v; + } + } + return $sections; + } + + /** + * Print a value as indented YAML-style output with Symfony Console color tags. + */ + private function printYamlBlock(mixed $value, int $indent): void + { + $pad = str_repeat(' ', $indent); + if (!is_array($value)) { + $this->output->writeln($pad . $this->colorScalar($value)); + return; + } + $isList = array_is_list($value); + foreach ($value as $k => $v) { + if ($isList) { + if (is_array($v)) { + $this->output->writeln($pad . '- '); + $this->printYamlBlock($v, $indent + 2); + } else { + $this->output->writeln($pad . '- ' . $this->colorScalar($v)); + } + } else { + if (is_array($v)) { + $this->output->writeln($pad . "{$k}:"); + $this->printYamlBlock($v, $indent + 2); + } else { + $this->output->writeln($pad . "{$k}: " . $this->colorScalar($v)); + } + } + } + } + + private function colorScalar(mixed $v): string + { + if (is_bool($v)) { + return '' . ($v ? 'true' : 'false') . ''; + } + if (is_int($v) || is_float($v)) { + return '' . $v . ''; + } + if ($v === null) { + return 'null'; + } + // Strings that look like URLs + if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) { + return '' . $v . ''; + } + return '' . $v . ''; + } + + private function toRelativePath(string $absolutePath): string + { + $normalized = realpath($absolutePath) ?: $absolutePath; + $root = rtrim(ROOT_DIR, '/') . '/'; + if (str_starts_with($normalized, $root)) { + return substr($normalized, strlen($root)); + } + return $normalized; + } + + /** + * Build cache status data for display/JSON. + * Returns null when there is no artifact config for this package. + */ + private function resolveCacheInfo(string $name, ?array $artifactConfig): ?array + { + if ($artifactConfig === null) { + return null; + } + $cache = ApplicationContext::get(ArtifactCache::class); + $currentPlatform = SystemTarget::getCurrentPlatformString(); + $hasSource = array_key_exists('source', $artifactConfig) || array_key_exists('source-mirror', $artifactConfig); + $hasBinary = array_key_exists('binary', $artifactConfig) || array_key_exists('binary-mirror', $artifactConfig); + return [ + 'current_platform' => $currentPlatform, + 'has_source' => $hasSource, + 'has_binary' => $hasBinary, + 'source' => $hasSource ? [ + 'downloaded' => $cache->isSourceDownloaded($name), + 'info' => $cache->getSourceInfo($name), + ] : null, + 'binary' => $hasBinary ? $cache->getAllBinaryInfo($name) : null, + ]; + } + + private function outputCacheSection(?array $cacheInfo): void + { + if ($cacheInfo === null) { + $this->output->writeln('── Cache Status ── (no artifact config)'); + $this->output->writeln(''); + return; + } + + $platform = $cacheInfo['current_platform']; + $this->output->writeln("── Cache Status ── current platform: {$platform}"); + + // Source + $this->output->writeln(''); + $this->output->writeln(' source:'); + if (!$cacheInfo['has_source']) { + $this->output->writeln(' ─ not applicable'); + } elseif ($cacheInfo['source']['downloaded'] && $cacheInfo['source']['info'] !== null) { + $this->output->writeln(' ✓ downloaded ' . $this->formatCacheEntry($cacheInfo['source']['info'])); + } else { + $this->output->writeln(' ✗ not downloaded'); + } + + // Binary + $this->output->writeln(''); + $this->output->writeln(' binary:'); + if (!$cacheInfo['has_binary']) { + $this->output->writeln(' ─ not applicable'); + } elseif (empty($cacheInfo['binary'])) { + $this->output->writeln(" ✗ {$platform} (current — not cached)"); + } else { + $allBinary = $cacheInfo['binary']; + foreach ($allBinary as $binPlatform => $binInfo) { + $isCurrent = $binPlatform === $platform; + $tag = $isCurrent ? ' (current)' : ''; + if ($binInfo !== null) { + $this->output->writeln(" ✓ {$binPlatform}{$tag} " . $this->formatCacheEntry($binInfo)); + } else { + $this->output->writeln(" ✗ {$binPlatform}{$tag}"); + } + } + // Show current platform if not already listed + if (!array_key_exists($platform, $allBinary)) { + $this->output->writeln(" ✗ {$platform} (current — not cached)"); + } + } + + $this->output->writeln(''); + } + + private function formatCacheEntry(array $info): string + { + $type = $info['cache_type'] ?? '?'; + $version = $info['version'] !== null ? " {$info['version']}" : ''; + $time = isset($info['time']) ? ' ' . date('Y-m-d H:i', (int) $info['time']) : ''; + $file = match ($type) { + 'archive', 'file' => isset($info['filename']) ? " {$info['filename']}" : '', + 'git', 'local' => isset($info['dirname']) ? " {$info['dirname']}" : '', + default => '', + }; + return "[{$type}]{$version}{$time}{$file}"; + } +} diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php index 6ae6d68a..40303d14 100644 --- a/src/StaticPHP/Command/DoctorCommand.php +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -26,6 +26,7 @@ class DoctorCommand extends BaseCommand }; $doctor = new Doctor($this->output, $fix_policy); if ($doctor->checkAll()) { + Doctor::markPassed(); $this->output->writeln('Doctor check complete !'); return static::SUCCESS; } diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php index 270f5538..e021e58b 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -56,6 +56,8 @@ class DownloadCommand extends BaseCommand return $this->handleClean(); } + $this->checkDoctorCache(); + $downloader = new ArtifactDownloader(DownloaderOptions::extractFromConsoleOptions($this->input->getOptions())); // arguments diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index 23032261..864fd379 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -34,9 +34,9 @@ class InstallPackageCommand extends BaseCommand public function handle(): int { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true]); + $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true], true); $installer->addInstallPackage($this->input->getArgument('package')); - $installer->run(true, true); + $installer->run(true); return static::SUCCESS; } } diff --git a/src/StaticPHP/Command/ReturnCode.php b/src/StaticPHP/Command/ReturnCode.php index d152101e..5accebc3 100644 --- a/src/StaticPHP/Command/ReturnCode.php +++ b/src/StaticPHP/Command/ReturnCode.php @@ -17,26 +17,28 @@ trait ReturnCode { public const int OK = 0; - public const SUCCESS = 0; // alias of OK + public const SUCCESS = 0; // alias - public const int INTERNAL_ERROR = 1; // unsorted or internal error + public const FAILURE = 1; // generic failure - /** @deprecated Use specified error code instead */ - public const FAILURE = 1; + // 64-69: reserved for standard errors + public const int USER_ERROR = 64; // wrong usage, bad arguments - public const int USER_ERROR = 2; // wrong usage or user error + public const int VALIDATION_ERROR = 65; // invalid input or config values - public const int ENVIRONMENT_ERROR = 3; // environment not suitable for operation + public const int ENVIRONMENT_ERROR = 69; // required tools/env not available - public const int VALIDATION_ERROR = 4; // validation failed + // 70+: application-specific errors + public const int INTERNAL_ERROR = 70; // internal logic error or unexpected state - public const int FILE_SYSTEM_ERROR = 5; // file system related error + public const int BUILD_ERROR = 72; // build / compile process failed - public const int DOWNLOAD_ERROR = 6; // network related error + public const int PATCH_ERROR = 73; // patching or modifying files failed - public const int BUILD_ERROR = 7; // build process error + public const int FILE_SYSTEM_ERROR = 74; // filesystem / IO error - public const int PATCH_ERROR = 8; // patching process error + public const int DOWNLOAD_ERROR = 75; // network / remote resource error - public const int INTERRUPT_SIGNAL = 130; // process interrupted by user (e.g., Ctrl+C) + // 128+: reserved for standard signals and interrupts + public const int INTERRUPT_SIGNAL = 130; // SIGINT (Ctrl+C) } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index fbf88321..919de86d 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -16,6 +16,7 @@ class ConfigValidator public const array PACKAGE_FIELD_TYPES = [ // package fields 'type' => ConfigType::STRING, + 'description' => ConfigType::STRING, 'depends' => ConfigType::LIST_ARRAY, // @ 'suggests' => ConfigType::LIST_ARRAY, // @ 'artifact' => [self::class, 'validateArtifactField'], // STRING or OBJECT @@ -39,10 +40,14 @@ class ConfigValidator 'static-libs' => ConfigType::LIST_ARRAY, // @ 'pkg-configs' => ConfigType::LIST_ARRAY, 'static-bins' => ConfigType::LIST_ARRAY, // @ + 'path' => ConfigType::LIST_ARRAY, // @ + 'env' => ConfigType::ASSOC_ARRAY, // @ + 'append-env' => ConfigType::ASSOC_ARRAY, // @ ]; public const array PACKAGE_FIELDS = [ 'type' => true, + 'description' => false, 'depends' => false, // @ 'suggests' => false, // @ 'artifact' => false, @@ -58,6 +63,9 @@ class ConfigValidator 'static-libs' => false, // @ 'pkg-configs' => false, 'static-bins' => false, // @ + 'path' => false, // @ + 'env' => false, // @ + 'append-env' => false, // @ ]; public const array SUFFIX_ALLOWED_FIELDS = [ @@ -66,6 +74,9 @@ class ConfigValidator 'headers', 'static-libs', 'static-bins', + 'path', + 'env', + 'append-env', ]; public const array PHP_EXTENSION_FIELDS = [ @@ -89,7 +100,8 @@ class ConfigValidator 'bitbuckettag' => [['repo'], ['extract']], 'local' => [['dirname'], ['extract']], 'pie' => [['repo'], ['extract']], - 'php-release' => [[], ['extract']], + 'pecl' => [['name'], ['extract', 'prefer-stable']], + 'php-release' => [['domain'], ['extract']], 'custom' => [[], ['func']], ]; diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index c4f22a52..0e2d0af1 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -23,6 +23,7 @@ class PackageConfig if (!is_dir($dir)) { throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config."); } + $dir = rtrim($dir, '/'); $loaded = []; $files = FileSystem::scanDirFiles($dir, false); if (is_array($files)) { diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 8608f761..a02b38c7 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,9 +6,13 @@ namespace StaticPHP; use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; +use StaticPHP\Command\CheckUpdateCommand; +use StaticPHP\Command\Dev\DumpCapabilitiesCommand; +use StaticPHP\Command\Dev\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\LintConfigCommand; +use StaticPHP\Command\Dev\PackageInfoCommand; use StaticPHP\Command\Dev\PackLibCommand; use StaticPHP\Command\Dev\ShellCommand; use StaticPHP\Command\DoctorCommand; @@ -60,6 +64,7 @@ class ConsoleApplication extends Application new SPCConfigCommand(), new DumpLicenseCommand(), new ResetCommand(), + new CheckUpdateCommand(), // dev commands new ShellCommand(), @@ -67,6 +72,9 @@ class ConsoleApplication extends Application new EnvCommand(), new LintConfigCommand(), new PackLibCommand(), + new DumpStagesCommand(), + new DumpCapabilitiesCommand(), + new PackageInfoCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 36db37ae..1239a30c 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -9,6 +9,7 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; use StaticPHP\Registry\DoctorLoader; use StaticPHP\Runtime\Shell\Shell; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Output\OutputInterface; use ZM\Logger\ConsoleColor; @@ -17,7 +18,7 @@ use function Laravel\Prompts\confirm; readonly class Doctor { - public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT) + public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT, public readonly bool $interactive = true) { // debug shows all loaded doctor items $items = DoctorLoader::getDoctorItems(); @@ -25,17 +26,40 @@ readonly class Doctor logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names)); } + /** + * Returns true if doctor was previously passed with the current SPC version. + */ + public static function isHealthy(): bool + { + $lock = self::getLockPath(); + return file_exists($lock) && trim((string) @file_get_contents($lock)) === \StaticPHP\ConsoleApplication::VERSION; + } + + /** + * Write current SPC version to the lock file, marking doctor as passed. + */ + public static function markPassed(): void + { + $primary = self::getLockPath(); + if (!is_dir(dirname($primary))) { + @mkdir(dirname($primary), 0755, true); + } + if (@file_put_contents($primary, \StaticPHP\ConsoleApplication::VERSION) === false) { + @file_put_contents((getcwd() ?: '.') . DIRECTORY_SEPARATOR . '.spc-doctor.lock', \StaticPHP\ConsoleApplication::VERSION); + } + } + /** * Check all valid check items. * @return bool true if all checks passed, false otherwise */ - public function checkAll(bool $interactive = true): bool + public function checkAll(): bool { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Starting doctor checks ...'); } foreach ($this->getValidCheckList() as $check) { - if (!$this->checkItem($check, $interactive)) { + if (!$this->checkItem($check)) { return false; } } @@ -48,7 +72,7 @@ readonly class Doctor * @param CheckItem|string $check The check item to be checked * @return bool True if the check passed or was fixed, false otherwise */ - public function checkItem(CheckItem|string $check, bool $interactive = true): bool + public function checkItem(CheckItem|string $check): bool { if (is_string($check)) { $found = null; @@ -64,7 +88,7 @@ readonly class Doctor } $check = $found; } - $prepend = $interactive ? ' - ' : ''; + $prepend = $this->interactive ? ' - ' : ''; $this->output?->write("{$prepend}Checking {$check->item_name} ... "); // call check @@ -119,6 +143,30 @@ readonly class Doctor return false; } + private static function getLockPath(): string + { + if (SystemTarget::getTargetOS() === 'Windows') { + $trial_ls = [ + getenv('LOCALAPPDATA') ?: ((getenv('USERPROFILE') ?: 'C:\Users\Default') . '\AppData\Local') . '\.spc-doctor.lock', + sys_get_temp_dir() . '\.spc-doctor.lock', + WORKING_DIR . '\.spc-doctor.lock', + ]; + } else { + $trial_ls = [ + getenv('XDG_CACHE_HOME') ?: ((getenv('HOME') ?: '/tmp') . '/.cache') . '/.spc-doctor.lock', + sys_get_temp_dir() . '/.spc-doctor.lock', + WORKING_DIR . '/.spc-doctor.lock', + ]; + } + foreach ($trial_ls as $path) { + if (is_writable(dirname($path))) { + return $path; + } + } + // fallback to current directory + return WORKING_DIR . DIRECTORY_SEPARATOR . '.spc-doctor.lock'; + } + private function emitFix(string $fix_item, array $fix_item_params = []): bool { keyboard_interrupt_register(function () { diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index b01b7b7b..df3b5241 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -59,8 +59,8 @@ class LinuxMuslCheck #[FixItem('fix-musl-wrapper')] public function fixMusl(): bool { - $downloader = new ArtifactDownloader(); - $downloader->add('musl-wrapper')->download(false); + $downloader = new ArtifactDownloader(interactive: false); + $downloader->add('musl-wrapper')->download(); $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); $extractor->extract('musl-wrapper'); @@ -96,9 +96,9 @@ class LinuxMuslCheck Shell::passthruCallback(function () { InteractiveTerm::advance(); }); - $downloader = new ArtifactDownloader(); + $downloader = new ArtifactDownloader(interactive: false); $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); - $downloader->add('musl-toolchain')->download(false); + $downloader->add('musl-toolchain')->download(); $extractor->extract('musl-toolchain'); $pkg_root = PKG_ROOT_PATH . '/musl-toolchain'; f_passthru("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); diff --git a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php index b69528ad..54d62e44 100644 --- a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php +++ b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php @@ -7,6 +7,7 @@ namespace StaticPHP\Doctor\Item; use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; use StaticPHP\Doctor\CheckResult; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\System\MacOSUtil; class MacOSToolCheck @@ -58,6 +59,20 @@ class MacOSToolCheck return CheckResult::ok(); } + #[CheckItem('if homebrew llvm are installed', limit_os: 'Darwin')] + public function checkBrewLLVM(): ?CheckResult + { + if (getenv('SPC_USE_LLVM') === 'brew') { + $homebrew_prefix = getenv('HOMEBREW_PREFIX') ?: (SystemTarget::getTargetArch() === 'aarch64' ? '/opt/homebrew' : '/usr/local/homebrew'); + + if (($path = MacOSUtil::findCommand('clang', ["{$homebrew_prefix}/opt/llvm/bin"])) === null) { + return CheckResult::fail('Homebrew llvm is not installed', 'build-tools', ['missing' => ['llvm']]); + } + return CheckResult::ok($path); + } + return null; + } + #[CheckItem('if bison version is 3.0 or later', limit_os: 'Darwin')] public function checkBisonVersion(array $command_path = []): ?CheckResult { diff --git a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php index 4a0ba498..88865163 100644 --- a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php +++ b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php @@ -45,9 +45,9 @@ class PkgConfigCheck public function fix(): bool { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller(['dl-binary-only' => true]); + $installer = new PackageInstaller(['dl-binary-only' => true], interactive: false); $installer->addInstallPackage('pkg-config'); - $installer->run(false, true); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php index fce3350b..3316be3f 100644 --- a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php +++ b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php @@ -30,9 +30,9 @@ class Re2cVersionCheck #[FixItem('build-re2c')] public function buildRe2c(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('re2c'); - $installer->run(false); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php index e6a042d3..08e140f4 100644 --- a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php +++ b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php @@ -107,7 +107,7 @@ class WindowsToolCheck { $installer = new PackageInstaller(); $installer->addInstallPackage('strawberry-perl'); - $installer->run(false); + $installer->run(true); GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl'); return true; } @@ -116,27 +116,27 @@ class WindowsToolCheck public function installSDK(): bool { FileSystem::removeDir(getenv('PHP_SDK_PATH')); - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('php-sdk-binary-tools'); - $installer->run(false); + $installer->run(true); return true; } #[FixItem('install-nasm')] public function installNasm(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('nasm'); - $installer->run(false); + $installer->run(true); return true; } #[FixItem('install-vswhere')] public function installVSWhere(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('vswhere'); - $installer->run(false); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index c3a6aa9f..baa6d4cb 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -34,9 +34,9 @@ class ZigCheck #[FixItem('install-zig')] public function installZig(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('zig'); - $installer->run(false); + $installer->run(true); return $installer->isPackageInstalled('zig'); } } diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 20cf9395..053d82a3 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -29,12 +29,6 @@ class ExceptionHandler RegistryException::class, ]; - public const array MINOR_LOG_EXCEPTIONS = [ - InterruptException::class, - WrongUsageException::class, - RegistryException::class, - ]; - /** @var array Build PHP extra info binding */ private static array $build_php_extra_info = []; @@ -57,10 +51,7 @@ class ExceptionHandler }; self::logError($head_msg); - // ---------------------------------------- - $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); - - if ($minor_logs) { + if ($e->isSimpleOutput()) { return self::getReturnCode($e); } @@ -124,7 +115,7 @@ class ExceptionHandler $msg = explode("\n", (string) $message); foreach ($msg as $v) { $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); - fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); + spc_write_log($spc_log, strip_ansi_colors($line) . PHP_EOL); if ($output_log) { InteractiveTerm::plain(ConsoleColor::$color($line) . '', 'error'); } @@ -283,6 +274,6 @@ class ExceptionHandler self::printArrayInfo($info); } - self::logError("---------------------------------------------------------\n", color: 'none'); + self::logError("-----------------------------------------------------------\n", color: 'none'); } } diff --git a/src/StaticPHP/Exception/InterruptException.php b/src/StaticPHP/Exception/InterruptException.php index 77b5240a..3f55d7a8 100644 --- a/src/StaticPHP/Exception/InterruptException.php +++ b/src/StaticPHP/Exception/InterruptException.php @@ -7,4 +7,7 @@ namespace StaticPHP\Exception; /** * Exception caused by manual intervention. */ -class InterruptException extends SPCException {} +class InterruptException extends SPCException +{ + protected bool $simple_output = true; +} diff --git a/src/StaticPHP/Exception/RegistryException.php b/src/StaticPHP/Exception/RegistryException.php index 347a132a..17d65cf2 100644 --- a/src/StaticPHP/Exception/RegistryException.php +++ b/src/StaticPHP/Exception/RegistryException.php @@ -4,4 +4,7 @@ declare(strict_types=1); namespace StaticPHP\Exception; -class RegistryException extends SPCException {} +class RegistryException extends SPCException +{ + protected bool $simple_output = true; +} diff --git a/src/StaticPHP/Exception/SPCException.php b/src/StaticPHP/Exception/SPCException.php index 307cf6cd..7ec27abe 100644 --- a/src/StaticPHP/Exception/SPCException.php +++ b/src/StaticPHP/Exception/SPCException.php @@ -20,6 +20,8 @@ use StaticPHP\Package\TargetPackage; */ abstract class SPCException extends \Exception { + protected bool $simple_output = false; + /** @var null|array Package information */ private ?array $package_info = null; @@ -155,6 +157,16 @@ abstract class SPCException extends \Exception return $this->extra_log_files; } + public function isSimpleOutput(): bool + { + return $this->simple_output; + } + + public function setSimpleOutput(bool $simple_output = true): void + { + $this->simple_output = $simple_output; + } + /** * Load stack trace information to detect Package, Builder, and Installer context. */ diff --git a/src/StaticPHP/Exception/WrongUsageException.php b/src/StaticPHP/Exception/WrongUsageException.php index 2044a82c..631a242a 100644 --- a/src/StaticPHP/Exception/WrongUsageException.php +++ b/src/StaticPHP/Exception/WrongUsageException.php @@ -10,4 +10,7 @@ namespace StaticPHP\Exception; * This exception is used to indicate that the SPC is being used incorrectly. * Such as when a command is not supported or an invalid argument is provided. */ -class WrongUsageException extends SPCException {} +class WrongUsageException extends SPCException +{ + protected bool $simple_output = true; +} diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 64b9f2e4..1ec1a503 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -128,6 +128,26 @@ abstract class Package $this->stages[$name] = $stage; } + /** + * Get all defined stages for this package. + * + * @return array + */ + public function getStages(): array + { + return $this->stages; + } + + /** + * Get the list of OS families that have a registered build function (via #[BuildFor]). + * + * @return string[] e.g. ['Linux', 'Darwin'] + */ + public function getBuildForOSList(): array + { + return array_keys($this->build_functions); + } + /** * Check if the package has a specific stage defined. * diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 628900fa..417c4e1b 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -9,6 +9,7 @@ use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\ArtifactExtractor; use StaticPHP\Artifact\DownloaderOptions; +use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\PackageLoader; @@ -46,7 +47,7 @@ class PackageInstaller /** @var null|BuildRootTracker buildroot file tracker for debugging purpose */ protected ?BuildRootTracker $tracker = null; - public function __construct(protected array $options = []) + public function __construct(protected array $options = [], public readonly bool $interactive = true) { ApplicationContext::set(PackageInstaller::class, $this); $builder = new PackageBuilder($options); @@ -143,7 +144,7 @@ class PackageInstaller /** * Run the package installation process. */ - public function run(bool $interactive = true, bool $disable_delay_msg = false): void + public function run(bool $disable_delay_msg = false): void { // apply build toolchain envs GlobalEnvManager::afterInit(); @@ -153,7 +154,7 @@ class PackageInstaller $this->resolvePackages(); } - if ($interactive && !$disable_delay_msg) { + if ($this->interactive && !$disable_delay_msg) { // show install or build options in terminal with beautiful output $this->printInstallerInfo(); @@ -167,14 +168,17 @@ class PackageInstaller // check download if ($this->download) { $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl'); - $downloader = new ArtifactDownloader([...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))]); - $downloader->addArtifacts($this->getArtifacts())->download($interactive); + $downloader = new ArtifactDownloader( + [...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))], + $this->interactive + ); + $downloader->addArtifacts($this->getArtifacts())->download(); } else { logger()->notice('Skipping download (--no-download option enabled)'); } // extract sources - $this->extractSourceArtifacts(interactive: $interactive); + $this->extractSourceArtifacts(); // validate packages foreach ($this->packages as $package) { @@ -183,7 +187,7 @@ class PackageInstaller } // build/install packages - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Building/Installing packages ...'); keyboard_interrupt_register(function () { InteractiveTerm::finish('Build/Install process interrupted by user!', false); @@ -198,7 +202,7 @@ class PackageInstaller $has_source = $package->hasSource(); if (!$is_to_build && $should_use_binary) { // install binary - if ($interactive) { + if ($this->interactive) { InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); } try { @@ -210,17 +214,17 @@ class PackageInstaller } catch (\Throwable $e) { // Stop tracking on error $this->tracker?->stopTracking(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); } } elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } try { @@ -243,22 +247,20 @@ class PackageInstaller } catch (\Throwable $e) { // Stop tracking on error $this->tracker?->stopTracking(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); } } } - $this->dumpLicenseFiles($this->packages); - if ($interactive) { - InteractiveTerm::success('Exported package licenses', true); - } + // perform after-install actions and emit post-install events + $this->emitPostInstallEvents(); } public function isBuildPackage(Package|string $package): bool @@ -311,6 +313,17 @@ class PackageInstaller return false; } + public function emitPostInstallEvents(): void + { + foreach ($this->packages as $package) { + if ($package->hasStage('postInstall')) { + $package->runStage('postInstall'); + } + } + + $this->dumpLicenseFiles($this->packages); + } + /** * Returns the download status of all artifacts for the resolved packages. * @@ -368,7 +381,7 @@ class PackageInstaller /** * Extract all artifacts for resolved packages. */ - public function extractSourceArtifacts(bool $interactive = true): void + public function extractSourceArtifacts(): void { FileSystem::createDir(SOURCE_PATH); $packages = array_values($this->packages); @@ -403,7 +416,7 @@ class PackageInstaller } // Extract each artifact - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Extracting source for ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . ' ...'); InteractiveTerm::indicateProgress('Extracting artifacts'); } @@ -411,7 +424,7 @@ class PackageInstaller try { V2CompatLayer::beforeExtsExtractHook(); foreach ($artifacts as $artifact) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::setMessage('Extracting source: ' . ConsoleColor::green($artifact->getName())); } if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) { @@ -423,12 +436,12 @@ class PackageInstaller } } V2CompatLayer::afterExtsExtractHook(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Extracted all sources successfully.'); echo PHP_EOL; } } catch (\Throwable $e) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Artifact extraction failed!', false); echo PHP_EOL; } @@ -525,6 +538,9 @@ class PackageInstaller } } $dumper->dump(BUILD_ROOT_PATH . '/license'); + if ($this->interactive) { + InteractiveTerm::success('Exported package licenses', true); + } } /** @@ -565,6 +581,30 @@ class PackageInstaller foreach ($resolved_packages as $pkg_name) { $this->packages[$pkg_name] = PackageLoader::getPackage($pkg_name); } + + foreach ($this->packages as $package) { + $this->injectPackageEnvs($package); + } + } + + private function injectPackageEnvs(Package $package): void + { + $name = $package->getName(); + + $paths = PackageConfig::get($name, 'path', []); + foreach ($paths as $path) { + GlobalEnvManager::addPathIfNotExists(FileSystem::replacePathVariable($path)); + } + + $envs = PackageConfig::get($name, 'env', []); + foreach ($envs as $k => $v) { + GlobalEnvManager::putenv("{$k}=" . FileSystem::replacePathVariable((string) $v)); + } + + $append_envs = PackageConfig::get($name, 'append-env', []); + foreach ($append_envs as $k => $v) { + GlobalEnvManager::appendEnv($k, FileSystem::replacePathVariable((string) $v)); + } } private function handlePhpTargetPackage(TargetPackage $package): void diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 3f2f18cf..baaa2753 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -79,8 +79,8 @@ class PhpExtensionPackage extends Package return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]); } $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; - $name = str_replace('_', '-', $this->getName()); - $ext_config = PackageConfig::get($name, 'php-extension', []); + $name = str_replace('_', '-', $this->getExtensionName()); + $ext_config = PackageConfig::get($this->getName(), 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { 'Windows' => $ext_config['arg-type@windows'] ?? $ext_config['arg-type'] ?? 'enable', @@ -94,7 +94,7 @@ class PhpExtensionPackage extends Package 'enable-path' => $shared ? "--enable-{$name}=shared,{$escapedPath}" : "--enable-{$name}={$escapedPath}", 'with' => $shared ? "--with-{$name}=shared" : "--with-{$name}", 'with-path' => $shared ? "--with-{$name}=shared,{$escapedPath}" : "--with-{$name}={$escapedPath}", - 'custom' => '', + 'custom', 'none' => '', default => $arg_type, }; // customize argument from config string @@ -146,6 +146,54 @@ class PhpExtensionPackage extends Package } } + /** + * Get the dist name used for `--ri` check in smoke test. + * Reads from config `display-name` field, defaults to extension name. + */ + public function getDistName(): string + { + return $this->extension_config['display-name'] ?? $this->getExtensionName(); + } + + /** + * Run smoke test for the extension on Unix CLI. + * Override this method in a subclass. + */ + public function runSmokeTestCliUnix(): void + { + if (($this->extension_config['smoke-test'] ?? true) === false) { + return; + } + + $distName = $this->getDistName(); + // empty display-name → no --ri check (e.g. password_argon2) + if ($distName === '') { + return; + } + + $sharedExtensions = $this->getSharedExtensionLoadString(); + [$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $distName . '"', false); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed compile check: php-cli returned {$ret}", + validation_module: 'Extension ' . $this->getName() . ' sanity check' + ); + } + + $test_file = ROOT_DIR . '/src/globals/ext-tests/' . $this->getExtensionName() . '.php'; + if (file_exists($test_file)) { + // Trim additional content & escape special characters to allow inline usage + $test = self::escapeInlineTest(file_get_contents($test_file)); + [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' -r "' . trim($test) . '"'); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed sanity check. Code: {$ret}, output: " . implode("\n", $out), + validation_module: 'Extension ' . $this->getName() . ' function check' + ); + } + } + } + /** * Get shared extension build environment variables for Unix. * @@ -232,6 +280,27 @@ class PhpExtensionPackage extends Package $builder->deployBinary($soFile, $soFile, false); } + /** + * Get per-OS build support status for this php-extension. + * + * Rules (same as v2): + * - OS not listed in 'support' config => 'yes' (fully supported) + * - OS listed with 'wip' => 'wip' + * - OS listed with 'partial' => 'partial' + * - OS listed with 'no' => 'no' + * + * @return array e.g. ['Linux' => 'yes', 'Darwin' => 'partial', 'Windows' => 'no'] + */ + public function getBuildSupportStatus(): array + { + $exceptions = $this->extension_config['support'] ?? []; + $result = []; + foreach (['Linux', 'Darwin', 'Windows'] as $os) { + $result[$os] = $exceptions[$os] ?? 'yes'; + } + return $result; + } + /** * Register default stages if not already defined by attributes. * This is called after all attributes have been loaded. @@ -258,6 +327,34 @@ class PhpExtensionPackage extends Package } } + /** + * Builds the `-d extension_dir=... -d extension=...` string for all resolved shared extensions. + * Used in CLI smoke test to load shared extension dependencies at runtime. + */ + public function getSharedExtensionLoadString(): string + { + $sharedExts = array_filter( + $this->getInstaller()->getResolvedPackages(PhpExtensionPackage::class), + fn (PhpExtensionPackage $ext) => $ext->isBuildShared() && !$ext->isBuildWithPhp() + ); + + if (empty($sharedExts)) { + return ''; + } + + $ret = ' -d "extension_dir=' . BUILD_MODULES_PATH . '"'; + foreach ($sharedExts as $ext) { + $extConfig = PackageConfig::get($ext->getName(), 'php-extension', []); + if ($extConfig['zend-extension'] ?? false) { + $ret .= ' -d "zend_extension=' . $ext->getExtensionName() . '"'; + } else { + $ret .= ' -d "extension=' . $ext->getExtensionName() . '"'; + } + } + + return $ret; + } + /** * Splits a given string of library flags into static and shared libraries. * @@ -284,4 +381,17 @@ class PhpExtensionPackage extends Package } return [trim($staticLibString), trim($sharedLibString)]; } + + /** + * Escape PHP test file content for inline `-r` usage. + * Strips getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { self::processCustomSourceAttribute($ref, $method, $class_instance); + self::processCustomSourceCheckUpdateAttribute($ref, $method, $class_instance); self::processCustomBinaryAttribute($ref, $method, $class_instance); + self::processCustomBinaryCheckUpdateAttribute($ref, $method, $class_instance); self::processSourceExtractAttribute($ref, $method, $class_instance); self::processBinaryExtractAttribute($ref, $method, $class_instance); self::processAfterSourceExtractAttribute($ref, $method, $class_instance); @@ -98,6 +102,24 @@ class ArtifactLoader } } + /** + * Process #[CustomSourceCheckUpdate] attribute. + */ + private static function processCustomSourceCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomSourceCheckUpdate::class); + foreach ($attributes as $attribute) { + /** @var CustomSourceCheckUpdate $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setCustomSourceCheckUpdateCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomSourceCheckUpdate] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + /** * Process #[CustomBinary] attribute. */ @@ -118,6 +140,26 @@ class ArtifactLoader } } + /** + * Process #[CustomBinaryCheckUpdate] attribute. + */ + private static function processCustomBinaryCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomBinaryCheckUpdate::class); + foreach ($attributes as $attribute) { + /** @var CustomBinaryCheckUpdate $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + foreach ($instance->support_os as $os) { + self::$artifacts[$artifact_name]->setCustomBinaryCheckUpdateCallback($os, [$class_instance, $method->getName()]); + } + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomBinaryCheckUpdate] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + /** * Process #[SourceExtract] attribute. * This attribute allows completely taking over the source extraction process. diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index ca195ff0..421403c9 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -40,6 +40,42 @@ class PackageLoader /** @var array Track loaded classes to prevent duplicates */ private static array $loaded_classes = []; + /** + * Annotation metadata keyed by package name, capturing the defining class and its method-level attributes. + * + * @var array}>>}> + */ + private static array $annotation_map = []; + + /** + * Source metadata for #[BeforeStage] hooks, keyed by target package name → stage name. + * + * @var array>> + */ + private static array $before_stage_meta = []; + + /** + * Source metadata for #[AfterStage] hooks, keyed by target package name → stage name. + * + * @var array>> + */ + private static array $after_stage_meta = []; + + /** + * Reverse index of #[BeforeStage] hooks, keyed by registering class → target package → stage. + * Enables O(1) "outbound hook" lookup: what stages does a given class hook into on other packages? + * + * @var array>>> + */ + private static array $class_before_stage_meta = []; + + /** + * Reverse index of #[AfterStage] hooks, keyed by registering class → target package → stage. + * + * @var array>>> + */ + private static array $class_after_stage_meta = []; + public static function initPackageInstances(): void { if (self::$packages !== null) { @@ -213,8 +249,19 @@ class PackageLoader Validate::class => $pkg->setValidateCallback([$instance_class, $method->getName()]), default => null, }; + + // Capture annotation metadata for inspection (dev:info, future event-trace commands) + $meta_attr = self::annotationShortName($method_attribute->getName()); + if ($meta_attr !== null) { + self::$annotation_map[$pkg->getName()]['methods'][$method->getName()][] = [ + 'attr' => $meta_attr, + 'args' => self::annotationArgs($method_instance), + ]; + } } } + // Record which class defines this package (set once; IS_REPEATABLE may loop more than once) + self::$annotation_map[$pkg->getName()]['class'] ??= $class_name; // register package self::$packages[$pkg->getName()] = $pkg; } @@ -240,6 +287,83 @@ class PackageLoader } } + /** + * Get all registered before-stage callbacks (raw). + * + * @return array>> + */ + public static function getAllBeforeStages(): array + { + return self::$before_stages; + } + + /** + * Get all registered after-stage callbacks (raw). + * + * @return array>> + */ + public static function getAllAfterStages(): array + { + return self::$after_stages; + } + + /** + * Get annotation metadata for a specific package. + * + * Returns null if no annotation class was loaded for this package (config-only package). + * The returned structure includes the defining class name, per-method attribute list, + * inbound BeforeStage/AfterStage hooks targeting this package, and outbound hooks that + * this package's class registers on other packages. + * + * @return null|array{ + * class: string, + * methods: array}>>, + * before_stages: array>, + * after_stages: array>, + * outbound_before_stages: array>>, + * outbound_after_stages: array>> + * } + */ + public static function getPackageAnnotationInfo(string $name): ?array + { + $class_info = self::$annotation_map[$name] ?? null; + if ($class_info === null) { + return null; + } + $class = $class_info['class']; + return [ + 'class' => $class, + 'methods' => $class_info['methods'], + 'before_stages' => self::$before_stage_meta[$name] ?? [], + 'after_stages' => self::$after_stage_meta[$name] ?? [], + 'outbound_before_stages' => self::$class_before_stage_meta[$class] ?? [], + 'outbound_after_stages' => self::$class_after_stage_meta[$class] ?? [], + ]; + } + + /** + * Get all annotation metadata keyed by package name. + * Useful for future event-trace commands or cross-package inspection. + * + * @return array + */ + public static function getAllAnnotations(): array + { + $result = []; + foreach (self::$annotation_map as $name => $info) { + $class = $info['class']; + $result[$name] = [ + 'class' => $class, + 'methods' => $info['methods'], + 'before_stages' => self::$before_stage_meta[$name] ?? [], + 'after_stages' => self::$after_stage_meta[$name] ?? [], + 'outbound_before_stages' => self::$class_before_stage_meta[$class] ?? [], + 'outbound_after_stages' => self::$class_after_stage_meta[$class] ?? [], + ]; + } + return $result; + } + public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable { // match condition @@ -365,6 +489,16 @@ class PackageLoader } $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; self::$before_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + $registering_class = get_class($instance_class); + self::$before_stage_meta[$package_name][$stage][] = [ + 'class' => $registering_class, + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; + self::$class_before_stage_meta[$registering_class][$package_name][$stage][] = [ + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; } private static function addAfterStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void @@ -380,5 +514,49 @@ class PackageLoader } $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; self::$after_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + $registering_class = get_class($instance_class); + self::$after_stage_meta[$package_name][$stage][] = [ + 'class' => $registering_class, + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; + self::$class_after_stage_meta[$registering_class][$package_name][$stage][] = [ + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; + } + + /** + * Map a fully-qualified attribute class name to a short display name for metadata storage. + * Returns null for attributes that are not tracked in the annotation map. + */ + private static function annotationShortName(string $attr): ?string + { + return match ($attr) { + Stage::class => 'Stage', + BuildFor::class => 'BuildFor', + PatchBeforeBuild::class => 'PatchBeforeBuild', + CustomPhpConfigureArg::class => 'CustomPhpConfigureArg', + InitPackage::class => 'InitPackage', + ResolveBuild::class => 'ResolveBuild', + Info::class => 'Info', + Validate::class => 'Validate', + default => null, + }; + } + + /** + * Extract the meaningful constructor arguments from an attribute instance as a key-value array. + * + * @return array + */ + private static function annotationArgs(object $inst): array + { + return match (true) { + $inst instanceof Stage => array_filter(['function' => $inst->function], fn ($v) => $v !== null), + $inst instanceof BuildFor => ['os' => $inst->os], + $inst instanceof CustomPhpConfigureArg => array_filter(['os' => $inst->os], fn ($v) => $v !== ''), + default => [], + }; } } diff --git a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php index 41bc6e78..c59859cf 100644 --- a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php @@ -20,8 +20,6 @@ class UnixAutoconfExecutor extends Executor protected array $configure_args = []; - protected array $ignore_args = []; - protected PackageInstaller $installer; public function __construct(protected LibraryPackage $package, ?PackageInstaller $installer = null) @@ -40,6 +38,8 @@ class UnixAutoconfExecutor extends Executor if (!$this->package->hasStage('build')) { throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined."); } + + $this->configure_args = $this->getDefaultConfigureArgs(); } /** @@ -48,18 +48,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); InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (./configure)'); 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 * @@ -134,7 +128,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; } diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 5b50d152..66dfb7ab 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -25,7 +25,7 @@ class DefaultShell extends Shell /** * Execute a cURL command to fetch data from a URL. */ - public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): false|string + public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0, bool $compressed = false): false|string { foreach ($hooks as $hook) { $hook($method, $url, $headers); @@ -39,7 +39,8 @@ class DefaultShell extends Shell }; $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; - $cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}"; + $compressed_arg = $compressed ? '--compressed' : ''; + $cmd = SPC_CURL_EXEC . " -sfSL --max-time 3600 {$retry_arg} {$compressed_arg} {$method_arg} {$header_arg} {$url_arg}"; $this->logCommandInfo($cmd); $result = $this->passthru($cmd, capture_output: true, throw_on_error: false); @@ -72,7 +73,7 @@ class DefaultShell extends Shell $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; $check = $this->console_putput ? '#' : 's'; - $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); + $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL --max-time 3600 {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); $this->logCommandInfo($cmd); logger()->debug('[CURL DOWNLOAD] ' . $cmd); $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); @@ -93,7 +94,7 @@ class DefaultShell extends Shell $path_arg = escapeshellarg($path); $shallow_arg = $shallow ? '--depth 1 --single-branch' : ''; $submodules_arg = ($submodules === null && $shallow) ? '--recursive --shallow-submodules' : ($submodules === null ? '--recursive' : ''); - $cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); + $cmd = clean_spaces("{$git} clone -c http.lowSpeedLimit=1 -c http.lowSpeedTime=3600 --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); $this->logCommandInfo($cmd); logger()->debug("[GIT CLONE] {$cmd}"); $this->passthru($cmd, $this->console_putput); diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index 2d0d90b8..f9f4f175 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -114,22 +114,22 @@ abstract class Shell if (!$this->enable_log_file) { return; } - // write executed command to log file using fwrite + // write executed command to log file using spc_write_log $log_file = fopen(SPC_SHELL_LOG, 'a'); - fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); - fwrite($log_file, "> Executing command: {$cmd}\n"); + spc_write_log($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); + spc_write_log($log_file, "> Executing command: {$cmd}\n"); // get the backtrace to find the file and line number $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); if (isset($backtrace[1]['file'], $backtrace[1]['line'])) { $file = $backtrace[1]['file']; $line = $backtrace[1]['line']; - fwrite($log_file, "> Called from: {$file} at line {$line}\n"); + spc_write_log($log_file, "> Called from: {$file} at line {$line}\n"); } - fwrite($log_file, "> Environment variables: {$this->getEnvString()}\n"); + spc_write_log($log_file, "> Environment variables: {$this->getEnvString()}\n"); if ($this->cd !== null) { - fwrite($log_file, "> Working dir: {$this->cd}\n"); + spc_write_log($log_file, "> Working dir: {$this->cd}\n"); } - fwrite($log_file, "\n"); + spc_write_log($log_file, "\n"); } /** @@ -154,7 +154,7 @@ abstract class Shell ): array { $file_res = null; if ($this->enable_log_file) { - // write executed command to the log file using fwrite + // write executed command to the log file using spc_write_log $file_res = fopen(SPC_SHELL_LOG, 'a'); } if ($console_output) { @@ -194,10 +194,10 @@ abstract class Shell foreach ([$pipes[1], $pipes[2]] as $pipe) { while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { if ($console_output) { - fwrite($console_res, $chunk); + spc_write_log($console_res, $chunk); } if ($file_res !== null) { - fwrite($file_res, $chunk); + spc_write_log($file_res, $chunk); } if ($capture_output) { $output_value .= $chunk; @@ -207,7 +207,7 @@ abstract class Shell // check exit code if ($throw_on_error && $status['exitcode'] !== 0) { if ($file_res !== null) { - fwrite($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); + spc_write_log($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); } throw new ExecutionException( cmd: $original_command ?? $cmd, @@ -238,10 +238,10 @@ abstract class Shell foreach ($read as $pipe) { while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { if ($console_output) { - fwrite($console_res, $chunk); + spc_write_log($console_res, $chunk); } if ($file_res !== null) { - fwrite($file_res, $chunk); + spc_write_log($file_res, $chunk); } if ($capture_output) { $output_value .= $chunk; diff --git a/src/StaticPHP/Toolchain/ClangBrewToolchain.php b/src/StaticPHP/Toolchain/ClangBrewToolchain.php new file mode 100644 index 00000000..5d8963ef --- /dev/null +++ b/src/StaticPHP/Toolchain/ClangBrewToolchain.php @@ -0,0 +1,21 @@ + ZigToolchain::class, 'Windows' => MSVCToolchain::class, - 'Darwin' => ClangNativeToolchain::class, + 'Darwin' => match (getenv('SPC_USE_LLVM') ?: 'system') { + 'brew' => ClangBrewToolchain::class, + default => ClangNativeToolchain::class, + }, default => throw new WrongUsageException('Unsupported OS family: ' . PHP_OS_FAMILY), }; } diff --git a/src/StaticPHP/Toolchain/ZigToolchain.php b/src/StaticPHP/Toolchain/ZigToolchain.php index 344ce3e9..e817abd7 100644 --- a/src/StaticPHP/Toolchain/ZigToolchain.php +++ b/src/StaticPHP/Toolchain/ZigToolchain.php @@ -67,6 +67,9 @@ class ZigToolchain implements UnixToolchainInterface $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}"); } + // zig-cc/clang treats strlcpy/strlcat as compiler builtins, so configure link tests pass (HAVE_STRLCPY=1) + $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=ac_cv_func_strlcpy=no ac_cv_func_strlcat=no {$extra_vars}"); } public function getCompilerInfo(): ?string diff --git a/src/StaticPHP/Util/BuildRootTracker.php b/src/StaticPHP/Util/BuildRootTracker.php index 306bf90c..eae9a21d 100644 --- a/src/StaticPHP/Util/BuildRootTracker.php +++ b/src/StaticPHP/Util/BuildRootTracker.php @@ -15,7 +15,7 @@ class BuildRootTracker /** @var array}> Tracking data */ protected array $tracking_data = []; - protected static string $tracker_file = BUILD_ROOT_PATH . '/.spc-tracker.json'; + protected static string $tracker_file = BUILD_ROOT_PATH . '/.build.json'; protected ?DirDiff $current_diff = null; diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php index 2db74abd..129468f9 100644 --- a/src/StaticPHP/Util/DependencyResolver.php +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -45,6 +45,35 @@ class DependencyResolver } } + // Build a lookup set of explicitly requested packages for the promotion step below. + $input_package_set = []; + foreach ($packages as $pkg) { + $input_package_set[is_string($pkg) ? $pkg : $pkg->getName()] = true; + } + + // Virtual-target packages (e.g. php-fpm) are built as part of their real parent's + // build step, so any dependency they declare must be available before the real parent + // is built. Promote those deps directly onto the real parent's dependency list so + // that the topological sort places them before the parent. + // Only applies to virtual-targets that are in the input request — if a virtual-target + // is not being built, its deps must not be injected into the parent. + foreach ($dep_list_clean as $pkg_name => $pkg_item) { + if (!isset($input_package_set[$pkg_name]) || PackageConfig::get($pkg_name, 'type') !== 'virtual-target') { + continue; + } + foreach ($pkg_item['depends'] as $dep_name) { + if (isset($dep_list_clean[$dep_name]) && PackageConfig::get($dep_name, 'type') !== 'virtual-target') { + // $dep_name is the real parent; add all other deps of this virtual-target to it + $other_deps = array_values(array_filter($pkg_item['depends'], fn ($d) => $d !== $dep_name)); + if (!empty($other_deps)) { + $dep_list_clean[$dep_name]['depends'] = array_values(array_unique( + array_merge($dep_list_clean[$dep_name]['depends'], $other_deps) + )); + } + } + } + } + $resolved = self::doVisitPlat($packages, $dep_list_clean); // Build reverse dependency map if $why is requested diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index c8da5353..3015b489 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -481,7 +481,7 @@ class FileSystem public static function fullpath(string $path, string $relative_path_base): string { if (FileSystem::isRelativePath($path)) { - $path = $relative_path_base . DIRECTORY_SEPARATOR . $path; + $path = rtrim($relative_path_base, '/') . DIRECTORY_SEPARATOR . $path; } if (!file_exists($path)) { throw new FileSystemException("Path does not exist: {$path}"); diff --git a/src/StaticPHP/Util/GlobalEnvManager.php b/src/StaticPHP/Util/GlobalEnvManager.php index 86fcc652..5b4b16b2 100644 --- a/src/StaticPHP/Util/GlobalEnvManager.php +++ b/src/StaticPHP/Util/GlobalEnvManager.php @@ -112,6 +112,17 @@ class GlobalEnvManager } } + public static function appendEnv(string $key, string $value): void + { + $existing = getenv($key); + if ($existing !== false && $existing !== '') { + $separator = SystemTarget::isUnix() ? ':' : ';'; + self::putenv("{$key}={$value}{$separator}{$existing}"); + } else { + self::putenv("{$key}={$value}"); + } + } + /** * Initialize the toolchain after the environment variables are set. * The toolchain or environment availability check is done here. diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 32ef3bc6..8b6fe6b3 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -389,7 +389,9 @@ class SPCConfigUtil } if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { - $lib_names[] = '-lcrypt'; + if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { + $lib_names[] = '-lcrypt'; + } } if (!$use_short_libs) { $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 6a16f041..b4e2e1c7 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -209,9 +209,6 @@ class SourcePatcher $patch_dir = $tmp_dir; } $php_package = PackageLoader::getTargetPackage('php'); - if (!file_exists("{$php_package->getSourceDir()}/sapi/micro/php_micro.c")) { - return false; - } $ver_file = "{$php_package->getSourceDir()}/main/php_version.h"; if (!file_exists($ver_file)) { throw new PatchException('php-src patcher (original micro patches)', 'Patch failed, cannot find php source files'); diff --git a/src/StaticPHP/Util/System/UnixUtil.php b/src/StaticPHP/Util/System/UnixUtil.php index aca50d9e..4a41c524 100644 --- a/src/StaticPHP/Util/System/UnixUtil.php +++ b/src/StaticPHP/Util/System/UnixUtil.php @@ -74,10 +74,10 @@ abstract class UnixUtil throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available."); } // https://github.com/ziglang/zig/issues/24662 - if (ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { - return '-Wl,--export-dynamic'; + $toolchain = ApplicationContext::get(ToolchainInterface::class); + if ($toolchain instanceof ZigToolchain) { + return '-Wl,--export-dynamic'; // needs release 0.16, can be removed then } - // macOS if (SystemTarget::getTargetOS() !== 'Linux') { return "-Wl,-exported_symbols_list,{$symbol_file}"; } diff --git a/src/bootstrap.php b/src/bootstrap.php index 95384b71..7856c0b2 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -52,7 +52,7 @@ if (filter_var(getenv('SPC_ENABLE_LOG_FILE'), FILTER_VALIDATE_BOOLEAN)) { $log_file_fd = fopen(SPC_OUTPUT_LOG, 'a'); $ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) { if ($log_file_fd) { - fwrite($log_file_fd, strip_ansi_colors($output) . "\n"); + spc_write_log($log_file_fd, strip_ansi_colors($output) . "\n"); } return true; }); diff --git a/src/globals/defines.php b/src/globals/defines.php index dbcb63f2..38490046 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -104,7 +104,7 @@ const SPC_DOWNLOAD_TYPE_DISPLAY_NAME = [ 'local' => 'local dir', 'pie' => 'PHP Installer for Extensions (PIE)', 'url' => 'url', - 'php-release' => 'php.net', + 'php-release' => 'PHP website release', 'custom' => 'custom downloader', ]; diff --git a/src/globals/functions.php b/src/globals/functions.php index 712cf621..ee279328 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -132,6 +132,32 @@ function patch_point(): string return ''; } +// Add log filter value(s) to prevent secret leak +function spc_add_log_filter(array|string $filter): void +{ + global $spc_log_filters; + if (!is_array($spc_log_filters)) { + $spc_log_filters = []; + } + if (is_string($filter)) { + if (!in_array($filter, $spc_log_filters, true)) { + $spc_log_filters[] = $filter; + } + } elseif (is_array($filter)) { + $spc_log_filters = array_values(array_unique(array_merge($spc_log_filters, $filter))); + } +} + +function spc_write_log(mixed $stream, string $data): false|int +{ + // get filter + global $spc_log_filters; + if (is_array($spc_log_filters)) { + $data = str_replace($spc_log_filters, '***', $data); + } + return fwrite($stream, $data); +} + function patch_point_interrupt(int $retcode, string $msg = ''): InterruptException { return new InterruptException(message: $msg, code: $retcode); diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 8b22658c..ba02e672 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,10 +13,10 @@ declare(strict_types=1); // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - '8.1', - '8.2', - '8.3', - '8.4', + // '8.1', + // '8.2', + // '8.3', + // '8.4', '8.5', // 'git', ]; @@ -26,12 +26,12 @@ $test_os = [ // 'macos-15-intel', // bin/spc for x86_64 // 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - // 'ubuntu-24.04', // bin/spc for x86_64 - // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 - // 'ubuntu-24.04-arm', // bin/spc for arm64 + 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + 'ubuntu-24.04', // bin/spc for x86_64 + 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - 'windows-2025', + // 'windows-2025', ]; // whether enable thread safe @@ -50,13 +50,13 @@ $prefer_pre_built = false; // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'mysqli,gmp', + 'Linux', 'Darwin' => 'pgsql', 'Windows' => 'com_dotnet', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => 'grpc,mysqlnd_parsec,mysqlnd_ed25519', + 'Linux' => '', 'Darwin' => '', 'Windows' => '', }; @@ -66,7 +66,7 @@ $with_suggested_libs = false; // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'libwebp', + 'Linux', 'Darwin' => '', 'Windows' => '', };