From 0bfc65add499ca33a25fe8ceccf5e4cce717cf3d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 8 Feb 2025 11:01:31 +0800 Subject: [PATCH] Adjust dump-extensions --- src/SPC/command/BaseCommand.php | 10 +- src/SPC/command/DumpExtensionsCommand.php | 168 +++++++++++++++++----- 2 files changed, 134 insertions(+), 44 deletions(-) diff --git a/src/SPC/command/BaseCommand.php b/src/SPC/command/BaseCommand.php index 7b79a4c8..6a6c2313 100644 --- a/src/SPC/command/BaseCommand.php +++ b/src/SPC/command/BaseCommand.php @@ -154,24 +154,24 @@ abstract class BaseCommand extends Command /** * Parse extension list from string, replace alias and filter internal extensions. * - * @param string $ext_list Extension string list, e.g. "mbstring,posix,sockets" + * @param array|string $ext_list Extension string list, e.g. "mbstring,posix,sockets" or array */ - protected function parseExtensionList(string $ext_list): array + protected function parseExtensionList(array|string $ext_list): array { // replace alias $ls = array_map(function ($x) { $lower = strtolower(trim($x)); if (isset(SPC_EXTENSION_ALIAS[$lower])) { - logger()->notice("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.'); + logger()->debug("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.'); return SPC_EXTENSION_ALIAS[$lower]; } return $lower; - }, explode(',', $ext_list)); + }, is_array($ext_list) ? $ext_list : explode(',', $ext_list)); // filter internals return array_values(array_filter($ls, function ($x) { if (in_array($x, SPC_INTERNAL_EXTENSIONS)) { - logger()->warning("Extension [{$x}] is an builtin extension, it will be ignored."); + logger()->debug("Extension [{$x}] is an builtin extension, it will be ignored."); return false; } return true; diff --git a/src/SPC/command/DumpExtensionsCommand.php b/src/SPC/command/DumpExtensionsCommand.php index 027716b9..ffb80a46 100644 --- a/src/SPC/command/DumpExtensionsCommand.php +++ b/src/SPC/command/DumpExtensionsCommand.php @@ -4,67 +4,157 @@ declare(strict_types=1); namespace SPC\command; +use SPC\store\FileSystem; use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; #[AsCommand(name: 'dump-extensions', description: 'Determines the required php extensions')] class DumpExtensionsCommand extends BaseCommand { - private array $files = [ - 'vendor/composer/installed.json', - 'composer.lock', - 'composer.json', - ]; + protected bool $no_motd = true; + + public function configure(): void + { + // path to project files or specific composer file + $this->addArgument('path', InputArgument::OPTIONAL, 'Path to project root', '.'); + $this->addOption('format', 'F', InputOption::VALUE_REQUIRED, 'Parsed output format', 'default'); + // output zero extension replacement rather than exit as failure + $this->addOption('no-ext-output', 'N', InputOption::VALUE_REQUIRED, 'When no extensions found, output default combination (comma separated)'); + // no dev + $this->addOption('no-dev', null, null, 'Do not include dev dependencies'); + // no spc filter + $this->addOption('no-spc-filter', 'S', null, 'Do not use SPC filter to determine the required extensions'); + } public function handle(): int { - $fs = new Filesystem(); - $extensions = []; + $path = FileSystem::convertPath($this->getArgument('path')); - foreach ($this->files as $file) { - if ($fs->exists($file)) { - $this->output->writeln("Analyzing file: {$file}"); - $data = json_decode(file_get_contents($file), true); + $path_installed = FileSystem::convertPath(rtrim($path, '/\\') . '/vendor/composer/installed.json'); + $path_lock = FileSystem::convertPath(rtrim($path, '/\\') . '/composer.lock'); - if (!$data) { - $this->output->writeln("Error parsing {$file}"); - continue; - } - - $extensions = array_merge($extensions, $this->extractExtensions($data)); + $ext_installed = $this->extractFromInstalledJson($path_installed, !$this->getOption('no-dev')); + if ($ext_installed === null) { + if ($this->getOption('format') === 'default') { + $this->output->writeln('vendor/composer/installed.json load failed, skipped'); } + $ext_installed = []; } - if (empty($extensions)) { - $this->output->writeln('No extensions found.'); - return static::SUCCESS; + $ext_lock = $this->extractFromComposerLock($path_lock, !$this->getOption('no-dev')); + if ($ext_lock === null) { + $this->output->writeln('composer.lock load failed'); + return static::FAILURE; } - $extensions = array_unique($extensions); + $extensions = array_unique(array_merge($ext_installed, $ext_lock)); sort($extensions); - $this->output->writeln("\nRequired PHP extensions:"); - $this->output->writeln(implode(',', array_map(fn ($ext) => substr($ext, 4), $extensions))); + if (empty($extensions)) { + if ($this->getOption('no-ext-output')) { + $this->outputExtensions(explode(',', $this->getOption('no-ext-output'))); + return static::SUCCESS; + } + $this->output->writeln('No extensions found'); + return static::FAILURE; + } + $this->outputExtensions($extensions); return static::SUCCESS; } - private function extractExtensions(array $data): array - { - return array_merge( - ...array_map( - function ($package) { - return isset($package['require']) ? $this->filterExtensions($package['require']) : []; - }, - $data['packages'] ?? [$data] - ) - ); - } - private function filterExtensions(array $requirements): array { - return array_keys(array_filter($requirements, function ($key) { - return str_starts_with($key, 'ext-'); - }, ARRAY_FILTER_USE_KEY)); + return array_map( + fn ($key) => substr($key, 4), + array_keys( + array_filter($requirements, function ($key) { + return str_starts_with($key, 'ext-'); + }, ARRAY_FILTER_USE_KEY) + ) + ); + } + + private function loadJson(string $file): array|bool + { + if (!file_exists($file)) { + return false; + } + + $data = json_decode(file_get_contents($file), true); + if (!$data) { + return false; + } + return $data; + } + + private function extractFromInstalledJson(string $file, bool $include_dev = true): ?array + { + if (!($data = $this->loadJson($file))) { + return null; + } + + $packages = $data['packages'] ?? []; + + if (!$include_dev) { + $packages = array_filter($packages, fn ($package) => !in_array($package['name'], $data['dev-package-names'] ?? [])); + } + + return array_merge( + ...array_map(fn ($x) => isset($x['require']) ? $this->filterExtensions($x['require']) : [], $packages) + ); + } + + private function extractFromComposerLock(string $file, bool $include_dev = true): ?array + { + if (!($data = $this->loadJson($file))) { + return null; + } + + // get packages ext + $packages = $data['packages'] ?? []; + $exts = array_merge( + ...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages) + ); + + // get dev packages ext + if ($include_dev) { + $packages = $data['packages-dev'] ?? []; + $exts = array_merge( + $exts, + ...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages) + ); + } + + // get require ext + $platform = $data['platform'] ?? []; + $exts = array_merge($exts, $this->filterExtensions($platform)); + + // get require-dev ext + if ($include_dev) { + $platform = $data['platform-dev'] ?? []; + $exts = array_merge($exts, $this->filterExtensions($platform)); + } + + return $exts; + } + + private function outputExtensions(array $extensions): void + { + if (!$this->getOption('no-spc-filter')) { + $extensions = $this->parseExtensionList($extensions); + } + switch ($this->getOption('format')) { + case 'json': + $this->output->writeln(json_encode($extensions, JSON_PRETTY_PRINT)); + break; + case 'text': + $this->output->writeln(implode(',', $extensions)); + break; + default: + $this->output->writeln('Required PHP extensions' . ($this->getOption('no-dev') ? ' (without dev)' : '') . ':'); + $this->output->writeln(implode(',', $extensions)); + } } }