Compare commits

..

34 Commits

Author SHA1 Message Date
Joseph Bielawski
be32190829 Sanity check extensions by inline their test code 2023-09-13 23:42:10 +08:00
crazywhalecc
f1d5916090 remove unused old deploy command 2023-09-13 21:45:19 +08:00
Joseph Bielawski
b2ea479fac Remove the phar workflow temporary 2023-09-13 21:45:19 +08:00
Joseph Bielawski
16cc5df66d Temporary disable phar build in GH actions 2023-09-13 21:45:19 +08:00
Joseph Bielawski
a63e3f4575 Add version into phar 2023-09-13 21:45:19 +08:00
Joseph Bielawski
6b061e6332 Adjust box configuration for smaller phar size 2023-09-13 21:45:19 +08:00
Joseph Bielawski
4a17491aaa Initial code to get Box for phar building 2023-09-13 21:45:19 +08:00
Jerry Ma
06f29712e2 Update README.md 2023-09-13 10:03:31 +08:00
Kévin Dunglas
4b1d59c5a9 Link to FrankenPHP docs about embed SAPI in README-en.md 2023-09-13 10:03:31 +08:00
crazywhalecc
1a81fe6a0d change help message for dev:extensions 2023-09-12 23:30:23 +08:00
crazywhalecc
c63136d484 change dev:extensions argument to comma separated 2023-09-12 23:30:23 +08:00
crazywhalecc
4b500f2dd8 remove redundant libssh2 cmake build target option 2023-09-12 23:09:58 +08:00
crazywhalecc
9da20497cc fix ln src and dst for brotli 2023-09-12 22:53:47 +08:00
crazywhalecc
5efc15b404 fix libwebp pkgconfig patch, add custom patch argument 2023-09-12 22:52:20 +08:00
Joseph Bielawski
fa0740f216 Explicitly define application commands 2023-09-12 22:51:50 +08:00
Joseph Bielawski
52430cbdde Rework doctor command 2023-09-12 22:46:44 +08:00
crazywhalecc
01c4538ce0 update README for embed SAPI 2023-09-12 22:44:19 +08:00
Kévin Dunglas
3183ecceaf --disable-opcache-jit 2023-09-12 22:30:13 +08:00
Kévin Dunglas
dd2e7cc129 static opcache patch 2023-09-12 22:30:13 +08:00
Kévin Dunglas
adfa620ef4 Revert "always enable micro"
This reverts commit eb8cefd9ca.
2023-09-12 22:30:13 +08:00
Kévin Dunglas
1a32ddc70a Revert "temporary workaround for PECL certificate expiration"
This reverts commit 7c78003c9f.
2023-09-12 22:30:13 +08:00
Kévin Dunglas
4bfca6fe93 always enable micro 2023-09-12 22:30:13 +08:00
Kévin Dunglas
dbec043894 temporary workaround for PECL certificate expiration 2023-09-12 22:30:13 +08:00
Kévin Dunglas
727d78a689 Revert "fix: replace down URL"
This reverts commit 6eadbca21bf9a3b57058160a2f7ea8f4cc5f21dd.
2023-09-12 22:30:13 +08:00
Kévin Dunglas
9b1e784604 fix: replace down URL 2023-09-12 22:30:13 +08:00
Kévin Dunglas
50d44d8310 explicitly disable unneeded SAPIs 2023-09-12 22:30:13 +08:00
Kévin Dunglas
3b300698f3 use lixml GH mirror because gitlab.gnome.org is down 2023-09-12 22:30:13 +08:00
Kévin Dunglas
899eb94b8b remove useless ls 2023-09-12 22:30:13 +08:00
Kévin Dunglas
451a0c0e34 workaround for macOS 2023-09-12 22:30:13 +08:00
Kévin Dunglas
085c1a159c feat: add support for libphp and the embed SAPI 2023-09-12 22:30:13 +08:00
Joseph Bielawski
ca3f8a350d Prevent fatal error when php source files are missing 2023-09-12 18:19:32 +08:00
crazywhalecc
5025850f71 fix Alpine docker install packages 2023-09-12 12:45:51 +08:00
crazywhalecc
fe2f658e08 dev:sort-config add \n at the end of json 2023-09-12 00:24:01 +08:00
crazywhalecc
00a49c662b add build:libs option --rebuild 2023-09-11 23:47:02 +08:00
26 changed files with 310 additions and 324 deletions

View File

