Adjust dump-extensions

This commit is contained in:
crazywhalecc
2025-02-08 11:01:31 +08:00
parent dfadcce4b2
commit 0bfc65add4
2 changed files with 134 additions and 44 deletions

View File

@@ -154,24 +154,24 @@ abstract class BaseCommand extends Command
/** /**
* Parse extension list from string, replace alias and filter internal extensions. * 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 // replace alias
$ls = array_map(function ($x) { $ls = array_map(function ($x) {
$lower = strtolower(trim($x)); $lower = strtolower(trim($x));
if (isset(SPC_EXTENSION_ALIAS[$lower])) { 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 SPC_EXTENSION_ALIAS[$lower];
} }
return $lower; return $lower;
}, explode(',', $ext_list)); }, is_array($ext_list) ? $ext_list : explode(',', $ext_list));
// filter internals // filter internals
return array_values(array_filter($ls, function ($x) { return array_values(array_filter($ls, function ($x) {
if (in_array($x, SPC_INTERNAL_EXTENSIONS)) { 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 false;
} }
return true; return true;

View File

@@ -4,67 +4,157 @@ declare(strict_types=1);
namespace SPC\command; namespace SPC\command;
use SPC\store\FileSystem;
use Symfony\Component\Console\Attribute\AsCommand; 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')] #[AsCommand(name: 'dump-extensions', description: 'Determines the required php extensions')]
class DumpExtensionsCommand extends BaseCommand class DumpExtensionsCommand extends BaseCommand
{ {
private array $files = [ protected bool $no_motd = true;
'vendor/composer/installed.json',
'composer.lock', public function configure(): void
'composer.json', {
]; // 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 public function handle(): int
{ {
$fs = new Filesystem(); $path = FileSystem::convertPath($this->getArgument('path'));
$extensions = [];
foreach ($this->files as $file) { $path_installed = FileSystem::convertPath(rtrim($path, '/\\') . '/vendor/composer/installed.json');
if ($fs->exists($file)) { $path_lock = FileSystem::convertPath(rtrim($path, '/\\') . '/composer.lock');
$this->output->writeln("<info>Analyzing file: {$file}</info>");
$data = json_decode(file_get_contents($file), true);
if (!$data) { $ext_installed = $this->extractFromInstalledJson($path_installed, !$this->getOption('no-dev'));
$this->output->writeln("<error>Error parsing {$file}</error>"); if ($ext_installed === null) {
continue; if ($this->getOption('format') === 'default') {
} $this->output->writeln('<comment>vendor/composer/installed.json load failed, skipped</comment>');
$extensions = array_merge($extensions, $this->extractExtensions($data));
} }
$ext_installed = [];
} }
if (empty($extensions)) { $ext_lock = $this->extractFromComposerLock($path_lock, !$this->getOption('no-dev'));
$this->output->writeln('<comment>No extensions found.</comment>'); if ($ext_lock === null) {
return static::SUCCESS; $this->output->writeln('<error>composer.lock load failed</error>');
return static::FAILURE;
} }
$extensions = array_unique($extensions); $extensions = array_unique(array_merge($ext_installed, $ext_lock));
sort($extensions); sort($extensions);
$this->output->writeln("\n<info>Required PHP extensions:</info>"); if (empty($extensions)) {
$this->output->writeln(implode(',', array_map(fn ($ext) => substr($ext, 4), $extensions))); if ($this->getOption('no-ext-output')) {
$this->outputExtensions(explode(',', $this->getOption('no-ext-output')));
return static::SUCCESS;
}
$this->output->writeln('<error>No extensions found</error>');
return static::FAILURE;
}
$this->outputExtensions($extensions);
return static::SUCCESS; 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 private function filterExtensions(array $requirements): array
{ {
return array_keys(array_filter($requirements, function ($key) { return array_map(
return str_starts_with($key, 'ext-'); fn ($key) => substr($key, 4),
}, ARRAY_FILTER_USE_KEY)); 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('<info>Required PHP extensions' . ($this->getOption('no-dev') ? ' (without dev)' : '') . ':</info>');
$this->output->writeln(implode(',', $extensions));
}
} }
} }