From b5b4e8f62254de5fff8097f51f80e1188eb9db42 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 2 Apr 2026 08:43:26 +0800 Subject: [PATCH] ttt --- TODO.md | 57 + src/Package/Extension/gettext.php | 30 + src/Package/Library/idn2.php | 6 + src/Package/Library/krb5.php | 7 + src/Package/Target/curl.php | 11 +- .../Runtime/Executor/UnixCMakeExecutor.php | 23 +- src/StaticPHP/Util/SPCConfigUtil.php | 1031 +++++++++-------- 7 files changed, 649 insertions(+), 516 deletions(-) create mode 100644 TODO.md create mode 100644 src/Package/Extension/gettext.php diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..f37e0888 --- /dev/null +++ b/TODO.md @@ -0,0 +1,57 @@ +# v3 TODO List + +Tracking items identified during the v2 → v3 migration audit. + +--- + +## Commands + +- [ ] Implement `craft` command (drives full build from `craft.yml`; should be easier with v3 vendor/registry mode) +- [ ] Migrate `micro:combine` command (combine `micro.sfx` with PHP code + INI injection) +- [ ] Implement `dump-extensions` command (extract required extensions from `composer.json` / `composer.lock`) +- [ ] Design and implement v3 dev toolchain commands (WIP — needs design decision): + - [ ] `dev:extensions` / equivalent listing command + - [ ] `dev:php-version`, `dev:ext-version`, `dev:lib-version` + - [ ] Doc generation commands (`dev:gen-ext-docs`, `dev:gen-ext-dep-docs`, `dev:gen-lib-dep-docs`) — pending v3 doc design + +--- + +## Source Patches (SourcePatcher → Artifact migration) + +The following v2 `SourcePatcher` hooks are not yet migrated to v3 `src/Package/Artifact/` classes: + +- [ ] Migrate `patchSQLSRVWin32` — removes `/sdl` compile flag to prevent Zend build failure on Windows +- [ ] Migrate `patchSQLSRVPhp85` — fixes `pdo_sqlsrv` directory layout for PHP 8.5 +- [ ] Migrate `patchYamlWin32` — patches `config.w32` `_a.lib` detection logic for the `yaml` extension +- [ ] Migrate `patchImagickWith84` — applies PHP 8.4 compatibility patch for `imagick` based on version detection + +--- + +## Extension Package Classes (Unix) + +Extensions that had non-trivial v2 build logic and are missing a v3 `src/Package/Extension/` class: + +- [x] `gettext` — macOS: fix `config.m4` bracket syntax for cross-version compatibility + append frameworks to linker flags (critical for macOS linking; this is a Unix-side gap, not Windows-only) + +--- + +## Windows Extensions (Early Stage) + +Windows extension support is still in early stage. The following extensions had Windows-specific configure args or patches in v2 and are pending v3 Windows implementation: + +- [ ] `amqp` — Windows configure args +- [ ] `com_dotnet` — Windows-only extension +- [ ] `dom` — remove `dllmain.c` from `config.w32` +- [ ] `ev` — fix `PHP_EV_SHARED` in `config.w32` +- [ ] `gmssl` — add `CHECK_LIB("gmssl.lib")` to `config.w32` +- [ ] `intl` — fix `PHP_INTL_SHARED` in `config.w32` +- [ ] `lz4` — Windows configure args +- [ ] `mbregex` — Windows configure args +- [ ] `sqlsrv` / `pdo_sqlsrv` — complex conditional build logic (independent `sqlsrv` without `pdo_sqlsrv`) +- [ ] `xml` — remove `dllmain.c` from `config.w32`; handles `soap`, `xmlreader`, `xmlwriter`, `simplexml` + +--- + +## Documentation + +- [ ] Write v3 user documentation (currently zero v3 docs) diff --git a/src/Package/Extension/gettext.php b/src/Package/Extension/gettext.php new file mode 100644 index 00000000..6bdcf0cb --- /dev/null +++ b/src/Package/Extension/gettext.php @@ -0,0 +1,30 @@ +getTargetPackage('php')->getSourceDir(); + FileSystem::replaceFileStr( + "{$php_src}/ext/gettext/config.m4", + ['AC_CHECK_LIB($GETTEXT_CHECK_IN_LIB', 'AC_CHECK_LIB([$GETTEXT_CHECK_IN_LIB'], + ['AC_CHECK_LIB(intl', 'AC_CHECK_LIB([intl'] // new php versions use a bracket + ); + } +} diff --git a/src/Package/Library/idn2.php b/src/Package/Library/idn2.php index 3cffd9be..3b0aa444 100644 --- a/src/Package/Library/idn2.php +++ b/src/Package/Library/idn2.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; +use StaticPHP\Util\FileSystem; #[Library('idn2')] class idn2 @@ -29,5 +30,10 @@ class idn2 ->make(); $lib->patchPkgconfPrefix(['libidn2.pc']); $lib->patchLaDependencyPrefix(); + // libunistring is in Libs.private of libidn2.pc, but CMake's pkg_check_modules + // does not follow Libs.private for static linking. Promote it to Libs so that + // consumers linking static binaries (e.g. the curl exe) can resolve _uc_* / _u32_* symbols. + $libidn2_pc = BUILD_ROOT_PATH . '/lib/pkgconfig/libidn2.pc'; + FileSystem::replaceFileStr($libidn2_pc, 'Libs: -L${libdir} -lidn2', 'Libs: -L${libdir} -lidn2 -lunistring'); } } diff --git a/src/Package/Library/krb5.php b/src/Package/Library/krb5.php index 303c3b63..59bf7d17 100644 --- a/src/Package/Library/krb5.php +++ b/src/Package/Library/krb5.php @@ -10,6 +10,7 @@ use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageInstaller; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\FileSystem; use StaticPHP\Util\SPCConfigUtil; #[Library('krb5')] @@ -59,5 +60,11 @@ class krb5 'mit-krb5.pc', 'gssrpc.pc', ]); + // libkrb5support is in Libs.private of mit-krb5.pc, but CMake's pkg_check_modules + // does not follow Libs.private for static linking. Promote it to Libs so that + // consumers linking static binaries (e.g. the curl exe) can resolve _k5_* symbols. + $mit_krb5_pc = BUILD_ROOT_PATH . '/lib/pkgconfig/mit-krb5.pc'; + FileSystem::replaceFileStr($mit_krb5_pc, 'Libs.private: -lkrb5support', 'Libs.private:'); + FileSystem::replaceFileStr($mit_krb5_pc, '-lcom_err', '-lcom_err -lkrb5support'); } } diff --git a/src/Package/Target/curl.php b/src/Package/Target/curl.php index 43a2904b..3135fa32 100644 --- a/src/Package/Target/curl.php +++ b/src/Package/Target/curl.php @@ -81,16 +81,19 @@ class curl ->addConfigureArgs( '-DBUILD_CURL_EXE=ON', '-DBUILD_LIBCURL_DOCS=OFF', + '-DCURL_USE_PKGCONFIG=ON', ) ->build(); // patch pkgconf $lib->patchPkgconfPrefix(['libcurl.pc']); - // curl's CMake embeds krb5 link flags directly without following Requires.private chain, - // so -lkrb5support (from mit-krb5.pc Libs.private) is missing from libcurl.pc. + // Ensure -lkrb5support is present in libcurl.pc for downstream consumers. + // krb5.php already promotes it to Libs in mit-krb5.pc before the build, so + // CMake should have embedded it; this is a safety fallback if it was missed. $pc_path = "{$lib->getLibDir()}/pkgconfig/libcurl.pc"; - if (str_contains(FileSystem::readFile($pc_path), '-lgssapi_krb5')) { - FileSystem::replaceFileRegex($pc_path, '/-lcom_err$/m', '-lcom_err -lkrb5support'); + $pc_content = FileSystem::readFile($pc_path); + if (str_contains($pc_content, '-lgssapi_krb5') && !str_contains($pc_content, '-lkrb5support')) { + FileSystem::replaceFileRegex($pc_path, '/-lcom_err\b/', '-lcom_err -lkrb5support'); } shell()->cd("{$lib->getLibDir()}/cmake/CURL/") ->exec("sed -ie 's|\"/lib/libcurl.a\"|\"{$lib->getLibDir()}/libcurl.a\"|g' CURLTargets-release.cmake"); diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 9442d30c..2269c323 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -10,10 +10,13 @@ use StaticPHP\Exception\SPCInternalException; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; +use StaticPHP\Package\TargetPackage; use StaticPHP\Runtime\Shell\UnixShell; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\PkgConfigUtil; +use StaticPHP\Util\SPCConfigUtil; use ZM\Logger\ConsoleColor; /** @@ -214,7 +217,7 @@ class UnixCMakeExecutor extends Executor */ private function getDefaultCMakeArgs(): array { - return $this->custom_default_args ?? [ + $args = $this->custom_default_args ?? [ '-DCMAKE_BUILD_TYPE=Release', "-DCMAKE_INSTALL_PREFIX={$this->package->getBuildRootPath()}", '-DCMAKE_INSTALL_BINDIR=bin', @@ -224,6 +227,20 @@ class UnixCMakeExecutor extends Executor '-DBUILD_SHARED_LIBS=OFF', "-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}", ]; + + // EXE linker flags: base system libs + framework flags for target packages + $exeLinkerFlags = SystemTarget::getRuntimeLibs(); + if ($this->package instanceof TargetPackage) { + $resolvedNames = array_keys($this->installer->getResolvedPackages()); + $resolvedNames[] = $this->package->getName(); + $fwFlags = SPCConfigUtil::getFrameworksString($resolvedNames); + if ($fwFlags !== '') { + $exeLinkerFlags .= " {$fwFlags}"; + } + } + $args[] = "-DCMAKE_EXE_LINKER_FLAGS=\"{$exeLinkerFlags}\""; + + return $args; } /** @@ -274,13 +291,13 @@ SET(CMAKE_PREFIX_PATH "{$root}") SET(CMAKE_INSTALL_PREFIX "{$root}") SET(CMAKE_INSTALL_LIBDIR "lib") -set(PKG_CONFIG_EXECUTABLE "{$pkgConfigExecutable}") +set(PKG_CONFIG_EXECUTABLE "{$pkgConfigExecutable}" CACHE FILEPATH "pkg-config executable" FORCE) set(PKG_CONFIG_ARGN "--static" CACHE STRING "Extra arguments for pkg-config" FORCE) +set(ENV{PKG_CONFIG_PATH} "{$root}/lib/pkgconfig") set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) -set(CMAKE_EXE_LINKER_FLAGS "-ldl -lpthread -lm -lutil") CMAKE; // Whoops, linux may need CMAKE_AR sometimes if (PHP_OS_FAMILY === 'Linux') { diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index d31e8201..9f7026ca 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -1,509 +1,522 @@ -no_php = $options['no_php'] ?? false; - $this->libs_only_deps = $options['libs_only_deps'] ?? false; - $this->absolute_libs = $options['absolute_libs'] ?? false; - } - - 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}"; - } - - $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 - 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"; - } - } - - $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), - ]; - } - - /** - * [Helper function] - * Get configuration for a specific extension(s) dependencies. - * - * @param array|PhpExtensionPackage $extension_packages Extension instance or list - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): 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, - ); - } - - /** - * [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 - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array - { - if (!is_array($lib)) { - $lib = [$lib]; - } - $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; - } - - /** - * Get build configuration for a package and its 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. - * - * @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, - * ldflags: string, - * libs: string - * } - */ - public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array - { - // Get sub-dependencies within the resolved set - $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); - - if (empty($sub_deps)) { - return [ - 'cflags' => '', - 'ldflags' => '', - 'libs' => '', - ]; - } - - // Use libs_only_deps mode and no_php for library linking - $save_no_php = $this->no_php; - $save_libs_only_deps = $this->libs_only_deps; - $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; - } - - /** - * Get configuration using already-resolved packages (skip dependency resolution). - * - * @param string[] $resolved_packages Already resolved package names in build order - * @return array{ - * cflags: string, - * ldflags: string, - * libs: string - * } - */ - public function configWithResolvedPackages(array $resolved_packages): array - { - $ldflags = $this->getLdflagsString(); - $cflags = $this->getIncludesString($resolved_packages); - $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); - - // additional OS-specific libraries (e.g. macOS -lresolv) - if ($extra_libs = SystemTarget::getRuntimeLibs()) { - $libs .= " {$extra_libs}"; - } - - $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_packages)}"; - } - - // C++ - if ($this->hasCpp($resolved_packages)) { - $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_packages) && 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 - if (!$this->no_php) { - $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')) { - $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), - ]; - } - - private function hasCpp(array $packages): bool - { - foreach ($packages as $package) { - $lang = PackageConfig::get($package, 'lang', 'c'); - if ($lang === 'cpp') { - return true; - } - } - return false; - } - - private function getIncludesString(array $packages): string - { - $base = BUILD_INCLUDE_PATH; - - // Windows MSVC uses /I flag instead of -I - if (SystemTarget::getTargetOS() === 'Windows') { - $includes = ["/I\"{$base}\""]; - - // link with libphp - if (!$this->no_php) { - $includes = [ - ...$includes, - "/I\"{$base}\\php\"", - "/I\"{$base}\\php\\main\"", - "/I\"{$base}\\php\\TSRM\"", - "/I\"{$base}\\php\\Zend\"", - "/I\"{$base}\\php\\ext\"", - ]; - } - } else { - $includes = ["-I{$base}"]; - - // link with libphp - if (!$this->no_php) { - $includes = [ - ...$includes, - "-I{$base}/php", - "-I{$base}/php/main", - "-I{$base}/php/TSRM", - "-I{$base}/php/Zend", - "-I{$base}/php/ext", - ]; - } - } - - // parse pkg-configs (only for Unix) - if (SystemTarget::isUnix()) { - foreach ($packages as $package) { - $pc = PackageConfig::get($package, 'pkg-configs', []); - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_filter(explode(':', $pkg_config_path)); - foreach ($pc as $file) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$file}.pc")) { - $found = true; - break; - } - } - if (!$found) { - throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist. Please build it first."); - } - } - $pc_cflags = implode(' ', $pc); - if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') { - $arr = explode(' ', $pc_cflags); - $arr = array_unique($arr); - $arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_')); - $pc_cflags = implode(' ', $arr); - $includes[] = $pc_cflags; - } - } - } - $includes = array_unique($includes); - return implode(' ', $includes); - } - - private function getLdflagsString(): string - { - // Windows MSVC uses /LIBPATH flag instead of -L - if (SystemTarget::getTargetOS() === 'Windows') { - return '/LIBPATH:"' . BUILD_LIB_PATH . '"'; - } - return '-L' . BUILD_LIB_PATH; - } - - private function getLibsString(array $packages, bool $use_short_libs = true): string - { - $lib_names = []; - $frameworks = []; - - foreach ($packages as $package) { - // parse pkg-configs only for unix systems - if (SystemTarget::isUnix()) { - // add pkg-configs libs - $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); - $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; - $search_paths = array_filter(explode(':', $pkg_config_path)); - foreach ($pkg_configs as $pkg_config) { - $found = false; - foreach ($search_paths as $path) { - if (file_exists($path . "/{$pkg_config}.pc")) { - $found = true; - break; - } - } - if (!$found) { - throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist. Please build it first."); - } - } - $pkg_configs = implode(' ', $pkg_configs); - if ($pkg_configs !== '') { - // static libs with dependencies come in reverse order, so reverse this too - $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); - $lib_names = [...$lib_names, ...$pc_libs]; - } - } - // convert all static-libs to short names - $libs = array_reverse(PackageConfig::get($package, '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."); - } - $lib_names[] = $this->getShortLibName($lib); - } else { - $lib_names[] = $lib; - } - } - // add frameworks for macOS - if (SystemTarget::getTargetOS() === 'Darwin') { - $frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); - } - } - - // post-process - $lib_names = array_filter($lib_names, fn ($x) => $x !== ''); - $lib_names = array_reverse(array_unique($lib_names)); - $frameworks = array_unique($frameworks); - - // process frameworks to short_name - if (SystemTarget::getTargetOS() === 'Darwin') { - foreach ($frameworks as $fw) { - $ks = '-framework ' . $fw; - if (!in_array($ks, $lib_names)) { - $lib_names[] = $ks; - } - } - } - - if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { - if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { - $lib_names[] = '-lcrypt'; - } - } - if (!$use_short_libs) { - $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); - } - return implode(' ', $lib_names); - } - - private function getShortLibName(string $lib): string - { - // Windows: library files are xxx.lib format (not libxxx.a) - if (SystemTarget::getTargetOS() === 'Windows') { - if (!str_ends_with($lib, '.lib')) { - return BUILD_LIB_PATH . '\\' . $lib; - } - // For Windows, return just the library filename (e.g., "libssl.lib") - return $lib; - } - - // Unix: library files are libxxx.a format - if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) { - return BUILD_LIB_PATH . '/' . $lib; - } - // get short name (e.g., "libssl.a" -> "-lssl") - return '-l' . substr($lib, 3, -2); - } - - private function getFullLibName(string $lib): string - { - // Windows: libraries don't use -l prefix, return as-is or with full path - if (SystemTarget::getTargetOS() === 'Windows') { - if (str_ends_with($lib, '.lib') && !str_contains($lib, '\\') && !str_contains($lib, '/')) { - // It's a short lib name like "libssl.lib", convert to full path - $fullPath = BUILD_LIB_PATH . '\\' . $lib; - if (file_exists($fullPath)) { - return $fullPath; - } - } - return $lib; - } - - // Unix: convert -lxxx to full path - if (!str_starts_with($lib, '-l')) { - return $lib; - } - $libname = substr($lib, 2); - $staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a"; - if (file_exists($staticLib)) { - return $staticLib; - } - return $lib; - } - - private function getFrameworksString(array $extensions): string - { - $list = []; - foreach ($extensions as $extension) { - foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { - $ks = '-framework ' . $fw; - if (!in_array($ks, $list)) { - $list[] = $ks; - } - } - } - return implode(' ', $list); - } -} +no_php = $options['no_php'] ?? false; + $this->libs_only_deps = $options['libs_only_deps'] ?? false; + $this->absolute_libs = $options['absolute_libs'] ?? false; + } + + 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}"; + } + + $extra_env = getenv('SPC_EXTRA_LIBS'); + if (is_string($extra_env) && !empty($extra_env)) { + $libs .= " {$extra_env}"; + } + // package frameworks + $fwStr = self::getFrameworksString($resolved); + if ($fwStr !== '') { + $libs .= " {$fwStr}"; + } + // 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 + 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"; + } + } + + $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), + ]; + } + + /** + * [Helper function] + * Get configuration for a specific extension(s) dependencies. + * + * @param array|PhpExtensionPackage $extension_packages Extension instance or list + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): 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, + ); + } + + /** + * [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 + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array + { + if (!is_array($lib)) { + $lib = [$lib]; + } + $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; + } + + /** + * Get build configuration for a package and its 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. + * + * @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, + * ldflags: string, + * libs: string + * } + */ + public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array + { + // Get sub-dependencies within the resolved set + $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); + + if (empty($sub_deps)) { + return [ + 'cflags' => '', + 'ldflags' => '', + 'libs' => '', + ]; + } + + // Use libs_only_deps mode and no_php for library linking + $save_no_php = $this->no_php; + $save_libs_only_deps = $this->libs_only_deps; + $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; + } + + /** + * Get configuration using already-resolved packages (skip dependency resolution). + * + * @param string[] $resolved_packages Already resolved package names in build order + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function configWithResolvedPackages(array $resolved_packages): array + { + $ldflags = $this->getLdflagsString(); + $cflags = $this->getIncludesString($resolved_packages); + $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); + + // additional OS-specific libraries (e.g. macOS -lresolv) + if ($extra_libs = SystemTarget::getRuntimeLibs()) { + $libs .= " {$extra_libs}"; + } + + $extra_env = getenv('SPC_EXTRA_LIBS'); + if (is_string($extra_env) && !empty($extra_env)) { + $libs .= " {$extra_env}"; + } + + // package frameworks + $fwStr = self::getFrameworksString($resolved_packages); + if ($fwStr !== '') { + $libs .= " {$fwStr}"; + } + + // C++ + if ($this->hasCpp($resolved_packages)) { + $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_packages) && 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 + if (!$this->no_php) { + $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')) { + $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), + ]; + } + + /** + * Get the frameworks string for a list of packages. + * + * Returns empty string on non-Darwin platforms. + * + * @param string[] $packages Package names to collect frameworks from + * @return string e.g. "-framework Kerberos -framework CoreFoundation" + */ + public static function getFrameworksString(array $packages): string + { + if (SystemTarget::getTargetOS() !== 'Darwin') { + return ''; + } + $list = []; + foreach ($packages as $package) { + foreach (PackageConfig::get($package, 'frameworks', []) as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $list)) { + $list[] = $ks; + } + } + } + return implode(' ', $list); + } + + private function hasCpp(array $packages): bool + { + foreach ($packages as $package) { + $lang = PackageConfig::get($package, 'lang', 'c'); + if ($lang === 'cpp') { + return true; + } + } + return false; + } + + private function getIncludesString(array $packages): string + { + $base = BUILD_INCLUDE_PATH; + + // Windows MSVC uses /I flag instead of -I + if (SystemTarget::getTargetOS() === 'Windows') { + $includes = ["/I\"{$base}\""]; + + // link with libphp + if (!$this->no_php) { + $includes = [ + ...$includes, + "/I\"{$base}\\php\"", + "/I\"{$base}\\php\\main\"", + "/I\"{$base}\\php\\TSRM\"", + "/I\"{$base}\\php\\Zend\"", + "/I\"{$base}\\php\\ext\"", + ]; + } + } else { + $includes = ["-I{$base}"]; + + // link with libphp + if (!$this->no_php) { + $includes = [ + ...$includes, + "-I{$base}/php", + "-I{$base}/php/main", + "-I{$base}/php/TSRM", + "-I{$base}/php/Zend", + "-I{$base}/php/ext", + ]; + } + } + + // parse pkg-configs (only for Unix) + if (SystemTarget::isUnix()) { + foreach ($packages as $package) { + $pc = PackageConfig::get($package, 'pkg-configs', []); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(':', $pkg_config_path)); + foreach ($pc as $file) { + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$file}.pc")) { + $found = true; + break; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist. Please build it first."); + } + } + $pc_cflags = implode(' ', $pc); + if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') { + $arr = explode(' ', $pc_cflags); + $arr = array_unique($arr); + $arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_')); + $pc_cflags = implode(' ', $arr); + $includes[] = $pc_cflags; + } + } + } + $includes = array_unique($includes); + return implode(' ', $includes); + } + + private function getLdflagsString(): string + { + // Windows MSVC uses /LIBPATH flag instead of -L + if (SystemTarget::getTargetOS() === 'Windows') { + return '/LIBPATH:"' . BUILD_LIB_PATH . '"'; + } + return '-L' . BUILD_LIB_PATH; + } + + private function getLibsString(array $packages, bool $use_short_libs = true): string + { + $lib_names = []; + $frameworks = []; + + foreach ($packages as $package) { + // parse pkg-configs only for unix systems + if (SystemTarget::isUnix()) { + // add pkg-configs libs + $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); + $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; + $search_paths = array_filter(explode(':', $pkg_config_path)); + foreach ($pkg_configs as $pkg_config) { + $found = false; + foreach ($search_paths as $path) { + if (file_exists($path . "/{$pkg_config}.pc")) { + $found = true; + break; + } + } + if (!$found) { + throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist. Please build it first."); + } + } + $pkg_configs = implode(' ', $pkg_configs); + if ($pkg_configs !== '') { + // static libs with dependencies come in reverse order, so reverse this too + $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); + $lib_names = [...$lib_names, ...$pc_libs]; + } + } + // convert all static-libs to short names + $libs = array_reverse(PackageConfig::get($package, '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."); + } + $lib_names[] = $this->getShortLibName($lib); + } else { + $lib_names[] = $lib; + } + } + // add frameworks for macOS + if (SystemTarget::getTargetOS() === 'Darwin') { + $frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); + } + } + + // post-process + $lib_names = array_filter($lib_names, fn ($x) => $x !== ''); + $lib_names = array_reverse(array_unique($lib_names)); + $frameworks = array_unique($frameworks); + + // process frameworks to short_name + if (SystemTarget::getTargetOS() === 'Darwin') { + foreach ($frameworks as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $lib_names)) { + $lib_names[] = $ks; + } + } + } + + if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { + if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { + $lib_names[] = '-lcrypt'; + } + } + if (!$use_short_libs) { + $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); + } + return implode(' ', $lib_names); + } + + private function getShortLibName(string $lib): string + { + // Windows: library files are xxx.lib format (not libxxx.a) + if (SystemTarget::getTargetOS() === 'Windows') { + if (!str_ends_with($lib, '.lib')) { + return BUILD_LIB_PATH . '\\' . $lib; + } + // For Windows, return just the library filename (e.g., "libssl.lib") + return $lib; + } + + // Unix: library files are libxxx.a format + if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) { + return BUILD_LIB_PATH . '/' . $lib; + } + // get short name (e.g., "libssl.a" -> "-lssl") + return '-l' . substr($lib, 3, -2); + } + + private function getFullLibName(string $lib): string + { + // Windows: libraries don't use -l prefix, return as-is or with full path + if (SystemTarget::getTargetOS() === 'Windows') { + if (str_ends_with($lib, '.lib') && !str_contains($lib, '\\') && !str_contains($lib, '/')) { + // It's a short lib name like "libssl.lib", convert to full path + $fullPath = BUILD_LIB_PATH . '\\' . $lib; + if (file_exists($fullPath)) { + return $fullPath; + } + } + return $lib; + } + + // Unix: convert -lxxx to full path + if (!str_starts_with($lib, '-l')) { + return $lib; + } + $libname = substr($lib, 2); + $staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a"; + if (file_exists($staticLib)) { + return $staticLib; + } + return $lib; + } +}