From e9279940d7af55195420f1fcfba4a54badbf75f8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 16:09:18 +0800 Subject: [PATCH] Add DumpStagesCommand to dump package stages and their locations --- .../Command/Dev/DumpStagesCommand.php | 157 ++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Package/Package.php | 10 ++ src/StaticPHP/Registry/PackageLoader.php | 20 +++ 4 files changed, 189 insertions(+) create mode 100644 src/StaticPHP/Command/Dev/DumpStagesCommand.php diff --git a/src/StaticPHP/Command/Dev/DumpStagesCommand.php b/src/StaticPHP/Command/Dev/DumpStagesCommand.php new file mode 100644 index 00000000..4b20fe21 --- /dev/null +++ b/src/StaticPHP/Command/Dev/DumpStagesCommand.php @@ -0,0 +1,157 @@ +addArgument('packages', InputArgument::OPTIONAL, 'Comma-separated list of packages to dump, e.g. "openssl,zlib,curl". Dumps all packages if omitted.'); + $this->addArgument('output', InputArgument::OPTIONAL, 'Output file path', ROOT_DIR . '/dump-stages.json'); + $this->addOption('relative', 'r', InputOption::VALUE_NONE, 'Output file paths relative to ROOT_DIR'); + } + + public function handle(): int + { + $outputFile = $this->getArgument('output'); + $useRelative = (bool) $this->getOption('relative'); + + $filterPackages = null; + if ($packagesArg = $this->getArgument('packages')) { + $filterPackages = array_flip(parse_comma_list($packagesArg)); + } + + $result = []; + + foreach (PackageLoader::getPackages() as $name => $pkg) { + if ($filterPackages !== null && !isset($filterPackages[$name])) { + continue; + } + $entry = [ + 'type' => $pkg->getType(), + 'stages' => [], + 'before_stages' => [], + 'after_stages' => [], + ]; + + // Resolve main stages + foreach ($pkg->getStages() as $stageName => $callable) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry['stages'][$stageName] = $location; + } + + $result[$name] = $entry; + } + + // Resolve before/after stage external callbacks + foreach (PackageLoader::getAllBeforeStages() as $pkgName => $stages) { + if ($filterPackages !== null && !isset($filterPackages[$pkgName])) { + continue; + } + foreach ($stages as $stageName => $callbacks) { + foreach ($callbacks as [$callable, $onlyWhen]) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry_data = $location ?? []; + if ($onlyWhen !== null) { + $entry_data['only_when_package_resolved'] = $onlyWhen; + } + $result[$pkgName]['before_stages'][$stageName][] = $entry_data; + } + } + } + + foreach (PackageLoader::getAllAfterStages() as $pkgName => $stages) { + if ($filterPackages !== null && !isset($filterPackages[$pkgName])) { + continue; + } + foreach ($stages as $stageName => $callbacks) { + foreach ($callbacks as [$callable, $onlyWhen]) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry_data = $location ?? []; + if ($onlyWhen !== null) { + $entry_data['only_when_package_resolved'] = $onlyWhen; + } + $result[$pkgName]['after_stages'][$stageName][] = $entry_data; + } + } + } + + $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + file_put_contents($outputFile, $json . PHP_EOL); + + $this->output->writeln('Dumped stages for ' . count($result) . " package(s) to: {$outputFile}"); + return static::SUCCESS; + } + + /** + * Resolve the file, start line, class and method name of a callable using reflection. + * + * @return null|array{file: string, line: false|int, class: string, method: string} + */ + private function resolveCallableLocation(mixed $callable): ?array + { + try { + if (is_array($callable) && count($callable) === 2) { + $ref = new \ReflectionMethod($callable[0], $callable[1]); + return [ + 'class' => $ref->getDeclaringClass()->getName(), + 'method' => $ref->getName(), + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + if ($callable instanceof \Closure) { + $ref = new \ReflectionFunction($callable); + $scopeClass = $ref->getClosureScopeClass(); + return [ + 'class' => $scopeClass !== null ? $scopeClass->getName() : '{closure}', + 'method' => '{closure}', + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + if (is_string($callable) && str_contains($callable, '::')) { + [$class, $method] = explode('::', $callable, 2); + $ref = new \ReflectionMethod($class, $method); + return [ + 'class' => $ref->getDeclaringClass()->getName(), + 'method' => $ref->getName(), + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + } catch (\ReflectionException) { + // ignore + } + return null; + } + + private function toRelativePath(string $absolutePath): string + { + $root = rtrim(ROOT_DIR, '/') . '/'; + if (str_starts_with($absolutePath, $root)) { + return substr($absolutePath, strlen($root)); + } + return $absolutePath; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 8608f761..4afa221c 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,6 +6,7 @@ namespace StaticPHP; use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; +use StaticPHP\Command\Dev\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\LintConfigCommand; @@ -67,6 +68,7 @@ class ConsoleApplication extends Application new EnvCommand(), new LintConfigCommand(), new PackLibCommand(), + new DumpStagesCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 64b9f2e4..fd59e98b 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -128,6 +128,16 @@ abstract class Package $this->stages[$name] = $stage; } + /** + * Get all defined stages for this package. + * + * @return array + */ + public function getStages(): array + { + return $this->stages; + } + /** * Check if the package has a specific stage defined. * diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index ca195ff0..573ad7da 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -240,6 +240,26 @@ class PackageLoader } } + /** + * Get all registered before-stage callbacks (raw). + * + * @return array>> + */ + public static function getAllBeforeStages(): array + { + return self::$before_stages; + } + + /** + * Get all registered after-stage callbacks (raw). + * + * @return array>> + */ + public static function getAllAfterStages(): array + { + return self::$after_stages; + } + public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable { // match condition