diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php index defb00ba..b03f8089 100644 --- a/src/StaticPHP/Util/DependencyResolver.php +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -55,6 +55,58 @@ class DependencyResolver return $resolved; } + /** + * Get all dependencies of a specific package within a resolved package set. + * This is useful when you need to get build flags for a specific library and its deps. + * + * The method will only include dependencies that exist in the resolved set, + * which properly handles optional dependencies (suggests) - only those that + * were actually resolved will be included. + * + * @param string $package_name The package to get dependencies for + * @param string[] $resolved_packages The resolved package list (from resolve()) + * @param bool $include_suggests Whether to include suggests that are in resolved set + * @return string[] Dependencies of the package (NOT including itself), ordered for building + */ + public static function getSubDependencies(string $package_name, array $resolved_packages, bool $include_suggests = false): array + { + // Create a lookup set for O(1) membership check + $resolved_set = array_flip($resolved_packages); + + // Verify the target package is in the resolved set + if (!isset($resolved_set[$package_name])) { + return []; + } + + // Build dependency map from config + $dep_map = []; + foreach ($resolved_packages as $pkg) { + $dep_map[$pkg] = [ + 'depends' => PackageConfig::get($pkg, 'depends', []), + 'suggests' => PackageConfig::get($pkg, 'suggests', []), + ]; + } + + // Collect all sub-dependencies recursively (excluding the package itself) + $visited = []; + $sorted = []; + + // Get dependencies to process for the target package + $deps = $dep_map[$package_name]['depends'] ?? []; + if ($include_suggests) { + $deps = array_merge($deps, $dep_map[$package_name]['suggests'] ?? []); + } + + // Only visit dependencies that are in the resolved set + foreach ($deps as $dep) { + if (isset($resolved_set[$dep])) { + self::visitSubDeps($dep, $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. @@ -89,6 +141,39 @@ class DependencyResolver return $why; } + /** + * Recursive helper for getSubDependencies. + * Visits dependencies in topological order (dependencies first). + */ + private static function visitSubDeps( + string $pkg_name, + array $dep_map, + array $resolved_set, + bool $include_suggests, + array &$visited, + array &$sorted + ): void { + if (isset($visited[$pkg_name])) { + return; + } + $visited[$pkg_name] = true; + + // Get dependencies to process + $deps = $dep_map[$pkg_name]['depends'] ?? []; + if ($include_suggests) { + $deps = array_merge($deps, $dep_map[$pkg_name]['suggests'] ?? []); + } + + // Only visit dependencies that are in the resolved set + foreach ($deps as $dep) { + if (isset($resolved_set[$dep])) { + self::visitSubDeps($dep, $dep_map, $resolved_set, $include_suggests, $visited, $sorted); + } + } + + $sorted[] = $pkg_name; + } + /** * Visitor pattern implementation for dependency resolution. * diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 3a20e56f..a31b771f 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -149,6 +149,117 @@ class SPCConfigUtil 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)) { + $libcpp = SystemTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } + + 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) {