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