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); } }