diff --git a/README.md b/README.md index 38544e84e..b4ef97a5b 100755 --- a/README.md +++ b/README.md @@ -194,6 +194,8 @@ Basic usage for building php with some extensions: # fetch all libraries ./bin/spc download --all +# dump a list of extensions required by your project +./bin/spc dump-extensions # only fetch necessary sources by needed extensions (recommended) ./bin/spc download --for-extensions="openssl,pcntl,mbstring,pdo_sqlite" # download pre-built libraries first (save time for compiling dependencies) diff --git a/docs/en/guide/manual-build.md b/docs/en/guide/manual-build.md index a0b02b08b..f86374b8e 100644 --- a/docs/en/guide/manual-build.md +++ b/docs/en/guide/manual-build.md @@ -397,6 +397,31 @@ manually unpack and copy the package to a specified location, and we can use com bin/spc extract php-src,libxml2 ``` +## Command - dump-extensions + +Use the command `bin/spc dump-extensions` to export required extensions of the current project. + +```bash +# Print the extension list of the project, pass in the root directory of the project containing composer.json +bin/spc dump-extensions /path/to/your/project/ + +# Print the extension list of the project, excluding development dependencies +bin/spc dump-extensions /path-to/tour/project/ --no-dev + +# Output in the extension list format acceptable to the spc command (comma separated) +bin/spc dump-extensions /path-to/tour/project/ --format=text + +# Output as a JSON list +bin/spc dump-extensions /path-to/tour/project/ --format=json + +# When the project does not have any extensions, output the specified extension combination instead of returning failure +bin/spc dump-extensions /path-to/your/project/ --no-ext-output=mbstring,posix,pcntl,phar + +# Do not exclude extensions not supported by spc when outputting +bin/spc dump-extensions /path/to/your/project/ --no-spc-filter +``` +It should be noted that the project directory must contain the `vendor/installed.json` and `composer.lock` files, otherwise they cannot be found normally. + ## Dev Command - dev Debug commands refer to a collection of commands that can assist in outputting some information diff --git a/docs/zh/guide/manual-build.md b/docs/zh/guide/manual-build.md index 3d523fe60..a05ccfc4b 100644 --- a/docs/zh/guide/manual-build.md +++ b/docs/zh/guide/manual-build.md @@ -353,6 +353,32 @@ memory_limit=1G bin/spc extract php-src,libxml2 ``` +## 命令 dump-extensions - 导出项目扩展依赖 + +使用命令 `bin/spc dump-extensions` 可以导出当前项目的扩展依赖。 + +```bash +# 打印项目的扩展列表,传入项目包含composer.json的根目录 +bin/spc dump-extensions /path/to/your/project/ + +# 打印项目的扩展列表,不包含开发依赖 +bin/spc dump-extensions /path-to/tour/project/ --no-dev + +# 输出为 spc 命令可接受的扩展列表格式(逗号分割) +bin/spc dump-extensions /path-to/tour/project/ --format=text + +# 输出为 JSON 列表 +bin/spc dump-extensions /path-to/tour/project/ --format=json + +# 当项目没有任何扩展时,输出指定扩展组合,而不是返回失败 +bin/spc dump-extensions /path-to/your/project/ --no-ext-output=mbstring,posix,pcntl,phar + +# 输出时不排除 spc 不支持的扩展 +bin/spc dump-extensions /path/to/your/project/ --no-spc-filter +``` + +需要注意的是,项目的目录下必须包含 `vendor/installed.json` 和 `composer.lock` 文件,否则无法正常获取。 + ## 调试命令 dev - 调试命令集合 调试命令指的是你在使用 static-php-cli 构建 PHP 或改造、增强 static-php-cli 项目本身的时候,可以辅助输出一些信息的命令集合。 diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 2609d3179..8d3fd59bf 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -18,6 +18,7 @@ use SPC\command\dev\SortConfigCommand; use SPC\command\DoctorCommand; use SPC\command\DownloadCommand; +use SPC\command\DumpExtensionsCommand; use SPC\command\DumpLicenseCommand; use SPC\command\ExtractCommand; use SPC\command\InstallPkgCommand; @@ -54,6 +55,7 @@ public function __construct() new MicroCombineCommand(), new SwitchPhpVersionCommand(), new SPCConfigCommand(), + new DumpExtensionsCommand(), // Dev commands new AllExtCommand(), diff --git a/src/SPC/command/BaseCommand.php b/src/SPC/command/BaseCommand.php index 7b79a4c8e..6a6c23137 100644 --- a/src/SPC/command/BaseCommand.php +++ b/src/SPC/command/BaseCommand.php @@ -154,24 +154,24 @@ protected function logWithResult(bool $result, string $success_msg, string $fail /** * 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 new file mode 100644 index 000000000..ffb80a46b --- /dev/null +++ b/src/SPC/command/DumpExtensionsCommand.php @@ -0,0 +1,160 @@ +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 + { + $path = FileSystem::convertPath($this->getArgument('path')); + + $path_installed = FileSystem::convertPath(rtrim($path, '/\\') . '/vendor/composer/installed.json'); + $path_lock = FileSystem::convertPath(rtrim($path, '/\\') . '/composer.lock'); + + $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 = []; + } + + $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(array_merge($ext_installed, $ext_lock)); + sort($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 filterExtensions(array $requirements): array + { + 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)); + } + } +}