diff --git a/config/env.ini b/config/env.ini index 483ae4083..c3f01672a 100644 --- a/config/env.ini +++ b/config/env.ini @@ -100,9 +100,10 @@ SPC_TARGET=${GNU_ARCH}-linux-musl CC=${SPC_DEFAULT_CC} CXX=${SPC_DEFAULT_CXX} AR=${SPC_DEFAULT_AR} +RANLIB=${SPC_DEFAULT_RANLIB} LD=${SPC_DEFAULT_LD} ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build -SPC_DEFAULT_CFLAGS="-fPIC -O3 -pipe -fno-plt -fno-semantic-interposition -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections" +SPC_DEFAULT_CFLAGS="-fPIC -O3 -pipe -fno-plt -fno-semantic-interposition -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections -Wno-unused-command-line-argument" SPC_DEFAULT_CXXFLAGS="${SPC_DEFAULT_CFLAGS}" SPC_DEFAULT_LDFLAGS="-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -Wl,-z,noexecstack -Wl,--gc-sections" ; upx executable path @@ -125,6 +126,8 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fPIE -fvisibility=hidden -fvisibility-inlines-hidden ${SPC_DEFAULT_CXXFLAGS}" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" +; EXTRA_LDFLAGS_PROGRAM for `make` php; appended only to SAPI executable links (cli/fpm/cgi/micro/embed). Used by PGO to inject -fprofile-use= without polluting libphp.{a,so}. +SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM="" ; optional, path to openssl conf. This affects where openssl will look for the default CA. ; default on Debian/Alpine: /etc/ssl, default on RHEL: /etc/pki/tls @@ -140,6 +143,7 @@ SPC_USE_LLVM=system CC=${SPC_DEFAULT_CC} CXX=${SPC_DEFAULT_CXX} AR=${SPC_DEFAULT_AR} +RANLIB=${SPC_DEFAULT_RANLIB} LD=${SPC_DEFAULT_LD} ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build SPC_DEFAULT_CFLAGS="--target=${MAC_ARCH}-apple-darwin -O3 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections" @@ -163,5 +167,7 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -fvis SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fpie -fvisibility=hidden -fvisibility-inlines-hidden -Werror=unknown-warning-option ${SPC_DEFAULT_CXXFLAGS}" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.dylib SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" +; EXTRA_LDFLAGS_PROGRAM for `make` php; appended only to SAPI executable links (cli/fpm/cgi/micro/embed). Used by PGO to inject -fprofile-use= without polluting libphp.{a,dylib}. +SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM="" ; minimum compatible macOS version (LLVM vars, availability not guaranteed) MACOSX_DEPLOYMENT_TARGET=12.0 diff --git a/config/pkg/ext/ext-amqp.yml b/config/pkg/ext/ext-amqp.yml index 6c73bf203..35bce4d93 100644 --- a/config/pkg/ext/ext-amqp.yml +++ b/config/pkg/ext/ext-amqp.yml @@ -2,8 +2,8 @@ ext-amqp: type: php-extension artifact: source: - type: pecl - name: amqp + type: pie + repo: php-amqp/php-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 index 331b04f76..42e215408 100644 --- a/config/pkg/ext/ext-apcu.yml +++ b/config/pkg/ext/ext-apcu.yml @@ -2,8 +2,8 @@ ext-apcu: type: php-extension artifact: source: - type: pecl - name: APCu + type: pie + repo: apcu/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 index 776b82dff..201010f11 100644 --- a/config/pkg/ext/ext-ast.yml +++ b/config/pkg/ext/ext-ast.yml @@ -2,8 +2,8 @@ ext-ast: type: php-extension artifact: source: - type: pecl - name: ast + type: pie + repo: nikic/php-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 index 147ecb636..b87fb9ef3 100644 --- a/config/pkg/ext/ext-brotli.yml +++ b/config/pkg/ext/ext-brotli.yml @@ -2,12 +2,13 @@ ext-brotli: type: php-extension artifact: source: - type: git + type: ghtagtar + repo: kjdev/php-ext-brotli extract: php-src/ext/brotli - rev: master - url: 'https://github.com/kjdev/php-ext-brotli' metadata: license-files: [LICENSE] license: MIT depends: - brotli + php-extension: + arg-type: '--enable-brotli@shared_suffix@ --with-libbrotli' diff --git a/config/pkg/ext/ext-dio.yml b/config/pkg/ext/ext-dio.yml index b445940c3..18201a795 100644 --- a/config/pkg/ext/ext-dio.yml +++ b/config/pkg/ext/ext-dio.yml @@ -2,8 +2,8 @@ ext-dio: type: php-extension artifact: source: - type: pecl - name: dio + type: pie + repo: pecl/dio metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-ev.yml b/config/pkg/ext/ext-ev.yml index 174e5f843..dea1a1fab 100644 --- a/config/pkg/ext/ext-ev.yml +++ b/config/pkg/ext/ext-ev.yml @@ -2,8 +2,8 @@ ext-ev: type: php-extension artifact: source: - type: pecl - name: ev + type: pie + repo: osmanov/pecl-ev metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-excimer.yml b/config/pkg/ext/ext-excimer.yml index a896fe0bd..81219610a 100644 --- a/config/pkg/ext/ext-excimer.yml +++ b/config/pkg/ext/ext-excimer.yml @@ -2,8 +2,9 @@ ext-excimer: type: php-extension artifact: source: - type: pecl - name: excimer + type: ghtagtar + repo: wikimedia/mediawiki-php-excimer + extract: php-src/ext/excimer metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-fastchart.yml b/config/pkg/ext/ext-fastchart.yml new file mode 100644 index 000000000..0abbc8923 --- /dev/null +++ b/config/pkg/ext/ext-fastchart.yml @@ -0,0 +1,20 @@ +ext-fastchart: + type: php-extension + artifact: + source: + type: ghtar + repo: iliaal/fastchart + extract: php-src/ext/fastchart + prefer-stable: true + metadata: + license-files: [LICENSE] + depends: + - freetype + suggests: + - libpng + - libjpeg + - libwebp + php-extension: + os: + - Linux + - Darwin diff --git a/config/pkg/ext/ext-fastjson.yml b/config/pkg/ext/ext-fastjson.yml new file mode 100644 index 000000000..4644e8201 --- /dev/null +++ b/config/pkg/ext/ext-fastjson.yml @@ -0,0 +1,14 @@ +ext-fastjson: + type: php-extension + artifact: + source: + type: ghtar + repo: iliaal/fastjson + extract: php-src/ext/fastjson + prefer-stable: true + metadata: + license-files: [LICENSE] + php-extension: + os: + - Linux + - Darwin diff --git a/config/pkg/ext/ext-gearman.yml b/config/pkg/ext/ext-gearman.yml index 7699a344b..3a21204fa 100644 --- a/config/pkg/ext/ext-gearman.yml +++ b/config/pkg/ext/ext-gearman.yml @@ -2,8 +2,9 @@ ext-gearman: type: php-extension artifact: source: - type: pecl - name: gearman + type: ghtar + repo: php/pecl-networking-gearman + extract: php-src/ext/gearman metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-gmssl.yml b/config/pkg/ext/ext-gmssl.yml index 7ed8981d7..41a32fb9b 100644 --- a/config/pkg/ext/ext-gmssl.yml +++ b/config/pkg/ext/ext-gmssl.yml @@ -10,3 +10,5 @@ ext-gmssl: license: PHP-3.01 depends: - gmssl + php-extension: + arg-type: with-path diff --git a/config/pkg/ext/ext-grpc.yml b/config/pkg/ext/ext-grpc.yml index ae63cad2b..72b4d3550 100644 --- a/config/pkg/ext/ext-grpc.yml +++ b/config/pkg/ext/ext-grpc.yml @@ -2,8 +2,8 @@ ext-grpc: type: php-extension artifact: source: - type: pecl - name: grpc + type: pie + repo: grpc/grpc-php-ext metadata: license-files: [LICENSE] license: Apache-2.0 diff --git a/config/pkg/ext/ext-igbinary.yml b/config/pkg/ext/ext-igbinary.yml index 1a80831bb..c71a1f363 100644 --- a/config/pkg/ext/ext-igbinary.yml +++ b/config/pkg/ext/ext-igbinary.yml @@ -2,8 +2,10 @@ ext-igbinary: type: php-extension artifact: source: - type: pecl - name: igbinary + type: ghtagtar + repo: igbinary/igbinary + match: tags/\d+(\.\d+)+$ + extract: php-src/ext/igbinary metadata: license-files: [COPYING] license: BSD-3-Clause diff --git a/config/pkg/ext/ext-imagick.yml b/config/pkg/ext/ext-imagick.yml index 2a1c221c3..21e3915eb 100644 --- a/config/pkg/ext/ext-imagick.yml +++ b/config/pkg/ext/ext-imagick.yml @@ -2,8 +2,8 @@ ext-imagick: type: php-extension artifact: source: - type: pecl - name: imagick + type: pie + repo: imagick/imagick metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-imap.yml b/config/pkg/ext/ext-imap.yml index 3abcebb8b..52188ca77 100644 --- a/config/pkg/ext/ext-imap.yml +++ b/config/pkg/ext/ext-imap.yml @@ -2,8 +2,9 @@ ext-imap: type: php-extension artifact: source: - type: pecl - name: imap + type: ghtagtar + repo: php/pecl-mail-imap + extract: php-src/ext/imap metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-inotify.yml b/config/pkg/ext/ext-inotify.yml index d69847ee4..ee8ec8bb6 100644 --- a/config/pkg/ext/ext-inotify.yml +++ b/config/pkg/ext/ext-inotify.yml @@ -2,8 +2,8 @@ ext-inotify: type: php-extension artifact: source: - type: pecl - name: inotify + type: pie + repo: arnaud-lb/inotify metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-memcache.yml b/config/pkg/ext/ext-memcache.yml index 6e1f4385c..119299685 100644 --- a/config/pkg/ext/ext-memcache.yml +++ b/config/pkg/ext/ext-memcache.yml @@ -2,8 +2,9 @@ ext-memcache: type: php-extension artifact: source: - type: pecl - name: memcache + type: ghtar + repo: websupport-sk/pecl-memcache + extract: php-src/ext/memcache metadata: license-files: [LICENSE] license: PHP-3.0 diff --git a/config/pkg/ext/ext-memcached.yml b/config/pkg/ext/ext-memcached.yml index b320c6699..5f3eb81b3 100644 --- a/config/pkg/ext/ext-memcached.yml +++ b/config/pkg/ext/ext-memcached.yml @@ -2,8 +2,8 @@ ext-memcached: type: php-extension artifact: source: - type: pecl - name: memcached + type: pie + repo: php-memcached/php-memcached metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-msgpack.yml b/config/pkg/ext/ext-msgpack.yml index 8b230c31a..52e35edec 100644 --- a/config/pkg/ext/ext-msgpack.yml +++ b/config/pkg/ext/ext-msgpack.yml @@ -2,8 +2,8 @@ ext-msgpack: type: php-extension artifact: source: - type: pecl - name: msgpack + type: pie + repo: msgpack/msgpack-php metadata: license-files: [LICENSE] license: BSD-3-Clause diff --git a/config/pkg/ext/ext-mysqlnd_ed25519.yml b/config/pkg/ext/ext-mysqlnd_ed25519.yml index e7aa3de89..f6f79977f 100644 --- a/config/pkg/ext/ext-mysqlnd_ed25519.yml +++ b/config/pkg/ext/ext-mysqlnd_ed25519.yml @@ -4,7 +4,6 @@ ext-mysqlnd_ed25519: source: type: pie repo: mariadb/mysqlnd_ed25519 - extract: php-src/ext/mysqlnd_ed25519 metadata: license-files: [LICENSE] license: BSD-3-Clause diff --git a/config/pkg/ext/ext-mysqlnd_parsec.yml b/config/pkg/ext/ext-mysqlnd_parsec.yml index 903d65c40..955058e7c 100644 --- a/config/pkg/ext/ext-mysqlnd_parsec.yml +++ b/config/pkg/ext/ext-mysqlnd_parsec.yml @@ -4,7 +4,6 @@ ext-mysqlnd_parsec: source: type: pie repo: mariadb/mysqlnd_parsec - extract: php-src/ext/mysqlnd_parsec metadata: license-files: [LICENSE] license: BSD-3-Clause diff --git a/config/pkg/ext/ext-opentelemetry.yml b/config/pkg/ext/ext-opentelemetry.yml index 5caebef2a..5e80ad450 100644 --- a/config/pkg/ext/ext-opentelemetry.yml +++ b/config/pkg/ext/ext-opentelemetry.yml @@ -2,8 +2,8 @@ ext-opentelemetry: type: php-extension artifact: source: - type: pecl - name: opentelemetry + type: pie + repo: open-telemetry/ext-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 index 94103f578..d9ed0b2fe 100644 --- a/config/pkg/ext/ext-parallel.yml +++ b/config/pkg/ext/ext-parallel.yml @@ -2,8 +2,8 @@ ext-parallel: type: php-extension artifact: source: - type: pecl - name: parallel + type: pie + repo: pecl/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 index 3fac61d08..59a5fbbed 100644 --- a/config/pkg/ext/ext-pcov.yml +++ b/config/pkg/ext/ext-pcov.yml @@ -2,8 +2,8 @@ ext-pcov: type: php-extension artifact: source: - type: pecl - name: pcov + type: pie + repo: pecl/pcov metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-redis.yml b/config/pkg/ext/ext-redis.yml index c05b4ee26..84f603e3f 100644 --- a/config/pkg/ext/ext-redis.yml +++ b/config/pkg/ext/ext-redis.yml @@ -2,8 +2,8 @@ ext-redis: type: php-extension artifact: source: - type: pecl - name: redis + type: pie + repo: phpredis/phpredis metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-spx.yml b/config/pkg/ext/ext-spx.yml index edf41f514..32f7a2e11 100644 --- a/config/pkg/ext/ext-spx.yml +++ b/config/pkg/ext/ext-spx.yml @@ -4,7 +4,6 @@ ext-spx: source: type: pie repo: noisebynorthwest/php-spx - extract: php-src/ext/spx metadata: license-files: [LICENSE] license: GPL-3.0-or-later diff --git a/config/pkg/ext/ext-ssh2.yml b/config/pkg/ext/ext-ssh2.yml index 14c9bf327..53b065be5 100644 --- a/config/pkg/ext/ext-ssh2.yml +++ b/config/pkg/ext/ext-ssh2.yml @@ -2,8 +2,9 @@ ext-ssh2: type: php-extension artifact: source: - type: pecl - name: ssh2 + type: ghtar + repo: php/pecl-networking-ssh2 + extract: php-src/ext/ssh2 metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-trader.yml b/config/pkg/ext/ext-trader.yml index 03dcadc60..4ef8369fa 100644 --- a/config/pkg/ext/ext-trader.yml +++ b/config/pkg/ext/ext-trader.yml @@ -2,8 +2,9 @@ ext-trader: type: php-extension artifact: source: - type: pecl - name: trader + type: ghtagtar + repo: php/pecl-math-trader + extract: php-src/ext/trader metadata: license-files: [LICENSE] license: BSD-2-Clause diff --git a/config/pkg/ext/ext-uuid.yml b/config/pkg/ext/ext-uuid.yml index 3d71aa8b4..e2e055310 100644 --- a/config/pkg/ext/ext-uuid.yml +++ b/config/pkg/ext/ext-uuid.yml @@ -2,8 +2,8 @@ ext-uuid: type: php-extension artifact: source: - type: pecl - name: uuid + type: pie + repo: pecl/uuid metadata: license-files: [LICENSE] license: LGPL-2.1-only diff --git a/config/pkg/ext/ext-uv.yml b/config/pkg/ext/ext-uv.yml index 59d4a8c90..a974ed05e 100644 --- a/config/pkg/ext/ext-uv.yml +++ b/config/pkg/ext/ext-uv.yml @@ -2,9 +2,9 @@ ext-uv: type: php-extension artifact: source: - type: pecl - name: uv - prefer-stable: false + type: ghtagtar + repo: amphp/ext-uv + extract: php-src/ext/uv metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-xhprof.yml b/config/pkg/ext/ext-xhprof.yml index a554d8375..f88baec88 100644 --- a/config/pkg/ext/ext-xhprof.yml +++ b/config/pkg/ext/ext-xhprof.yml @@ -2,8 +2,8 @@ ext-xhprof: type: php-extension artifact: source: - type: pecl - name: xhprof + type: ghtar + repo: longxinH/xhprof extract: php-src/ext/xhprof-src metadata: license-files: [LICENSE] diff --git a/config/pkg/ext/ext-xlswriter.yml b/config/pkg/ext/ext-xlswriter.yml index 01b99ed90..7b211cbc8 100644 --- a/config/pkg/ext/ext-xlswriter.yml +++ b/config/pkg/ext/ext-xlswriter.yml @@ -2,8 +2,8 @@ ext-xlswriter: type: php-extension artifact: source: - type: pecl - name: xlswriter + type: pie + repo: viest/xlswriter metadata: license-files: [LICENSE] license: BSD-2-Clause diff --git a/config/pkg/ext/ext-yac.yml b/config/pkg/ext/ext-yac.yml index e10bea064..9608cad6b 100644 --- a/config/pkg/ext/ext-yac.yml +++ b/config/pkg/ext/ext-yac.yml @@ -2,8 +2,9 @@ ext-yac: type: php-extension artifact: source: - type: pecl - name: yac + type: ghtagtar + repo: laruence/yac + extract: php-src/ext/yac metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-zip.yml b/config/pkg/ext/ext-zip.yml index 9a4b0282b..925353058 100644 --- a/config/pkg/ext/ext-zip.yml +++ b/config/pkg/ext/ext-zip.yml @@ -2,9 +2,8 @@ ext-zip: type: php-extension artifact: source: - type: pecl - name: zip - extract: ext-zip + type: pie + repo: pecl/zip metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-zstd.yml b/config/pkg/ext/ext-zstd.yml index 9b01422be..b07db490c 100644 --- a/config/pkg/ext/ext-zstd.yml +++ b/config/pkg/ext/ext-zstd.yml @@ -2,9 +2,8 @@ ext-zstd: type: php-extension artifact: source: - type: git - url: 'https://github.com/kjdev/php-ext-zstd' - rev: master + type: ghtar + repo: kjdev/php-ext-zstd extract: php-src/ext/zstd metadata: license-files: [LICENSE] diff --git a/config/pkg/lib/gmssl.yml b/config/pkg/lib/gmssl.yml index 076234353..ff4250871 100644 --- a/config/pkg/lib/gmssl.yml +++ b/config/pkg/lib/gmssl.yml @@ -2,8 +2,9 @@ gmssl: type: library artifact: source: - type: ghtar - repo: guanzhi/GmSSL + type: git + url: 'https://github.com/guanzhi/GmSSL.git' + rev: master metadata: license-files: [LICENSE] license: Apache-2.0 diff --git a/config/pkg/target/llvm-compiler-rt.yml b/config/pkg/target/llvm-compiler-rt.yml new file mode 100644 index 000000000..b7c7da74d --- /dev/null +++ b/config/pkg/target/llvm-compiler-rt.yml @@ -0,0 +1,6 @@ +llvm-compiler-rt: + type: target + artifact: + binary: custom + depends: + - zig diff --git a/config/pkg/target/llvm-tools.yml b/config/pkg/target/llvm-tools.yml new file mode 100644 index 000000000..c43721ac6 --- /dev/null +++ b/config/pkg/target/llvm-tools.yml @@ -0,0 +1,6 @@ +llvm-tools: + type: target + artifact: + binary: custom + depends: + - zig diff --git a/src/Package/Artifact/gmssl.php b/src/Package/Artifact/gmssl.php index 4caa4d1e2..833c7b415 100644 --- a/src/Package/Artifact/gmssl.php +++ b/src/Package/Artifact/gmssl.php @@ -17,4 +17,11 @@ public function patch(string $target_path): void FileSystem::replaceFileStr($target_path . '/src/hex.c', 'unsigned char *OPENSSL_hexstr2buf(const char *str, size_t *len)', 'unsigned char *GMSSL_hexstr2buf(const char *str, size_t *len)'); FileSystem::replaceFileStr($target_path . '/src/hex.c', 'OPENSSL_hexchar2int', 'GMSSL_hexchar2int'); } + + #[AfterSourceExtract('ext-gmssl')] + #[PatchDescription('Rename pbkdf2_hmac_sm3_genkey() to sm3_pbkdf2() (renamed upstream in GmSSL 3.2+; identical signature)')] + public function patchExt(string $target_path): void + { + FileSystem::replaceFileStr($target_path . '/gmssl.c', 'pbkdf2_hmac_sm3_genkey(', 'sm3_pbkdf2('); + } } diff --git a/src/Package/Artifact/go_win.php b/src/Package/Artifact/go_win.php index e06e615ab..7a87d2db2 100644 --- a/src/Package/Artifact/go_win.php +++ b/src/Package/Artifact/go_win.php @@ -20,8 +20,6 @@ class go_win ])] public function downBinary(ArtifactDownloader $downloader): DownloadResult { - $pkgroot = PKG_ROOT_PATH; - // get version [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text', retries: $downloader->getRetry()) ?: ''); if ($version === '') { @@ -52,7 +50,7 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult throw new DownloaderException("Hash mismatch for downloaded go-win binary. Expected {$hash}, got {$file_hash}"); } - return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-win", verified: true, version: $version); + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: '{pkg_root_path}/go-win', verified: true, version: $version); } #[CustomBinaryCheckUpdate('go-win', ['windows-x86_64'])] diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 51ccfb87b..08bf02d32 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -25,7 +25,6 @@ class go_xcaddy ])] public function downBinary(ArtifactDownloader $downloader): DownloadResult { - $pkgroot = PKG_ROOT_PATH; $name = SystemTarget::getCurrentPlatformString(); $arch = match (explode('-', $name)[1]) { 'x86_64' => 'amd64', @@ -64,7 +63,7 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult if ($file_hash !== $hash) { throw new DownloaderException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}"); } - return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version); + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: '{pkg_root_path}/go-xcaddy', verified: true, version: $version); } #[CustomBinaryCheckUpdate('go-xcaddy', [ @@ -109,7 +108,7 @@ public function afterExtract(string $target_path): void 'GOROOT' => "{$target_path}", 'GOBIN' => "{$target_path}/bin", 'GOPATH' => "{$target_path}/go", - ])->exec('CC=cc go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest'); + ])->exec('CGO_ENABLED=0 go install github.com/caddyserver/xcaddy/cmd/xcaddy@master'); GlobalEnvManager::addPathIfNotExists("{$target_path}/bin"); } } diff --git a/src/Package/Artifact/llvm_compiler_rt.php b/src/Package/Artifact/llvm_compiler_rt.php new file mode 100644 index 000000000..13f643793 --- /dev/null +++ b/src/Package/Artifact/llvm_compiler_rt.php @@ -0,0 +1,193 @@ +detectZigLlvmVersion() + ?? throw new DownloaderException('llvm-compiler-rt: could not detect bundled clang version from zig cc --version'); + $tarball = "compiler-rt-{$llvmVersion}.src.tar.xz"; + $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-{$llvmVersion}/{$tarball}"; + $tarballPath = DOWNLOAD_PATH . '/' . $tarball; + default_shell()->executeCurlDownload($url, $tarballPath, headers: $this->getGitHubTokenHeaders(), retries: $downloader->getRetry()); + return DownloadResult::archive($tarball, ['url' => $url, 'version' => $llvmVersion], extract: '{source_path}/llvm-compiler-rt', verified: false, version: $llvmVersion); + } + + #[CustomBinaryCheckUpdate('llvm-compiler-rt', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $llvmVersion = $this->detectZigLlvmVersion() + ?? throw new DownloaderException('llvm-compiler-rt: could not detect bundled clang version from zig cc --version'); + return new CheckUpdateResult( + old: $old_version, + new: $llvmVersion, + needUpdate: $old_version === null || $llvmVersion !== $old_version, + ); + } + + #[AfterBinaryExtract('llvm-compiler-rt', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function postExtract(string $target_path): void + { + $this->buildForTriple($target_path); + } + + public function buildForTriple(?string $sourceDir = null, ?string $triple = null): void + { + $sourceDir ??= SOURCE_PATH . '/llvm-compiler-rt'; + $triple ??= SystemTarget::getCanonicalTriple(); + $libDir = zig::path() . '/lib/' . $triple; + if ($this->isBuilt($libDir)) { + return; + } + if (!is_dir($sourceDir) || !is_dir("{$sourceDir}/lib/profile")) { + throw new BuildFailureException("llvm-compiler-rt: missing source at {$sourceDir} (extraction layout changed?)"); + } + f_mkdir($libDir, recursive: true); + $profileLib = "{$libDir}/libclang_rt.profile.a"; + $crtBegin = "{$libDir}/clang_rt.crtbegin.o"; + $crtEnd = "{$libDir}/clang_rt.crtend.o"; + if (!file_exists($profileLib)) { + $this->buildProfileRuntime($sourceDir, $profileLib, $triple); + } + if (!file_exists($crtBegin) || !file_exists($crtEnd)) { + $this->buildCrtObjects($sourceDir, $crtBegin, $crtEnd, $triple); + } + $cpuModelLib = "{$libDir}/libclang_rt.cpu_model.a"; + if (self::cpuModelArch($triple) !== null && !file_exists($cpuModelLib)) { + $this->buildCpuModelBuiltins($sourceDir, $cpuModelLib, $triple); + } + } + + public function isBuilt(string $libDir): bool + { + return file_exists("{$libDir}/libclang_rt.profile.a") + && file_exists("{$libDir}/clang_rt.crtbegin.o") + && file_exists("{$libDir}/clang_rt.crtend.o") + && (self::cpuModelArch(basename($libDir)) === null || file_exists("{$libDir}/libclang_rt.cpu_model.a")); + } + + private function detectZigLlvmVersion(): ?string + { + if (!zig::isInstalled()) { + return null; + } + [$rc, $out] = shell()->execWithResult(escapeshellarg(zig::binary()) . ' cc --version', false); + if ($rc !== 0) { + return null; + } + return preg_match('/clang version (\d+\.\d+\.\d+)/', implode("\n", $out), $m) ? $m[1] : null; + } + + private function buildProfileRuntime(string $srcRoot, string $libPath, string $triple): void + { + $profileSrc = "{$srcRoot}/lib/profile"; + $profileInc = "{$srcRoot}/include"; + // Skip OS-specific sources we can't satisfy without their SDKs. + $skip = ['/PlatformAIX', '/PlatformDarwin', '/PlatformFuchsia', '/PlatformOther', '/PlatformWindows', '/WindowsMMap']; + $sources = array_filter( + array_merge(glob("{$profileSrc}/*.c") ?: [], glob("{$profileSrc}/*.cpp") ?: []), + fn ($f) => !array_any($skip, fn ($s) => str_contains($f, $s)), + ); + + $objDir = "{$srcRoot}/obj-profile-{$triple}"; + f_mkdir($objDir, recursive: true); + $cflags = "-target {$triple} -c -O2 -fPIC -fvisibility=hidden " + . '-I' . escapeshellarg($profileInc) . ' ' + . '-DCOMPILER_RT_HAS_ATOMICS=1 -DCOMPILER_RT_HAS_FCNTL_LCK=1 -DCOMPILER_RT_HAS_UNAME=1'; + $srcArgs = implode(' ', array_map('escapeshellarg', $sources)); + shell()->cd($objDir)->exec("zig cc {$cflags} {$srcArgs}"); + shell()->cd($objDir)->exec('zig ar rcs ' . escapeshellarg($libPath) . ' *.o'); + } + + private function buildCrtObjects(string $srcRoot, string $crtBegin, string $crtEnd, string $triple): void + { + $beginSrc = "{$srcRoot}/lib/builtins/crtbegin.c"; + $endSrc = "{$srcRoot}/lib/builtins/crtend.c"; + if (!is_file($beginSrc) || !is_file($endSrc)) { + throw new BuildFailureException("llvm-compiler-rt: crtbegin/crtend source missing under {$srcRoot}/lib/builtins"); + } + $cflags = "-target {$triple} -c -O2 -fPIC -fvisibility=hidden -DCRT_HAS_INITFINI_ARRAY"; + foreach ([[$beginSrc, $crtBegin], [$endSrc, $crtEnd]] as [$src, $dst]) { + shell()->exec("zig cc {$cflags} -o " . escapeshellarg($dst) . ' ' . escapeshellarg($src)); + } + } + + /** + * Build libclang_rt.cpu_model.a, provides + * the globals that __builtin_cpu_supports() reference. + */ + private function buildCpuModelBuiltins(string $srcRoot, string $libPath, string $triple): void + { + $builtins = "{$srcRoot}/lib/builtins"; + $family = self::cpuModelArch($triple); + $cpuModelDir = "{$builtins}/cpu_model"; + if (is_dir($cpuModelDir)) { + $src = "{$cpuModelDir}/{$family}.c"; + $includes = '-I' . escapeshellarg($builtins) . ' -I' . escapeshellarg($cpuModelDir); + } else { + $src = "{$builtins}/cpu_model.c"; + $includes = '-I' . escapeshellarg($builtins); + } + if (!is_file($src)) { + throw new BuildFailureException("llvm-compiler-rt: cpu_model source not found for {$triple} under {$builtins}"); + } + + $objDir = "{$srcRoot}/obj-cpu-model-{$triple}"; + f_mkdir($objDir, recursive: true); + $obj = "{$objDir}/cpu_model.o"; + $cflags = "-target {$triple} -c -O2 -fPIC {$includes}"; + shell()->exec('zig cc ' . $cflags . ' -o ' . escapeshellarg($obj) . ' ' . escapeshellarg($src)); + shell()->exec('zig ar rcs ' . escapeshellarg($libPath) . ' ' . escapeshellarg($obj)); + } + + private static function cpuModelArch(string $triple): ?string + { + $arch = explode('-', $triple)[0]; + return match (true) { + in_array($arch, ['x86_64', 'amd64', 'i386', 'i686', 'x86'], true) => 'x86', + in_array($arch, ['aarch64', 'arm64'], true) => 'aarch64', + str_starts_with($arch, 'riscv') => 'riscv', + default => null, + }; + } +} diff --git a/src/Package/Artifact/llvm_tools.php b/src/Package/Artifact/llvm_tools.php new file mode 100644 index 000000000..4a64cbbdc --- /dev/null +++ b/src/Package/Artifact/llvm_tools.php @@ -0,0 +1,149 @@ +detectLlvmVersion() + ?? throw new DownloaderException('Could not detect a clang version on host; install zig or clang first'); + $tarball = "llvm-project-{$llvmVersion}.src.tar.xz"; + $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-{$llvmVersion}/{$tarball}"; + $tarballPath = DOWNLOAD_PATH . '/' . $tarball; + default_shell()->executeCurlDownload($url, $tarballPath, headers: $this->getGitHubTokenHeaders(), retries: $downloader->getRetry()); + return DownloadResult::archive($tarball, ['url' => $url, 'version' => $llvmVersion], extract: '{source_path}/llvm-tools', verified: false, version: $llvmVersion); + } + + #[CustomBinaryCheckUpdate('llvm-tools', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $llvmVersion = $this->detectLlvmVersion() + ?? throw new DownloaderException('Could not detect a clang version on host; install zig or clang first'); + return new CheckUpdateResult( + old: $old_version, + new: $llvmVersion, + needUpdate: $old_version === null || $llvmVersion !== $old_version, + ); + } + + #[AfterBinaryExtract('llvm-tools', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function postExtract(string $target_path): void + { + $this->buildForHost($target_path); + } + + public function buildForHost(?string $sourceRoot = null): void + { + $sourceRoot ??= SOURCE_PATH . '/llvm-tools'; + $binDir = PKG_ROOT_PATH . '/llvm-tools/bin'; + if ($this->allBuilt($binDir)) { + return; + } + $llvmDir = "{$sourceRoot}/llvm"; + if (!is_dir($llvmDir)) { + throw new BuildFailureException("llvm-tools: missing source at {$llvmDir} (extraction layout changed?)"); + } + $buildDir = "{$sourceRoot}/build"; + $installDir = PKG_ROOT_PATH . '/llvm-tools'; + f_mkdir($buildDir, recursive: true); + f_mkdir($binDir, recursive: true); + + $cmakeArgs = implode(' ', array_map('escapeshellarg', [ + '-S', $llvmDir, + '-B', $buildDir, + '-DCMAKE_BUILD_TYPE=Release', + '-DLLVM_ENABLE_PROJECTS=', + '-DLLVM_TARGETS_TO_BUILD=', + '-DLLVM_INCLUDE_BENCHMARKS=OFF', + '-DLLVM_INCLUDE_TESTS=OFF', + '-DLLVM_INCLUDE_EXAMPLES=OFF', + '-DLLVM_INCLUDE_DOCS=OFF', + '-DLLVM_ENABLE_ZLIB=OFF', + '-DLLVM_ENABLE_ZSTD=OFF', + '-DLLVM_ENABLE_LIBXML2=OFF', + '-DLLVM_ENABLE_TERMINFO=OFF', + '-DLLVM_ENABLE_LIBEDIT=OFF', + '-DLLVM_ENABLE_LIBPFM=OFF', + '-DLLVM_BUILD_LLVM_DYLIB=OFF', + '-DLLVM_LINK_LLVM_DYLIB=OFF', + '-DBUILD_SHARED_LIBS=OFF', + '-DCMAKE_C_COMPILER=' . zig::binary('zig-cc'), + '-DCMAKE_CXX_COMPILER=' . zig::binary('zig-c++'), + '-DCMAKE_INSTALL_PREFIX=' . $installDir, + ])); + $jobs = ApplicationContext::get(PackageBuilder::class)->concurrency; + $targetArgs = implode(' ', array_map(fn ($t) => '--target ' . escapeshellarg($t), self::TOOLS)); + + shell() + ->setEnv(['SPC_TARGET' => GNU_ARCH . '-linux-musl']) + ->exec('cmake ' . $cmakeArgs) + ->exec('cmake --build ' . escapeshellarg($buildDir) . ' ' . $targetArgs . " -j {$jobs}"); + + foreach (self::TOOLS as $t) { + $built = "{$buildDir}/bin/{$t}"; + if (!is_file($built)) { + throw new BuildFailureException("llvm-tools: missing build output {$built}"); + } + copy($built, "{$binDir}/{$t}"); + chmod("{$binDir}/{$t}", 0755); + } + } + + public function allBuilt(string $binDir): bool + { + foreach (self::TOOLS as $t) { + $p = "{$binDir}/{$t}"; + if (!is_file($p) || !is_executable($p)) { + return false; + } + } + return true; + } + + private function detectLlvmVersion(): ?string + { + if (!zig::isInstalled()) { + return null; + } + [$rc, $out] = shell()->execWithResult(escapeshellarg(zig::binary()) . ' cc --version', false); + if ($rc !== 0) { + return null; + } + return preg_match('/clang version (\d+\.\d+\.\d+)/', implode("\n", $out), $m) ? $m[1] : null; + } +} diff --git a/src/Package/Artifact/rust.php b/src/Package/Artifact/rust.php index e5c9f5259..cc9bb1757 100644 --- a/src/Package/Artifact/rust.php +++ b/src/Package/Artifact/rust.php @@ -46,7 +46,7 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult $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); + return DownloadResult::archive(basename($path), ['url' => $download_url, 'version' => $latest_version], extract: '{pkg_root_path}/rust-install', verified: false, version: $latest_version); } #[CustomBinaryCheckUpdate('rust', [ diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 95520aa4b..123139d96 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -15,6 +15,23 @@ class zig { + /** Directory zig extracts into. */ + public static function path(): string + { + return PKG_ROOT_PATH . '/zig'; + } + + /** Path to a binary inside the zig install dir (zig, zig-cc, zig-c++, zig-ar, …). */ + public static function binary(string $name = 'zig'): string + { + return self::path() . '/' . $name; + } + + public static function isInstalled(): bool + { + return is_file(self::binary()); + } + #[CustomBinary('zig', [ 'linux-x86_64', 'linux-aarch64', @@ -61,7 +78,7 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult if ($file_hash !== $sha256) { throw new DownloaderException("Hash mismatch for downloaded Zig binary. Expected {$sha256}, got {$file_hash}"); } - return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version); + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: '{pkg_root_path}/zig', verified: true, version: $latest_version); } #[CustomBinaryCheckUpdate('zig', [ @@ -110,26 +127,24 @@ public function postExtractZig(string $target_path): void break; } } - if ($all_exist) { - return; - } - - $script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh'; - $script_content = file_get_contents($script_path); + if (!$all_exist) { + $script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh'; + $script_content = file_get_contents($script_path); - file_put_contents("{$target_path}/zig-cc", $script_content); - chmod("{$target_path}/zig-cc", 0755); + file_put_contents("{$target_path}/zig-cc", $script_content); + chmod("{$target_path}/zig-cc", 0755); - $script_content = str_replace('zig cc', 'zig c++', $script_content); - file_put_contents("{$target_path}/zig-c++", $script_content); - file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@"); - file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@"); - file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@"); - file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@"); - chmod("{$target_path}/zig-c++", 0755); - chmod("{$target_path}/zig-ar", 0755); - chmod("{$target_path}/zig-ld.lld", 0755); - chmod("{$target_path}/zig-ranlib", 0755); - chmod("{$target_path}/zig-objcopy", 0755); + $script_content = str_replace('zig cc', 'zig c++', $script_content); + file_put_contents("{$target_path}/zig-c++", $script_content); + file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@"); + file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@"); + file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@"); + file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@"); + chmod("{$target_path}/zig-c++", 0755); + chmod("{$target_path}/zig-ar", 0755); + chmod("{$target_path}/zig-ld.lld", 0755); + chmod("{$target_path}/zig-ranlib", 0755); + chmod("{$target_path}/zig-objcopy", 0755); + } } } diff --git a/src/Package/Extension/event.php b/src/Package/Extension/event.php index db1192745..2b9515cf7 100644 --- a/src/Package/Extension/event.php +++ b/src/Package/Extension/event.php @@ -35,12 +35,37 @@ public function getUnixConfigureArg(PackageInstaller $installer): string #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-event')] #[PatchDescription('Prevent event extension compile error on macOS')] + #[PatchDescription('Patch libevent http_connection.c to use a const peer address')] public function patchBeforeMake(PackageInstaller $installer): void { + $php_src = $installer->getTargetPackage('php')->getSourceDir(); // 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', ''); } + $this->patchLibeventConstPeer("{$php_src}/ext/event"); + } + + #[BeforeStage('ext-event', [PhpExtensionPackage::class, 'makeForUnix'])] + #[PatchDescription('Patch libevent http_connection.c to use a const peer address')] + public function patchBeforeSharedMake(PhpExtensionPackage $pkg): void + { + $this->patchLibeventConstPeer($pkg->getSourceDir()); + } + + private function patchLibeventConstPeer(string $event_source_dir): bool + { + $file = "{$event_source_dir}/php8/classes/http_connection.c"; + if (!is_file($file)) { + return false; + } + // libevent >= 2.2 changed evhttp_connection_get_peer()'s + // address arg to `const char **` + $header = $this->getBuilder()->getBuildRootPath() . '/include/event2/http.h'; + $isConst = is_file($header) + && preg_match('/evhttp_connection_get_peer\s*\([^;]*\bconst\s+char\s*\*\*\s*address/s', file_get_contents($header)) === 1; + return (bool) ($isConst + ? FileSystem::replaceFileRegex($file, '/^\tchar \*address;$/m', "\tconst char *address;") + : FileSystem::replaceFileRegex($file, '/^\tconst char \*address;$/m', "\tchar *address;")); } } diff --git a/src/Package/Extension/swoole.php b/src/Package/Extension/swoole.php index c269ba544..b0232aa03 100644 --- a/src/Package/Extension/swoole.php +++ b/src/Package/Extension/swoole.php @@ -62,6 +62,23 @@ public function patchBeforeMake2(): void } } + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-swoole')] + #[PatchDescription('Initialise SWOOLE_G(cli) to false in the module globals ctor (upstream bug: it is left uninitialised, so under a threaded SAPI like frankenphp its garbage value passes the CLI-only guard in RINIT and segfaults on the first request)')] + public function patchBeforeMake3(): void + { + // php_swoole_init_globals() sets every other global explicitly but never assigns + // `cli`, relying on zero-initialisation. That holds for CLI/embed but not for + // frankenphp's per-thread ZTS globals, where `cli` reads garbage-nonzero. Since + // sapi_module.name is always "frankenphp" there, the `cli = true` conditional never + // fires — so the fix is simply to default `cli` to false at the source of the gate. + // Reported upstream; drop this patch once swoole ships the initialiser. + FileSystem::replaceFileStr( + $this->getSourceDir() . '/ext-src/php_swoole.cc', + " swoole_globals->leak_detection = false;\n\n if (strcmp(\"cli\", sapi_module.name) == 0", + " swoole_globals->leak_detection = false;\n swoole_globals->cli = false;\n\n if (strcmp(\"cli\", sapi_module.name) == 0", + ); + } + #[CustomPhpConfigureArg('Darwin')] #[CustomPhpConfigureArg('Linux')] public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, PackageInstaller $installer): string diff --git a/src/Package/Library/bzip2.php b/src/Package/Library/bzip2.php index 90fcce7c6..7f99c22b4 100644 --- a/src/Package/Library/bzip2.php +++ b/src/Package/Library/bzip2.php @@ -17,7 +17,9 @@ class bzip2 #[PatchBeforeBuild] public function patchBeforeBuild(LibraryPackage $lib): void { - FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall'); + // Makefile pins -O2 -fPIC; inject SPC_DEFAULT_CFLAGS + $extra = deduplicate_flags(trim((string) getenv('SPC_DEFAULT_CFLAGS')) . ' -fPIC -Wall'); + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', "CFLAGS={$extra}"); } #[BuildFor('Windows')] diff --git a/src/Package/Library/fastlz.php b/src/Package/Library/fastlz.php index f9dd010dc..7c3792cd2 100644 --- a/src/Package/Library/fastlz.php +++ b/src/Package/Library/fastlz.php @@ -18,9 +18,11 @@ public function build(LibraryPackage $lib): void { $cc = getenv('CC') ?: 'cc'; $ar = getenv('AR') ?: 'ar'; + $extra = trim((string) getenv('SPC_DEFAULT_CFLAGS')); + $extra = $extra !== '' ? $extra . ' -fPIC' : '-O3 -fPIC'; shell()->cd($lib->getSourceDir())->initializeEnv($lib) - ->exec("{$cc} -c -O3 -fPIC fastlz.c -o fastlz.o") + ->exec("{$cc} -c {$extra} fastlz.c -o fastlz.o") ->exec("{$ar} rcs libfastlz.a fastlz.o"); // Copy header file diff --git a/src/Package/Library/icu.php b/src/Package/Library/icu.php index 7364e94ab..bab245081 100644 --- a/src/Package/Library/icu.php +++ b/src/Package/Library/icu.php @@ -24,9 +24,12 @@ public function beforePack(LibraryPackage $lib): void #[BuildFor('Linux')] public function buildLinux(LibraryPackage $lib, ToolchainInterface $toolchain, PackageBuilder $builder): void { + // runConfigureICU bakes CXXFLAGS/LDFLAGS, apply user flags too + $userCxxFlags = trim((string) getenv('SPC_DEFAULT_CXXFLAGS')); + $userLdFlags = trim((string) getenv('SPC_DEFAULT_LDFLAGS')); $cppflags = 'CPPFLAGS="-DU_CHARSET_IS_UTF8=1 -DU_USING_ICU_NAMESPACE=1 -DU_STATIC_IMPLEMENTATION=1 -DPIC -fPIC"'; - $cxxflags = 'CXXFLAGS="-std=c++17 -DPIC -fPIC -fno-ident"'; - $ldflags = $toolchain->isStatic() ? 'LDFLAGS="-static"' : ''; + $cxxflags = "CXXFLAGS=\"-std=c++17 -DPIC -fPIC -fno-ident {$userCxxFlags}\""; + $ldflags = $toolchain->isStatic() ? "LDFLAGS=\"-static {$userLdFlags}\"" : "LDFLAGS=\"{$userLdFlags}\""; shell()->cd($lib->getSourceDir() . '/source')->initializeEnv($lib) ->exec( "{$cppflags} {$cxxflags} {$ldflags} " . diff --git a/src/Package/Library/imagemagick.php b/src/Package/Library/imagemagick.php index fd5de9ed8..d423163a2 100644 --- a/src/Package/Library/imagemagick.php +++ b/src/Package/Library/imagemagick.php @@ -42,6 +42,9 @@ public function buildUnix(LibraryPackage $lib, ToolchainInterface $toolchain): v ->addConfigureArgs( '--disable-openmp', '--without-x', + // implicit --with-gcc-arch + // bleeds host cpu features into built binaries + '--without-gcc-arch', ); // special: linux-static target needs `-static` diff --git a/src/Package/Library/jbig.php b/src/Package/Library/jbig.php index 1cfe60b74..94527f457 100644 --- a/src/Package/Library/jbig.php +++ b/src/Package/Library/jbig.php @@ -17,7 +17,9 @@ class jbig #[PatchBeforeBuild] public function patchBeforeBuild(LibraryPackage $lib): void { - FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS = -O2 -W -Wno-unused-result', 'CFLAGS = -O2 -W -Wno-unused-result -fPIC'); + $extra = trim((string) getenv('SPC_DEFAULT_CFLAGS')); + $cflags = ($extra !== '' ? $extra : '-O2') . ' -W -Wno-unused-result -fPIC'; + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS = -O2 -W -Wno-unused-result', "CFLAGS = {$cflags}"); } #[BuildFor('Darwin')] diff --git a/src/Package/Library/krb5.php b/src/Package/Library/krb5.php index 1370afccb..428c6d9b3 100644 --- a/src/Package/Library/krb5.php +++ b/src/Package/Library/krb5.php @@ -27,7 +27,7 @@ public function build(LibraryPackage $lib, PackageInstaller $installer, Toolchai $resolved = array_keys($installer->getResolvedPackages()); $spc = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); - $config = $spc->getPackageDepsConfig($lib->getName(), $resolved, include_suggests: true); + $config = $spc->getPackageDepsConfig($lib->getName(), $resolved); $extraEnv = [ 'CFLAGS' => '-fcommon', 'LIBS' => $config['libs'], diff --git a/src/Package/Library/libaom.php b/src/Package/Library/libaom.php index 7e578242b..7549125d5 100644 --- a/src/Package/Library/libaom.php +++ b/src/Package/Library/libaom.php @@ -9,8 +9,10 @@ use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\ZigToolchain; +use StaticPHP\Util\System\UnixUtil; #[Library('libaom')] class libaom extends LibraryPackage @@ -39,9 +41,23 @@ public function buildUnix(ToolchainInterface $toolchain): void $new = trim($extra . ' -D_GNU_SOURCE'); f_putenv("SPC_COMPILER_EXTRA={$new}"); } + $targetCpu = SystemTarget::getTargetArch(); + if (str_starts_with($targetCpu, 'aarch')) { + $targetCpu = str_replace('aarch', 'arm', $targetCpu); + } + if (!UnixUtil::findCommand('nasm') && !UnixUtil::findCommand('yasm')) { + $targetCpu = 'generic'; + } UnixCMakeExecutor::create($this) ->setBuildDir("{$this->getSourceDir()}/builddir") - ->addConfigureArgs('-DAOM_TARGET_CPU=generic') + ->addConfigureArgs( + "-DAOM_TARGET_CPU={$targetCpu}", + '-DCONFIG_RUNTIME_CPU_DETECT=1', + '-DENABLE_EXAMPLES=OFF', + '-DENABLE_TESTS=OFF', + '-DENABLE_TOOLS=OFF', + '-DENABLE_DOCS=OFF', + ) ->build(); f_putenv("SPC_COMPILER_EXTRA={$extra}"); $this->patchPkgconfPrefix(['aom.pc']); diff --git a/src/Package/Library/libffi.php b/src/Package/Library/libffi.php index 351b9076c..e40127fb3 100644 --- a/src/Package/Library/libffi.php +++ b/src/Package/Library/libffi.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Runtime\SystemTarget; #[Library('libffi')] class libffi extends LibraryPackage @@ -28,7 +29,7 @@ public function buildLinux(): void #[BuildFor('Darwin')] public function buildDarwin(): void { - $arch = getenv('SPC_ARCH'); + $arch = SystemTarget::getTargetArch(); UnixAutoconfExecutor::create($this) ->configure( "--host={$arch}-apple-darwin", diff --git a/src/Package/Library/libheif.php b/src/Package/Library/libheif.php index 65545f365..19ad4c619 100644 --- a/src/Package/Library/libheif.php +++ b/src/Package/Library/libheif.php @@ -24,6 +24,17 @@ public function patchBeforeBuild(LibraryPackage $lib): void 'list(APPEND REQUIRES_PRIVATE "libbrotlidec")' . "\n" . ' list(APPEND REQUIRES_PRIVATE "libbrotlienc")' ); } + // libheif 1.22+ ships a C-incompatible header: `struct heif_bad_pixel` + $heif_properties = $lib->getSourceDir() . '/libheif/api/libheif/heif_properties.h'; + if (file_exists($heif_properties) + && str_contains(file_get_contents($heif_properties), 'struct heif_bad_pixel { uint32_t row; uint32_t column; };') + ) { + FileSystem::replaceFileStr( + $heif_properties, + 'struct heif_bad_pixel { uint32_t row; uint32_t column; };', + 'typedef struct heif_bad_pixel { uint32_t row; uint32_t column; } heif_bad_pixel;' + ); + } } #[BuildFor('Darwin')] diff --git a/src/Package/Library/liblz4.php b/src/Package/Library/liblz4.php index 5dfbc9956..1be56fe03 100644 --- a/src/Package/Library/liblz4.php +++ b/src/Package/Library/liblz4.php @@ -17,10 +17,17 @@ class liblz4 { #[PatchBeforeBuild] - #[PatchDescription('Fix Makefile install target for static liblz4')] + #[PatchDescription('Compile lib sources individually so -flto -c with multiple inputs works under zig-cc/clang')] public function patchBeforeBuild(LibraryPackage $lib): void { - FileSystem::replaceFileStr($lib->getSourceDir() . '/programs/Makefile', 'install: lz4', "install: lz4\n\ninstallewfwef: lz4"); + // `-flto -c` with multiple input files only writes a .o for the + // first source — the others are silently dropped, leaving liblz4.a with a + // single object. Compile each source individually so all .o files exist. + FileSystem::replaceFileStr( + $lib->getSourceDir() . '/lib/Makefile', + "liblz4.a: \$(SRCFILES)\nifeq (\$(BUILD_STATIC),yes) # can be disabled on command line\n\t@echo compiling static library\n\t\$(COMPILE.c) \$^\n\t\$(AR) rcs \$@ *.o\nendif", + "liblz4.a: \$(SRCFILES:.c=.o)\nifeq (\$(BUILD_STATIC),yes) # can be disabled on command line\n\t@echo compiling static library\n\t\$(AR) rcs \$@ \$^\nendif" + ); } #[BuildFor('Windows')] diff --git a/src/Package/Library/libpng.php b/src/Package/Library/libpng.php index 6a1690105..b36004d9d 100644 --- a/src/Package/Library/libpng.php +++ b/src/Package/Library/libpng.php @@ -9,6 +9,7 @@ use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Runtime\Executor\WindowsCMakeExecutor; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; #[Library('libpng')] @@ -24,7 +25,7 @@ public function buildUnix(LibraryPackage $lib): void ]; // Enable architecture-specific optimizations - match (getenv('SPC_ARCH')) { + match (SystemTarget::getTargetArch()) { 'x86_64' => $args[] = '--enable-intel-sse', 'aarch64' => $args[] = '--enable-arm-neon', default => null, diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php index dd591a6f9..74e3a30f6 100644 --- a/src/Package/Library/ncurses.php +++ b/src/Package/Library/ncurses.php @@ -6,6 +6,8 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Toolchain\Interface\ToolchainInterface; @@ -16,6 +18,24 @@ #[Library('ncursesw')] class ncurses { + #[PatchBeforeBuild] + #[PatchDescription('Filter clang/zig "N warning(s) generated." line out of MKlib_gen.sh preprocessor pipe')] + public function patchBeforeBuild(LibraryPackage $lib): void + { + // MKlib_gen.sh feeds the C preprocessor's stdout through a sed/awk + // pipeline into lib_gen.c. zig-cc/clang emits "N warning(s) generated." + // on stdout (not stderr), and that line ends up as invalid C in the + // generated source. Filter it out of the pipe before sed sees it. + $mklibGen = $lib->getSourceDir() . '/ncurses/base/MKlib_gen.sh'; + if (is_file($mklibGen) && !str_contains((string) file_get_contents($mklibGen), "| grep -v ' generated")) { + FileSystem::replaceFileStr( + $mklibGen, + '$preprocessor $TMP 2>/dev/null \\', + "\$preprocessor \$TMP 2>/dev/null \\\n| grep -v ' generated\\.\$' \\", + ); + } + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function build(LibraryPackage $package, ToolchainInterface $toolchain): void @@ -45,6 +65,7 @@ public function build(LibraryPackage $package, ToolchainInterface $toolchain): v '--without-tests', '--without-dlsym', '--without-debug', + '--disable-stripping', '--enable-symlinks', "--with-terminfo-dirs={$terminfo_dirs}", "--bindir={$package->getBinDir()}", diff --git a/src/Package/Library/openssl.php b/src/Package/Library/openssl.php index 69297198d..5e5809321 100644 --- a/src/Package/Library/openssl.php +++ b/src/Package/Library/openssl.php @@ -76,7 +76,7 @@ public function buildWin(LibraryPackage $lib, PackageBuilder $builder): void public function buildForDarwin(LibraryPackage $pkg): void { $zlib_libs = $pkg->getInstaller()->getLibraryPackage('zlib')->getStaticLibFiles(); - $arch = getenv('SPC_ARCH'); + $arch = SystemTarget::getTargetArch(); shell()->cd($pkg->getSourceDir())->initializeEnv($pkg) ->exec( @@ -95,12 +95,7 @@ public function buildForDarwin(LibraryPackage $pkg): void #[BuildFor('Linux')] public function build(LibraryPackage $lib): void { - $arch = getenv('SPC_ARCH'); - - $env = "CC='" . getenv('CC') . ' -idirafter ' . BUILD_INCLUDE_PATH . - ' -idirafter /usr/include/ ' . - ' -idirafter /usr/include/' . getenv('SPC_ARCH') . '-linux-gnu/ ' . - "' "; + $arch = SystemTarget::getTargetArch(); $ex_lib = trim($lib->getInstaller()->getLibraryPackage('zlib')->getStaticLibFiles()) . ' -ldl -pthread'; $zlib_extra = @@ -111,9 +106,15 @@ public function build(LibraryPackage $lib): void $openssl_dir ??= LinuxUtil::getOSRelease()['dist'] === 'redhat' ? '/etc/pki/tls' : '/etc/ssl'; $ex_lib = trim($ex_lib); + // anything we want included (PGO -fprofile-*, LTO, custom hardening) + // has to be appended on the command line *after* the target name. + $userCFlags = trim((string) getenv('SPC_DEFAULT_CFLAGS')); + $userLdFlags = trim((string) getenv('SPC_DEFAULT_LDFLAGS')); + $userExtra = trim($userCFlags . ' ' . $userLdFlags); + shell()->cd($lib->getSourceDir())->initializeEnv($lib) ->exec( - "{$env} ./Configure no-shared zlib " . + './Configure no-shared zlib ' . "--prefix={$lib->getBuildRootPath()} " . "--libdir={$lib->getLibDir()} " . "--openssldir={$openssl_dir} " . @@ -121,7 +122,8 @@ public function build(LibraryPackage $lib): void 'enable-pie ' . 'no-legacy ' . 'no-tests ' . - "linux-{$arch}" + "linux-{$arch} " . + $userExtra ) ->exec('make clean') ->exec("make -j{$lib->getBuilder()->concurrency} CNF_EX_LIBS=\"{$ex_lib}\"") diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 45c48577c..b449e859e 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -58,7 +58,7 @@ public function patchBeforeBuild(): bool public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): void { $spc_config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); - $config = $spc_config->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()), include_suggests: $builder->getOption('with-suggests', false)); + $config = $spc_config->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages())); $env_vars = [ 'CFLAGS' => $config['cflags'] . ' -std=c17', diff --git a/src/Package/Library/qdbm.php b/src/Package/Library/qdbm.php index 358846fb6..c7785683a 100644 --- a/src/Package/Library/qdbm.php +++ b/src/Package/Library/qdbm.php @@ -20,6 +20,11 @@ public function buildUnix(LibraryPackage $lib): void { $ac = UnixAutoconfExecutor::create($lib)->configure(); FileSystem::replaceFileRegex($lib->getSourceDir() . '/Makefile', '/MYLIBS = libqdbm.a.*/m', 'MYLIBS = libqdbm.a'); + // Makefile pins -O3, replace with SPC_DEFAULT_CFLAGS + $extra = trim((string) getenv('SPC_DEFAULT_CFLAGS')); + if ($extra !== '') { + FileSystem::replaceFileRegex($lib->getSourceDir() . '/Makefile', '/^CFLAGS = .*$/m', "CFLAGS = -Wall {$extra}"); + } $ac->make(SystemTarget::getTargetOS() === 'Darwin' ? 'mac' : ''); $lib->patchPkgconfPrefix(['qdbm.pc']); } diff --git a/src/Package/Library/unixodbc.php b/src/Package/Library/unixodbc.php index e482e68be..62149b1f7 100644 --- a/src/Package/Library/unixodbc.php +++ b/src/Package/Library/unixodbc.php @@ -27,7 +27,20 @@ public function buildUnix(): void 'Linux' => '/etc', default => throw new WrongUsageException("Unsupported OS: {$os}"), }; - UnixAutoconfExecutor::create($this) + + // unixodbc bundles libltdl; libltdl is incompatible with -flto + // (https://bugs.gentoo.org/532672). + $stripLto = static fn (string $s): string => clean_spaces((string) preg_replace('/(^|\s)-flto(=\S+)?(?=\s|$)/', ' ', $s)); + $cflags = $stripLto((string) getenv('SPC_DEFAULT_CFLAGS')); + $cxxflags = $stripLto((string) getenv('SPC_DEFAULT_CXXFLAGS')); + $ldflags = $stripLto((string) getenv('SPC_DEFAULT_LDFLAGS')); + + $make = UnixAutoconfExecutor::create($this) + ->setEnv([ + 'CFLAGS' => $cflags, + 'CXXFLAGS' => $cxxflags, + 'LDFLAGS' => $ldflags, + ]) ->configure( '--disable-debug', '--disable-dependency-tracking', @@ -35,8 +48,15 @@ public function buildUnix(): void '--with-included-ltdl', "--sysconfdir={$sysconf_selector}", '--enable-gui=no', - ) - ->make(); + ); + + // The exe/ subdirectory builds odbcinst/iusql/etc, turn it into a no-op + file_put_contents( + "{$this->getSourceDir()}/exe/Makefile", + ".PHONY: all install clean check distclean install-strip\nall install clean check distclean install-strip:\n\t@true\n", + ); + + $make->make(); $this->patchPkgconfPrefix(['odbc.pc', 'odbccr.pc', 'odbcinst.pc']); $this->patchLaDependencyPrefix(); } diff --git a/src/Package/Library/watcher.php b/src/Package/Library/watcher.php index 56f93d931..34f8eb9aa 100644 --- a/src/Package/Library/watcher.php +++ b/src/Package/Library/watcher.php @@ -20,9 +20,8 @@ public function build(): void if (stripos($cflags, '-fpic') === false) { $cflags .= ' -fPIC'; } - $ldflags = $this->getLibExtraLdFlags() ? ' ' . $this->getLibExtraLdFlags() : ''; shell()->cd("{$this->getSourceDir()}/watcher-c") - ->exec(getenv('CXX') . " -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra {$cflags}{$ldflags}") + ->exec(getenv('CXX') . " -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra {$cflags}") ->exec(getenv('AR') . ' rcs libwatcher-c.a libwatcher-c.o'); copy("{$this->getSourceDir()}/watcher-c/libwatcher-c.a", "{$this->getLibDir()}/libwatcher-c.a"); diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index a4ae5c56a..1704aa4ec 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -5,6 +5,7 @@ namespace Package\Target; use Package\Target\php\frankenphp; +use Package\Target\php\pgo; use Package\Target\php\unix; use Package\Target\php\windows; use StaticPHP\Artifact\ArtifactCache; @@ -48,6 +49,7 @@ class php extends TargetPackage use unix; use windows; use frankenphp; + use pgo; /** @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']; @@ -141,6 +143,7 @@ public function init(TargetPackage $package): void $package->addBuildOption('disable-opcache-jit', null, null, 'Disable opcache jit'); $package->addBuildOption('with-config-file-path', null, InputOption::VALUE_REQUIRED, 'Set the path in which to look for php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php'); $package->addBuildOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php/conf.d'); + $package->addBuildOption('with-sysconfdir', null, InputOption::VALUE_REQUIRED, 'Set the sysconfdir (e.g. /etc/php-zts) used by configure (not available on Windows)'); $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); @@ -268,12 +271,14 @@ public function validate(Package $package): void } } // linux does not support loading shared libraries when target is pure static - $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; - if (SystemTarget::getTargetOS() === 'Linux' && ApplicationContext::get(ToolchainInterface::class)->isStatic() && $embed_type === 'shared') { - throw new WrongUsageException( - 'Linux does not support loading shared libraries when linking libc statically. ' . - 'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.' - ); + if ($package->getName() === 'php-embed') { + $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; + if (SystemTarget::getTargetOS() === 'Linux' && ApplicationContext::get(ToolchainInterface::class)->isStatic() && $embed_type === 'shared') { + throw new WrongUsageException( + 'Linux does not support loading shared libraries when linking libc statically. ' . + 'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.' + ); + } } } @@ -332,7 +337,7 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void logger()->info("Adding hardcoded INI [{$source_name} = {$ini_value}]"); } if (!empty($custom_ini)) { - ApplicationContext::invoke([SourcePatcher::class, 'patchHardcodedINI'], [$package->getSourceDir(), $custom_ini]); + ApplicationContext::invoke([SourcePatcher::class, 'patchHardcodedINI'], ['php_source_dir' => $package->getSourceDir(), 'ini' => $custom_ini]); } // Patch StaticPHP version diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 4c4286234..50c4af456 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -73,10 +73,7 @@ public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $staticFlags = ''; } - $resolved = array_keys($installer->getResolvedPackages()); - // remove self from deps - $resolved = array_filter($resolved, fn ($pkg_name) => $pkg_name !== $package->getName()); - $config = new SPCConfigUtil()->config($resolved); + $config = new SPCConfigUtil()->config(['frankenphp']); $cflags = "{$package->getLibExtraCFlags()} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . " -DFRANKENPHP_VERSION={$frankenphp_version}"; $libs = $config['libs']; @@ -88,10 +85,13 @@ public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $libs .= ' -lgcov'; } + $extraLdProgram = clean_spaces((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM')); $env = [ 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => clean_spaces($cflags), - 'CGO_LDFLAGS' => "{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs}", + 'CGO_LDFLAGS' => clean_spaces("{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs} {$extraLdProgram}"), + // cgo strips flags not on its safe allowlist; widen it + 'CGO_LDFLAGS_ALLOW' => '-Wl,-z,.*|-Wl,--.*|-flto.*|-fprofile-.*', 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . @@ -101,10 +101,12 @@ public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; + $pgo = file_exists("{$source_dir}/caddy/frankenphp/default.pgo") ? "--pgo {$source_dir}/caddy/frankenphp/default.pgo " : ''; InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('building with xcaddy')); shell()->cd(BUILD_LIB_PATH) ->setEnv($env) - ->exec("xcaddy build --output frankenphp {$xcaddy_modules}"); + ->exec('go clean -cache') // fix stale include evaluation + ->exec("xcaddy build --output frankenphp {$pgo}{$xcaddy_modules}"); $builder->deployBinary(BUILD_LIB_PATH . '/frankenphp', BUILD_BIN_PATH . '/frankenphp'); $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '/frankenphp'); diff --git a/src/Package/Target/php/pgo.php b/src/Package/Target/php/pgo.php new file mode 100644 index 000000000..40be88e6a --- /dev/null +++ b/src/Package/Target/php/pgo.php @@ -0,0 +1,127 @@ +isInstrument() && !$pgo->isCsInstrument()) { + return; + } + foreach (PgoContext::SHUTDOWN_PATCHES as $dir => $patch) { + $cwd = SOURCE_PATH . '/' . $dir; + if (!is_dir($cwd)) { + continue; + } + if (!SourcePatcher::patchFile($patch, $cwd)) { + throw new WrongUsageException("PGO --phase=instrument: patch {$patch} failed to apply in {$cwd}"); + } + logger()->info("PGO --phase=instrument: applied {$patch}"); + } + } + + #[ConditionalOn(PgoContext::class)] + #[AfterStage('php', [self::class, 'configureForUnix'], 'php')] + #[PatchDescription('Patch libtool to passthrough -fcs-profile-* for context-sensitive PGO')] + public function pgoPatchLibtoolForCsInstrument(PgoContext $pgo): void + { + if (!$pgo->isCsInstrument()) { + return; + } + $libtool = SOURCE_PATH . '/php-src/libtool'; + if (!is_file($libtool)) { + return; + } + $contents = (string) file_get_contents($libtool); + if (str_contains($contents, '-fcs-profile-*')) { + return; + } + $patched = str_replace('-fprofile-*|-F*', '-fprofile-*|-fcs-profile-*|-F*', $contents); + if ($patched === $contents) { + logger()->warning('PGO --phase=cs-instrument: could not patch libtool for -fcs-profile-* passthrough'); + return; + } + file_put_contents($libtool, $patched); + logger()->info('PGO --phase=cs-instrument: patched libtool for -fcs-profile-* passthrough'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'configureForUnix'], 'php')] + public function pgoApplyConfigureFlags(PgoContext $pgo): void + { + $sapis = $pgo->trainableSapis(); + if ($sapis === []) { + return; + } + $pgo->applyEnvFor($sapis[0]); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeCliForUnix'], 'php')] + public function pgoBeforeMakeCli(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'cli'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeCgiForUnix'], 'php')] + public function pgoBeforeMakeCgi(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'cgi'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeFpmForUnix'], 'php')] + public function pgoBeforeMakeFpm(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'fpm'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeMicroForUnix'], 'php')] + public function pgoBeforeMakeMicro(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'micro'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeEmbedForUnix'], 'php')] + public function pgoBeforeMakeEmbed(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'embed'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'buildFrankenphpForUnix'], 'php')] + public function pgoBeforeBuildFrankenphp(PgoContext $pgo): void + { + $pgo->applyEnvFor('frankenphp'); + logger()->info("PGO {$pgo->mode}: applying flags for frankenphp"); + } + + private function pgoBeforeSapiMake(PgoContext $pgo, TargetPackage $package, string $sapi): void + { + $resolved = $pgo->resolveSapi($sapi); + if (!in_array($resolved, $pgo->trainableSapis(), true)) { + return; + } + shell()->cd($package->getSourceDir())->exec('make clean'); + $pgo->applyEnvFor($sapi); + logger()->info("PGO {$pgo->mode}: applying flags for {$sapi}"); + } +} diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 7d4d00631..b141a12d1 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -20,6 +20,7 @@ use StaticPHP\Package\TargetPackage; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; +use StaticPHP\Toolchain\ToolchainManager; use StaticPHP\Toolchain\ZigToolchain; use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; @@ -41,6 +42,15 @@ public function patchBeforeBuildconf(TargetPackage $package): void // php-src patches from micro (reads SPC_MICRO_PATCHES env var) SourcePatcher::patchPhpSrc(); + $microFileinfo = "{$package->getSourceDir()}/sapi/micro/php_micro_fileinfo.c"; + if (is_file($microFileinfo) && !str_contains((string) file_get_contents($microFileinfo), 'Elf32_Shdr')) { + FileSystem::replaceFileStr( + $microFileinfo, + 'typedef Elf32_Ehdr Elf_Ehdr;', + 'typedef Elf32_Ehdr Elf_Ehdr; typedef Elf32_Shdr Elf_Shdr;', + ); + } + // patch configure.ac for musl and musl-toolchain $musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl'; FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac'); @@ -59,12 +69,13 @@ public function patchBeforeBuildconf(TargetPackage $package): void } if (self::getPHPVersionID() >= 80300 && self::getPHPVersionID() < 80400) { - SourcePatcher::patchFile('spc_fix_avx512_cache_before_80400.patch', $this->getSourceDir()); + SourcePatcher::patchFile('spc_fix_avx512_cache_before_80400.patch', SOURCE_PATH . '/php-src'); } } #[BeforeStage('php', [self::class, 'configureForUnix'], 'php')] #[PatchDescription('Patch configure to use -std=gnu17 instead of -std=gnu23 for PHP <= 8.2')] + #[PatchDescription('Strip build-time env vars from phpinfo\'s "Configure Command"')] public function patchBeforeConfigure(TargetPackage $package): void { if (SystemTarget::isUnix() && self::getPHPVersionID() < 80300) { @@ -74,6 +85,22 @@ public function patchBeforeConfigure(TargetPackage $package): void "for ac_arg in '' -std=gnu17", ); } + + // strip our build-time env vars from phpinfo's "Configure Command" ('|' delimiter: PHP_BUILD_PROVIDER may contain '#') + if (SystemTarget::isUnix()) { + FileSystem::replaceFileStr( + "{$package->getSourceDir()}/configure", + 'for var in CFLAGS CXXFLAGS CPPFLAGS LDFLAGS EXTRA_LDFLAGS_PROGRAM LIBS CC CXX; do', + 'for var in CFLAGS CXXFLAGS CPPFLAGS LDFLAGS EXTRA_LDFLAGS_PROGRAM LIBS CC CXX ' + . 'PKG_CONFIG PKG_CONFIG_PATH EXTENSION_DIR OPENSSL_LIBS ' + . 'PHP_BUILD_SYSTEM PHP_BUILD_PROVIDER PHP_BUILD_COMPILER PHP_BUILD_ARCH; do', + ); + FileSystem::replaceFileStr( + "{$package->getSourceDir()}/configure", + 'clean_configure_args=$(echo $clean_configure_args | $SED -e "s#\'$var=$val\'##")', + 'clean_configure_args=$(echo $clean_configure_args | $SED -e "s|\'$var=$val\'||")', + ); + } } #[Stage] @@ -92,12 +119,10 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $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')) { - $compiler_extra = getenv('SPC_COMPILER_EXTRA') ?: ''; - GlobalEnvManager::putenv('SPC_COMPILER_EXTRA=' . trim($compiler_extra . ' -fno-sanitize=undefined')); - } + // disable undefined behavior sanitizer for zig, trips up on lua minijit and opcache-jit + if (SystemTarget::getTargetOS() === 'Linux' && ToolchainManager::getToolchainClass() === ZigToolchain::class) { + $compiler_extra = getenv('SPC_COMPILER_EXTRA') ?: ''; + GlobalEnvManager::putenv('SPC_COMPILER_EXTRA=' . trim($compiler_extra . ' -fno-sanitize=undefined')); } // PHP JSON extension is built-in since PHP 8.0 if ($version_id < 80000) { @@ -117,6 +142,9 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) { $args[] = "--with-config-file-scan-dir={$option}"; } + if ($option = $package->getBuildOption('with-sysconfdir', false)) { + $args[] = "--sysconfdir={$option}"; + } // perform enable cli options $args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli'; $args[] = $installer->isPackageResolved('php-fpm') @@ -129,6 +157,10 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi'; $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; $args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; + // Cross-compile: pass --host so configure picks the correct fiber asm file and host_cpu logic + if ($host_triple = SystemTarget::getAutoconfHostTriple()) { + $args[] = "--host={$host_triple}"; + } $args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null; $args = implode(' ', array_filter($args)); @@ -141,7 +173,7 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $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'), + 'LDFLAGS' => "-L{$package->getLibDir()}", 'LIBS' => $vars['EXTRA_LIBS'] ?? '', ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); } @@ -168,7 +200,7 @@ public function beforeMakeUnix(ToolchainInterface $toolchain): void #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] #[PatchDescription('Patch Makefile to fix //lib path for Linux builds')] - #[PatchDescription('Patch BUILD_CC to use system cc instead of zig-cc (prevents minilua crash)')] + #[PatchDescription('Under CI: patch BUILD_CC to system cc — zig-cc-built minilua segfaults there for reasons we cannot reproduce locally')] public function tryPatchMakefileUnix(TargetPackage $package, ToolchainInterface $toolchain): void { if (SystemTarget::getTargetOS() !== 'Linux') { @@ -178,7 +210,8 @@ public function tryPatchMakefileUnix(TargetPackage $package, ToolchainInterface // replace //lib with /lib in Makefile shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - if ($toolchain instanceof ZigToolchain) { + // CI escape hatch: in CI, zig-cc-built minilua segfaults + if ($toolchain instanceof ZigToolchain && getenv('CI')) { $makefile = "{$package->getSourceDir()}/Makefile"; FileSystem::replaceFileRegex($makefile, '/^BUILD_CC\s*=\s*zig-cc\s*$/m', 'BUILD_CC = cc'); } @@ -229,6 +262,7 @@ public function makeForUnix(TargetPackage $package, PackageInstaller $installer) #[Stage] public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + $start = microtime(true); InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli')); $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); @@ -239,11 +273,13 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install $builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php'); $package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php'); + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-cli'), true, $start); } #[Stage] public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + $start = microtime(true); InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi')); $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); @@ -254,11 +290,13 @@ public function makeCgiForUnix(TargetPackage $package, PackageInstaller $install $builder->deployBinary("{$package->getSourceDir()}/sapi/cgi/php-cgi", BUILD_BIN_PATH . '/php-cgi'); $package->setOutput('Binary path for cgi SAPI', BUILD_BIN_PATH . '/php-cgi'); + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-cgi'), true, $start); } #[Stage] public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + $start = microtime(true); InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm')); $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); @@ -269,43 +307,58 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install $builder->deployBinary("{$package->getSourceDir()}/sapi/fpm/php-fpm", BUILD_BIN_PATH . '/php-fpm'); $package->setOutput('Binary path for fpm SAPI', BUILD_BIN_PATH . '/php-fpm'); + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-fpm'), true, $start); } #[Stage] #[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 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'); + $start = microtime(true); + $phar_patched = false; + try { + if ($installer->isPackageResolved('ext-phar')) { + $phar_patched = true; + SourcePatcher::patchMicroPhar(self::getPHPVersionID()); + } + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); + // apply --with-micro-fake-cli option + $vars = $this->makeVars($installer); + $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; + $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 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); + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-micro'), true, $start); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); } - $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); } #[Stage] public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + $start = microtime(true); InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed')); $shared_exts = array_filter( $installer->getResolvedPackages(), @@ -319,22 +372,43 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta $root = BUILD_ROOT_PATH; $sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i'; + $vars = $this->makeVars($installer); + $makeArgs = $this->makeVarsToArgs($vars); shell()->cd($package->getSourceDir()) - ->setEnv($this->makeVars($installer)) + ->setEnv($vars) ->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile') - ->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs"); + ->exec("make -j{$builder->concurrency} {$makeArgs} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs"); + + // install-modules deref'd libtool's `$ext.so → $ext-X.so` symlink for each built-with-php ext; restore them. + $release = null; + if (preg_match('/-release\s+(\S+)/', (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $m)) { + $release = $m[1]; + foreach ($shared_exts as $ext) { + $name = $ext->getExtensionName(); + $u = BUILD_MODULES_PATH . "/{$name}.so"; + $v = BUILD_MODULES_PATH . "/{$name}-{$release}.so"; + if (file_exists($v) && file_exists($u) && !is_link($u)) { + unlink($u); + symlink(basename($v), $u); + } + } + } // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared ------------- - // process libphp.so for shared embed + // INSTALL_IT for embed copies through libtool's symlink, leaving only unversioned libphp.{so,dylib} — rename and symlink back so shared exts can `-lphp`. (static libphp.a is never versioned, even with -release.) $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; $libphp_so = "{$package->getLibDir()}/libphp.{$suffix}"; if (file_exists($libphp_so)) { - // rename libphp.so if -release is set - if (SystemTarget::getTargetOS() === 'Linux') { - $this->processLibphpSoFile($libphp_so, $installer); + if ($release !== null) { + $versioned = "{$package->getLibDir()}/libphp-{$release}.{$suffix}"; + if (file_exists($versioned)) { + @unlink($versioned); + } + rename($libphp_so, $versioned); + symlink(basename($versioned), $libphp_so); + $libphp_so = $versioned; } - // deploy $builder->deployBinary($libphp_so, $libphp_so, false); $package->setOutput('Library path for embed SAPI', $libphp_so); } @@ -343,6 +417,9 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta $increment_files = $diff->getChangedFiles(); $files = []; foreach ($increment_files as $increment_file) { + if (is_link($increment_file) || !file_exists($increment_file)) { + continue; + } $builder->deployBinary($increment_file, $increment_file, false); $files[] = basename($increment_file); } @@ -350,6 +427,11 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta $package->setOutput('Built shared extensions', implode(', ', $files)); } + // phpize needs prefix patched whether libphp is .a or .so + $package->runStage([$this, 'patchUnixEmbedScripts']); + + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-embed'), true, $start); + // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static ------------- // process libphp.a for static embed (only when present) @@ -359,9 +441,6 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}"); UnixUtil::exportDynamicSymbols($libphp_a); } - - // deploy embed php scripts - $package->runStage([$this, 'patchUnixEmbedScripts']); } #[Stage] @@ -394,8 +473,15 @@ public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterfa try { logger()->debug('Building shared extensions...'); foreach ($shared_extensions as $extension) { - InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName())); - $extension->buildShared(); + $ext_start = microtime(true); + InteractiveTerm::setMessage('Building shared extension: ' . ConsoleColor::yellow($extension->getName())); + try { + $extension->buildShared(); + } catch (\Throwable $e) { + InteractiveTerm::error('Building shared extension failed: ' . ConsoleColor::red($extension->getName())); + throw $e; + } + InteractiveTerm::success('Built shared extension: ' . ConsoleColor::green($extension->getName()), true, $ext_start); } } finally { // restore php-config @@ -487,6 +573,8 @@ public function build(TargetPackage $package, PackageInstaller $installer): void $package->runStage([$this, 'makeForUnix']); } + // shared extensions build before frankenphp so their undefined references are + // collected into libphp's exported dynamic-symbol list. $package->runStage([$this, 'unixBuildSharedExt']); } @@ -599,7 +687,7 @@ public function smokeTestEmbedForUnix(PackageInstaller $installer, ToolchainInte 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($installer->getAvailableResolvedPackageNames()); + $config = new SPCConfigUtil()->config(['php']); $lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}"; if ($toolchain->isStatic()) { $lens .= ' -static'; @@ -680,96 +768,37 @@ protected function generateMicroExtTests(PackageInstaller $installer): string return $php; } - /** - * Rename libphp.so to libphp-.so if -release is set in LDFLAGS. - */ - private function processLibphpSoFile(string $libphpSo, PackageInstaller $installer): void - { - $ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: ''; - $libDir = BUILD_LIB_PATH; - $modulesDir = BUILD_MODULES_PATH; - $realLibName = 'libphp.so'; - $cwd = getcwd(); - - if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) { - $release = $matches[1]; - $realLibName = "libphp-{$release}.so"; - $libphpRelease = "{$libDir}/{$realLibName}"; - if (!file_exists($libphpRelease) && file_exists($libphpSo)) { - rename($libphpSo, $libphpRelease); - } - if (file_exists($libphpRelease)) { - chdir($libDir); - if (file_exists($libphpSo)) { - unlink($libphpSo); - } - symlink($realLibName, 'libphp.so'); - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg($realLibName), - escapeshellarg($libphpRelease) - )); - } - if (is_dir($modulesDir)) { - chdir($modulesDir); - foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { - if (!$ext->isBuildShared()) { - continue; - } - $name = $ext->getName(); - $versioned = "{$name}-{$release}.so"; - $unversioned = "{$name}.so"; - $src = "{$modulesDir}/{$versioned}"; - $dst = "{$modulesDir}/{$unversioned}"; - if (is_file($src)) { - rename($src, $dst); - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg($unversioned), - escapeshellarg($dst) - )); - } - } - } - chdir($cwd); - } - - $target = "{$libDir}/{$realLibName}"; - if (file_exists($target)) { - [, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target)); - $output = implode("\n", $output); - if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) { - $currentSoname = $sonameMatch[1]; - if ($currentSoname !== basename($target)) { - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg(basename($target)), - escapeshellarg($target) - )); - } - } - } - } - /** * Make environment variables for php make. * This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking. */ private function makeVars(PackageInstaller $installer): array { - $config = new SPCConfigUtil(['libs_only_deps' => true])->config($installer->getAvailableResolvedPackageNames()); + $config = new SPCConfigUtil(['libs_only_deps' => true])->config(['php']); $static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : ''; $pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : ''; + $lib = BUILD_LIB_PATH; // Append SPC_EXTRA_LIBS to libs for dynamic linking support (e.g., X11) $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; $libs = trim($config['libs'] . ' ' . $extra_libs); + // libtool input (libphp.la). `make EXTRA_LDFLAGS=…` cmdline overrides fully replace the Makefile value, so re-include $config['ldflags'] for -L paths. + $extra_ldflags = clean_spaces($config['ldflags'] . ' ' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS')); + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared' + && !str_contains($extra_ldflags, '-avoid-version') + && !preg_match('/-release\s+\S+/', $extra_ldflags)) { + $extra_ldflags = trim($extra_ldflags . ' -avoid-version -module'); + } + + $extra_ldflags_program_env = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM') ?: ''; + $extra_ldflags_program = clean_spaces("-L{$lib} {$static} {$pie} {$extra_ldflags_program_env}"); + 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' => deduplicate_flags(getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . " {$config['ldflags']} {$static} {$pie}"), - 'EXTRA_LDFLAGS' => $config['ldflags'], + 'EXTRA_LDFLAGS' => $extra_ldflags, + 'EXTRA_LDFLAGS_PROGRAM' => $extra_ldflags_program, 'EXTRA_LIBS' => $libs, ]); } diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 224bcff30..12b81a891 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -27,12 +27,18 @@ class Artifact /** @var null|callable Bind custom source fetcher callback */ protected mixed $custom_source_callback = null; + /** @var null|string Display label describing where the custom source callback came from */ + protected ?string $custom_source_callback_origin = 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 Display label per platform describing where the custom binary callback came from */ + protected array $custom_binary_callback_origins = []; + /** @var array Bind custom binary check-update callbacks */ protected array $custom_binary_check_update_callbacks = []; @@ -285,15 +291,19 @@ public function getDownloadConfig(string $type): mixed * Get source extraction directory. * * Rules: - * 1. If extract is not specified: SOURCE_PATH/{artifact_name} - * 2. If extract is relative path: SOURCE_PATH/{value} - * 3. If extract is absolute path: {value} - * 4. If extract is array (dict): handled by extractor (selective extraction) + * 1. If cache_type is 'local': use the absolute dirname recorded at download time (no symlink/copy). + * 2. If extract is not specified: SOURCE_PATH/{artifact_name} + * 3. If extract is relative path: SOURCE_PATH/{value} + * 4. If extract is absolute path: {value} + * 5. If extract is array (dict): handled by extractor (selective extraction) */ public function getSourceDir(): string { // Prefer cache extract path, fall back to config $cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name); + if (($cache_info['cache_type'] ?? null) === 'local' && isset($cache_info['dirname'])) { + return FileSystem::convertPath($cache_info['dirname']); + } $extract = is_string($cache_info['extract'] ?? null) ? $cache_info['extract'] : ($this->config['source']['extract'] ?? null); @@ -407,10 +417,13 @@ public function getBinaryDir(): ?string /** * Set custom source fetcher callback. + * + * @param string $origin Short label shown in progress output (e.g. 'package downloader', 'custom url') */ - public function setCustomSourceCallback(callable $callback): void + public function setCustomSourceCallback(callable $callback, string $origin = 'package downloader'): void { $this->custom_source_callback = $callback; + $this->custom_source_callback_origin = $origin; } public function getCustomSourceCallback(): ?callable @@ -418,6 +431,11 @@ public function getCustomSourceCallback(): ?callable return $this->custom_source_callback ?? null; } + public function getCustomSourceCallbackOrigin(): ?string + { + return $this->custom_source_callback_origin; + } + /** * Set custom source check-update callback. */ @@ -452,11 +470,19 @@ public function emitCustomBinary(): void * * @param string $target_os Target OS platform string (e.g. linux-x86_64) * @param callable $callback Custom binary fetcher callback + * @param string $origin Short label shown in progress output (e.g. 'package downloader') */ - public function setCustomBinaryCallback(string $target_os, callable $callback): void + public function setCustomBinaryCallback(string $target_os, callable $callback, string $origin = 'package downloader'): void { ConfigValidator::validatePlatformString($target_os); $this->custom_binary_callbacks[$target_os] = $callback; + $this->custom_binary_callback_origins[$target_os] = $origin; + } + + public function getCustomBinaryCallbackOrigin(): ?string + { + $current_platform = SystemTarget::getCurrentPlatformString(); + return $this->custom_binary_callback_origins[$current_platform] ?? null; } /** diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index f52036c64..4b5a399a0 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -317,7 +317,10 @@ public function download(): void if (!is_dir(DOWNLOAD_PATH)) { FileSystem::createDir(DOWNLOAD_PATH); } - logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ..."); + $pending = array_values(array_filter($this->artifacts, fn ($a) => $this->generateQueue($a) !== [])); + if ($pending !== []) { + logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $pending)) . " with concurrency {$this->parallel} ..."); + } // Download artifacts parallelly if ($this->parallel > 1) { $this->downloadWithConcurrency(); @@ -551,8 +554,8 @@ private function downloadWithType(Artifact $artifact, int $current, int $total, $instance = null; $call = $this->downloaders[$item['config']['type']] ?? null; $type_display_name = match (true) { - $item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader', - $item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader', + $item['lock'] === 'source' && $artifact->getCustomSourceCallback() !== null => $artifact->getCustomSourceCallbackOrigin() ?? 'source package downloader', + $item['lock'] === 'binary' && $artifact->getCustomBinaryCallback() !== null => $artifact->getCustomBinaryCallbackOrigin() ?? 'binary package downloader', default => SPC_DOWNLOAD_TYPE_DISPLAY_NAME[$item['config']['type']] ?? $item['config']['type'], }; $try_h = $try ? 'Try downloading' : 'Downloading'; @@ -731,6 +734,16 @@ private function generateQueue(Artifact $artifact): array $binary_downloaded = $artifact->isBinaryDownloaded(compare_hash: true); $source_downloaded = $artifact->isSourceDownloaded(compare_hash: true); + if ($source_downloaded && $artifact->getName() === 'php-src' && ($requested = $this->getOption('with-php'))) { + $info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo('php-src'); + $cv = $info['version'] ?? null; + $ct = $info['cache_type'] ?? null; + $matches = $requested === 'git' ? $ct === 'git' : ($cv !== null && $ct !== 'git' && ($cv === $requested || str_starts_with($cv, $requested . '.'))); + if (!$matches) { + $source_downloaded = false; + } + } + $item_source = ['display' => 'source', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source')]; $item_source_mirror = ['display' => 'source (mirror)', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source-mirror')]; @@ -825,21 +838,21 @@ private function applyCustomDownloads(): void if (isset($this->artifacts[$artifact_name])) { $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $custom_url) { return (new Url())->download($artifact_name, ['url' => $custom_url], $downloader); - }); + }, 'custom url'); } } foreach ($this->custom_gits as $artifact_name => [$branch, $git_url]) { if (isset($this->artifacts[$artifact_name])) { $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $branch, $git_url) { return (new Git())->download($artifact_name, ['rev' => $branch, 'url' => $git_url], $downloader); - }); + }, 'custom git'); } } foreach ($this->custom_locals as $artifact_name => $local_path) { if (isset($this->artifacts[$artifact_name])) { $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $local_path) { return (new LocalDir())->download($artifact_name, ['dirname' => $local_path], $downloader); - }); + }, 'custom local dir'); } } } diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 987ec554e..2a9862461 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -136,6 +136,12 @@ protected function extractSource(Artifact $artifact): int throw new WrongUsageException("Artifact source [{$name}] not downloaded, please download it first!"); } + // Local (--custom-local): source lives in place at $cache_info['dirname']. + if (($cache_info['cache_type'] ?? null) === 'local') { + $artifact->emitAfterSourceExtract($artifact->getSourceDir()); + return SPC_STATUS_ALREADY_EXTRACTED; + } + $source_file = $this->cache->getCacheFullPath($cache_info); $target_path = $artifact->getSourceDir(); @@ -171,8 +177,12 @@ protected function extractSource(Artifact $artifact): int return SPC_STATUS_ALREADY_EXTRACTED; } - // Remove old directory if hash mismatch - if (is_dir($target_path)) { + // Remove old directory if hash mismatch. + // Guard: a symlink at $target_path (left over from older local-source handling) must be + // unlinked directly — never recurse into the link target, that would wipe the user's tree. + if (is_link($target_path)) { + @unlink($target_path); + } elseif (is_dir($target_path)) { logger()->notice("Source [{$name}] hash mismatch, re-extracting..."); FileSystem::removeDir($target_path); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 20db71275..fd9b1f95a 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -57,6 +57,16 @@ public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_ if (!is_array($data)) { throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); } + // GitHub's /releases list is ordered by publish date, so a newer-tagged prerelease + // (e.g. 2.2.x-alpha) can precede the latest stable (2.1.x-stable). /releases/latest + // returns the semantically latest stable release regardless of order; check it first. + if ($prefer_stable) { + $latest_url = str_replace('{repo}', $repo, self::API_URL) . '/latest'; + $latest = json_decode(default_shell()->executeCurl($latest_url, headers: $headers, retries: $retries) ?: '', true); + if (is_array($latest) && isset($latest['assets'])) { + array_unshift($data, $latest); + } + } foreach ($data as $release) { if ($prefer_stable && $release['prerelease'] === true) { continue; diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php index 412a4c1c3..1850cd5ba 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php @@ -14,17 +14,18 @@ public function getGitHubTokenHeaders(): array public static function getGitHubTokenHeadersStatic(): array { // GITHUB_TOKEN support - if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) { + $token = getenv('GITHUB_TOKEN'); + if ($token === false || $token === '') { + return []; + } + if (($user = getenv('GITHUB_USER')) !== false && $user !== '') { logger()->debug("Using 'GITHUB_TOKEN' with user {$user} for authentication"); $encoded = base64_encode("{$user}:{$token}"); spc_add_log_filter([$user, $token, $encoded]); return ["Authorization: Basic {$encoded}"]; } - if (($token = getenv('GITHUB_TOKEN')) !== false) { - logger()->debug("Using 'GITHUB_TOKEN' for authentication"); - spc_add_log_filter($token); - return ["Authorization: Bearer {$token}"]; - } - return []; + logger()->debug("Using 'GITHUB_TOKEN' for authentication"); + spc_add_log_filter($token); + return ["Authorization: Bearer {$token}"]; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index 14996c5af..1dd1c1c0f 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -22,12 +22,13 @@ public function download(string $name, array $config, ArtifactDownloader $downlo if (!$dist_url || !$dist_type) { throw new DownloaderException("failed to find {$name} dist info from packagist"); } + $extract = $config['extract'] ?? ('php-src/ext/' . strtolower(preg_replace('/^ext-/i', '', $name))); $name = str_replace('/', '_', $config['repo']); $version = $first['version'] ?? 'unknown'; $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, version: $version, downloader: static::class); + return DownloadResult::archive($filename, $config, $extract, version: $version, downloader: static::class); } public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult @@ -56,7 +57,23 @@ protected function fetchPackagistInfo(string $name, array $config, ArtifactDownl 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] ?? []; + // p2 metadata is minified: each entry only carries fields changed vs the previous one, '__unset' removes a field + $releases = []; + $current = []; + foreach ($data['packages'][$config['repo']] as $release) { + $current = array_merge($current, $release); + $current = array_filter($current, static fn ($v) => $v !== '__unset'); + $releases[] = $current; + } + // packagist lists newest first including RC/beta/alpha — pick the newest stable release + $first = null; + foreach ($releases as $release) { + if (!preg_match('/[._-]?(?:alpha|a|beta|b|rc)[._-]?\d*$/i', $release['version'] ?? '')) { + $first = $release; + break; + } + } + $first ??= $releases[0] ?? []; if (!isset($first['php-ext'])) { throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); } diff --git a/src/StaticPHP/Command/CraftCommand.php b/src/StaticPHP/Command/CraftCommand.php index f390e4335..6459e2f30 100644 --- a/src/StaticPHP/Command/CraftCommand.php +++ b/src/StaticPHP/Command/CraftCommand.php @@ -4,12 +4,18 @@ namespace StaticPHP\Command; +use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Doctor\Doctor; use StaticPHP\Exception\ValidationException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; +use StaticPHP\Registry\PackageLoader; +use StaticPHP\Toolchain\ToolchainManager; +use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\GlobalEnvManager; +use StaticPHP\Util\Pgo\PgoContext; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Exception\ParseException; @@ -21,6 +27,8 @@ class CraftCommand extends BaseCommand public function configure(): void { $this->addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml'); + $this->addOption('libs-only', null, null, 'Build only the libraries needed by the configured extensions (skip PHP and SAPI build).'); + PgoContext::registerOptions($this); } public function handle(): int @@ -39,6 +47,13 @@ public function handle(): int // apply env array_walk($craft['extra-env'], fn ($v, $k) => f_putenv("{$k}={$v}")); + // re-substitute env.ini's CC=${SPC_DEFAULT_CC} bindings. + ToolchainManager::initToolchain(); + GlobalEnvManager::reapplyOsIni(); + + // stash craft for doctor checks that depend on what's being built (e.g. frankenphp → go-xcaddy) + ApplicationContext::set('craft', $craft); + // run doctor if ($craft['craft-options']['doctor']) { $doctor = new Doctor($this->output, FIX_POLICY_AUTOFIX); @@ -83,23 +98,84 @@ public function handle(): int FileSystem::resetDir(SOURCE_PATH); } + $pgo = $this->getOption('libs-only') ? null : PgoContext::tryFromInput($this->input, $craft['sapi'], $build_options); + $starttime = microtime(true); // run installer $installer = new PackageInstaller($build_options); ApplicationContext::get(PackageBuilder::class)->setArgument('extensions', implode(',', $craft['extensions'])); - $installer->addBuildPackage('php'); + + if ($this->getOption('libs-only')) { + $with_suggests = (bool) ($craft['build-options']['with-suggests'] ?? false); + $libs = $this->resolveLibsForExtensions($craft, $with_suggests); + if ($libs === []) { + $this->output->writeln('No libraries needed for the configured extensions; nothing to do.'); + return static::SUCCESS; + } + foreach ($libs as $lib) { + $installer->addBuildPackage($lib); + } + } else { + $installer->addBuildPackage('php'); + } $installer->run(true); $usedtime = round(microtime(true) - $starttime, 1); + $tag = $pgo !== null ? " (PGO {$pgo->mode})" : ''; $this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - $this->output->writeln("✔ BUILD SUCCESSFUL ({$usedtime} s)"); + $this->output->writeln("✔ BUILD SUCCESSFUL{$tag} ({$usedtime} s)"); $this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + if ($pgo !== null && $pgo->isInstrument()) { + $this->output->writeln("Next: exercise the instrumented binary, then re-run craft with --pgo to consume {$pgo->profileRoot}."); + } + $installer->printBuildPackageOutputs(); return static::SUCCESS; } + /** @return list library package names transitively required by the configured extensions and SAPIs */ + private function resolveLibsForExtensions(array $craft, bool $include_suggests): array + { + $exts = array_merge($craft['extensions'], $craft['shared-extensions'] ?? []); + $ext_pkgs = array_map(fn ($x) => "ext-{$x}", $exts); + $extra = $craft['packages'] ?? []; + + // SAPI targets contribute their own lib deps/suggests (frankenphp → brotli/watcher, + // php-fpm → libacl); resolve them too so the buildroot contains everything the + // full build will need. Sapi names map to packages either directly or as php-. + $sapi_pkgs = []; + foreach ($craft['sapi'] ?? [] as $sapi) { + foreach ([$sapi, "php-{$sapi}"] as $candidate) { + if (PackageLoader::hasPackage($candidate)) { + $sapi_pkgs[] = $candidate; + break; + } + } + } + + $resolved = DependencyResolver::resolve( + array_merge($ext_pkgs, $extra, $sapi_pkgs), + include_suggests: $include_suggests, + ); + + $libs = []; + foreach ($resolved as $pkg_name) { + if (str_starts_with($pkg_name, 'ext-') || !PackageLoader::hasPackage($pkg_name)) { + continue; + } + $type = PackageLoader::getPackage($pkg_name)->getType(); + // Pure libraries, plus library-producing targets (curl ships libcurl.a): both + // must land in the buildroot, or the full build rebuilds them on every run. + if ($type === 'library' + || ($type === 'target' && PackageConfig::get($pkg_name, 'static-libs', []) !== [])) { + $libs[] = $pkg_name; + } + } + return $libs; + } + /** * Validate and parse craft.yml file to array. * diff --git a/src/StaticPHP/Command/ExtractCommand.php b/src/StaticPHP/Command/ExtractCommand.php index e2e79bdf0..8be8bf4b2 100644 --- a/src/StaticPHP/Command/ExtractCommand.php +++ b/src/StaticPHP/Command/ExtractCommand.php @@ -10,6 +10,7 @@ use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\DependencyResolver; +use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -34,6 +35,7 @@ public function configure(): void public function handle(): int { + GlobalEnvManager::afterInit(); $cache = ApplicationContext::get(ArtifactCache::class); $extractor = new ArtifactExtractor($cache); $force_source = (bool) $this->getOption('source-only'); diff --git a/src/StaticPHP/Command/SPCConfigCommand.php b/src/StaticPHP/Command/SPCConfigCommand.php index 2b1ac6bd2..742662ff9 100644 --- a/src/StaticPHP/Command/SPCConfigCommand.php +++ b/src/StaticPHP/Command/SPCConfigCommand.php @@ -44,7 +44,7 @@ public function handle(): int 'absolute_libs' => (bool) $this->getOption('absolute-libs'), ]); $packages = array_merge(array_map(fn ($x) => "ext-{$x}", $extensions), $libraries); - $config = $util->config($packages, $include_suggests); + $config = $util->config($packages); $this->output->writeln(match (true) { $this->getOption('includes') => $config['cflags'], diff --git a/src/StaticPHP/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php index 73ff9f2d4..2eca635c5 100644 --- a/src/StaticPHP/DI/ApplicationContext.php +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -98,6 +98,25 @@ public static function has(string $id): bool return self::getContainer()->has($id); } + /** + * Resolve $id, returning null if it can't be constructed. + * PHP-DI's has() returns true for any autowirable class even when get() + * would throw on missing scalar args — for "is this resolvable right now" + * semantics use this. + * + * @template T + * @param class-string $id + * @return null|T + */ + public static function tryGet(string $id): mixed + { + try { + return self::getContainer()->get($id); + } catch (\Throwable) { + return null; + } + } + /** * Set a service in the container. * Use sparingly - prefer configuration-based definitions. diff --git a/src/StaticPHP/Doctor/Item/GoXcaddyCheck.php b/src/StaticPHP/Doctor/Item/GoXcaddyCheck.php new file mode 100644 index 000000000..112687e52 --- /dev/null +++ b/src/StaticPHP/Doctor/Item/GoXcaddyCheck.php @@ -0,0 +1,44 @@ +addInstallPackage('go-xcaddy')->isPackageInstalled('go-xcaddy')) { + return CheckResult::fail('go-xcaddy is not installed', 'install-go-xcaddy'); + } + return CheckResult::ok(PKG_ROOT_PATH . '/go-xcaddy/bin/xcaddy'); + } + + #[FixItem('install-go-xcaddy')] + public function installGoXcaddy(): bool + { + $installer = new PackageInstaller(interactive: false); + $installer->addInstallPackage('go-xcaddy'); + $installer->run(true); + return $installer->isPackageInstalled('go-xcaddy'); + } +} diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index df3b5241c..1fe7ed00d 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -73,13 +73,20 @@ public function fixMusl(): bool $prefix = 'sudo '; logger()->warning('Current user is not root, using sudo for running command'); } + $sysEnv = ['CC' => 'gcc', 'CXX' => 'g++', 'AR' => 'ar', 'LD' => 'ld', 'RANLIB' => 'ranlib']; + $envFlags = ''; + foreach ($sysEnv as $k => $v) { + $envFlags .= "{$k}={$v} "; + } + $envFlags = rtrim($envFlags); $shell = shell()->cd(SOURCE_PATH . '/musl-wrapper') - ->exec('CC=gcc CXX=g++ AR=ar LD=ld ./configure --disable-gcc-wrapper') - ->exec('CC=gcc CXX=g++ AR=ar LD=ld make -j'); + ->setEnv($sysEnv) + ->exec('./configure --disable-gcc-wrapper') + ->exec('make -j'); if ($prefix !== '') { - f_passthru('cd ' . SOURCE_PATH . "/musl-wrapper && CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); + f_passthru('cd ' . SOURCE_PATH . "/musl-wrapper && {$envFlags} {$prefix}make install"); } else { - $shell->exec("CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); + $shell->exec("{$prefix}make install"); } return true; } diff --git a/src/StaticPHP/Doctor/Item/LlvmCompilerRtCheck.php b/src/StaticPHP/Doctor/Item/LlvmCompilerRtCheck.php new file mode 100644 index 000000000..5fff75c43 --- /dev/null +++ b/src/StaticPHP/Doctor/Item/LlvmCompilerRtCheck.php @@ -0,0 +1,48 @@ +isBuilt($libDir)) { + return CheckResult::ok($libDir); + } + return CheckResult::fail('llvm-compiler-rt is not built for ' . SystemTarget::getCanonicalTriple(), 'build-llvm-compiler-rt'); + } + + #[FixItem('build-llvm-compiler-rt')] + public function fixLlvmCompilerRt(): bool + { + $installer = new PackageInstaller(interactive: false); + $installer->addInstallPackage('llvm-compiler-rt'); + $installer->run(true); + new llvm_compiler_rt()->buildForTriple(); + $libDir = zig::path() . '/lib/' . SystemTarget::getCanonicalTriple(); + return new llvm_compiler_rt()->isBuilt($libDir); + } +} diff --git a/src/StaticPHP/Doctor/Item/LlvmToolsCheck.php b/src/StaticPHP/Doctor/Item/LlvmToolsCheck.php new file mode 100644 index 000000000..cc14af94e --- /dev/null +++ b/src/StaticPHP/Doctor/Item/LlvmToolsCheck.php @@ -0,0 +1,45 @@ +allBuilt($binDir)) { + return CheckResult::ok($binDir); + } + return CheckResult::fail('llvm-tools are not built', 'build-llvm-tools'); + } + + #[FixItem('build-llvm-tools')] + public function fixLlvmTools(): bool + { + $installer = new PackageInstaller(interactive: false); + $installer->addInstallPackage('llvm-tools'); + $installer->run(true); + new llvm_tools()->buildForHost(); + return new llvm_tools()->allBuilt(PKG_ROOT_PATH . '/llvm-tools/bin'); + } +} diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index baa6d4cbc..0f84263c0 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -4,6 +4,7 @@ namespace StaticPHP\Doctor\Item; +use Package\Artifact\zig; use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; use StaticPHP\Attribute\Doctor\OptionalCheck; @@ -26,7 +27,7 @@ public static function optionalCheck(): bool public function checkZig(): CheckResult { if (new PackageInstaller()->addInstallPackage('zig')->isPackageInstalled('zig')) { - return CheckResult::ok(); + return CheckResult::ok(zig::binary()); } return CheckResult::fail('zig is not installed', 'install-zig'); } diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index 582fa474d..ff7ac2c15 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -11,6 +11,8 @@ use StaticPHP\Exception\WrongUsageException; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Toolchain\Interface\ToolchainInterface; +use StaticPHP\Toolchain\ZigToolchain; use StaticPHP\Util\FileSystem; use StaticPHP\Util\GlobalPathTrait; use StaticPHP\Util\InteractiveTerm; @@ -178,14 +180,15 @@ public function extractDebugInfo(string $binary_path): string if (SystemTarget::getTargetOS() === 'Darwin') { shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}"); } elseif (SystemTarget::getTargetOS() === 'Linux') { + $objcopy = getenv('OBJCOPY') ?: 'objcopy'; if ($eu_strip = LinuxUtil::findCommand('eu-strip')) { shell() ->exec("{$eu_strip} -f {$debug_file} {$binary_path}") - ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); + ->exec("{$objcopy} --add-gnu-debuglink={$debug_file} {$binary_path}"); } else { shell() - ->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}") - ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); + ->exec("{$objcopy} --only-keep-debug {$binary_path} {$debug_file}") + ->exec("{$objcopy} --add-gnu-debuglink={$debug_file} {$binary_path}"); } } else { logger()->debug('extractDebugInfo is only supported on Linux and macOS'); @@ -199,9 +202,12 @@ public function extractDebugInfo(string $binary_path): string */ public function stripBinary(string $binary_path): void { + $strip = ApplicationContext::tryGet(ToolchainInterface::class) instanceof ZigToolchain + ? PKG_ROOT_PATH . '/llvm-tools/bin/llvm-strip' + : 'strip'; shell()->exec(match (SystemTarget::getTargetOS()) { - 'Darwin' => "strip -S {$binary_path}", - 'Linux' => "strip --strip-unneeded {$binary_path}", + 'Darwin' => "{$strip} -S {$binary_path}", + 'Linux' => "{$strip} --strip-unneeded {$binary_path}", 'Windows' => 'echo "Skip strip on Windows"', // Windows strip is not available for now default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'), }); diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 9cd0a1067..b68c855b6 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -154,6 +154,9 @@ public function run(bool $disable_delay_msg = false): void $this->resolvePackages(); } + $this->reconcilePhpSrcVersion(); + $this->emitPreInstallEvents(); + if ($this->interactive && !$disable_delay_msg) { // show install or build options in terminal with beautiful output $this->printInstallerInfo(); @@ -215,7 +218,7 @@ public function run(bool $disable_delay_msg = false): void if (!$is_to_build && $should_use_binary) { // install binary if ($this->interactive) { - InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); + InteractiveTerm::indicateProgress('Installing ' . $this->kindLabel($package) . ': ' . ConsoleColor::yellow($package->getName())); } try { // Start tracking for binary installation @@ -227,17 +230,17 @@ public function run(bool $disable_delay_msg = false): void // Stop tracking on error $this->tracker?->stopTracking(); if ($this->interactive) { - InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); + InteractiveTerm::finish('Installing ' . $this->kindLabel($package) . ' failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } if ($this->interactive) { - InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); + InteractiveTerm::finish('Installed ' . $this->kindLabel($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 ($this->interactive) { - InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); + InteractiveTerm::indicateProgress('Building ' . $this->kindLabel($package) . ': ' . ConsoleColor::yellow($package->getName())); } try { // Start tracking for build @@ -260,13 +263,13 @@ public function run(bool $disable_delay_msg = false): void // Stop tracking on error $this->tracker?->stopTracking(); if ($this->interactive) { - InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); + InteractiveTerm::finish('Building ' . $this->kindLabel($package) . ' failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } if ($this->interactive) { - InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); + InteractiveTerm::finish('Built ' . $this->kindLabel($package) . ': ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); } } } @@ -359,6 +362,15 @@ public function isPackageInstalled(Package|string $package_name): bool return false; } + public function emitPreInstallEvents(): void + { + foreach ($this->packages as $package) { + if ($package->hasStage('preInstall')) { + $package->runStage('preInstall'); + } + } + } + public function emitPostInstallEvents(): void { foreach ($this->packages as $package) { @@ -571,6 +583,77 @@ public function getPhpExtensionPackage(string $package_or_ext_name): ?PhpExtensi return null; } + private function reconcilePhpSrcVersion(): void + { + $src_dir = SOURCE_PATH . '/php-src'; + $cache = ApplicationContext::get(ArtifactCache::class); + $requested = $this->options['dl-with-php'] ?? null; + if ($requested !== null && $requested !== '' && $requested !== 'git') { + $info = $cache->getSourceInfo('php-src') ?? []; + $cv = $info['version'] ?? null; + if (($info['cache_type'] ?? null) === 'git' || $cv === null + || ($cv !== $requested && !str_starts_with($cv, $requested . '.'))) { + $resolved = null; + $candidates = glob(DOWNLOAD_PATH . '/php-' . $requested . '.*.tar.xz') ?: []; + if ($candidates !== []) { + usort($candidates, 'strnatcmp'); + if (preg_match('/^php-([0-9.]+)\.tar\.xz$/', basename(end($candidates)), $vm)) { + $resolved = $vm[1]; + } + } elseif ($this->download) { + $j = @file_get_contents('https://www.php.net/releases/index.php?json&version=' . urlencode($requested)); + $rel = is_string($j) ? json_decode($j, true) : null; + $resolved = is_array($rel) ? ($rel['version'] ?? null) : null; + } else { + throw new WrongUsageException("Requested PHP '{$requested}' but no php-{$requested}.*.tar.xz in downloads/; drop --no-download or run 'bin/spc download php-src --with-php={$requested}' first."); + } + if ($resolved !== null) { + $cf = DOWNLOAD_PATH . '/.cache.json'; + $j = json_decode(@file_get_contents($cf) ?: '{}', true) ?: []; + $tarball = DOWNLOAD_PATH . "/php-{$resolved}.tar.xz"; + $j['php-src']['source'] = [ + 'lock_type' => 'source', 'cache_type' => 'archive', + 'filename' => "php-{$resolved}.tar.xz", + 'extract' => $info['extract'] ?? null, + 'hash' => is_file($tarball) ? sha1_file($tarball) : null, + 'time' => time(), 'version' => $resolved, + 'config' => $info['config'] ?? ['type' => 'php-release', 'domain' => 'https://www.php.net'], + 'downloader' => $info['downloader'] ?? \StaticPHP\Artifact\Downloader\Type\PhpRelease::class, + ]; + $j['php-src']['binary'] ??= []; + file_put_contents($cf, json_encode($j, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + ApplicationContext::set(ArtifactCache::class, $cache = new ArtifactCache()); + } + } + } + if (!is_dir($src_dir) || ($info = $cache->getSourceInfo('php-src')) === null) { + return; + } + if (($info['cache_type'] ?? null) === 'git') { + if (!is_dir($src_dir . '/.git')) { + FileSystem::removeDir($src_dir); + } + return; + } + $vh = $src_dir . '/main/php_version.h'; + if (is_file($vh) + && preg_match('/#define\s+PHP_VERSION\s+"([^"]+)"/', file_get_contents($vh), $m) + && $m[1] !== ($info['version'] ?? null) + ) { + FileSystem::removeDir($src_dir); + } + } + + private function kindLabel(Package $package): string + { + return match (true) { + $package instanceof PhpExtensionPackage => 'extension', + $package instanceof TargetPackage => 'target', + $package instanceof LibraryPackage => 'library', + default => 'package', + }; + } + /** * @param Package[] $packages */ @@ -688,7 +771,7 @@ private function handlePhpTargetPackage(TargetPackage $package): void if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) { $frankenphp = PackageLoader::getPackage('frankenphp'); $this->install_packages[$frankenphp->getName()] = $frankenphp; - $this->build_packages[$package->getName()] = $package; + $this->build_packages[$frankenphp->getName()] = $frankenphp; $added = true; } $this->build_packages[$package->getName()] = $package; diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index fef88efd4..8a6dc4bcf 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -12,6 +12,7 @@ use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\ToolchainManager; use StaticPHP\Toolchain\ZigToolchain; +use StaticPHP\Util\FileSystem; use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\SPCConfigUtil; @@ -274,14 +275,19 @@ public function getSharedExtensionEnv(): array $compiler_extra = trim($compiler_extra . ' -lcompiler_rt'); GlobalEnvManager::putenv("SPC_COMPILER_EXTRA={$compiler_extra}"); } - $config = (new SPCConfigUtil())->getExtensionConfig($this); + $config = new SPCConfigUtil(['no_php' => true])->getExtensionConfig($this); [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; + // -Wl,-Bsymbolic: bind zend_* refs to the .so's own copies, not via global lookup + $ldflags = (string) $config['ldflags']; + if (PHP_OS_FAMILY !== 'Darwin' && !str_contains($ldflags, '-Wl,-Bsymbolic')) { + $ldflags = clean_spaces($ldflags . ' -Wl,-Bsymbolic'); + } return [ 'CFLAGS' => $config['cflags'], - 'CXXFLAGS' => $config['cflags'], - 'LDFLAGS' => $config['ldflags'], + 'CXXFLAGS' => $config['cxxflags'], + 'LDFLAGS' => $ldflags, 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; @@ -303,6 +309,7 @@ public function phpizeForUnix(array $env, PhpExtensionPackage $package): void public function configureForUnix(array $env, PhpExtensionPackage $package): void { $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + // CustomPhpConfigureArg keys are OS names ('Linux'/'Darwin'), not platform strings shell()->cd($package->getSourceDir()) ->setEnv($env) ->exec( @@ -318,11 +325,53 @@ public function configureForUnix(array $env, PhpExtensionPackage $package): void #[Stage] public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void { + // phpize Makefile's _SHARED_LIBADD line misses our static archives — splice them in + $package->patchSharedLibAdd(); + $extra_ldflags = (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'); + $makeArgs = $extra_ldflags !== '' ? 'EXTRA_LDFLAGS=' . escapeshellarg($extra_ldflags) : ''; shell()->cd($package->getSourceDir()) ->setEnv($env) ->exec('make clean') - ->exec("make -j{$builder->concurrency}") - ->exec('make install'); + ->exec("make -j{$builder->concurrency} {$makeArgs}") + ->exec("make install {$makeArgs}"); + + // install-modules deref'd libtool's `$ext.so → $ext-X.so` symlink into two regular files; restore the symlink. + if (preg_match('/-release\s+(\S+)/', $extra_ldflags, $m)) { + $name = $package->getExtensionName(); + $unversioned = BUILD_MODULES_PATH . "/{$name}.so"; + $versioned = BUILD_MODULES_PATH . "/{$name}-{$m[1]}.so"; + if (file_exists($versioned) && file_exists($unversioned) && !is_link($unversioned)) { + unlink($unversioned); + symlink(basename($versioned), $unversioned); + } + } + } + + public function patchSharedLibAdd(): void + { + $config = new SPCConfigUtil()->getExtensionConfig($this); + [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); + $lstdcpp = str_contains($sharedLibs, '-l:libstdc++.a') + ? '-l:libstdc++.a' + : (str_contains($sharedLibs, '-lstdc++') ? '-lstdc++' : ''); + + $makefile = $this->getSourceDir() . '/Makefile'; + if (!is_file($makefile)) { + return; + } + $content = (string) file_get_contents($makefile); + if (!preg_match('/^(.*_SHARED_LIBADD\s*=\s*)(.*)$/m', $content, $m)) { + return; + } + $prefix = $m[1]; + $current = trim($m[2]); + $merged = clean_spaces("{$current} {$staticLibs} {$lstdcpp}"); + $merged = deduplicate_flags($merged); + FileSystem::replaceFileRegex( + $makefile, + '/^(.*_SHARED_LIBADD\s*=.*)$/m', + $prefix . $merged + ); } /** @@ -333,14 +382,31 @@ public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBui */ public function buildSharedForUnix(PackageBuilder $builder): void { + // skip virtual addons (arg-type=none + display-name → owning ext); the parent ext built it + $argType = $this->extension_config['arg-type'] ?? null; + $displayName = $this->extension_config['display-name'] ?? null; + if ($argType === 'none' && $displayName !== null && $displayName !== $this->getExtensionName()) { + logger()->info("Skipping virtual extension [{$this->getName()}] — it's part of [{$displayName}]."); + return; + } + + if (!is_dir($this->getSourceDir())) { + throw new ValidationException( + "Extension source directory not found: {$this->getSourceDir()}", + validation_module: "Extension {$this->getName()} source" + ); + } + $env = $this->getSharedExtensionEnv(); $this->runStage([$this, 'phpizeForUnix'], ['env' => $env]); $this->runStage([$this, 'configureForUnix'], ['env' => $env]); $this->runStage([$this, 'makeForUnix'], ['env' => $env]); - // process *.so file - $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so'; + // libtool's -release X gives $name-X.so as the real file + $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() + . (preg_match('/-release\s+(\S+)/', (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $m) ? "-{$m[1]}" : '') + . '.so'; if (!file_exists($soFile)) { throw new ValidationException("Extension {$this->getExtensionName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getExtensionName()} build"); } diff --git a/src/StaticPHP/Registry/ArtifactLoader.php b/src/StaticPHP/Registry/ArtifactLoader.php index ed4ef1e7c..ecbeccfa5 100644 --- a/src/StaticPHP/Registry/ArtifactLoader.php +++ b/src/StaticPHP/Registry/ArtifactLoader.php @@ -36,6 +36,13 @@ public static function initArtifactInstances(): void public static function getArtifactInstance(string $artifact_name): ?Artifact { self::initArtifactInstances(); + if (!isset(self::$artifacts[$artifact_name])) { + // Artifact may have been registered after initArtifactInstances() ran (e.g., from a vendor registry) + $config = ArtifactConfig::get($artifact_name); + if ($config !== null) { + self::$artifacts[$artifact_name] = new Artifact($artifact_name, $config); + } + } return self::$artifacts[$artifact_name] ?? null; } diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index cf0301c04..4930ac7f9 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -367,15 +367,18 @@ public static function getAllAnnotations(): array public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable { - // match condition + // match condition; '*' is a wildcard that fires for every package's stage $installer = ApplicationContext::get(PackageInstaller::class); - $stages = self::$before_stages[$package_name][$stage] ?? []; + $stages = array_merge( + self::$before_stages[$package_name][$stage] ?? [], + $package_name === '*' ? [] : (self::$before_stages['*'][$stage] ?? []), + ); foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) { if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { continue; } foreach ($conditionals as $class) { - if (!ApplicationContext::has($class)) { + if (ApplicationContext::tryGet($class) === null) { continue 2; } } @@ -385,16 +388,19 @@ public static function getBeforeStageCallbacks(string $package_name, string $sta public static function getAfterStageCallbacks(string $package_name, string $stage): array { - // match condition + // match condition; '*' is a wildcard that fires for every package's stage $installer = ApplicationContext::get(PackageInstaller::class); - $stages = self::$after_stages[$package_name][$stage] ?? []; + $stages = array_merge( + self::$after_stages[$package_name][$stage] ?? [], + $package_name === '*' ? [] : (self::$after_stages['*'][$stage] ?? []), + ); $result = []; foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) { if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { continue; } foreach ($conditionals as $class) { - if (!ApplicationContext::has($class)) { + if (ApplicationContext::tryGet($class) === null) { continue 2; } } @@ -425,6 +431,20 @@ public static function checkLoadedStageEvents(): void { foreach (['BeforeStage' => self::$before_stages, 'AfterStage' => self::$after_stages] as $event_name => $ev_all) { foreach ($ev_all as $package_name => $stages) { + // wildcard hooks fire for every package's stage; nothing to validate against + if ($package_name === '*') { + foreach ($stages as $stage_name => $before_events) { + foreach ($before_events as [$event_callable, $only_when_package_resolved, $conditionals]) { + if ($only_when_package_resolved !== null && !self::hasPackage($only_when_package_resolved)) { + throw new RegistryException("{$event_name} event for wildcard [*] stage [{$stage_name}] has unknown only_when_package_resolved package [{$only_when_package_resolved}]."); + } + if (!is_callable($event_callable)) { + throw new RegistryException("{$event_name} event for wildcard [*] stage [{$stage_name}] has invalid callable."); + } + } + } + continue; + } // check package exists if (!self::hasPackage($package_name)) { throw new RegistryException( diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 8f206c300..2cd119ec1 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -89,14 +89,19 @@ public static function loadRegistry(string $registry_file, bool $auto_require = self::$current_registry_name = $registry_name; try { - // Load composer autoload if specified (for external registries with their own dependencies) + // resolve autoload manually — path-repo installs have no vendor/, FileSystem::fullpath would throw if (isset($data['autoload']) && is_string($data['autoload'])) { - $autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file)); + $base = dirname($registry_file); + $autoload_path = FileSystem::isRelativePath($data['autoload']) + ? rtrim($base, '/') . DIRECTORY_SEPARATOR . $data['autoload'] + : $data['autoload']; if (file_exists($autoload_path)) { logger()->debug("Loading external autoload from: {$autoload_path}"); require_once $autoload_path; + } elseif (str_contains(rtrim(FileSystem::convertPath($base), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)) { + logger()->debug("Registry autoload not present, relying on consumer autoloader: {$autoload_path}"); } else { - logger()->warning("Autoload file not found: {$autoload_path}"); + throw new RegistryException("Path does not exist: {$autoload_path}"); } } diff --git a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php index c59859cf4..b206fee19 100644 --- a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php @@ -11,6 +11,7 @@ use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; use StaticPHP\Runtime\Shell\UnixShell; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\InteractiveTerm; use ZM\Logger\ConsoleColor; @@ -149,13 +150,17 @@ public function appendEnv(array $env): static */ private function getDefaultConfigureArgs(): array { - return [ + $args = [ '--disable-shared', '--enable-static', "--prefix={$this->package->getBuildRootPath()}", '--with-pic', '--enable-pic', ]; + if ($host_triple = SystemTarget::getAutoconfHostTriple()) { + $args[] = "--host={$host_triple}"; + } + return $args; } /** diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 82fa468ae..bf60b6878 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -228,8 +228,15 @@ private function getDefaultCMakeArgs(): array "-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}", ]; - // EXE linker flags: base system libs + framework flags for target packages - $exeLinkerFlags = SystemTarget::getRuntimeLibs(); + // EXE linker flags: framework flags for target packages (Darwin). On Linux the + // runtime libs (-ldl & co.) are NOT passed here: CMake puts EXE_LINKER_FLAGS before + // the objects/archives, where -Wl,--as-needed (Debian gcc default, and our own + // LDFLAGS) discards them and breaks try_compile probes linking static archives that + // need them (curl's SSL_set_quic_tls_cbs check vs libcrypto.a needing dlopen on + // glibc < 2.34). They go into the toolchain file as CMAKE__STANDARD_LIBRARIES + // instead, which cmake appends AFTER the objects — correct order, as-needed intact, + // and the toolchain file is re-evaluated inside every try_compile. + $exeLinkerFlags = SystemTarget::getTargetOS() === 'Linux' ? '' : SystemTarget::getRuntimeLibs(); if ($this->package instanceof TargetPackage && SystemTarget::getTargetOS() === 'Darwin') { $resolvedNames = array_keys($this->installer->getResolvedPackages()); $resolvedNames[] = $this->package->getName(); @@ -302,9 +309,20 @@ private function makeCmakeToolchainFile(): string set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES "{$include}") set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES "{$include}") CMAKE; - // Whoops, linux may need CMAKE_AR sometimes + // pin AR/RANLIB so cmake uses zig-ar/zig-ranlib instead of system /usr/bin/ranlib (zig archives need it) if (PHP_OS_FAMILY === 'Linux') { - $toolchain .= "\nSET(CMAKE_AR \"ar\")"; + $ar = getenv('SPC_DEFAULT_AR') ?: getenv('AR') ?: 'ar'; + $ranlib = getenv('SPC_DEFAULT_RANLIB') ?: (getenv('RANLIB') ?: 'ranlib'); + $toolchain .= "\nSET(CMAKE_AR \"{$ar}\")"; + $toolchain .= "\nSET(CMAKE_RANLIB \"{$ranlib}\")"; + // Runtime libs as standard libraries: appended after objects/archives on every + // link line (incl. try_compile probes), so -Wl,--as-needed keeps the ones that + // are actually referenced. + $runtimeLibs = SystemTarget::getRuntimeLibs(); + if ($runtimeLibs !== '') { + $toolchain .= "\nset(CMAKE_C_STANDARD_LIBRARIES_INIT \"{$runtimeLibs}\")"; + $toolchain .= "\nset(CMAKE_CXX_STANDARD_LIBRARIES_INIT \"{$runtimeLibs}\")"; + } } FileSystem::writeFile(SOURCE_PATH . '/toolchain.cmake', $toolchain); return $created = realpath(SOURCE_PATH . '/toolchain.cmake'); diff --git a/src/StaticPHP/Runtime/SystemTarget.php b/src/StaticPHP/Runtime/SystemTarget.php index 489f84384..3949443bf 100644 --- a/src/StaticPHP/Runtime/SystemTarget.php +++ b/src/StaticPHP/Runtime/SystemTarget.php @@ -4,6 +4,7 @@ namespace StaticPHP\Runtime; +use StaticPHP\Toolchain\ZigToolchain; use StaticPHP\Util\System\LinuxUtil; /** @@ -16,7 +17,7 @@ class SystemTarget */ public static function getLibc(): ?string { - if ($target = getenv('SPC_TARGET')) { + if ($target = self::target()) { if (str_contains($target, '-gnu')) { return 'glibc'; } @@ -57,7 +58,7 @@ public static function getRuntimeLibs(): string public static function getLibcVersion(): ?string { if (PHP_OS_FAMILY === 'Linux') { - $target = (string) getenv('SPC_TARGET'); + $target = self::target(); if (str_contains($target, '-gnu.2.')) { return preg_match('/-gnu\.(2\.\d+)/', $target, $matches) ? $matches[1] : null; } @@ -75,7 +76,7 @@ public static function getLibcVersion(): ?string */ public static function getTargetOS(): string { - $target = (string) getenv('SPC_TARGET'); + $target = self::target(); return match (true) { str_contains($target, '-linux') => 'Linux', str_contains($target, '-macos') => 'Darwin', @@ -91,7 +92,7 @@ public static function getTargetOS(): string */ public static function getTargetArch(): string { - $target = (string) getenv('SPC_TARGET'); + $target = self::target(); return match (true) { str_contains($target, 'x86_64') || str_contains($target, 'amd64') => 'x86_64', str_contains($target, 'aarch64') || str_contains($target, 'arm64') => 'aarch64', @@ -127,4 +128,70 @@ public static function isUnix(): bool { return in_array(self::getTargetOS(), ['Linux', 'Darwin', 'BSD']); } + + /** + * Returns the canonical target triple (arch-os-abi) for per-target build + * artifacts. Always returns a non-null triple, falling back to a host-derived + * triple when SPC_TARGET is unset or names 'native'. + * Strips libc version suffix (-gnu.2.17 → -gnu) and trailing flags (' -dynamic'). + */ + public static function getCanonicalTriple(): string + { + $target = self::target(); + if ($target !== '' && !str_contains($target, 'native')) { + $cleaned = (string) preg_replace('/(-gnu|-musl)\.[\d.]+/', '$1', $target); + $cleaned = preg_split('/\s+/', trim($cleaned))[0] ?? ''; + if ($cleaned !== '') { + return $cleaned; + } + } + $arch = self::getTargetArch(); + return match (self::getTargetOS()) { + 'Linux' => $arch . '-linux-' . (self::getLibc() === 'musl' ? 'musl' : 'gnu'), + 'Darwin' => $arch . '-macos-none', + 'Windows' => $arch . '-windows-gnu', + default => $arch . '-unknown-unknown', + }; + } + + /** + * Returns a GNU host triple for autoconf --host= when SPC_TARGET names an + * architecture different from the build host (true cross-compile). + * Returns null for same-arch builds. + * Strips libc version suffix (-gnu.2.17 → -gnu) and trailing flags (e.g. ' -dynamic'). + */ + public static function getAutoconfHostTriple(): ?string + { + $target = self::target(); + if ($target === '' || str_contains($target, 'native')) { + return null; + } + $cleaned = preg_split('/\s+/', trim((string) preg_replace('/(-gnu|-musl)\.[\d.]+/', '$1', $target)))[0]; + if ($cleaned === '') { + return null; + } + // Only emit --host for true cross-arch builds; same-arch (incl. cross-libc) lets autoconf detect. + $target_arch_token = explode('-', $cleaned)[0]; + $arch_aliases = [ + 'x86_64' => ['x86_64', 'amd64'], + 'aarch64' => ['aarch64', 'arm64'], + 'arm' => ['arm', 'armv6', 'armv7', 'armhf', 'armel'], + 'i386' => ['i386', 'i486', 'i586', 'i686'], + ]; + $host_arch = GNU_ARCH; + if (array_any($arch_aliases, fn ($aliases) => in_array($target_arch_token, $aliases, true) && in_array($host_arch, $aliases, true))) { + return null; + } + return $cleaned; + } + + /** native toolchains ignore SPC_TARGET */ + private static function target(): string + { + $tc = (string) getenv('SPC_TOOLCHAIN'); + if ($tc !== '' && $tc !== ZigToolchain::class) { + return ''; + } + return (string) getenv('SPC_TARGET'); + } } diff --git a/src/StaticPHP/Toolchain/ClangBrewToolchain.php b/src/StaticPHP/Toolchain/ClangBrewToolchain.php index 5d8963ef4..232609548 100644 --- a/src/StaticPHP/Toolchain/ClangBrewToolchain.php +++ b/src/StaticPHP/Toolchain/ClangBrewToolchain.php @@ -15,6 +15,7 @@ public function initEnv(): void GlobalEnvManager::putenv("SPC_DEFAULT_CC={$homebrew_prefix}/opt/llvm/bin/clang"); GlobalEnvManager::putenv("SPC_DEFAULT_CXX={$homebrew_prefix}/opt/llvm/bin/clang++"); GlobalEnvManager::putenv("SPC_DEFAULT_AR={$homebrew_prefix}/opt/llvm/bin/llvm-ar"); + GlobalEnvManager::putenv("SPC_DEFAULT_RANLIB={$homebrew_prefix}/opt/llvm/bin/llvm-ranlib"); GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld'); GlobalEnvManager::addPathIfNotExists("{$homebrew_prefix}/opt/llvm/bin"); } diff --git a/src/StaticPHP/Toolchain/ClangNativeToolchain.php b/src/StaticPHP/Toolchain/ClangNativeToolchain.php index 27ae2b65b..7602c3ed5 100644 --- a/src/StaticPHP/Toolchain/ClangNativeToolchain.php +++ b/src/StaticPHP/Toolchain/ClangNativeToolchain.php @@ -21,6 +21,7 @@ public function initEnv(): void GlobalEnvManager::putenv('SPC_DEFAULT_CC=clang'); GlobalEnvManager::putenv('SPC_DEFAULT_CXX=clang++'); GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar'); + GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=ranlib'); GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld'); } diff --git a/src/StaticPHP/Toolchain/GccNativeToolchain.php b/src/StaticPHP/Toolchain/GccNativeToolchain.php index 7c339e69e..9aecb7e05 100644 --- a/src/StaticPHP/Toolchain/GccNativeToolchain.php +++ b/src/StaticPHP/Toolchain/GccNativeToolchain.php @@ -18,6 +18,7 @@ public function initEnv(): void GlobalEnvManager::putenv('SPC_DEFAULT_CC=gcc'); GlobalEnvManager::putenv('SPC_DEFAULT_CXX=g++'); GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar'); + GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=ranlib'); GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld'); } diff --git a/src/StaticPHP/Toolchain/ZigToolchain.php b/src/StaticPHP/Toolchain/ZigToolchain.php index 36e42d040..c8dbb72ef 100644 --- a/src/StaticPHP/Toolchain/ZigToolchain.php +++ b/src/StaticPHP/Toolchain/ZigToolchain.php @@ -4,46 +4,49 @@ namespace StaticPHP\Toolchain; +use Package\Artifact\llvm_compiler_rt; +use Package\Artifact\zig; +use StaticPHP\DI\ApplicationContext; +use StaticPHP\Package\PackageBuilder; +use StaticPHP\Package\PackageInstaller; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\UnixToolchainInterface; use StaticPHP\Util\GlobalEnvManager; +use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\System\LinuxUtil; +use ZM\Logger\ConsoleColor; class ZigToolchain implements UnixToolchainInterface { + private static bool $afterInitDone = false; + public function initEnv(): void { // Set environment variables for zig toolchain GlobalEnvManager::putenv('SPC_DEFAULT_CC=zig-cc'); GlobalEnvManager::putenv('SPC_DEFAULT_CXX=zig-c++'); GlobalEnvManager::putenv('SPC_DEFAULT_AR=zig-ar'); + GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=zig-ranlib'); GlobalEnvManager::putenv('SPC_DEFAULT_LD=zig-ld.lld'); - - // Generate additional objects needed for zig toolchain - $paths = ['/usr/lib/gcc', '/usr/local/lib/gcc']; - $objects = ['crtbeginS.o', 'crtendS.o']; - $found = []; - - foreach ($objects as $obj) { - $located = null; - foreach ($paths as $base) { - $output = shell_exec("find {$base} -name {$obj} 2>/dev/null | grep -v '/32/' | head -n 1"); - $line = trim((string) $output); - if ($line !== '') { - $located = $line; - break; - } - } - if ($located) { - $found[] = $located; - } - } - GlobalEnvManager::putenv('SPC_EXTRA_RUNTIME_OBJECTS=' . implode(' ', $found)); + GlobalEnvManager::addPathIfNotExists($this->getPath()); } public function afterInit(): void { - GlobalEnvManager::addPathIfNotExists($this->getPath()); - f_passthru('ulimit -n 2048'); // zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough + if (self::$afterInitDone) { + return; + } + self::$afterInitDone = true; + // zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough + if (function_exists('posix_setrlimit') && function_exists('posix_getrlimit')) { + $limits = posix_getrlimit(); + $hard = (int) ($limits['hardopenfiles'] ?? 2048); + $soft = (int) ($limits['softopenfiles'] ?? 1024); + $target = min(max($soft, 2048), $hard > 0 ? $hard : 2048); + if ($target > $soft) { + posix_setrlimit(POSIX_RLIMIT_NOFILE, $target, $hard); + } + } $cflags = getenv('SPC_DEFAULT_CFLAGS') ?: ''; $cxxflags = getenv('SPC_DEFAULT_CXXFLAGS') ?: ''; $extraCflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: ''; @@ -54,7 +57,8 @@ public function afterInit(): void GlobalEnvManager::putenv("SPC_DEFAULT_CXXFLAGS={$cxxflags}"); GlobalEnvManager::putenv("SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS={$extraCflags}"); GlobalEnvManager::putenv('RANLIB=zig-ranlib'); - GlobalEnvManager::putenv('OBJCOPY=zig-objcopy'); + GlobalEnvManager::putenv('SPC_COMPILER_RT_DIR=' . zig::path() . '/lib/' . SystemTarget::getCanonicalTriple()); + GlobalEnvManager::putenv('OBJCOPY=' . PKG_ROOT_PATH . '/llvm-tools/bin/llvm-objcopy'); $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; if (!str_contains($extra_libs, '-lunwind')) { // Add unwind library if not already present @@ -70,6 +74,18 @@ public function afterInit(): void // 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}"); + + // An __attribute__((ifunc)) in LTO bitcode crashes zig 0.16's lld (LLVM 21) + // during thin-link symbol resolution + $all_flags = $cflags . ' ' . (getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: '') . ' ' . (getenv('SPC_DEFAULT_LD_FLAGS') ?: ''); + if (str_contains($all_flags, '-flto')) { + $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + if (!str_contains($extra_vars, 'ax_cv_have_func_attribute_ifunc')) { + GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS={$extra_vars} ax_cv_have_func_attribute_ifunc=no"); + } + } + + $this->ensureCompilerRt(); } public function getCompilerInfo(): ?string @@ -105,8 +121,53 @@ public function isStatic(): bool return false; } + private function ensureCompilerRt(): void + { + if (!zig::isInstalled()) { + return; + } + $rt = new llvm_compiler_rt(); + $triple = SystemTarget::getCanonicalTriple(); + $libDir = zig::path() . '/lib/' . $triple; + if ($rt->isBuilt($libDir)) { + return; + } + if (!is_dir(SOURCE_PATH . '/llvm-compiler-rt/lib/profile')) { + // Source not yet downloaded; install via nested PackageInstaller. The recursion guard + // on afterInit prevents the nested run from re-entering this method. Save the outer + // installer/builder in the container so executors keep seeing the outer one after. + // The PackageInstaller surfaces its own spinner for the install; AfterBinaryExtract + // builds for the current triple, so we're done after run(). + $outerInstaller = ApplicationContext::tryGet(PackageInstaller::class); + $outerBuilder = ApplicationContext::tryGet(PackageBuilder::class); + try { + new PackageInstaller() + ->addInstallPackage('llvm-compiler-rt') + ->run(true); + } finally { + if ($outerInstaller !== null) { + ApplicationContext::set(PackageInstaller::class, $outerInstaller); + } + if ($outerBuilder !== null) { + ApplicationContext::set(PackageBuilder::class, $outerBuilder); + } + } + return; + } + // Source already extracted from a previous run on a different triple; rebuild here with our + // own progress spinner since we're outside the PackageInstaller flow. + InteractiveTerm::indicateProgress('Building llvm-compiler-rt for ' . ConsoleColor::yellow($triple)); + try { + $rt->buildForTriple(); + } catch (\Throwable $e) { + InteractiveTerm::finish('Build llvm-compiler-rt for ' . ConsoleColor::red($triple) . ' failed', false); + throw $e; + } + InteractiveTerm::finish('Built llvm-compiler-rt for ' . ConsoleColor::green($triple)); + } + private function getPath(): string { - return PKG_ROOT_PATH . '/zig'; + return zig::path(); } } diff --git a/src/StaticPHP/Util/GlobalEnvManager.php b/src/StaticPHP/Util/GlobalEnvManager.php index 5b4b16b25..eb44112e8 100644 --- a/src/StaticPHP/Util/GlobalEnvManager.php +++ b/src/StaticPHP/Util/GlobalEnvManager.php @@ -123,6 +123,24 @@ public static function appendEnv(string $key, string $value): void } } + /** Re-substitute env.ini's CC=${SPC_DEFAULT_CC} bindings after a toolchain swap. */ + public static function reapplyOsIni(): void + { + $ini = self::readIniFile(); + $os_ini = match (PHP_OS_FAMILY) { + 'Windows' => $ini['windows'] ?? [], + 'Darwin' => $ini['macos'] ?? [], + 'Linux' => $ini['linux'] ?? [], + 'BSD' => $ini['freebsd'] ?? [], + default => [], + }; + foreach (['CC', 'CXX', 'AR', 'RANLIB', 'LD'] as $k) { + if (isset($os_ini[$k])) { + self::putenv("{$k}={$os_ini[$k]}"); + } + } + } + /** * Initialize the toolchain after the environment variables are set. * The toolchain or environment availability check is done here. diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php index 0570f31c6..1842cce4c 100644 --- a/src/StaticPHP/Util/InteractiveTerm.php +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -5,6 +5,7 @@ namespace StaticPHP\Util; use StaticPHP\DI\ApplicationContext; +use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\ProgressIndicator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; @@ -27,14 +28,19 @@ public static function notice(string $message, bool $indent = false): void } } - public static function success(string $message, bool $indent = false): void + public static function success(string $message, bool $indent = false, ?float $start_time = null): void { $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + if ($start_time !== null) { + $message .= ' (' . Helper::formatTime(microtime(true) - $start_time) . ')'; + } if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); } else { - $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message)); + // wipe the current indicator line so our persistent ✔ line doesn't get appended to a spinner + $clear = (self::$indicator !== null && $output->isDecorated() && !$no_ansi) ? "\x0D\x1B[2K" : ''; + $output->writeln($clear . ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message)); logger()->debug(strip_ansi_colors($message)); } } diff --git a/src/StaticPHP/Util/Pgo/PgoContext.php b/src/StaticPHP/Util/Pgo/PgoContext.php new file mode 100644 index 000000000..80a8464ab --- /dev/null +++ b/src/StaticPHP/Util/Pgo/PgoContext.php @@ -0,0 +1,265 @@ + + */ + public const array TRAINABLE = [ + 'cli' => 'build-cli', + 'micro' => 'build-micro', + 'cgi' => 'build-cgi', + 'fpm' => 'build-fpm', + 'embed' => 'build-embed', + 'frankenphp' => 'build-frankenphp', + ]; + + public const array SHUTDOWN_PATCHES = [ + 'php-src' => 'spc_pgo_flush_php_main.patch', + 'frankenphp' => 'spc_pgo_flush_frankenphp.patch', + ]; + + /** @var list */ + private array $trainableSapis = []; + + public function __construct( + public readonly string $mode, + public readonly string $profileRoot, + ) { + if (!in_array($mode, [self::MODE_INSTRUMENT, self::MODE_CS_INSTRUMENT, self::MODE_USE], true)) { + throw new WrongUsageException("PgoContext: unknown mode '{$mode}'"); + } + } + + public static function registerOptions(BaseCommand $cmd): void + { + $cmd->addOption('pgi', null, null, 'PGO instrument pass: build with -fprofile-generate so the resulting binary writes .profraw on shutdown.'); + $cmd->addOption('cs-pgi', null, null, 'PGO context-sensitive instrument pass: -fprofile-use= + -fcs-profile-generate. Requires a prior --pgi/--pgo cycle.'); + $cmd->addOption('pgo', null, null, 'PGO use pass: merge the collected .profraw into .profdata, then rebuild with -fprofile-use.'); + } + + /** + * @param array $sapis + * @param array $build_options + */ + public static function tryFromInput(InputInterface $input, array $sapis, array &$build_options): ?self + { + $modes = array_filter(['pgi', 'cs-pgi', 'pgo'], fn ($m) => (bool) $input->getOption($m)); + if (count($modes) > 1) { + throw new WrongUsageException('--pgi, --cs-pgi, and --pgo are mutually exclusive'); + } + $picked = array_values($modes)[0] ?? null; + if ($picked === null) { + return null; + } + $mode = match ($picked) { + 'pgi' => self::MODE_INSTRUMENT, + 'cs-pgi' => self::MODE_CS_INSTRUMENT, + 'pgo' => self::MODE_USE, + }; + $ctx = new self($mode, BUILD_ROOT_PATH . '/pgo-data'); + $ctx->setTrainableSapis($sapis); + + match ($mode) { + self::MODE_INSTRUMENT => $ctx->setupInstrument(), + self::MODE_CS_INSTRUMENT => $ctx->setupCsInstrument(), + self::MODE_USE => $ctx->mergeProfiles(), + }; + + if ($ctx->isInstrument() || $ctx->isCsInstrument()) { + $build_options['no-strip'] = true; + } + ApplicationContext::set(self::class, $ctx); + return $ctx; + } + + public function isInstrument(): bool + { + return $this->mode === self::MODE_INSTRUMENT; + } + + public function isCsInstrument(): bool + { + return $this->mode === self::MODE_CS_INSTRUMENT; + } + + public function isUse(): bool + { + return $this->mode === self::MODE_USE; + } + + /** + * @param list $sapis + */ + public function setTrainableSapis(array $sapis): void + { + $resolved = []; + foreach ($sapis as $sapi) { + $r = $this->resolveSapi($sapi); + if (!in_array($r, $resolved, true)) { + $resolved[] = $r; + } + } + if ($resolved === []) { + throw new WrongUsageException( + 'PGO: no trainable SAPI selected; supply one of ' . implode(', ', array_keys(self::TRAINABLE)) + ); + } + $this->trainableSapis = $resolved; + } + + /** @return list */ + public function trainableSapis(): array + { + return $this->trainableSapis; + } + + /** + * Static-embed mode links libphp.a into frankenphp, sharing a single binary + * and profdata. Shared-embed keeps them separate. + */ + public function resolveSapi(string $sapi): string + { + if ($sapi === 'embed' && getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { + return 'frankenphp'; + } + return $sapi; + } + + public function rawDir(string $sapi): string + { + return $this->profileRoot . '/' . $sapi; + } + + public function csRawDir(string $sapi): string + { + return $this->profileRoot . '/cs-' . $sapi; + } + + public function profDataFile(string $sapi): string + { + return $this->profileRoot . '/' . $sapi . '.profdata'; + } + + public function cflagsFor(string $sapi): string + { + $sapi = $this->resolveSapi($sapi); + if ($this->mode === self::MODE_USE && !is_file($this->profDataFile($sapi))) { + return ''; + } + return match ($this->mode) { + self::MODE_INSTRUMENT => '-fprofile-generate=' . $this->rawDir($sapi) + . ' -fprofile-update=atomic', + self::MODE_CS_INSTRUMENT => '-fprofile-use=' . $this->profDataFile($sapi) + . ' -fcs-profile-generate=' . $this->csRawDir($sapi) + . ' -fprofile-update=atomic' + . ' -Wno-error=profile-instr-unprofiled' + . ' -Wno-error=profile-instr-out-of-date' + . ' -Wno-backend-plugin', + self::MODE_USE => '-fprofile-use=' . $this->profDataFile($sapi) + . ' -Wno-error=profile-instr-unprofiled' + . ' -Wno-error=profile-instr-out-of-date' + . ' -Wno-backend-plugin', + default => throw new WrongUsageException("PgoContext: unreachable mode '{$this->mode}'"), + }; + } + + public function ldflagsFor(string $sapi): string + { + $resolved = $this->resolveSapi($sapi); + $flags = $this->cflagsFor($sapi); + $patterns = ['/\s*-Wno-error=\S+/', '/\s*-Wno-backend-plugin/']; + if ($resolved === 'frankenphp') { + $patterns[] = '/\s*-fprofile-use=\S+/'; + $patterns[] = '/\s*-fcs-profile-generate=\S+/'; + } + return trim((string) preg_replace($patterns, '', $flags)); + } + + public function applyEnvFor(string $sapi): void + { + self::overwritePgoFlags('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS', $this->cflagsFor($sapi)); + self::overwritePgoFlags('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', $this->ldflagsFor($sapi)); + } + + public function setupInstrument(): void + { + FileSystem::removeDir($this->profileRoot); + FileSystem::createDir($this->profileRoot); + foreach ($this->trainableSapis as $sapi) { + FileSystem::createDir($this->rawDir($sapi)); + } + } + + public function setupCsInstrument(): void + { + foreach ($this->trainableSapis as $sapi) { + if (!is_file($this->profDataFile($sapi))) { + throw new WrongUsageException( + "PGO --phase=cs-instrument: missing {$sapi}.profdata; run --phase=instrument and --phase=use first" + ); + } + FileSystem::createDir($this->csRawDir($sapi)); + } + } + + public function mergeProfiles(): void + { + foreach ($this->trainableSapis as $sapi) { + $this->mergeSapi($sapi); + } + } + + private function mergeSapi(string $sapi): void + { + $raws = glob($this->rawDir($sapi) . '/*.profraw') ?: []; + $csRaws = glob($this->csRawDir($sapi) . '/*.profraw') ?: []; + if ($raws === [] && $csRaws === []) { + if ($sapi === 'frankenphp') { + logger()->warning( + 'PGO --phase=use: no .profraw for frankenphp (cgo-glue PGO will be skipped); ' . + 'run --phase=instrument, exercise frankenphp longer, then re-run --phase=use' + ); + return; + } + throw new WrongUsageException( + "PGO --phase=use: no .profraw for {$sapi}; run --phase=instrument, exercise the binary, then re-run --phase=use" + ); + } + $out = $this->profDataFile($sapi); + $inputs = array_merge($raws, $csRaws); + $argv = implode(' ', array_map('escapeshellarg', $inputs)); + $profdata = PKG_ROOT_PATH . '/llvm-tools/bin/llvm-profdata'; + shell()->exec(escapeshellarg($profdata) . ' merge --failure-mode=warn -output=' . escapeshellarg($out) . ' ' . $argv); + if (!is_file($out) || filesize($out) === 0) { + throw new WrongUsageException("PGO --phase=use: empty merge output for {$sapi}"); + } + logger()->info("PGO merged {$sapi}: " . filesize($out) . ' bytes'); + } + + private static function overwritePgoFlags(string $var, string $append): void + { + $cur = (string) getenv($var); + $cur = preg_replace('/\s*-f(cs-)?profile-(generate|use)=\S+/', '', $cur) ?? $cur; + $cur = preg_replace('/\s*-fprofile-update=atomic/', '', $cur) ?? $cur; + $cur = preg_replace('/\s*-Wno-error=profile-instr-\S+/', '', $cur) ?? $cur; + $cur = preg_replace('/\s*-Wno-backend-plugin/', '', $cur) ?? $cur; + f_putenv($var . '=' . trim(trim($cur) . ' ' . $append)); + } +} diff --git a/src/StaticPHP/Util/PkgConfigUtil.php b/src/StaticPHP/Util/PkgConfigUtil.php index d5b03757f..ba3026aa7 100644 --- a/src/StaticPHP/Util/PkgConfigUtil.php +++ b/src/StaticPHP/Util/PkgConfigUtil.php @@ -67,7 +67,7 @@ public static function getModuleVersion(string $pkg_config_str): string public static function getCflags(string $pkg_config_str): string { // get other things - $result = self::execWithResult("pkg-config --static --cflags-only-other {$pkg_config_str}"); + $result = self::execWithResult("pkg-config --static --cflags {$pkg_config_str}"); return trim($result); } diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 63b0e90ef..163a21fb6 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -5,9 +5,12 @@ namespace StaticPHP\Util; use StaticPHP\Config\PackageConfig; +use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Package\TargetPackage; use StaticPHP\Runtime\SystemTarget; class SPCConfigUtil @@ -32,123 +35,82 @@ public function __construct(array $options = []) $this->absolute_libs = $options['absolute_libs'] ?? false; } - public function config(array $packages = [], bool $include_suggests = false): array + public function config(array $packages = []): array { - // if have php, make php as all extension's dependency - if (!$this->no_php) { - $dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))]; - } else { - $dep_override = []; - } - $resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests); - - $ldflags = $this->getLdflagsString(); - $cflags = $this->getIncludesString($resolved); - $libs = $this->getLibsString($resolved, !$this->absolute_libs); - - // additional OS-specific libraries (e.g. macOS -lresolv) - // embed - if ($extra_libs = SystemTarget::getRuntimeLibs()) { - $libs .= " {$extra_libs}"; - } + // Walk depends+suggests within the resolved set; reaching `php` fans out to its + // effective link closure (resolved static exts + virtual-target SAPIs). + $installer = ApplicationContext::get(PackageInstaller::class); + $resolved_set = array_flip(array_keys($installer->getResolvedPackages())); - $extra_env = getenv('SPC_EXTRA_LIBS'); - if (is_string($extra_env) && !empty($extra_env)) { - $libs .= " {$extra_env}"; - } - // package frameworks - if (SystemTarget::getTargetOS() === 'Darwin') { - $libs .= " {$this->getFrameworksString($resolved)}"; - } - // C++ - if ($this->hasCpp($resolved)) { - $target_os = SystemTarget::getTargetOS(); - if ($target_os === 'Darwin') { - $libcpp = '-lc++'; - $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; - } elseif ($target_os !== 'Windows') { - // Linux and other Unix-like systems use libstdc++ - $libcpp = '-lstdc++'; - $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; - } - // Windows (MSVC): C++ runtime is linked automatically, no explicit lib needed - } - - if ($this->libs_only_deps) { - // mimalloc must come first - if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); - } - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), - ]; - } - - // embed + $php_extras = []; if (!$this->no_php) { - if (SystemTarget::getTargetOS() === 'Windows') { - // Windows: use php8embed.lib directly (either full path or short name) - $major = intdiv(PHP_VERSION_ID, 10000); - $php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.lib"; - // Windows system libs required by PHP - // Use same system libs as PHP Makefile: LIBS=kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib - $libs = "{$php_lib} {$libs} kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib"; - } else { - $libs = "-lphp {$libs} -lc"; + foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { + if ($ext->isBuildStatic()) { + $php_extras[] = $ext->getName(); + } + } + foreach ($installer->getResolvedPackages(TargetPackage::class) as $target) { + if ($target->isVirtualTarget()) { + $php_extras[] = $target->getName(); + } } } - $allLibs = getenv('LIBS') . ' ' . $libs; - - // mimalloc must come first - if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { - $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); + $sorted = []; + $visited = []; + foreach ($packages as $pkg) { + self::visitConfigDeps( + is_string($pkg) ? $pkg : $pkg->getName(), + $resolved_set, + $php_extras, + $visited, + $sorted, + ); } - - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces($allLibs), - ]; + return $this->configWithResolvedPackages($sorted); } /** * [Helper function] * Get configuration for a specific extension(s) dependencies. * + * Uses the installer's resolved package set as the source of truth — only libraries that + * are actually enabled in this build appear in the result. The resolved set already + * reflects the user's `--with-suggests` choice. + * * @param array|PhpExtensionPackage $extension_packages Extension instance or list * @return array{ * cflags: string, + * cxxflags: string, * ldflags: string, * libs: string * } */ - public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array + public function getExtensionConfig(array|PhpExtensionPackage $extension_packages): array { if (!is_array($extension_packages)) { $extension_packages = [$extension_packages]; } - return $this->config( - packages: array_map(fn ($y) => $y->getName(), $extension_packages), - include_suggests: $include_suggests, - ); + $names = array_map(fn ($y) => $y->getName(), $extension_packages); + return $this->configWithResolvedPackages($this->collectEnabledLinkPackages($names)); } /** * [Helper function] * Get configuration for a specific library(s) dependencies. * - * @param array|LibraryPackage $lib Library instance or list - * @param bool $include_suggests Whether to include suggested libraries + * Like {@see getExtensionConfig()}, draws from the resolved package set so we never + * link against a library that wasn't built. + * + * @param array|LibraryPackage $lib Library instance or list * @return array{ * cflags: string, + * cxxflags: string, * ldflags: string, * libs: string * } */ - public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array + public function getLibraryConfig(array|LibraryPackage $lib): array { if (!is_array($lib)) { $lib = [$lib]; @@ -157,39 +119,37 @@ public function getLibraryConfig(array|LibraryPackage $lib, bool $include_sugges $this->no_php = true; $save_libs_only_deps = $this->libs_only_deps; $this->libs_only_deps = true; - $ret = $this->config( - packages: array_map(fn ($y) => $y->getName(), $lib), - include_suggests: $include_suggests, - ); + $names = array_map(fn ($y) => $y->getName(), $lib); + $ret = $this->configWithResolvedPackages($this->collectEnabledLinkPackages($names)); $this->no_php = $save_no_php; $this->libs_only_deps = $save_libs_only_deps; return $ret; } /** - * Get build configuration for a package and its sub-dependencies within a resolved set. + * Get build configuration for a package's sub-dependencies within a resolved set. * - * This is useful when you need to statically link something against a specific - * library and all its transitive dependencies. It properly handles optional - * dependencies by only including those that were actually resolved. + * Walks both depends and suggests edges — the resolved set is the filter, so anything + * reachable but unbuilt is naturally excluded. No `include_suggests` knob is needed. * * @param string $package_name The package to get config for * @param string[] $resolved_packages The full resolved package list - * @param bool $include_suggests Whether to include resolved suggests * @return array{ * cflags: string, + * cxxflags: string, * ldflags: string, * libs: string * } */ - public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array + public function getPackageDepsConfig(string $package_name, array $resolved_packages): array { // Get sub-dependencies within the resolved set - $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); + $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, include_suggests: true); if (empty($sub_deps)) { return [ 'cflags' => '', + 'cxxflags' => '', 'ldflags' => '', 'libs' => '', ]; @@ -210,11 +170,12 @@ public function getPackageDepsConfig(string $package_name, array $resolved_packa } /** - * Get configuration using already-resolved packages (skip dependency resolution). + * Build cflags/cxxflags/ldflags/libs for an already-resolved package set (skip dependency resolution). * - * @param string[] $resolved_packages Already resolved package names in build order + * @param string[] $resolved_packages Resolved package names in build order * @return array{ * cflags: string, + * cxxflags: string, * ldflags: string, * libs: string * } @@ -222,9 +183,14 @@ public function getPackageDepsConfig(string $package_name, array $resolved_packa public function configWithResolvedPackages(array $resolved_packages): array { $ldflags = $this->getLdflagsString(); - $cflags = $this->getIncludesString($resolved_packages); + $includes = $this->getIncludesString($resolved_packages); $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); + // includes (-I…) are language-agnostic — same source for cflags/cxxflags, swap only the env names + $cflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_CFLAGS') ?: '') . ' ' . getenv('CFLAGS') . ' ' . $includes)); + $cxxflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_CXXFLAGS') ?: '') . ' ' . getenv('CXXFLAGS') . ' ' . $includes)); + $ldflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_LDFLAGS') ?: '') . ' ' . getenv('LDFLAGS') . ' ' . $ldflags)); + // additional OS-specific libraries (e.g. macOS -lresolv) if ($extra_libs = SystemTarget::getRuntimeLibs()) { $libs .= " {$extra_libs}"; @@ -260,15 +226,25 @@ public function configWithResolvedPackages(array $resolved_packages): array $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); } return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'cflags' => $cflags, + 'cxxflags' => $cxxflags, + 'ldflags' => $ldflags, 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), ]; } // embed if (!$this->no_php) { - $libs = "-lphp {$libs} -lc"; + if (SystemTarget::getTargetOS() === 'Windows') { + // Windows: use php8embed.lib directly (either full path or short name) + $major = intdiv(PHP_VERSION_ID, 10000); + $php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.lib"; + // Windows system libs required by PHP + // Use same system libs as PHP Makefile: LIBS=kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib + $libs = "{$php_lib} {$libs} kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib"; + } else { + $libs = "-lphp {$libs} -lc"; + } } $allLibs = getenv('LIBS') . ' ' . $libs; @@ -279,8 +255,9 @@ public function configWithResolvedPackages(array $resolved_packages): array } return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'cflags' => $cflags, + 'cxxflags' => $cxxflags, + 'ldflags' => $ldflags, 'libs' => clean_spaces($allLibs), ]; } @@ -299,6 +276,50 @@ public function getFrameworksString(array $extensions): string return implode(' ', $list); } + private static function visitConfigDeps( + string $name, + array $resolved_set, + array $php_extras, + array &$visited, + array &$sorted, + ): void { + if (isset($visited[$name]) || !isset($resolved_set[$name])) { + return; + } + $visited[$name] = true; + + $deps = array_merge( + PackageConfig::get($name, 'depends', []), + PackageConfig::get($name, 'suggests', []), + ); + if ($name === 'php') { + $deps = array_merge($deps, $php_extras); + } + + foreach ($deps as $dep) { + self::visitConfigDeps($dep, $resolved_set, $php_extras, $visited, $sorted); + } + $sorted[] = $name; + } + + /** + * For each input package name, gather its transitive deps within the installer's resolved + * set (walking depends + suggests edges), plus the package itself, deduped and in build order. + * + * @param string[] $package_names Input package names + * @return string[] Resolved packages to link against + */ + private function collectEnabledLinkPackages(array $package_names): array + { + $resolved = array_keys(ApplicationContext::get(PackageInstaller::class)->getResolvedPackages()); + $out = []; + foreach ($package_names as $name) { + $sub = DependencyResolver::getSubDependencies($name, $resolved, include_suggests: true); + $out = [...$out, ...$sub, $name]; + } + return array_values(array_unique($out)); + } + private function hasCpp(array $packages): bool { foreach ($packages as $package) { diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 70525e67f..520ef3a8f 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -168,6 +168,9 @@ public static function unpatchHardcodedINI(string $php_source_dir): bool */ public static function patchMicroPhar(int $version_id): void { + if (file_exists(SOURCE_PATH . '/php-src/ext/phar/phar.c.bak')) { + return; + } FileSystem::backupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c'); FileSystem::replaceFileStr( SOURCE_PATH . '/php-src/ext/phar/phar.c', @@ -193,6 +196,12 @@ public static function patchMicroPhar(int $version_id): void public static function unpatchMicroPhar(): void { + // Tolerate a missing .bak: both drivers call this, and the first restore + // consumes the backup. Without this guard the second call throws + // "Cannot find bak file". No .bak means the source is already pristine. + if (!file_exists(SOURCE_PATH . '/php-src/ext/phar/phar.c.bak')) { + return; + } FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c'); } diff --git a/src/globals/functions.php b/src/globals/functions.php index 549ef498d..c824bd3df 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -256,10 +256,30 @@ function clean_spaces(string $string): string */ function deduplicate_flags(string $flags): string { - $tokens = preg_split('/\s+/', trim($flags)); + // Flags that take their value as a separate token. + static $paired = [ + '-Xclang', '-Xpreprocessor', '-Xlinker', '-Xassembler', + '-framework', '-arch', '-target', + '-include', '-imacros', '-isystem', '-isysroot', '-iquote', '-idirafter', + '-MT', '-MF', '-MQ', + ]; + + $tokens = preg_split('/\s+/', trim($flags)) ?: []; + + // Group paired flag+value into a single atom before dedup. + $atoms = []; + $n = count($tokens); + for ($i = 0; $i < $n; ++$i) { + if (in_array($tokens[$i], $paired, true) && $i + 1 < $n) { + $atoms[] = $tokens[$i] . ' ' . $tokens[$i + 1]; + ++$i; + } else { + $atoms[] = $tokens[$i]; + } + } // Reverse, unique, reverse back - keeps last occurrence of duplicates - $deduplicated = array_reverse(array_unique(array_reverse($tokens))); + $deduplicated = array_reverse(array_unique(array_reverse($atoms))); return implode(' ', $deduplicated); } diff --git a/src/globals/internal-env.php b/src/globals/internal-env.php index 97df88417..74e352007 100644 --- a/src/globals/internal-env.php +++ b/src/globals/internal-env.php @@ -72,7 +72,6 @@ putenv('SOURCE_PATH=' . SOURCE_PATH); putenv('DOWNLOAD_PATH=' . DOWNLOAD_PATH); putenv('CPU_COUNT=' . CPU_COUNT); -putenv('SPC_ARCH=' . php_uname('m')); putenv('GNU_ARCH=' . GNU_ARCH); putenv('MAC_ARCH=' . MAC_ARCH); diff --git "a/src/globals/patch/spc_fix_avx512_cache_before_80400.patch\342\200\216" b/src/globals/patch/spc_fix_avx512_cache_before_80400.patch similarity index 100% rename from "src/globals/patch/spc_fix_avx512_cache_before_80400.patch\342\200\216" rename to src/globals/patch/spc_fix_avx512_cache_before_80400.patch diff --git a/src/globals/patch/spc_pgo_flush_frankenphp.patch b/src/globals/patch/spc_pgo_flush_frankenphp.patch new file mode 100644 index 000000000..7c58fafff --- /dev/null +++ b/src/globals/patch/spc_pgo_flush_frankenphp.patch @@ -0,0 +1,15 @@ +--- a/frankenphp.c ++++ b/frankenphp.c +@@ -1254,6 +1254,12 @@ + + go_frankenphp_shutdown_main_thread(); + ++ /* spc-pgo: explicit profile flush so the cgo-instrumented frankenphp ++ * still writes .profraw on Go-runtime exit (which bypasses libc atexit). ++ * Weak symbol → no-op in non-PGO builds. */ ++ { extern int __llvm_profile_write_file(void) __attribute__((weak)); ++ if (__llvm_profile_write_file) __llvm_profile_write_file(); } ++ + return NULL; + } + diff --git a/src/globals/patch/spc_pgo_flush_php_main.patch b/src/globals/patch/spc_pgo_flush_php_main.patch new file mode 100644 index 000000000..6bf905bf0 --- /dev/null +++ b/src/globals/patch/spc_pgo_flush_php_main.patch @@ -0,0 +1,12 @@ +--- a/main/main.c ++++ b/main/main.c +@@ -2563,6 +2563,9 @@ + #endif + + zend_observer_shutdown(); ++ ++ { extern int __llvm_profile_write_file(void) __attribute__((weak)); ++ if (__llvm_profile_write_file) __llvm_profile_write_file(); } + } + /* }}} */ + diff --git a/src/globals/scripts/zig-cc.sh b/src/globals/scripts/zig-cc.sh index 56ae95055..9c4ab1dd8 100755 --- a/src/globals/scripts/zig-cc.sh +++ b/src/globals/scripts/zig-cc.sh @@ -1,9 +1,14 @@ #!/usr/bin/env bash SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" -BUILDROOT_ABS="${BUILD_ROOT_PATH:-$(realpath "$SCRIPT_DIR/../../../buildroot/include" 2>/dev/null || true)}" +BUILDROOT_INC="${BUILD_INCLUDE_PATH:-$SCRIPT_DIR/../../../buildroot/include}" +BUILDROOT_ABS="$(realpath "$BUILDROOT_INC" 2>/dev/null || true)" PARSED_ARGS=() +is_buildroot_inc() { + [[ -n "$BUILDROOT_ABS" && "$1" == "$BUILDROOT_ABS" ]] +} + while [[ $# -gt 0 ]]; do case "$1" in -isystem) @@ -11,27 +16,37 @@ while [[ $# -gt 0 ]]; do ARG="$1" shift ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" - [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem" "$ARG") + is_buildroot_inc "$ARG_ABS" && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem" "$ARG") ;; -isystem*) ARG="${1#-isystem}" shift ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" - [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG") + is_buildroot_inc "$ARG_ABS" && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG") ;; -march=*|-mcpu=*) OPT_NAME="${1%%=*}" OPT_VALUE="${1#*=}" - # Skip armv8- flags entirely as Zig doesn't support them - if [[ "$OPT_VALUE" == armv8-* ]]; then - shift - continue + # zig rejects -march=armv8-a but accepts -mcpu=generic+v8a; rewrite + # armv[.]-a[+feat] -> generic+v[_]a[+feat] so it goes through. + if [[ "$OPT_VALUE" =~ ^armv([89])(\.([0-9]+))?-a(\+.*)?$ ]]; then + arch_feat="v${BASH_REMATCH[1]}" + [[ -n "${BASH_REMATCH[3]}" ]] && arch_feat="${arch_feat}_${BASH_REMATCH[3]}" + OPT_VALUE="generic+${arch_feat}a${BASH_REMATCH[4]}" fi - # replace -march=x86-64 with -march=x86_64 + # zig uses snake_case in CPU/feature names (x86-64 -> x86_64). OPT_VALUE="${OPT_VALUE//-/_}" PARSED_ARGS+=("${OPT_NAME}=${OPT_VALUE}") shift ;; + -mtune=generic) + PARSED_ARGS+=("-mtune=baseline") + shift + ;; + -Wlogical-op|-Wduplicated-cond|-Wduplicated-branches|-Wno-clobbered|-Wjump-misses-init|-Wformat-truncation|-Warray-bounds=*|-Wimplicit-fallthrough=*) + # GCC-only warning flags that clang/zig doesn't recognize; drop to silence -Wunknown-warning-option noise + shift + ;; *) PARSED_ARGS+=("$1") shift @@ -39,6 +54,29 @@ while [[ $# -gt 0 ]]; do esac done +IS_LINK=1 +NEED_PROFILE_RT=0 # https://codeberg.org/ziglang/zig/issues/32066 +NEED_CRT=0 # https://codeberg.org/ziglang/zig/issues/32064 +for _a in "${PARSED_ARGS[@]}"; do + case "$_a" in + -c|-S|-E|-M|-MM) IS_LINK=0 ;; + -fprofile-generate*|-fprofile-instr-generate*|-fcs-profile-generate*) NEED_PROFILE_RT=1 ;; + -shared) NEED_CRT=1 ;; + esac +done +[[ "$SPC_COMPILER_EXTRA" == *-fprofile-generate* || "$SPC_COMPILER_EXTRA" == *-fcs-profile-generate* ]] && NEED_PROFILE_RT=1 + +RT_DIR="${SPC_COMPILER_RT_DIR:-}" +if [[ $IS_LINK -eq 1 && $NEED_PROFILE_RT -eq 1 && -n "$RT_DIR" && -f "$RT_DIR/libclang_rt.profile.a" ]]; then + PARSED_ARGS+=("-x" "none" "$RT_DIR/libclang_rt.profile.a" "-Wl,-u,__llvm_profile_runtime") +fi +if [[ $IS_LINK -eq 1 && $NEED_CRT -eq 1 && -n "$RT_DIR" && -f "$RT_DIR/clang_rt.crtbegin.o" && -f "$RT_DIR/clang_rt.crtend.o" ]]; then + PARSED_ARGS+=("-x" "none" "$RT_DIR/clang_rt.crtbegin.o" "$RT_DIR/clang_rt.crtend.o") +fi +if [[ $IS_LINK -eq 1 && -n "$RT_DIR" && -f "$RT_DIR/libclang_rt.cpu_model.a" ]]; then + PARSED_ARGS+=("-x" "none" "$RT_DIR/libclang_rt.cpu_model.a") +fi + [[ -n "$SPC_TARGET" ]] && TARGET="-target $SPC_TARGET" || TARGET="" if [[ "$SPC_TARGET" =~ \.[0-9]+\.[0-9]+ ]]; then