diff --git a/config/env.ini b/config/env.ini index 0be1abdfb..f9cde5a08 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-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-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/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_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/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/rdkafka.php b/src/Package/Extension/rdkafka.php index 4bb28ee57..2a77ab611 100644 --- a/src/Package/Extension/rdkafka.php +++ b/src/Package/Extension/rdkafka.php @@ -49,7 +49,8 @@ public function patchBeforeMake(): bool #[CustomPhpConfigureArg('Linux')] public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): string { - $pkgconf_libs = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->getExtensionConfig($this); + $pkgconf_libs = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]) + ->configForResolvedBuild([$this->getName()], $this->getInstaller()); return '--with-rdkafka=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath() . " RDKAFKA_LIBS=\"{$pkgconf_libs['libs']}\""; } } diff --git a/src/Package/Extension/swoole.php b/src/Package/Extension/swoole.php index 10fb19284..70ca033a6 100644 --- a/src/Package/Extension/swoole.php +++ b/src/Package/Extension/swoole.php @@ -93,7 +93,8 @@ public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, Packa $arg .= $installer->getPhpExtensionPackage('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd'; $arg .= $installer->getPhpExtensionPackage('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite'; if ($installer->getPhpExtensionPackage('swoole-hook-odbc')) { - $config = new SPCConfigUtil()->getLibraryConfig($installer->getLibraryPackage('unixodbc')); + $config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]) + ->configForResolvedBuild(['unixodbc'], $installer); $arg .= " --with-swoole-odbc=unixODBC,{$builder->getBuildRootPath()} SWOOLE_ODBC_LIBS=\"{$config['libs']}\""; } 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/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..a896d9b57 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,27 @@ 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 ( + in_array($targetCpu, ['x86', 'x86_64'], true) + && !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/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 f673bb67f..fbde9cf24 100644 --- a/src/Package/Library/openssl.php +++ b/src/Package/Library/openssl.php @@ -11,6 +11,7 @@ use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\ToolPackage; use StaticPHP\Registry\PackageLoader; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; use StaticPHP\Util\System\LinuxUtil; @@ -57,7 +58,11 @@ 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'); + $targetArch = SystemTarget::getTargetArch(); + $arch = match ($targetArch) { + 'aarch64' => 'arm64', + default => $targetArch, + }; shell()->cd($pkg->getSourceDir())->initializeEnv($pkg) ->exec( @@ -76,12 +81,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 = @@ -92,9 +92,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} " . @@ -102,7 +108,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 25d600e26..0d8c9ca71 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -129,7 +129,7 @@ public function buildWin(LibraryPackage $lib): void 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/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 854dc8cf8..c2e67c2f8 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -122,10 +122,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()->configForResolvedBuild([$package->getName()], $installer); $cflags = "{$package->getLibExtraCFlags()} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . " -DFRANKENPHP_VERSION={$frankenphp_version}"; $libs = $config['libs']; diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 7dcc6d8bb..e306647f5 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -590,7 +590,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()->configForResolvedBuild(['php'], $installer); $lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}"; if ($toolchain->isStatic()) { $lens .= ' -static'; @@ -785,7 +785,7 @@ private function processLibphpSoFile(string $libphpSo, PackageInstaller $install */ private function makeVars(PackageInstaller $installer): array { - $config = new SPCConfigUtil(['libs_only_deps' => true])->config($installer->getAvailableResolvedPackageNames()); + $config = new SPCConfigUtil(['libs_only_deps' => true])->configForResolvedBuild(['php'], $installer); $static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : ''; $pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : ''; diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 915b1821e..7bfcd53eb 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -708,7 +708,7 @@ public function smokeTestEmbedForWindows(PackageInstaller $installer, TargetPack // Get build configuration using spc-config $util = new \StaticPHP\Util\SPCConfigUtil(); - $config = $util->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + $config = $util->configForResolvedBuild(['php'], $installer); // Build the embed test executable using cl.exe // Note: MSVCToolchain already initialized the VC environment, no need for vcvarsall 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/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index 1fe7ed00d..2d871f0c7 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -13,6 +13,7 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Doctor\CheckResult; use StaticPHP\Runtime\Shell\Shell; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\MuslToolchain; use StaticPHP\Toolchain\ZigToolchain; @@ -27,7 +28,14 @@ class LinuxMuslCheck public static function optionalCheck(): bool { $toolchain = ApplicationContext::get(ToolchainInterface::class); - return $toolchain instanceof MuslToolchain || $toolchain instanceof ZigToolchain && !LinuxUtil::isMuslDist() && !str_contains(getenv('SPC_TARGET') ?: '', 'gnu'); + return PHP_OS_FAMILY === 'Linux' && ( + $toolchain instanceof MuslToolchain || ( + $toolchain instanceof ZigToolchain + && !LinuxUtil::isMuslDist() + && SystemTarget::getLibc() === 'musl' + && !$toolchain->isStatic() + ) + ); } /** @noinspection PhpUnused */ diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index cd8c626e5..92cd627d7 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -305,8 +305,8 @@ public function packPrebuilt(): void */ public function getStaticLibFiles(): string { - $config = new SPCConfigUtil(['libs_only_deps' => true, 'absolute_libs' => true]); - $res = $config->config([$this->getName()]); + $config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true, 'absolute_libs' => true]); + $res = $config->configForResolvedBuild([$this->getName()], $this->getInstaller()); return $res['libs']; } diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index fef88efd4..68a5b8a33 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -274,13 +274,13 @@ 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())->configForResolvedBuild([$this->getName()], $this->getInstaller()); [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; return [ 'CFLAGS' => $config['cflags'], - 'CXXFLAGS' => $config['cflags'], + 'CXXFLAGS' => $config['cxxflags'], 'LDFLAGS' => $config['ldflags'], 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php index 129468f92..9833e0672 100644 --- a/src/StaticPHP/Util/DependencyResolver.php +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -136,6 +136,52 @@ public static function getSubDependencies(string $package_name, array $resolved_ return $sorted; } + /** + * Get the link closure for one or more roots within an already-resolved package set. + * + * Both depends and suggests may be traversed, but only packages present in the + * resolved set are returned. Dependency overrides add build-specific graph edges, + * such as PHP's resolved static extensions and virtual SAPI targets. + * + * @param array $packages + * @param string[] $resolved_packages + * @param array $dependency_overrides + * @return string[] Packages in dependency order, including the roots + */ + public static function getResolvedPackageClosure( + array $packages, + array $resolved_packages, + array $dependency_overrides = [], + bool $include_suggests = true, + ): array { + $resolved_set = array_flip($resolved_packages); + $dep_map = []; + foreach ($resolved_packages as $package) { + $dep_map[$package] = [ + 'depends' => array_merge( + PackageConfig::get($package, 'depends', []), + $dependency_overrides[$package] ?? [], + ), + 'suggests' => PackageConfig::get($package, 'suggests', []), + ]; + } + + $visited = []; + $sorted = []; + foreach ($packages as $package) { + $name = is_string($package) ? $package : $package->getName(); + if (!PackageConfig::isPackageExists($name)) { + throw new WrongUsageException("Package '{$name}' does not exist in config, please check your package name !"); + } + if (!isset($resolved_set[$name])) { + throw new WrongUsageException("Package '{$name}' is not part of the resolved package set."); + } + self::visitSubDeps($name, $dep_map, $resolved_set, $include_suggests, $visited, $sorted); + } + + return $sorted; + } + /** * Build a reverse dependency map for the resolved packages. * For each package that is depended upon, list which packages depend on it. diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 63b0e90ef..3c9fa37dd 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -7,7 +7,10 @@ use StaticPHP\Config\PackageConfig; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\Package; +use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Package\TargetPackage; use StaticPHP\Runtime\SystemTarget; class SPCConfigUtil @@ -32,95 +35,72 @@ public function __construct(array $options = []) $this->absolute_libs = $options['absolute_libs'] ?? false; } + /** + * Resolve a standalone package request and generate its configuration. + * + * This is the entry point for spc-config and callers that do not already have + * a PackageInstaller resolution result. + * + * @param array $packages + * @return array{cflags: string, cxxflags: string, ldflags: string, libs: string} + */ public function config(array $packages = [], bool $include_suggests = false): 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}"; - } + $package_names = array_map(fn ($package) => is_string($package) ? $package : $package->getName(), $packages); + $dep_override = $this->no_php + ? [] + : ['php' => array_values(array_filter($package_names, fn ($name) => str_starts_with($name, 'ext-')))]; - $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), - ]; - } + return $this->configWithResolvedPackages( + DependencyResolver::resolve($packages, $dep_override, $include_suggests) + ); + } - // embed + /** + * Build configuration for roots within the installer's resolved package graph. + * + * The installer decides whether suggested packages are enabled. This method always + * walks depends and suggests, then filters every edge through that resolved set. + * + * @param string[] $package_names Root package names whose link closure is required + * @return array{cflags: string, cxxflags: string, ldflags: string, libs: string} + */ + public function configForResolvedBuild(array $package_names, PackageInstaller $installer): array + { + $dependency_overrides = []; 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"; + $php_link_packages = []; + foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $extension) { + if ($extension->isBuildStatic()) { + $php_link_packages[] = $extension->getName(); + } } + foreach ($installer->getResolvedPackages(TargetPackage::class) as $target) { + if ($target->isVirtualTarget()) { + $php_link_packages[] = $target->getName(); + } + } + $dependency_overrides['php'] = $php_link_packages; } - $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); - } - - return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), - 'libs' => clean_spaces($allLibs), - ]; + return $this->configWithResolvedPackages( + DependencyResolver::getResolvedPackageClosure( + $package_names, + array_keys($installer->getResolvedPackages()), + $dependency_overrides, + ) + ); } /** * [Helper function] * Get configuration for a specific extension(s) dependencies. * - * @param array|PhpExtensionPackage $extension_packages Extension instance or list + * @param PhpExtensionPackage|PhpExtensionPackage[] $extension_packages Extension instance or list * @return array{ * cflags: string, + * cxxflags: string, * ldflags: string, * libs: string * } @@ -131,7 +111,7 @@ public function getExtensionConfig(array|PhpExtensionPackage $extension_packages $extension_packages = [$extension_packages]; } return $this->config( - packages: array_map(fn ($y) => $y->getName(), $extension_packages), + packages: array_map(fn ($extension_package) => $extension_package->getName(), $extension_packages), include_suggests: $include_suggests, ); } @@ -140,30 +120,33 @@ public function getExtensionConfig(array|PhpExtensionPackage $extension_packages * [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 + * @param LibraryPackage|LibraryPackage[] $library_packages Library instance or list + * @param bool $include_suggests Whether to include suggested libraries * @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 $library_packages, bool $include_suggests = false): array { - if (!is_array($lib)) { - $lib = [$lib]; + if (!is_array($library_packages)) { + $library_packages = [$library_packages]; } $save_no_php = $this->no_php; $this->no_php = true; $save_libs_only_deps = $this->libs_only_deps; $this->libs_only_deps = true; - $ret = $this->config( - packages: array_map(fn ($y) => $y->getName(), $lib), - include_suggests: $include_suggests, - ); - $this->no_php = $save_no_php; - $this->libs_only_deps = $save_libs_only_deps; - return $ret; + try { + return $this->config( + packages: array_map(fn ($library_package) => $library_package->getName(), $library_packages), + include_suggests: $include_suggests, + ); + } finally { + $this->no_php = $save_no_php; + $this->libs_only_deps = $save_libs_only_deps; + } } /** @@ -173,23 +156,25 @@ public function getLibraryConfig(array|LibraryPackage $lib, bool $include_sugges * library and all its transitive dependencies. It properly handles optional * dependencies by only including those that were actually resolved. * - * @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 + * @param string $package_name The package to get config for + * @param string[] $resolved_package_names The full resolved package name 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_package_names, bool $include_suggests = true): 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_package_names, $include_suggests); if (empty($sub_deps)) { return [ 'cflags' => '', + 'cxxflags' => '', 'ldflags' => '', 'libs' => '', ]; @@ -201,29 +186,36 @@ public function getPackageDepsConfig(string $package_name, array $resolved_packa $this->no_php = true; $this->libs_only_deps = true; - $ret = $this->configWithResolvedPackages($sub_deps); - - $this->no_php = $save_no_php; - $this->libs_only_deps = $save_libs_only_deps; - - return $ret; + try { + return $this->configWithResolvedPackages($sub_deps); + } finally { + $this->no_php = $save_no_php; + $this->libs_only_deps = $save_libs_only_deps; + } } /** - * Get configuration using already-resolved packages (skip dependency resolution). + * Generate configuration from an exact, dependency-ordered package list. + * + * This low-level method does not resolve dependencies or filter packages. * - * @param string[] $resolved_packages Already resolved package names in build order + * @param string[] $resolved_package_names Already resolved package names in build order * @return array{ * cflags: string, + * cxxflags: string, * ldflags: string, * libs: string * } */ - public function configWithResolvedPackages(array $resolved_packages): array + public function configWithResolvedPackages(array $resolved_package_names): array { $ldflags = $this->getLdflagsString(); - $cflags = $this->getIncludesString($resolved_packages); - $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); + $includes = $this->getIncludesString($resolved_package_names); + $libs = $this->getLibsString($resolved_package_names, !$this->absolute_libs); + + $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()) { @@ -237,11 +229,11 @@ public function configWithResolvedPackages(array $resolved_packages): array // package frameworks if (SystemTarget::getTargetOS() === 'Darwin') { - $libs .= " {$this->getFrameworksString($resolved_packages)}"; + $libs .= " {$this->getFrameworksString($resolved_package_names)}"; } // C++ - if ($this->hasCpp($resolved_packages)) { + if ($this->hasCpp($resolved_package_names)) { $target_os = SystemTarget::getTargetOS(); if ($target_os === 'Darwin') { $libcpp = '-lc++'; @@ -256,40 +248,49 @@ public function configWithResolvedPackages(array $resolved_packages): array if ($this->libs_only_deps) { // mimalloc must come first - if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + if (in_array('mimalloc', $resolved_package_names) && 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), + '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') { + $major = intdiv(PHP_VERSION_ID, 10000); + $php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.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; // mimalloc must come first - if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + if (in_array('mimalloc', $resolved_package_names) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); } return [ - 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), - 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'cflags' => $cflags, + 'cxxflags' => $cxxflags, + 'ldflags' => $ldflags, 'libs' => clean_spaces($allLibs), ]; } - public function getFrameworksString(array $extensions): string + /** @param string[] $package_names */ + public function getFrameworksString(array $package_names): string { $list = []; - foreach ($extensions as $extension) { - foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { + foreach ($package_names as $package_name) { + foreach (PackageConfig::get($package_name, 'frameworks', []) as $fw) { $ks = '-framework ' . $fw; if (!in_array($ks, $list)) { $list[] = $ks; @@ -299,10 +300,11 @@ public function getFrameworksString(array $extensions): string return implode(' ', $list); } - private function hasCpp(array $packages): bool + /** @param string[] $package_names */ + private function hasCpp(array $package_names): bool { - foreach ($packages as $package) { - $lang = PackageConfig::get($package, 'lang', 'c'); + foreach ($package_names as $package_name) { + $lang = PackageConfig::get($package_name, 'lang', 'c'); if ($lang === 'cpp') { return true; } @@ -310,7 +312,8 @@ private function hasCpp(array $packages): bool return false; } - private function getIncludesString(array $packages): string + /** @param string[] $package_names */ + private function getIncludesString(array $package_names): string { $base = BUILD_INCLUDE_PATH; @@ -347,8 +350,8 @@ private function getIncludesString(array $packages): string // parse pkg-configs (only for Unix) if (SystemTarget::isUnix()) { - foreach ($packages as $package) { - $pc = PackageConfig::get($package, 'pkg-configs', []); + foreach ($package_names as $package_name) { + $pc = PackageConfig::get($package_name, 'pkg-configs', []); $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; $search_paths = array_filter(explode(':', $pkg_config_path)); foreach ($pc as $file) { @@ -360,7 +363,7 @@ private function getIncludesString(array $packages): string } } if (!$found) { - throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist. Please build it first."); + throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package_name}] does not exist. Please build it first."); } } $pc_cflags = implode(' ', $pc); @@ -386,16 +389,17 @@ private function getLdflagsString(): string return '-L' . BUILD_LIB_PATH; } - private function getLibsString(array $packages, bool $use_short_libs = true): string + /** @param string[] $package_names */ + private function getLibsString(array $package_names, bool $use_short_libs = true): string { $lib_names = []; $frameworks = []; - foreach ($packages as $package) { + foreach ($package_names as $package_name) { // parse pkg-configs only for unix systems if (SystemTarget::isUnix()) { // add pkg-configs libs - $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); + $pkg_configs = PackageConfig::get($package_name, 'pkg-configs', []); $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; $search_paths = array_filter(explode(':', $pkg_config_path)); foreach ($pkg_configs as $pkg_config) { @@ -407,7 +411,7 @@ private function getLibsString(array $packages, bool $use_short_libs = true): st } } if (!$found) { - throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist. Please build it first."); + throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package_name}] does not exist. Please build it first."); } } $pkg_configs = implode(' ', $pkg_configs); @@ -418,12 +422,12 @@ private function getLibsString(array $packages, bool $use_short_libs = true): st } } // convert all static-libs to short names - $libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); + $libs = array_reverse(PackageConfig::get($package_name, 'static-libs', [])); foreach ($libs as $lib) { if (FileSystem::isRelativePath($lib)) { // check file existence if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { - throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); + throw new WrongUsageException("Library file '{$lib}' for lib [{$package_name}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); } $lib_names[] = $this->getShortLibName($lib); } else { @@ -432,7 +436,7 @@ private function getLibsString(array $packages, bool $use_short_libs = true): st } // add frameworks for macOS if (SystemTarget::getTargetOS() === 'Darwin') { - $frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); + $frameworks = array_merge($frameworks, PackageConfig::get($package_name, 'frameworks', [])); } } @@ -451,7 +455,7 @@ private function getLibsString(array $packages, bool $use_short_libs = true): st } } - if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { + if (in_array('imap', $package_names) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { $lib_names[] = '-lcrypt'; } diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 98fee7ec2..430a44edc 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -173,6 +173,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', @@ -198,6 +201,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/tests/StaticPHP/Command/SPCConfigCommandTest.php b/tests/StaticPHP/Command/SPCConfigCommandTest.php new file mode 100644 index 000000000..3c0d9b859 --- /dev/null +++ b/tests/StaticPHP/Command/SPCConfigCommandTest.php @@ -0,0 +1,79 @@ +setPackageConfig([ + 'root' => [ + 'type' => 'library', + 'suggests' => ['optional'], + 'static-libs' => ['/fixtures/libroot.a'], + ], + 'optional' => [ + 'type' => 'library', + 'static-libs' => ['/fixtures/liboptional.a'], + ], + ]); + } + + protected function tearDown(): void + { + $this->setPackageConfig([]); + ApplicationContext::reset(); + logger()->setLevel(LogLevel::ERROR); + parent::tearDown(); + } + + public function testWithSuggestsOptionStillAffectsStandaloneCommand(): void + { + $withoutSuggests = $this->runCommand(false); + $withSuggests = $this->runCommand(true); + + $this->assertStringNotContainsString('/fixtures/liboptional.a', $withoutSuggests); + $this->assertStringContainsString('/fixtures/liboptional.a', $withSuggests); + } + + private function runCommand(bool $withSuggests): string + { + $application = new Application(); + $application->setAutoExit(false); + $application->add(new SPCConfigCommand()); + $tester = new CommandTester($application->find('spc-config')); + $input = [ + '--with-libs' => 'root', + '--libs-only-deps' => true, + '--no-php' => true, + '--no-ansi' => true, + ]; + if ($withSuggests) { + $input['--with-suggests'] = true; + } + $tester->execute($input); + + return $tester->getDisplay(); + } + + private function setPackageConfig(array $config): void + { + $reflection = new \ReflectionClass(PackageConfig::class); + $reflection->getProperty('package_configs')->setValue(null, $config); + } +} diff --git a/tests/StaticPHP/Util/DependencyResolverTest.php b/tests/StaticPHP/Util/DependencyResolverTest.php index b9098c668..ce137de71 100644 --- a/tests/StaticPHP/Util/DependencyResolverTest.php +++ b/tests/StaticPHP/Util/DependencyResolverTest.php @@ -412,6 +412,39 @@ public function testGetSubDependenciesOnlyIncludesResolvedDeps(): void $this->assertNotContains('c', $subDeps, 'c is not in the resolved set, should be excluded'); } + public function testGetResolvedPackageClosureFiltersSuggestsThroughResolvedSet(): void + { + $this->loadConfig([ + 'a' => ['type' => 'target', 'depends' => ['b'], 'suggests' => ['c']], + 'b' => ['type' => 'library'], + 'c' => ['type' => 'library'], + ]); + + $withoutSuggested = DependencyResolver::getResolvedPackageClosure(['a'], ['b', 'a']); + $withSuggested = DependencyResolver::getResolvedPackageClosure(['a'], ['b', 'c', 'a']); + + $this->assertSame(['b', 'a'], $withoutSuggested); + $this->assertSame(['b', 'c', 'a'], $withSuggested); + } + + public function testGetResolvedPackageClosureAppliesDependencyOverrides(): void + { + $this->loadConfig([ + 'php' => ['type' => 'target', 'depends' => ['libxml2']], + 'libxml2' => ['type' => 'library'], + 'ext-demo' => ['type' => 'php-extension', 'depends' => ['demo-lib']], + 'demo-lib' => ['type' => 'library'], + ]); + + $closure = DependencyResolver::getResolvedPackageClosure( + ['php'], + ['libxml2', 'demo-lib', 'ext-demo', 'php'], + ['php' => ['ext-demo']], + ); + + $this->assertSame(['libxml2', 'demo-lib', 'ext-demo', 'php'], $closure); + } + // ────────────────────────────────────────────── // Edge cases & defensive // ────────────────────────────────────────────── diff --git a/tests/StaticPHP/Util/SPCConfigUtilTest.php b/tests/StaticPHP/Util/SPCConfigUtilTest.php new file mode 100644 index 000000000..987104991 --- /dev/null +++ b/tests/StaticPHP/Util/SPCConfigUtilTest.php @@ -0,0 +1,207 @@ + */ + private array $savedEnv = []; + + protected function setUp(): void + { + parent::setUp(); + ApplicationContext::reset(); + $this->resetPackageConfig(); + foreach (['CFLAGS', 'CXXFLAGS', 'LDFLAGS', 'LIBS', 'SPC_DEFAULT_CFLAGS', 'SPC_DEFAULT_CXXFLAGS', 'SPC_DEFAULT_LDFLAGS', 'SPC_EXTRA_LIBS'] as $name) { + $this->savedEnv[$name] = getenv($name); + } + } + + protected function tearDown(): void + { + foreach ($this->savedEnv as $name => $value) { + $value === false ? putenv($name) : putenv("{$name}={$value}"); + } + $this->resetPackageConfig(); + ApplicationContext::reset(); + parent::tearDown(); + } + + public function testStandaloneConfigStillControlsSuggestsDuringResolution(): void + { + $this->loadLinkFixture(); + $util = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); + + $withoutSuggests = $util->config(['root']); + $withSuggests = $util->config(['root'], include_suggests: true); + + $this->assertStringContainsString('/fixtures/librequired.a', $withoutSuggests['libs']); + $this->assertStringNotContainsString('/fixtures/liboptional.a', $withoutSuggests['libs']); + $this->assertStringContainsString('/fixtures/liboptional.a', $withSuggests['libs']); + } + + public function testResolvedConfigIncludesOnlySuggestedPackagesThatWereResolved(): void + { + $this->loadLinkFixture(); + $util = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); + + $withoutOptional = $util->configForResolvedBuild(['root'], $this->createInstaller([ + 'required' => 'library', + 'root' => 'library', + ])); + $withOptional = $util->configForResolvedBuild(['root'], $this->createInstaller([ + 'required' => 'library', + 'optional' => 'library', + 'root' => 'library', + ])); + + $this->assertStringNotContainsString('/fixtures/liboptional.a', $withoutOptional['libs']); + $this->assertStringContainsString('/fixtures/liboptional.a', $withOptional['libs']); + } + + public function testResolvedConfigSupportsExplicitPhpLinkClosureOverrides(): void + { + $this->loadConfig([ + 'php' => ['type' => 'target', 'depends' => ['core']], + 'core' => ['type' => 'library', 'static-libs' => ['/fixtures/libcore.a']], + 'ext-demo' => ['type' => 'php-extension', 'depends' => ['ext-lib']], + 'ext-lib' => ['type' => 'library', 'static-libs' => ['/fixtures/libext.a']], + 'php-fpm' => ['type' => 'virtual-target', 'depends' => ['php'], 'suggests' => ['fpm-lib']], + 'fpm-lib' => ['type' => 'library', 'static-libs' => ['/fixtures/libfpm.a']], + ]); + $util = new SPCConfigUtil(['libs_only_deps' => true]); + + $config = $util->configForResolvedBuild(['php'], $this->createInstaller([ + 'core' => 'library', + 'ext-lib' => 'library', + 'ext-demo' => 'static-extension', + 'fpm-lib' => 'library', + 'php-fpm' => 'virtual-target', + 'php' => 'target', + ])); + + $this->assertStringContainsString('/fixtures/libcore.a', $config['libs']); + $this->assertStringContainsString('/fixtures/libext.a', $config['libs']); + $this->assertStringContainsString('/fixtures/libfpm.a', $config['libs']); + } + + public function testResolvedConfigRejectsRootOutsideResolvedSet(): void + { + $this->loadLinkFixture(); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage("Package 'root' is not part of the resolved package set"); + + new SPCConfigUtil(['no_php' => true])->configForResolvedBuild(['root'], $this->createInstaller([ + 'required' => 'library', + ])); + } + + public function testCAndCxxFlagsRemainIndependent(): void + { + putenv('SPC_DEFAULT_CFLAGS=-DDEFAULT_C'); + putenv('SPC_DEFAULT_CXXFLAGS=-DDEFAULT_CXX'); + putenv('SPC_DEFAULT_LDFLAGS=-Wl,default'); + putenv('CFLAGS=-DUSER_C'); + putenv('CXXFLAGS=-DUSER_CXX'); + putenv('LDFLAGS=-Wl,user'); + + $config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->configWithResolvedPackages([]); + + $this->assertStringContainsString('-DDEFAULT_C', $config['cflags']); + $this->assertStringContainsString('-DUSER_C', $config['cflags']); + $this->assertStringNotContainsString('-DDEFAULT_CXX', $config['cflags']); + $this->assertStringContainsString('-DDEFAULT_CXX', $config['cxxflags']); + $this->assertStringContainsString('-DUSER_CXX', $config['cxxflags']); + $this->assertStringContainsString('-Wl,default', $config['ldflags']); + $this->assertStringContainsString('-Wl,user', $config['ldflags']); + } + + private function loadLinkFixture(): void + { + $this->loadConfig([ + 'root' => [ + 'type' => 'library', + 'depends' => ['required'], + 'suggests' => ['optional'], + 'static-libs' => ['/fixtures/libroot.a'], + ], + 'required' => ['type' => 'library', 'static-libs' => ['/fixtures/librequired.a']], + 'optional' => ['type' => 'library', 'static-libs' => ['/fixtures/liboptional.a']], + ]); + } + + private function loadConfig(array $configs): void + { + $reflection = new \ReflectionClass(PackageConfig::class); + $property = $reflection->getProperty('package_configs'); + $property->setValue(null, $configs); + } + + private function resetPackageConfig(): void + { + $this->loadConfig([]); + } + + /** + * @param array $packages + */ + private function createInstaller(array $packages): PackageInstaller + { + $installer = new PackageInstaller(['no-tracker' => true], false); + $resolved = []; + foreach ($packages as $name => $type) { + $package = match ($type) { + 'static-extension' => $this->createStaticExtension($name), + 'target' => $this->createTarget($name, false), + 'virtual-target' => $this->createTarget($name, true), + default => $this->createNamedPackage(LibraryPackage::class, $name), + }; + $resolved[$name] = $package; + } + + $reflection = new \ReflectionClass(PackageInstaller::class); + $reflection->getProperty('packages')->setValue($installer, $resolved); + return $installer; + } + + private function createStaticExtension(string $name): PhpExtensionPackage + { + $extension = $this->createMock(PhpExtensionPackage::class); + $extension->method('getName')->willReturn($name); + $extension->method('isBuildStatic')->willReturn(true); + return $extension; + } + + private function createTarget(string $name, bool $virtual): TargetPackage + { + $target = $this->createMock(TargetPackage::class); + $target->method('getName')->willReturn($name); + $target->method('isVirtualTarget')->willReturn($virtual); + return $target; + } + + /** @param class-string $class */ + private function createNamedPackage(string $class, string $name): Package + { + $package = $this->createMock($class); + $package->method('getName')->willReturn($name); + return $package; + } +}