2025-11-30 15:35:04 +08:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
|
|
|
|
namespace StaticPHP\Util;
|
|
|
|
|
|
|
|
|
|
use StaticPHP\Config\PackageConfig;
|
|
|
|
|
use StaticPHP\Exception\WrongUsageException;
|
|
|
|
|
use StaticPHP\Package\Package;
|
|
|
|
|
|
|
|
|
|
class DependencyResolver
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* Resolve dependencies for given packages.
|
|
|
|
|
* It will return an array of package names in the order they should be built/installed.
|
|
|
|
|
*
|
|
|
|
|
* @param Package[]|string[] $packages Package instance
|
|
|
|
|
* @param array<string, string[]> $dependency_overrides Override dependencies (e.g. ['php' => ['ext-gd', 'ext-curl']])
|
|
|
|
|
* @param bool $include_suggests Include suggested packages in the resolution
|
|
|
|
|
* @param null|array<string, string[]> &$why If provided, will be filled with a reverse dependency map
|
|
|
|
|
* @return array<string> Resolved package names in order
|
|
|
|
|
*/
|
|
|
|
|
public static function resolve(array $packages, array $dependency_overrides = [], bool $include_suggests = false, ?array &$why = null): array
|
|
|
|
|
{
|
|
|
|
|
$dep_list = PackageConfig::getAll();
|
|
|
|
|
$dep_list_clean = [];
|
|
|
|
|
// clear array for further step
|
|
|
|
|
foreach ($dep_list as $k => $v) {
|
|
|
|
|
$dep_list_clean[$k] = [
|
|
|
|
|
'depends' => PackageConfig::get($k, 'depends', []),
|
|
|
|
|
'suggests' => PackageConfig::get($k, 'suggests', []),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// apply dependency overrides
|
|
|
|
|
foreach ($dependency_overrides as $target_name => $deps) {
|
|
|
|
|
$dep_list_clean[$target_name]['depends'] = array_merge($dep_list_clean[$target_name]['depends'] ?? [], $deps);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mark suggests as depends
|
|
|
|
|
if ($include_suggests) {
|
|
|
|
|
foreach ($dep_list_clean as $pkg_name => $pkg_item) {
|
|
|
|
|
$dep_list_clean[$pkg_name]['depends'] = array_merge($pkg_item['depends'], $pkg_item['suggests']);
|
|
|
|
|
$dep_list_clean[$pkg_name]['suggests'] = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-09 15:18:03 +08:00
|
|
|
// Build a lookup set of explicitly requested packages for the promotion step below.
|
|
|
|
|
$input_package_set = [];
|
|
|
|
|
foreach ($packages as $pkg) {
|
|
|
|
|
$input_package_set[is_string($pkg) ? $pkg : $pkg->getName()] = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 00:42:34 +08:00
|
|
|
// Virtual-target packages (e.g. php-fpm) are built as part of their real parent's
|
|
|
|
|
// build step, so any dependency they declare must be available before the real parent
|
|
|
|
|
// is built. Promote those deps directly onto the real parent's dependency list so
|
|
|
|
|
// that the topological sort places them before the parent.
|
2026-03-09 15:18:03 +08:00
|
|
|
// Only applies to virtual-targets that are in the input request — if a virtual-target
|
|
|
|
|
// is not being built, its deps must not be injected into the parent.
|
2026-03-07 00:42:34 +08:00
|
|
|
foreach ($dep_list_clean as $pkg_name => $pkg_item) {
|
2026-03-09 15:18:03 +08:00
|
|
|
if (!isset($input_package_set[$pkg_name]) || PackageConfig::get($pkg_name, 'type') !== 'virtual-target') {
|
2026-03-07 00:42:34 +08:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
foreach ($pkg_item['depends'] as $dep_name) {
|
|
|
|
|
if (isset($dep_list_clean[$dep_name]) && PackageConfig::get($dep_name, 'type') !== 'virtual-target') {
|
|
|
|
|
// $dep_name is the real parent; add all other deps of this virtual-target to it
|
|
|
|
|
$other_deps = array_values(array_filter($pkg_item['depends'], fn ($d) => $d !== $dep_name));
|
|
|
|
|
if (!empty($other_deps)) {
|
|
|
|
|
$dep_list_clean[$dep_name]['depends'] = array_values(array_unique(
|
|
|
|
|
array_merge($dep_list_clean[$dep_name]['depends'], $other_deps)
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 15:35:04 +08:00
|
|
|
$resolved = self::doVisitPlat($packages, $dep_list_clean);
|
|
|
|
|
|
|
|
|
|
// Build reverse dependency map if $why is requested
|
|
|
|
|
if ($why !== null) {
|
|
|
|
|
$why = self::buildReverseDependencyMap($resolved, $dep_list_clean, $include_suggests);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $resolved;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 10:59:37 +08:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 15:35:04 +08:00
|
|
|
/**
|
|
|
|
|
* Build a reverse dependency map for the resolved packages.
|
|
|
|
|
* For each package that is depended upon, list which packages depend on it.
|
|
|
|
|
*
|
|
|
|
|
* @param array<string> $resolved Resolved package names
|
|
|
|
|
* @param array<string, array{depends: string[], suggests: string[]}> $dep_list Dependency declaration list
|
|
|
|
|
* @param bool $include_suggests Whether suggests are treated as depends
|
|
|
|
|
* @return array<string, string[]> Reverse dependency map [depended_pkg => [dependant1, dependant2, ...]]
|
|
|
|
|
*/
|
|
|
|
|
private static function buildReverseDependencyMap(array $resolved, array $dep_list, bool $include_suggests): array
|
|
|
|
|
{
|
|
|
|
|
$why = [];
|
|
|
|
|
$resolved_set = array_flip($resolved);
|
|
|
|
|
|
|
|
|
|
foreach ($resolved as $pkg_name) {
|
|
|
|
|
$deps = $dep_list[$pkg_name]['depends'] ?? [];
|
|
|
|
|
if ($include_suggests) {
|
|
|
|
|
$deps = array_merge($deps, $dep_list[$pkg_name]['suggests'] ?? []);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($deps as $dep) {
|
|
|
|
|
// Only include dependencies that are in the resolved set
|
|
|
|
|
if (isset($resolved_set[$dep])) {
|
|
|
|
|
if (!isset($why[$dep])) {
|
|
|
|
|
$why[$dep] = [];
|
|
|
|
|
}
|
|
|
|
|
$why[$dep][] = $pkg_name;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $why;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 10:59:37 +08:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-30 15:35:04 +08:00
|
|
|
/**
|
|
|
|
|
* Visitor pattern implementation for dependency resolution.
|
|
|
|
|
*
|
|
|
|
|
* @param Package[]|string[] $packages Packages list (input)
|
|
|
|
|
* @param array<string, array{depends: string[], suggests: string[]}> $dep_list Dependency declaration list
|
|
|
|
|
* @return array Resolved packages array
|
|
|
|
|
*/
|
|
|
|
|
private static function doVisitPlat(array $packages, array $dep_list): array
|
|
|
|
|
{
|
|
|
|
|
$sorted = [];
|
|
|
|
|
$visited = [];
|
|
|
|
|
foreach ($packages as $pkg) {
|
|
|
|
|
$pkg_name = is_string($pkg) ? $pkg : $pkg->getName();
|
|
|
|
|
if (!isset($dep_list[$pkg_name])) {
|
|
|
|
|
throw new WrongUsageException("Package '{$pkg_name}' does not exist in config, please check your package name !");
|
|
|
|
|
}
|
|
|
|
|
if (!isset($visited[$pkg_name])) {
|
|
|
|
|
self::visitPlatDeps($pkg_name, $dep_list, $visited, $sorted);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sorted_suggests = [];
|
|
|
|
|
$visited_suggests = [];
|
|
|
|
|
$final = [];
|
|
|
|
|
foreach ($packages as $pkg) {
|
|
|
|
|
$pkg_name = is_string($pkg) ? $pkg : $pkg->getName();
|
|
|
|
|
if (!isset($visited_suggests[$pkg_name])) {
|
|
|
|
|
self::visitPlatAllDeps($pkg_name, $dep_list, $visited_suggests, $sorted_suggests);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
foreach ($sorted_suggests as $suggest) {
|
|
|
|
|
if (in_array($suggest, $sorted)) {
|
|
|
|
|
$final[] = $suggest;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $final;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function visitPlatAllDeps(string $pkg_name, array $dep_list, array &$visited, array &$sorted): void
|
|
|
|
|
{
|
|
|
|
|
// 如果已经识别到了,那就不管
|
|
|
|
|
if (isset($visited[$pkg_name])) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$visited[$pkg_name] = true;
|
2026-02-05 16:11:16 +08:00
|
|
|
// 遍历该依赖的所有依赖
|
|
|
|
|
foreach (array_merge($dep_list[$pkg_name]['depends'] ?? [], $dep_list[$pkg_name]['suggests'] ?? []) as $dep) {
|
2025-11-30 15:35:04 +08:00
|
|
|
self::visitPlatAllDeps($dep, $dep_list, $visited, $sorted);
|
|
|
|
|
}
|
|
|
|
|
$sorted[] = $pkg_name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static function visitPlatDeps(string $pkg_name, array $dep_list, array &$visited, array &$sorted): void
|
|
|
|
|
{
|
|
|
|
|
// 如果已经识别到了,那就不管
|
|
|
|
|
if (isset($visited[$pkg_name])) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$visited[$pkg_name] = true;
|
2026-02-05 16:11:16 +08:00
|
|
|
// 遍历该依赖的所有依赖
|
2025-11-30 15:35:04 +08:00
|
|
|
if (!isset($dep_list[$pkg_name])) {
|
|
|
|
|
throw new WrongUsageException("{$pkg_name} not exist !");
|
|
|
|
|
}
|
2026-02-05 16:11:16 +08:00
|
|
|
foreach ($dep_list[$pkg_name]['depends'] ?? [] as $dep) {
|
2025-11-30 15:35:04 +08:00
|
|
|
self::visitPlatDeps($dep, $dep_list, $visited, $sorted);
|
|
|
|
|
}
|
|
|
|
|
$sorted[] = $pkg_name;
|
|
|
|
|
}
|
|
|
|
|
}
|