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 = []): array { // Walk depends+suggests within the resolved set; reaching `php` fans out to its // effective link closure (resolved static exts + virtual-target SAPIs). $installer = ApplicationContext::get(PackageInstaller::class); $resolved_set = array_flip(array_keys($installer->getResolvedPackages())); $php_extras = []; if (!$this->no_php) { foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { if ($ext->isBuildStatic()) { $php_extras[] = $ext->getName(); } } foreach ($installer->getResolvedPackages(TargetPackage::class) as $target) { if ($target->isVirtualTarget()) { $php_extras[] = $target->getName(); } } } $sorted = []; $visited = []; foreach ($packages as $pkg) { self::visitConfigDeps( is_string($pkg) ? $pkg : $pkg->getName(), $resolved_set, $php_extras, $visited, $sorted, ); } return $this->configWithResolvedPackages($sorted); } /** * [Helper function] * Get configuration for a specific extension(s) dependencies. * * Uses the installer's resolved package set as the source of truth — only libraries that * are actually enabled in this build appear in the result. The resolved set already * reflects the user's `--with-suggests` choice. * * @param array|PhpExtensionPackage $extension_packages Extension instance or list * @return array{ * cflags: string, * cxxflags: string, * ldflags: string, * libs: string * } */ public function getExtensionConfig(array|PhpExtensionPackage $extension_packages): array { if (!is_array($extension_packages)) { $extension_packages = [$extension_packages]; } $names = array_map(fn ($y) => $y->getName(), $extension_packages); return $this->configWithResolvedPackages($this->collectEnabledLinkPackages($names)); } /** * [Helper function] * Get configuration for a specific library(s) dependencies. * * Like {@see getExtensionConfig()}, draws from the resolved package set so we never * link against a library that wasn't built. * * @param array|LibraryPackage $lib Library instance or list * @return array{ * cflags: string, * cxxflags: string, * ldflags: string, * libs: string * } */ public function getLibraryConfig(array|LibraryPackage $lib): 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; $names = array_map(fn ($y) => $y->getName(), $lib); $ret = $this->configWithResolvedPackages($this->collectEnabledLinkPackages($names)); $this->no_php = $save_no_php; $this->libs_only_deps = $save_libs_only_deps; return $ret; } /** * Get build configuration for a package's sub-dependencies within a resolved set. * * Walks both depends and suggests edges — the resolved set is the filter, so anything * reachable but unbuilt is naturally excluded. No `include_suggests` knob is needed. * * @param string $package_name The package to get config for * @param string[] $resolved_packages The full resolved package list * @return array{ * cflags: string, * cxxflags: string, * ldflags: string, * libs: string * } */ public function getPackageDepsConfig(string $package_name, array $resolved_packages): array { // Get sub-dependencies within the resolved set $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, include_suggests: true); if (empty($sub_deps)) { return [ 'cflags' => '', 'cxxflags' => '', '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; } /** * Build cflags/cxxflags/ldflags/libs for an already-resolved package set (skip dependency resolution). * * @param string[] $resolved_packages Resolved package names in build order * @return array{ * cflags: string, * cxxflags: string, * ldflags: string, * libs: string * } */ public function configWithResolvedPackages(array $resolved_packages): array { $ldflags = $this->getLdflagsString(); $includes = $this->getIncludesString($resolved_packages); $libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); // includes (-I…) are language-agnostic — same source for cflags/cxxflags, swap only the env names $cflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_CFLAGS') ?: '') . ' ' . getenv('CFLAGS') . ' ' . $includes)); $cxxflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_CXXFLAGS') ?: '') . ' ' . getenv('CXXFLAGS') . ' ' . $includes)); $ldflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_LDFLAGS') ?: '') . ' ' . getenv('LDFLAGS') . ' ' . $ldflags)); // additional OS-specific libraries (e.g. macOS -lresolv) if ($extra_libs = SystemTarget::getRuntimeLibs()) { $libs .= " {$extra_libs}"; } $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' => $cflags, 'cxxflags' => $cxxflags, '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_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' => $cflags, 'cxxflags' => $cxxflags, 'ldflags' => $ldflags, 'libs' => clean_spaces($allLibs), ]; } public 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); } private static function visitConfigDeps( string $name, array $resolved_set, array $php_extras, array &$visited, array &$sorted, ): void { if (isset($visited[$name]) || !isset($resolved_set[$name])) { return; } $visited[$name] = true; $deps = array_merge( PackageConfig::get($name, 'depends', []), PackageConfig::get($name, 'suggests', []), ); if ($name === 'php') { $deps = array_merge($deps, $php_extras); } foreach ($deps as $dep) { self::visitConfigDeps($dep, $resolved_set, $php_extras, $visited, $sorted); } $sorted[] = $name; } /** * For each input package name, gather its transitive deps within the installer's resolved * set (walking depends + suggests edges), plus the package itself, deduped and in build order. * * @param string[] $package_names Input package names * @return string[] Resolved packages to link against */ private function collectEnabledLinkPackages(array $package_names): array { $resolved = array_keys(ApplicationContext::get(PackageInstaller::class)->getResolvedPackages()); $out = []; foreach ($package_names as $name) { $sub = DependencyResolver::getSubDependencies($name, $resolved, include_suggests: true); $out = [...$out, ...$sub, $name]; } return array_values(array_unique($out)); } private function hasCpp(array $packages): bool { foreach ($packages as $package) { $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; } }