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));
+ }
}
}