@@ -2,7 +2,7 @@
Build single static PHP binary, with PHP project together, with popular extensions included. Build single static PHP binary, with PHP project together, with popular extensions included.
The project name is static-php-cli, but it actually supports cli, fpm, micro and embed (on the way) SAPI 😎 The project name is static-php-cli, but it actually supports cli, fpm, micro and embed SAPI 😎
Compile a purely static php-cli binary file with various extensions to make PHP applications more portable! (cli SAPI) Compile a purely static php-cli binary file with various extensions to make PHP applications more portable! (cli SAPI)
@@ -126,6 +126,7 @@ Now we support `cli`, `micro`, `fpm`, you can use one or more of the following p
- `--build-cli`: build static cli executable - `--build-cli`: build static cli executable
- `--build-micro`: build static phpmicro self-extracted executable - `--build-micro`: build static phpmicro self-extracted executable
- `--build-fpm`: build static fpm binary - `--build-fpm`: build static fpm binary
- `--build-embed`: build embed (libphp)
- `--build-all`: build all - `--build-all`: build all
If anything goes wrong, use `--debug` option to display full terminal output: If anything goes wrong, use `--debug` option to display full terminal output:
@@ -194,9 +195,20 @@ Because php-fpm must specify a configuration file before running, the php-fpm co
Specifying `php-fpm.conf` can use the command parameter `-y`, for example: `./php-fpm -y php-fpm.conf`. Specifying `php-fpm.conf` can use the command parameter `-y`, for example: `./php-fpm -y php-fpm.conf`.
### Embed Usage
When using the project parameters `--build-embed` or `--build-all`,
the final compilation result will output a `libphp.a`, `php-config` and a series of header files,
stored in `buildroot/`. You can introduce them in your other projects.
If you know [embed SAPI](https://github.com/php/php-src/tree/master/sapi/embed), you should know how to use it.
You may require the introduction of other libraries during compilation,
you can use `buildroot/bin/php-config` to obtain the compile-time configuration.
For an advanced example of how to use this feature, take a look at [how to use it to build a static version of FrankenPHP](https://github.com/dunglas/frankenphp/blob/main/docs/static.md).
## Contribution ## Contribution
Currently, there are only a few supported extensions.
If the extension you need is missing, you can create an issue. If the extension you need is missing, you can create an issue.
If you are familiar with this project, you are also welcome to initiate a pull request. If you are familiar with this project, you are also welcome to initiate a pull request.

View File

@@ -19,7 +19,7 @@ Build single static PHP binary, with PHP project together, with popular extensio
[![](https://img.shields.io/badge/Extension%20Counter-55+-yellow.svg?style=flat-square)]() [![](https://img.shields.io/badge/Extension%20Counter-55+-yellow.svg?style=flat-square)]()
[![](https://img.shields.io/github/search/crazywhalecc/static-php-cli/TODO?label=TODO%20Counter&style=flat-square)]() [![](https://img.shields.io/github/search/crazywhalecc/static-php-cli/TODO?label=TODO%20Counter&style=flat-square)]()
> 项目名称是 static-php-cli但其实支持 cli、fpm、micro 和 embed(正在路上)SAPI 😎 > 项目名称是 static-php-cli但其实支持 cli、fpm、micro 和 embed SAPI 😎
## 编译环境需求 ## 编译环境需求
@@ -120,6 +120,7 @@ chmod +x bin/spc
- `--build-cli`:构建 cli 二进制 - `--build-cli`:构建 cli 二进制
- `--build-micro`:构建 phpmicro 自执行二进制 - `--build-micro`:构建 phpmicro 自执行二进制
- `--build-fpm`:构建 fpm - `--build-fpm`:构建 fpm
- `--build-embed`:构建 embedlibphp
- `--build-all`:构建所有 - `--build-all`:构建所有
如果出现了任何错误,可以使用 `--debug` 参数来展示完整的输出日志,以供排查错误: 如果出现了任何错误,可以使用 `--debug` 参数来展示完整的输出日志,以供排查错误:
@@ -181,9 +182,17 @@ cat micro.sfx code.php > single-app && chmod +x single-app
指定 `php-fpm.conf` 可以使用命令参数 `-y`,例如:`./php-fpm -y php-fpm.conf` 指定 `php-fpm.conf` 可以使用命令参数 `-y`,例如:`./php-fpm -y php-fpm.conf`
### 使用 php-embed
采用项目参数 `--build-embed``--build-all` 时,最后编译结果会输出一个 `libphp.a``php-config` 以及一系列头文件,存放在 `buildroot/`,你可以在你的其他代码中引入它们。
如果你知道 [embed SAPI](https://github.com/php/php-src/tree/master/sapi/embed),你应该知道如何使用它。对于有可能编译用到引入其他库的问题,你可以使用 `buildroot/bin/php-config` 来获取编译时的配置。
另外,有关如何使用此功能的高级示例,请查看[如何使用它构建 FrankenPHP 的静态版本](https://github.com/dunglas/frankenphp/blob/main/docs/static.md)。
## 贡献 ## 贡献
目前支持的扩展较少,如果缺少你需要的扩展,可发起 Issue。如果你对本项目较熟悉也欢迎为本项目发起 Pull Request。 如果缺少你需要的扩展,可发起 Issue。如果你对本项目较熟悉也欢迎为本项目发起 Pull Request。
贡献基本原则如下: 贡献基本原则如下:
@@ -202,7 +211,7 @@ cat micro.sfx code.php > single-app && chmod +x single-app
## 开源协议 ## 开源协议
本项目依据旧版本惯例采用 MIT License 开源,自身的部分代码引用或修改自以下项目: 本项目依据旧版本惯例采用 MIT License 开源,部分扩展的集成编译命令参考或修改自以下项目:
- [dixyes/lwmbs](https://github.com/dixyes/lwmbs)(木兰宽松许可证) - [dixyes/lwmbs](https://github.com/dixyes/lwmbs)(木兰宽松许可证)
- [swoole/swoole-cli](https://github.com/swoole/swoole-cli)Apache 2.0 LICENSE、SWOOLE-CLI LICENSE - [swoole/swoole-cli](https://github.com/swoole/swoole-cli)Apache 2.0 LICENSE、SWOOLE-CLI LICENSE

View File

@@ -64,7 +64,6 @@ RUN apk update; \
bison \ bison \
build-base \ build-base \
cmake \ cmake \
composer \
curl \ curl \
file \ file \
flex \ flex \
@@ -77,16 +76,20 @@ RUN apk update; \
linux-headers \ linux-headers \
m4 \ m4 \
make \ make \
php81 \ php82 \
php81-common \ php82-common \
php81-pcntl \ php82-pcntl \
php81-phar \ php82-phar \
php81-posix \ php82-posix \
php81-tokenizer \ php82-tokenizer \
php81-xml \ php82-dom \
php82-xml \
php82-xmlwriter \
composer \
pkgconfig \ pkgconfig \
wget \ wget \
xz xz
RUN ln -s /usr/bin/php82 /usr/bin/php
WORKDIR /app WORKDIR /app
ADD ./src /app/src ADD ./src /app/src
ADD ./composer.json /app/composer.json ADD ./composer.json /app/composer.json

18
box.json Normal file
View File

@@ -0,0 +1,18 @@
{
"alias": "spc-php.phar",
"banner": false,
"blacklist": [
".github"
],
"directories": [
"config",
"src",
"vendor/psr",
"vendor/laravel/prompts",
"vendor/symfony",
"vendor/zhamao"
],
"git-commit-short": "git_commit_short",
"metadata": "ConsoleApplication::VERSION",
"output": "spc.phar"
}

View File

@@ -20,6 +20,7 @@
"captainhook/captainhook": "^5.10", "captainhook/captainhook": "^5.10",
"captainhook/plugin-composer": "^5.3", "captainhook/plugin-composer": "^5.3",
"friendsofphp/php-cs-fixer": "^3.25", "friendsofphp/php-cs-fixer": "^3.25",
"humbug/box": "^4.3",
"nunomaduro/collision": "^7.8", "nunomaduro/collision": "^7.8",
"phpstan/phpstan": "^1.10", "phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^10.3" "phpunit/phpunit": "^10.3"
@@ -44,7 +45,8 @@
"scripts": { "scripts": {
"analyse": "phpstan analyse --memory-limit 300M", "analyse": "phpstan analyse --memory-limit 300M",
"cs-fix": "php-cs-fixer fix", "cs-fix": "php-cs-fixer fix",
"test": "vendor/bin/phpunit tests/ --no-coverage" "test": "vendor/bin/phpunit tests/ --no-coverage",
"build:phar": "vendor/bin/box compile"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {

View File

@@ -249,7 +249,7 @@
}, },
"libxml2": { "libxml2": {
"type": "url", "type": "url",
"url": "https://gitlab.gnome.org/GNOME/libxml2/-/archive/v2.9.14/libxml2-v2.9.14.tar.gz", "url": "https://github.com/GNOME/libxml2/archive/refs/tags/v2.9.14.tar.gz",
"license": { "license": {
"type": "file", "type": "file",
"path": "Copyright" "path": "Copyright"

View File

@@ -4,7 +4,16 @@ declare(strict_types=1);
namespace SPC; namespace SPC;
use SPC\store\FileSystem; use SPC\command\BuildCliCommand;
use SPC\command\BuildLibsCommand;
use SPC\command\dev\AllExtCommand;
use SPC\command\dev\PhpVerCommand;
use SPC\command\dev\SortConfigCommand;
use SPC\command\DoctorCommand;
use SPC\command\DownloadCommand;
use SPC\command\DumpLicenseCommand;
use SPC\command\ExtractCommand;
use SPC\command\MicroCombineCommand;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\HelpCommand; use Symfony\Component\Console\Command\HelpCommand;
use Symfony\Component\Console\Command\ListCommand; use Symfony\Component\Console\Command\ListCommand;
@@ -12,14 +21,10 @@ use Symfony\Component\Console\Command\ListCommand;
/** /**
* static-php-cli console app entry * static-php-cli console app entry
*/ */
class ConsoleApplication extends Application final class ConsoleApplication extends Application
{ {
public const VERSION = '2.0-rc6'; public const VERSION = '2.0-rc6';
/**
* @throws \ReflectionException
* @throws exception\FileSystemException
*/
public function __construct() public function __construct()
{ {
parent::__construct('static-php-cli', self::VERSION); parent::__construct('static-php-cli', self::VERSION);
@@ -29,21 +34,22 @@ class ConsoleApplication extends Application
// Detailed debugging errors are not displayed in the production environment. Only the error display provided by Symfony console is used. // Detailed debugging errors are not displayed in the production environment. Only the error display provided by Symfony console is used.
$this->setCatchExceptions(file_exists(ROOT_DIR . '/.prod') || !in_array('--debug', $argv)); $this->setCatchExceptions(file_exists(ROOT_DIR . '/.prod') || !in_array('--debug', $argv));
// Add subcommands by scanning the directory src/static-php-cli/command/ $this->addCommands(
$commands = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/command', 'SPC\\command'); [
$phar = class_exists('\\Phar') && \Phar::running() || !class_exists('\\Phar'); new BuildCliCommand(),
$commands = array_filter($commands, function ($y) use ($phar) { new BuildLibsCommand(),
$archive_blacklist = [ new DoctorCommand(),
'SPC\command\dev\SortConfigCommand', new DownloadCommand(),
'SPC\command\DeployCommand', new DumpLicenseCommand(),
]; new ExtractCommand(),
if ($phar && in_array($y, $archive_blacklist)) { new MicroCombineCommand(),
return false;
} // Dev commands
$reflection = new \ReflectionClass($y); new AllExtCommand(),
return !$reflection->isAbstract() && !$reflection->isInterface(); new PhpVerCommand(),
}); new SortConfigCommand(),
$this->addCommands(array_map(function ($x) { return new $x(); }, $commands)); ]
);
} }
protected function getDefaultCommands(): array protected function getDefaultCommands(): array

View File

@@ -85,7 +85,7 @@ abstract class BuilderBase
// build all libs // build all libs
foreach ($this->libs as $lib) { foreach ($this->libs as $lib) {
match ($lib->tryBuild()) { match ($lib->tryBuild($this->getOption('rebuild', false))) {
BUILD_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] build success'), BUILD_STATUS_OK => logger()->info('lib [' . $lib::NAME . '] build success'),
BUILD_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'), BUILD_STATUS_ALREADY => logger()->notice('lib [' . $lib::NAME . '] already built'),
BUILD_STATUS_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'), BUILD_STATUS_FAILED => logger()->error('lib [' . $lib::NAME . '] build failed'),
@@ -252,6 +252,9 @@ abstract class BuilderBase
if (($type & BUILD_TARGET_FPM) === BUILD_TARGET_FPM) { if (($type & BUILD_TARGET_FPM) === BUILD_TARGET_FPM) {
$ls[] = 'fpm'; $ls[] = 'fpm';
} }
if (($type & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) {
$ls[] = 'embed';
}
return implode(', ', $ls); return implode(', ', $ls);
} }

View File

@@ -179,6 +179,11 @@ class LinuxBuilder extends BuilderBase
$zts = ''; $zts = '';
} }
$enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI;
$enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM;
$enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
$enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
shell()->cd(SOURCE_PATH . '/php-src') shell()->cd(SOURCE_PATH . '/php-src')
->exec( ->exec(
'./configure ' . './configure ' .
@@ -189,12 +194,13 @@ class LinuxBuilder extends BuilderBase
'--disable-all ' . '--disable-all ' .
'--disable-cgi ' . '--disable-cgi ' .
'--disable-phpdbg ' . '--disable-phpdbg ' .
'--enable-cli ' . ($enableCli ? '--enable-cli ' : '--disable-cli ') .
'--enable-fpm ' . ($enableFpm ? '--enable-fpm ' : '--disable-fpm ') .
($enableEmbed ? '--enable-embed=static --disable-opcache-jit ' : '--disable-embed ') .
$json_74 . $json_74 .
$zts . $zts .
$maxExecutionTimers . $maxExecutionTimers .
'--enable-micro=all-static ' . ($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') .
$this->makeExtensionArgs() . ' ' . $this->makeExtensionArgs() . ' ' .
$envs $envs
); );
@@ -203,18 +209,22 @@ class LinuxBuilder extends BuilderBase
$this->cleanMake(); $this->cleanMake();
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { if ($enableCli) {
logger()->info('building cli'); logger()->info('building cli');
$this->buildCli($extra_libs, $use_lld); $this->buildCli($extra_libs, $use_lld);
} }
if (($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM) { if ($enableFpm) {
logger()->info('building fpm'); logger()->info('building fpm');
$this->buildFpm($extra_libs, $use_lld); $this->buildFpm($extra_libs, $use_lld);
} }
if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { if ($enableMicro) {
logger()->info('building micro'); logger()->info('building micro');
$this->buildMicro($extra_libs, $use_lld, $cflags); $this->buildMicro($extra_libs, $use_lld, $cflags);
} }
if ($enableEmbed) {
logger()->info('building embed');
$this->buildEmbed($extra_libs, $use_lld);
}
if (php_uname('m') === $this->getOption('arch')) { if (php_uname('m') === $this->getOption('arch')) {
$this->sanityCheck($build_target); $this->sanityCheck($build_target);
@@ -306,4 +316,18 @@ class LinuxBuilder extends BuilderBase
$this->deployBinary(BUILD_TARGET_FPM); $this->deployBinary(BUILD_TARGET_FPM);
} }
public function buildEmbed(string $extra_libs, string $use_lld): void
{
$vars = SystemUtil::makeEnvVarString([
'EXTRA_CFLAGS' => '-g -Os -fno-ident ' . implode(' ', array_map(fn ($x) => "-Xcompiler {$x}", $this->tune_c_flags)),
'EXTRA_LIBS' => $extra_libs,
'EXTRA_LDFLAGS_PROGRAM' => "{$use_lld} -all-static",
]);
shell()
->cd(SOURCE_PATH . '/php-src')
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec('make INSTALL_ROOT=' . BUILD_ROOT_PATH . " -j{$this->concurrency} {$vars} install");
}
} }

View File

@@ -151,6 +151,11 @@ class MacOSBuilder extends BuilderBase
$json_74 = $this->getPHPVersionID() < 80000 ? '--enable-json ' : ''; $json_74 = $this->getPHPVersionID() < 80000 ? '--enable-json ' : '';
$zts = $this->getOption('enable-zts', false) ? '--enable-zts --disable-zend-signals ' : ''; $zts = $this->getOption('enable-zts', false) ? '--enable-zts --disable-zend-signals ' : '';
$enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI;
$enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM;
$enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
$enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
shell()->cd(SOURCE_PATH . '/php-src') shell()->cd(SOURCE_PATH . '/php-src')
->exec( ->exec(
'./configure ' . './configure ' .
@@ -162,9 +167,10 @@ class MacOSBuilder extends BuilderBase
'--disable-all ' . '--disable-all ' .
'--disable-cgi ' . '--disable-cgi ' .
'--disable-phpdbg ' . '--disable-phpdbg ' .
'--enable-cli ' . ($enableCli ? '--enable-cli ' : '--disable-cli ') .
'--enable-fpm ' . ($enableFpm ? '--enable-fpm ' : '--disable-fpm ') .
'--enable-micro ' . ($enableEmbed ? '--enable-embed=static ' : '--disable-embed ') .
($enableMicro ? '--enable-micro ' : '--disable-micro ') .
$json_74 . $json_74 .
$zts . $zts .
$this->makeExtensionArgs() . ' ' . $this->makeExtensionArgs() . ' ' .
@@ -175,18 +181,22 @@ class MacOSBuilder extends BuilderBase
$this->cleanMake(); $this->cleanMake();
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { if ($enableCli) {
logger()->info('building cli'); logger()->info('building cli');
$this->buildCli(); $this->buildCli();
} }
if (($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM) { if ($enableFpm) {
logger()->info('building fpm'); logger()->info('building fpm');
$this->buildFpm(); $this->buildFpm();
} }
if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { if ($enableMicro) {
logger()->info('building micro'); logger()->info('building micro');
$this->buildMicro(); $this->buildMicro();
} }
if ($enableEmbed) {
logger()->info('building embed');
$this->buildEmbed();
}
if (php_uname('m') === $this->getOption('arch')) { if (php_uname('m') === $this->getOption('arch')) {
$this->sanityCheck($build_target); $this->sanityCheck($build_target);
@@ -207,7 +217,7 @@ class MacOSBuilder extends BuilderBase
{ {
$vars = SystemUtil::makeEnvVarString([ $vars = SystemUtil::makeEnvVarString([
'EXTRA_CFLAGS' => '-g -Os', // with debug information, but optimize for size 'EXTRA_CFLAGS' => '-g -Os', // with debug information, but optimize for size
'EXTRA_LIBS' => "{$this->getOption('extra-libs')} -lresolv", // link resolv library (macOS need it) 'EXTRA_LIBS' => "{$this->getOption('extra-libs')} -lresolv", // link resolv library (macOS needs it)
]); ]);
$shell = shell()->cd(SOURCE_PATH . '/php-src'); $shell = shell()->cd(SOURCE_PATH . '/php-src');
@@ -237,7 +247,7 @@ class MacOSBuilder extends BuilderBase
$vars = [ $vars = [
// with debug information, optimize for size, remove identifiers, patch fake cli for micro // with debug information, optimize for size, remove identifiers, patch fake cli for micro
'EXTRA_CFLAGS' => '-g -Os -fno-ident' . $enable_fake_cli, 'EXTRA_CFLAGS' => '-g -Os -fno-ident' . $enable_fake_cli,
// link resolv library (macOS need it) // link resolv library (macOS needs it)
'EXTRA_LIBS' => "{$this->getOption('extra-libs')} -lresolv", 'EXTRA_LIBS' => "{$this->getOption('extra-libs')} -lresolv",
]; ];
if (!$this->getOption('no-strip', false)) { if (!$this->getOption('no-strip', false)) {
@@ -260,7 +270,7 @@ class MacOSBuilder extends BuilderBase
{ {
$vars = SystemUtil::makeEnvVarString([ $vars = SystemUtil::makeEnvVarString([
'EXTRA_CFLAGS' => '-g -Os', // with debug information, but optimize for size 'EXTRA_CFLAGS' => '-g -Os', // with debug information, but optimize for size
'EXTRA_LIBS' => "{$this->getOption('extra-libs')} -lresolv", // link resolv library (macOS need it) 'EXTRA_LIBS' => "{$this->getOption('extra-libs')} -lresolv", // link resolv library (macOS needs it)
]); ]);
$shell = shell()->cd(SOURCE_PATH . '/php-src'); $shell = shell()->cd(SOURCE_PATH . '/php-src');
@@ -270,4 +280,24 @@ class MacOSBuilder extends BuilderBase
} }
$this->deployBinary(BUILD_TARGET_FPM); $this->deployBinary(BUILD_TARGET_FPM);
} }
public function buildEmbed(): void
{
$vars = SystemUtil::makeEnvVarString([
'EXTRA_CFLAGS' => '-g -Os', // with debug information, but optimize for size
'EXTRA_LIBS' => "{$this->getOption('extra-libs')} -lresolv", // link resolv library (macOS needs it)
]);
shell()
->cd(SOURCE_PATH . '/php-src')
->exec('make INSTALL_ROOT=' . BUILD_ROOT_PATH . " -j{$this->concurrency} {$vars} install")
// Workaround for https://github.com/php/php-src/issues/12082
->exec('rm -Rf ' . BUILD_ROOT_PATH . '/lib/php-o')
->exec('mkdir ' . BUILD_ROOT_PATH . '/lib/php-o')
->cd(BUILD_ROOT_PATH . '/lib/php-o')
->exec('ar x ' . BUILD_ROOT_PATH . '/lib/libphp.a')
->exec('rm ' . BUILD_ROOT_PATH . '/lib/libphp.a')
->exec('ar rcs ' . BUILD_ROOT_PATH . '/lib/libphp.a *.o')
->exec('rm -Rf ' . BUILD_ROOT_PATH . '/lib/php-o');
}
} }

View File

@@ -66,14 +66,23 @@ trait UnixBuilderTrait
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new RuntimeException('cli failed sanity check'); throw new RuntimeException('cli failed sanity check');
} }
foreach ($this->exts as $ext) { foreach ($this->exts as $ext) {
logger()->debug('testing ext: ' . $ext->getName()); logger()->debug('testing ext: ' . $ext->getName());
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "' . $ext->getDistName() . '"', false); [$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "' . $ext->getDistName() . '"', false);
if ($ret !== 0) { if ($ret !== 0) {
throw new RuntimeException('extension ' . $ext->getName() . ' failed compile check'); throw new RuntimeException('extension ' . $ext->getName() . ' failed compile check');
} }
if (file_exists(ROOT_DIR . '/src/globals/tests/' . $ext->getName() . '.php')) { if (file_exists(ROOT_DIR . '/src/globals/tests/' . $ext->getName() . '.php')) {
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php ' . ROOT_DIR . '/src/globals/tests/' . $ext->getName() . '.php'); // Trim additional content & escape special characters to allow inline usage
$test = str_replace(
['<?php', 'declare(strict_types=1);', "\n", '"', '$'],
['', '', '', '\"', '\$'],
file_get_contents(ROOT_DIR . '/src/globals/tests/' . $ext->getName() . '.php')
);
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "' . trim($test) . '"');
if ($ret !== 0) { if ($ret !== 0) {
throw new RuntimeException('extension ' . $ext->getName() . ' failed sanity check'); throw new RuntimeException('extension ' . $ext->getName() . ' failed sanity check');
} }

View File

@@ -66,7 +66,7 @@ trait UnixLibraryTrait
* @throws FileSystemException * @throws FileSystemException
* @throws RuntimeException * @throws RuntimeException
*/ */
public function patchPkgconfPrefix(array $files, int $patch_option = PKGCONF_PATCH_ALL): void public function patchPkgconfPrefix(array $files, int $patch_option = PKGCONF_PATCH_ALL, ?array $custom_replace = null): void
{ {
logger()->info('Patching library [' . static::NAME . '] pkgconfig'); logger()->info('Patching library [' . static::NAME . '] pkgconfig');
foreach ($files as $name) { foreach ($files as $name) {
@@ -81,6 +81,7 @@ trait UnixLibraryTrait
$file = ($patch_option & PKGCONF_PATCH_EXEC_PREFIX) === PKGCONF_PATCH_EXEC_PREFIX ? preg_replace('/^exec_prefix=.*$/m', 'exec_prefix=${prefix}', $file) : $file; $file = ($patch_option & PKGCONF_PATCH_EXEC_PREFIX) === PKGCONF_PATCH_EXEC_PREFIX ? preg_replace('/^exec_prefix=.*$/m', 'exec_prefix=${prefix}', $file) : $file;
$file = ($patch_option & PKGCONF_PATCH_LIBDIR) === PKGCONF_PATCH_LIBDIR ? preg_replace('/^libdir=.*$/m', 'libdir=${prefix}/lib', $file) : $file; $file = ($patch_option & PKGCONF_PATCH_LIBDIR) === PKGCONF_PATCH_LIBDIR ? preg_replace('/^libdir=.*$/m', 'libdir=${prefix}/lib', $file) : $file;
$file = ($patch_option & PKGCONF_PATCH_INCLUDEDIR) === PKGCONF_PATCH_INCLUDEDIR ? preg_replace('/^includedir=.*$/m', 'includedir=${prefix}/include', $file) : $file; $file = ($patch_option & PKGCONF_PATCH_INCLUDEDIR) === PKGCONF_PATCH_INCLUDEDIR ? preg_replace('/^includedir=.*$/m', 'includedir=${prefix}/include', $file) : $file;
$file = ($patch_option & PKGCONF_PATCH_CUSTOM) === PKGCONF_PATCH_CUSTOM && $custom_replace !== null ? preg_replace($custom_replace[0], $custom_replace[1], $file) : $file;
FileSystem::writeFile($realpath, $file); FileSystem::writeFile($realpath, $file);
} }
} }

View File

@@ -28,9 +28,9 @@ trait brotli
->exec('make install DESTDIR=' . BUILD_ROOT_PATH); ->exec('make install DESTDIR=' . BUILD_ROOT_PATH);
$this->patchPkgconfPrefix(['libbrotlicommon.pc', 'libbrotlidec.pc', 'libbrotlienc.pc']); $this->patchPkgconfPrefix(['libbrotlicommon.pc', 'libbrotlidec.pc', 'libbrotlienc.pc']);
shell()->cd(BUILD_ROOT_PATH . '/lib') shell()->cd(BUILD_ROOT_PATH . '/lib')
->exec('ln -s libbrotlicommon-static.a libbrotlicommon.a') ->exec('ln -s libbrotlicommon.a libbrotlicommon-static.a')
->exec('ln -s libbrotlidec-static.a libbrotlidec.a') ->exec('ln -s libbrotlidec.a libbrotlidec-static.a')
->exec('ln -s libbrotlienc-static.a libbrotlienc.a'); ->exec('ln -s libbrotlienc.a libbrotlienc-static.a');
foreach (FileSystem::scanDirFiles(BUILD_ROOT_PATH . '/lib/', false, true) as $filename) { foreach (FileSystem::scanDirFiles(BUILD_ROOT_PATH . '/lib/', false, true) as $filename) {
if (str_starts_with($filename, 'libbrotli') && (str_contains($filename, '.so') || str_ends_with($filename, '.dylib'))) { if (str_starts_with($filename, 'libbrotli') && (str_contains($filename, '.so') || str_ends_with($filename, '.dylib'))) {
unlink(BUILD_ROOT_PATH . '/lib/' . $filename); unlink(BUILD_ROOT_PATH . '/lib/' . $filename);

View File

@@ -29,7 +29,7 @@ trait libssh2
"-DENABLE_ZLIB_COMPRESSION={$enable_zlib} " . "-DENABLE_ZLIB_COMPRESSION={$enable_zlib} " .
'..' '..'
) )
->exec("cmake --build . -j {$this->builder->concurrency} --target libssh2") ->exec("cmake --build . -j {$this->builder->concurrency}")
->exec('make install DESTDIR=' . BUILD_ROOT_PATH); ->exec('make install DESTDIR=' . BUILD_ROOT_PATH);
$this->patchPkgconfPrefix(['libssh2.pc']); $this->patchPkgconfPrefix(['libssh2.pc']);
} }

View File

@@ -32,7 +32,8 @@ trait libwebp
->exec("cmake --build . -j {$this->builder->concurrency}") ->exec("cmake --build . -j {$this->builder->concurrency}")
->exec('make install DESTDIR=' . BUILD_ROOT_PATH); ->exec('make install DESTDIR=' . BUILD_ROOT_PATH);
// patch pkgconfig // patch pkgconfig
$this->patchPkgconfPrefix(['libsharpyuv.pc', 'libwebp.pc', 'libwebpdecoder.pc', 'libwebpdemux.pc', 'libwebpmux.pc'], PKGCONF_PATCH_PREFIX); $this->patchPkgconfPrefix(['libsharpyuv.pc', 'libwebp.pc', 'libwebpdecoder.pc', 'libwebpdemux.pc', 'libwebpmux.pc'], PKGCONF_PATCH_PREFIX | PKGCONF_PATCH_LIBDIR);
$this->patchPkgconfPrefix(['libsharpyuv.pc'], PKGCONF_PATCH_CUSTOM, ['/^includedir=.*$/m', 'includedir=${prefix}/include/webp']);
$this->cleanLaFiles(); $this->cleanLaFiles();
} }
} }

View File

@@ -25,7 +25,8 @@ class BuildCliCommand extends BuildCommand
$this->addOption('build-micro', null, null, 'build micro'); $this->addOption('build-micro', null, null, 'build micro');
$this->addOption('build-cli', null, null, 'build cli'); $this->addOption('build-cli', null, null, 'build cli');
$this->addOption('build-fpm', null, null, 'build fpm'); $this->addOption('build-fpm', null, null, 'build fpm');
$this->addOption('build-all', null, null, 'build cli, micro, fpm'); $this->addOption('build-embed', null, null, 'build embed');
$this->addOption('build-all', null, null, 'build cli, micro, fpm, embed');
$this->addOption('no-strip', null, null, 'build without strip, in order to debug and load external extensions'); $this->addOption('no-strip', null, null, 'build without strip, in order to debug and load external extensions');
$this->addOption('enable-zts', null, null, 'enable ZTS support'); $this->addOption('enable-zts', null, null, 'enable ZTS support');
$this->addOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI'); $this->addOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI');
@@ -43,13 +44,15 @@ class BuildCliCommand extends BuildCommand
$rule = $rule | ($this->getOption('build-cli') ? BUILD_TARGET_CLI : BUILD_TARGET_NONE); $rule = $rule | ($this->getOption('build-cli') ? BUILD_TARGET_CLI : BUILD_TARGET_NONE);
$rule = $rule | ($this->getOption('build-micro') ? BUILD_TARGET_MICRO : BUILD_TARGET_NONE); $rule = $rule | ($this->getOption('build-micro') ? BUILD_TARGET_MICRO : BUILD_TARGET_NONE);
$rule = $rule | ($this->getOption('build-fpm') ? BUILD_TARGET_FPM : BUILD_TARGET_NONE); $rule = $rule | ($this->getOption('build-fpm') ? BUILD_TARGET_FPM : BUILD_TARGET_NONE);
$rule = $rule | ($this->getOption('build-embed') ? BUILD_TARGET_EMBED : BUILD_TARGET_NONE);
$rule = $rule | ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE); $rule = $rule | ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE);
if ($rule === BUILD_TARGET_NONE) { if ($rule === BUILD_TARGET_NONE) {
$this->output->writeln('<error>Please add at least one build target!</error>'); $this->output->writeln('<error>Please add at least one build target!</error>');
$this->output->writeln("<comment>\t--build-cli\tBuild php-cli SAPI</comment>"); $this->output->writeln("<comment>\t--build-cli\tBuild php-cli SAPI</comment>");
$this->output->writeln("<comment>\t--build-micro\tBuild phpmicro SAPI</comment>"); $this->output->writeln("<comment>\t--build-micro\tBuild phpmicro SAPI</comment>");
$this->output->writeln("<comment>\t--build-fpm\tBuild php-fpm SAPI</comment>"); $this->output->writeln("<comment>\t--build-fpm\tBuild php-fpm SAPI</comment>");
$this->output->writeln("<comment>\t--build-all\tBuild all SAPI: cli, micro, fpm</comment>"); $this->output->writeln("<comment>\t--build-embed\tBuild embed SAPI/libphp</comment>");
$this->output->writeln("<comment>\t--build-all\tBuild all SAPI: cli, micro, fpm, embed</comment>");
return static::FAILURE; return static::FAILURE;
} }
try { try {

View File

@@ -20,6 +20,7 @@ class BuildLibsCommand extends BuildCommand
$this->addArgument('libraries', InputArgument::REQUIRED, 'The libraries will be compiled, comma separated'); $this->addArgument('libraries', InputArgument::REQUIRED, 'The libraries will be compiled, comma separated');
$this->addOption('clean', null, null, 'Clean old download cache and source before fetch'); $this->addOption('clean', null, null, 'Clean old download cache and source before fetch');
$this->addOption('all', 'A', null, 'Build all libs that static-php-cli needed'); $this->addOption('all', 'A', null, 'Build all libs that static-php-cli needed');
$this->addOption('rebuild', 'r', null, 'Delete old build and rebuild');
} }
public function initialize(InputInterface $input, OutputInterface $output): void public function initialize(InputInterface $input, OutputInterface $output): void

View File

@@ -1,166 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\store\FileSystem;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use function Laravel\Prompts\confirm;
use function Laravel\Prompts\text;
#[AsCommand('deploy', 'Deploy static-php-cli self to an .phar application')]
class DeployCommand extends BaseCommand
{
public function configure(): void
{
$this->addArgument('target', InputArgument::OPTIONAL, 'The file or directory to pack.');
$this->addOption('auto-phar-fix', null, InputOption::VALUE_NONE, 'Automatically fix ini option.');
$this->addOption('overwrite', 'W', InputOption::VALUE_NONE, 'Overwrite existing files.');
$this->addOption('with-no-dev', 'D', InputOption::VALUE_NONE, 'Automatically use non-dev composer dependencies to reduce size');
$this->addOption('with-dev', 'd', InputOption::VALUE_NONE, 'Automatically use dev composer dependencies');
}
/**
* @throws \PharException
*/
public function handle(): int
{
$composer = require ROOT_DIR . '/vendor/composer/installed.php';
if (($composer['root']['dev'] ?? false) === true) {
if (!$this->getOption('with-no-dev')) {
$this->output->writeln('<comment>Current static-php-cli dependencies have installed dev-dependencies</comment>');
$this->output->writeln('<comment>If you want to remove, you can choose "Yes" to run command "composer update --no-dev" to remove.</comment>');
$this->output->writeln('<comment>Or choose "No", just pack, deploy.</comment>');
$ask = confirm('Do you want to remove dev-dependencies to reduce size of phar file?');
} elseif (!$this->getOption('with-dev')) {
$ask = true;
} else {
$ask = false;
}
if ($ask) {
[$code] = shell()->execWithResult('composer update --no-dev');
if ($code !== 0) {
$this->output->writeln('<error>"composer update --no-dev" failed with exit code [' . $code . ']</error>');
$this->output->writeln('<error>You may need to run this command by your own.</error>');
return static::FAILURE;
}
$this->output->writeln('<info>Update successfully, you need to re-run deploy command to pack.</info>');
return static::SUCCESS;
}
}
// 首先得确认是不是关闭了readonly模式
if (ini_get('phar.readonly') == 1) {
if ($this->getOption('auto-phar-fix')) {
$ask = true;
} else {
$this->output->writeln('<comment>pack command needs "phar.readonly" = "Off" !</comment>');
$ask = confirm('Do you want to automatically set it and continue ?');
// $ask = $prompt->requireBool('<comment>pack command needs "phar.readonly" = "Off" !</comment>' . PHP_EOL . 'If you want to automatically set it and continue, just Enter', true);
}
if ($ask) {
global $argv;
$args = array_merge(['-d', 'phar.readonly=0'], $_SERVER['argv'], ['--no-motd']);
if (function_exists('pcntl_exec')) {
$this->output->writeln('<info>Changing to phar.readonly=0 mode ...</info>');
if (pcntl_exec(PHP_BINARY, $args) === false) {
throw new \PharException('Switching to read write mode failed, please check the environment.');
}
} else {
$this->output->writeln('<info>Now running command in child process.</info>');
passthru(PHP_BINARY . ' -d phar.readonly=0 ' . implode(' ', $argv), $retcode);
exit($retcode);
}
}
}
// 获取路径
$path = WORKING_DIR;
// 如果是目录,则将目录下的所有文件打包
$phar_path = text('Please input the phar target filename', default: '/tmp/static-php-cli.phar');
// $phar_path = $prompt->requireArgument('target', 'Please input the phar target filename', 'static-php-cli.phar');
if (FileSystem::isRelativePath($phar_path)) {
$phar_path = WORKING_DIR . '/' . $phar_path;
}
if (file_exists($phar_path)) {
if (!$this->getOption('overwrite')) {
$this->output->writeln('<comment>The file "' . $phar_path . '" already exists.</comment>');
$ask = confirm('Do you want to overwrite it?');
} else {
$ask = true;
}
if (!$ask) {
$this->output->writeln('<comment>User canceled.</comment>');
return static::FAILURE;
}
@unlink($phar_path);
}
$phar = new \Phar($phar_path);
$phar->startBuffering();
$all = FileSystem::scanDirFiles($path, true, true);
$all = array_filter($all, function ($x) {
$dirs = preg_match('/(^(config|src|vendor)\\/|^(composer\\.json|README\\.md|source\\.json|LICENSE|README-en\\.md)$)/', $x);
return !($dirs !== 1);
});
sort($all);
$map = [];
foreach ($all as $v) {
$map[$v] = $path . '/' . $v;
}
$this->output->writeln('<info>Start packing files...</info>');
try {
foreach ($this->progress()->iterate($map) as $file => $origin_file) {
$phar->addFromString($file, php_strip_whitespace($origin_file));
}
// $phar->buildFromIterator(new SeekableArrayIterator($map, new ProgressBar($output)));
$phar->addFromString(
'.phar-entry.php',
str_replace(
'/../vendor/autoload.php',
'/vendor/autoload.php',
file_get_contents(ROOT_DIR . '/bin/spc')
)
);
$stub = '.phar-entry.php';
$phar->setStub($phar->createDefaultStub($stub));
} catch (\Throwable $e) {
$this->output->writeln($e);
return static::FAILURE;
}
$phar->addFromString('.prod', 'true');
$phar->stopBuffering();
$this->output->writeln(PHP_EOL . 'Done! Phar file is generated at "' . $phar_path . '".');
if (file_exists(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx')) {
$this->output->writeln('Detected you have already compiled micro binary, I will make executable now for you!');
file_put_contents(
pathinfo($phar_path, PATHINFO_DIRNAME) . '/spc',
file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx') .
file_get_contents($phar_path)
);
chmod(pathinfo($phar_path, PATHINFO_DIRNAME) . '/spc', 0755);
$this->output->writeln('<info>Binary Executable: ' . pathinfo($phar_path, PATHINFO_DIRNAME) . '/spc</info>');
}
chmod($phar_path, 0755);
$this->output->writeln('<info>Phar Executable: ' . $phar_path . '</info>');
return static::SUCCESS;
}
private function progress(): ProgressBar
{
$progress = new ProgressBar($this->output, 0);
$progress->setBarCharacter('<fg=green>⚬</>');
$progress->setEmptyBarCharacter('<fg=red>⚬</>');
$progress->setProgressCharacter('<fg=green>➤</>');
$progress->setFormat(
"%current%/%max% [%bar%] %percent:3s%%\n🪅 %estimated:-20s% %memory:20s%" . PHP_EOL
);
return $progress;
}
}

View File

@@ -5,7 +5,11 @@ declare(strict_types=1);
namespace SPC\command; namespace SPC\command;
use SPC\doctor\CheckListHandler; use SPC\doctor\CheckListHandler;
use SPC\doctor\CheckResult;
use SPC\exception\RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Question\ConfirmationQuestion;
#[AsCommand('doctor', 'Diagnose whether the current environment can compile normally')] #[AsCommand('doctor', 'Diagnose whether the current environment can compile normally')]
class DoctorCommand extends BaseCommand class DoctorCommand extends BaseCommand
@@ -18,14 +22,65 @@ class DoctorCommand extends BaseCommand
public function handle(): int public function handle(): int
{ {
try { try {
$checker = new CheckListHandler($this->input, $this->output); $checker = new CheckListHandler();
$checker->runCheck($this->input->getOption('auto-fix') ? FIX_POLICY_AUTOFIX : FIX_POLICY_PROMPT);
$fix_policy = $this->input->getOption('auto-fix') ? FIX_POLICY_AUTOFIX : FIX_POLICY_PROMPT;
foreach ($checker->runChecks() as $check) {
if ($check->limit_os !== null && $check->limit_os !== PHP_OS_FAMILY) {
continue;
}
$this->output->write('Checking <comment>' . $check->item_name . '</comment> ... ');
$result = call_user_func($check->callback);
if ($result === null) {
$this->output->writeln('skipped');
} elseif ($result instanceof CheckResult) {
if ($result->isOK()) {
$this->output->writeln($result->getMessage() ?? 'ok');
continue;
}
// Failed
$this->output->writeln('<error>' . $result->getMessage() . '</error>');
switch ($fix_policy) {
case FIX_POLICY_DIE:
throw new RuntimeException('Some check items can not be fixed !');
case FIX_POLICY_PROMPT:
if ($result->getFixItem() !== '') {
$helper = new QuestionHelper();
$question = new ConfirmationQuestion('Do you want to fix it? [Y/n] ', true);
if ($helper->ask($this->input, $this->output, $question)) {
$checker->emitFix($this->output, $result);
} else {
throw new RuntimeException('You cancelled fix');
}
} else {
throw new RuntimeException('Some check items can not be fixed !');
}
break;
case FIX_POLICY_AUTOFIX:
if ($result->getFixItem() !== '') {
$this->output->writeln('Automatically fixing ' . $result->getFixItem() . ' ...');
$checker->emitFix($this->output, $result);
} else {
throw new RuntimeException('Some check items can not be fixed !');
}
break;
}
}
}
$this->output->writeln('<info>Doctor check complete !</info>'); $this->output->writeln('<info>Doctor check complete !</info>');
} catch (\Throwable $e) { } catch (\Throwable $e) {
$this->output->writeln('<error>' . $e->getMessage() . '</error>'); $this->output->writeln('<error>' . $e->getMessage() . '</error>');
pcntl_signal(SIGINT, SIG_IGN); pcntl_signal(SIGINT, SIG_IGN);
return static::FAILURE; return static::FAILURE;
} }
return static::SUCCESS; return static::SUCCESS;
} }
} }

View File

@@ -6,6 +6,7 @@ namespace SPC\command\dev;
use SPC\command\BaseCommand; use SPC\command\BaseCommand;
use SPC\exception\FileSystemException; use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException; use SPC\exception\WrongUsageException;
use SPC\store\Config; use SPC\store\Config;
use SPC\util\DependencyUtil; use SPC\util\DependencyUtil;
@@ -18,15 +19,17 @@ class AllExtCommand extends BaseCommand
{ {
public function configure(): void public function configure(): void
{ {
$this->addArgument('extensions', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, 'Extension name', null); $this->addArgument('extensions', InputArgument::OPTIONAL, 'List of extensions that will be displayed, comma separated');
} }
/** /**
* @throws FileSystemException * @throws FileSystemException
* @throws WrongUsageException
* @throws RuntimeException
*/ */
public function handle(): int public function handle(): int
{ {
$extensions = $this->input->getArgument('extensions') ?: []; $extensions = array_map('trim', array_filter(explode(',', $this->getArgument('extensions') ?? '')));
$style = new SymfonyStyle($this->input, $this->output); $style = new SymfonyStyle($this->input, $this->output);
$style->writeln($extensions ? 'Available extensions:' : 'Extensions:'); $style->writeln($extensions ? 'Available extensions:' : 'Extensions:');

View File

@@ -9,7 +9,7 @@ use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand('dev:php-ver', 'Dev command')] #[AsCommand('dev:php-version', 'Returns version of PHP located source directory', ['dev:php-ver'])]
class PhpVerCommand extends BaseCommand class PhpVerCommand extends BaseCommand
{ {
public function initialize(InputInterface $input, OutputInterface $output): void public function initialize(InputInterface $input, OutputInterface $output): void
@@ -22,11 +22,19 @@ class PhpVerCommand extends BaseCommand
{ {
// Find php from source/php-src // Find php from source/php-src
$file = SOURCE_PATH . '/php-src/main/php_version.h'; $file = SOURCE_PATH . '/php-src/main/php_version.h';
if (!file_exists($file)) {
$this->output->writeln('<error>PHP source not found, maybe you need to extract first ?</error>');
return static::FAILURE;
}
$result = preg_match('/#define PHP_VERSION "([^"]+)"/', file_get_contents($file), $match); $result = preg_match('/#define PHP_VERSION "([^"]+)"/', file_get_contents($file), $match);
if ($result === false) { if ($result === false) {
$this->output->writeln('<error>PHP source not found, maybe you need to extract first ?</error>'); $this->output->writeln('<error>PHP source not found, maybe you need to extract first ?</error>');
return static::FAILURE; return static::FAILURE;
} }
$this->output->writeln('<info>' . $match[1] . '</info>'); $this->output->writeln('<info>' . $match[1] . '</info>');
return static::SUCCESS; return static::SUCCESS;
} }

View File

@@ -15,7 +15,7 @@ use Symfony\Component\Console\Input\InputArgument;
/** /**
* Modify config file: sort lib, ext, source by name. * Modify config file: sort lib, ext, source by name.
*/ */
#[AsCommand('dev:sort-config', 'After config edited, sort it by alphabet', ['sort-config'])] #[AsCommand('dev:sort-config', 'After config edited, sort it by alphabet', ['sort-config'], true)]
class SortConfigCommand extends BaseCommand class SortConfigCommand extends BaseCommand
{ {
public function configure(): void public function configure(): void
@@ -34,7 +34,7 @@ class SortConfigCommand extends BaseCommand
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/lib.json'), true); $file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/lib.json'), true);
ConfigValidator::validateLibs($file); ConfigValidator::validateLibs($file);
ksort($file); ksort($file);
if (!file_put_contents(ROOT_DIR . '/config/lib.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) { if (!file_put_contents(ROOT_DIR . '/config/lib.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) {
$this->output->writeln('<error>Write file lib.json failed!</error>'); $this->output->writeln('<error>Write file lib.json failed!</error>');
return static::FAILURE; return static::FAILURE;
} }
@@ -43,7 +43,7 @@ class SortConfigCommand extends BaseCommand
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/source.json'), true); $file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/source.json'), true);
ConfigValidator::validateSource($file); ConfigValidator::validateSource($file);
uksort($file, fn ($a, $b) => $a === 'php-src' ? -1 : ($b === 'php-src' ? 1 : ($a < $b ? -1 : 1))); uksort($file, fn ($a, $b) => $a === 'php-src' ? -1 : ($b === 'php-src' ? 1 : ($a < $b ? -1 : 1)));
if (!file_put_contents(ROOT_DIR . '/config/source.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) { if (!file_put_contents(ROOT_DIR . '/config/source.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) {
$this->output->writeln('<error>Write file source.json failed!</error>'); $this->output->writeln('<error>Write file source.json failed!</error>');
return static::FAILURE; return static::FAILURE;
} }
@@ -52,7 +52,7 @@ class SortConfigCommand extends BaseCommand
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/ext.json'), true); $file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/ext.json'), true);
ConfigValidator::validateExts($file); ConfigValidator::validateExts($file);
ksort($file); ksort($file);
if (!file_put_contents(ROOT_DIR . '/config/ext.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE))) { if (!file_put_contents(ROOT_DIR . '/config/ext.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) {
$this->output->writeln('<error>Write file ext.json failed!</error>'); $this->output->writeln('<error>Write file ext.json failed!</error>');
return static::FAILURE; return static::FAILURE;
} }

View File

@@ -7,105 +7,62 @@ namespace SPC\doctor;
use SPC\exception\FileSystemException; use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException; use SPC\exception\RuntimeException;
use SPC\store\FileSystem; use SPC\store\FileSystem;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class CheckListHandler final class CheckListHandler
{ {
/** @var AsCheckItem[] */ /** @var AsCheckItem[] */
private array $check_list = []; private array $check_list = [];
private array $fix_map = []; private array $fix_map = [];
public function __construct() {}
/** /**
* @return array<AsCheckItem>
* @throws \ReflectionException * @throws \ReflectionException
* @throws RuntimeException
* @throws FileSystemException * @throws FileSystemException
* @throws RuntimeException
*/ */
public function __construct(private readonly InputInterface $input, private readonly OutputInterface $output, bool $include_manual = false) public function runChecks(bool $include_manual = false): array
{ {
$this->loadCheckList($include_manual); return $this->loadCheckList($include_manual);
} }
/** /**
* @throws RuntimeException * @throws RuntimeException
*/ */
public function runSingleCheck(string $item_name, int $fix_policy = FIX_POLICY_DIE): void public function emitFix(OutputInterface $output, CheckResult $result): void
{ {
foreach ($this->check_list as $item) { pcntl_signal(SIGINT, function () use ($output) {
if ($item->item_name === $item_name) { $output->writeln('<error>You cancelled fix</error>');
$this->check_list = [$item]; });
break;
}
}
$this->runCheck($fix_policy);
}
/** $fix_result = call_user_func($this->fix_map[$result->getFixItem()], ...$result->getFixParams());
* @throws RuntimeException pcntl_signal(SIGINT, SIG_IGN);
*/
public function runCheck(int $fix_policy = FIX_POLICY_DIE): void if ($fix_result) {
{ $output->writeln('<info>Fix done</info>');
foreach ($this->check_list as $item) { } else {
if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) { $output->writeln('<error>Fix failed</error>');
continue; throw new RuntimeException('Some check item are not fixed');
}
$this->output->write('Checking <comment>' . $item->item_name . '</comment> ... ');
$result = call_user_func($item->callback);
if ($result === null) {
$this->output->writeln('skipped');
} elseif ($result instanceof CheckResult) {
if ($result->isOK()) {
$this->output->writeln($result->getMessage() ?? 'ok');
continue;
}
// Failed
$this->output->writeln('<error>' . $result->getMessage() . '</error>');
switch ($fix_policy) {
case FIX_POLICY_DIE:
throw new RuntimeException('Some check items can not be fixed !');
case FIX_POLICY_PROMPT:
if ($result->getFixItem() !== '') {
$helper = new QuestionHelper();
$question = new ConfirmationQuestion('Do you want to fix it? [Y/n] ', true);
if ($helper->ask($this->input, $this->output, $question)) {
$this->emitFix($result);
} else {
throw new RuntimeException('You cancelled fix');
}
} else {
throw new RuntimeException('Some check items can not be fixed !');
}
break;
case FIX_POLICY_AUTOFIX:
if ($result->getFixItem() !== '') {
$this->output->writeln('Automatically fixing ' . $result->getFixItem() . ' ...');
$this->emitFix($result);
} else {
throw new RuntimeException('Some check items can not be fixed !');
}
break;
}
}
} }
} }
/** /**
* Load Doctor check item list * Load Doctor check item list
* *
* @return array<AsCheckItem>
* @throws \ReflectionException * @throws \ReflectionException
* @throws RuntimeException * @throws RuntimeException
* @throws FileSystemException * @throws FileSystemException
*/ */
private function loadCheckList(bool $include_manual = false): void private function loadCheckList(bool $include_manual = false): array
{ {
foreach (FileSystem::getClassesPsr4(__DIR__ . '/item', 'SPC\\doctor\\item') as $class) { foreach (FileSystem::getClassesPsr4(__DIR__ . '/item', 'SPC\\doctor\\item') as $class) {
$ref = new \ReflectionClass($class); $ref = new \ReflectionClass($class);
foreach ($ref->getMethods() as $method) { foreach ($ref->getMethods() as $method) {
$attr = $method->getAttributes(); foreach ($method->getAttributes() as $a) {
foreach ($attr as $a) {
if (is_a($a->getName(), AsCheckItem::class, true)) { if (is_a($a->getName(), AsCheckItem::class, true)) {
/** @var AsCheckItem $instance */ /** @var AsCheckItem $instance */
$instance = $a->newInstance(); $instance = $a->newInstance();
@@ -126,26 +83,10 @@ class CheckListHandler
} }
} }
} }
// sort check list by level
usort($this->check_list, fn ($a, $b) => $a->level > $b->level ? -1 : ($a->level == $b->level ? 0 : 1));
}
/** // sort check list by level
* @throws RuntimeException usort($this->check_list, fn (AsCheckItem $a, AsCheckItem $b) => $a->level > $b->level ? -1 : ($a->level == $b->level ? 0 : 1));
*/
private function emitFix(CheckResult $result): void return $this->check_list;
{
pcntl_signal(SIGINT, function () {
$this->output->writeln('<error>You cancelled fix</error>');
});
$fix = $this->fix_map[$result->getFixItem()];
$fix_result = call_user_func($fix, ...$result->getFixParams());
pcntl_signal(SIGINT, SIG_IGN);
if ($fix_result) {
$this->output->writeln('<info>Fix done</info>');
} else {
$this->output->writeln('<error>Fix failed</error>');
throw new RuntimeException('Some check item are not fixed');
}
} }
} }

View File

@@ -43,7 +43,8 @@ const BUILD_TARGET_NONE = 0; // no target
const BUILD_TARGET_CLI = 1; // build cli const BUILD_TARGET_CLI = 1; // build cli
const BUILD_TARGET_MICRO = 2; // build micro const BUILD_TARGET_MICRO = 2; // build micro
const BUILD_TARGET_FPM = 4; // build fpm const BUILD_TARGET_FPM = 4; // build fpm
const BUILD_TARGET_ALL = 7; // build all const BUILD_TARGET_EMBED = 8; // build embed
const BUILD_TARGET_ALL = 15; // build all
// doctor error fix policy // doctor error fix policy
const FIX_POLICY_DIE = 1; // die directly const FIX_POLICY_DIE = 1; // die directly
@@ -55,6 +56,7 @@ const PKGCONF_PATCH_PREFIX = 1;
const PKGCONF_PATCH_EXEC_PREFIX = 2; const PKGCONF_PATCH_EXEC_PREFIX = 2;
const PKGCONF_PATCH_LIBDIR = 4; const PKGCONF_PATCH_LIBDIR = 4;
const PKGCONF_PATCH_INCLUDEDIR = 8; const PKGCONF_PATCH_INCLUDEDIR = 8;
const PKGCONF_PATCH_ALL = 15; const PKGCONF_PATCH_CUSTOM = 16;
const PKGCONF_PATCH_ALL = 31;
ConsoleLogger::$date_format = 'H:i:s'; ConsoleLogger::$date_format = 'H:i:s';

View File

@@ -4,5 +4,5 @@ declare(strict_types=1);
assert(class_exists('\\DOMDocument')); assert(class_exists('\\DOMDocument'));
$doc = new DOMDocument(); $doc = new DOMDocument();
$doc->loadHtml("<html><head><meta charset=\"UTF-8\"><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"></head><body id='app'>Hello</body></html>"); $doc->loadHtml('<html><head><meta charset="UTF-8"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></head><body id="app">Hello</body></html>');
assert($doc->getElementById('app')->nodeValue === 'Hello'); assert($doc->getElementById('app')->nodeValue === 'Hello');

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\doctor;
use PHPUnit\Framework\TestCase;
use SPC\doctor\CheckListHandler;
/**
* @internal
*/
final class CheckListHandlerTest extends TestCase
{
public function testRunChecksReturnsListOfCheck(): void
{
$list = new CheckListHandler();
$this->assertCount(6, $list->runChecks());
}
}