Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions docs/en/guide/manual-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions docs/zh/guide/manual-build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 项目本身的时候,可以辅助输出一些信息的命令集合。
Expand Down
2 changes: 2 additions & 0 deletions src/SPC/ConsoleApplication.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -54,6 +55,7 @@ public function __construct()
new MicroCombineCommand(),
new SwitchPhpVersionCommand(),
new SPCConfigCommand(),
new DumpExtensionsCommand(),

// Dev commands
new AllExtCommand(),
Expand Down
10 changes: 5 additions & 5 deletions src/SPC/command/BaseCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
160 changes: 160 additions & 0 deletions src/SPC/command/DumpExtensionsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<?php

declare(strict_types=1);

namespace SPC\command;

use SPC\store\FileSystem;
use Symfony\Component\Console\Attribute\AsCommand;
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
{
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
{
$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('<comment>vendor/composer/installed.json load failed, skipped</comment>');
}
$ext_installed = [];
}

$ext_lock = $this->extractFromComposerLock($path_lock, !$this->getOption('no-dev'));
if ($ext_lock === null) {
$this->output->writeln('<error>composer.lock load failed</error>');
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('<error>No extensions found</error>');
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('<info>Required PHP extensions' . ($this->getOption('no-dev') ? ' (without dev)' : '') . ':</info>');
$this->output->writeln(implode(',', $extensions));
}
}
}
Loading