feat(installer): add libphp.so installer for Linux environments

- Updated swoole/phpx dependency from v2.3.9 to v2.3.10
- Created InteractiveConsole class for terminal interactions
- Implemented LibPhpInstaller with automatic PHP embed library building
- Added LinuxPackageManager detection for dependency installation
- Integrated PhpBuildConfiguration for proper PHP compilation options
- Added documentation for libphp.so automatic building feature
- Added unit tests for the new installer components
- Integrated installer into build pipeline for Linux platforms
pull/17/head
韩天峰 1 month ago
parent b0455f143a
commit b5d5d3b259
  1. 2
      README.md
  2. 12
      composer.lock
  3. 51
      docs/LIBPHP_INSTALLER.md
  4. 66
      phpunit/src/Installer/PhpBuildConfigurationTest.php
  5. 11
      src/Build/SourcePipelineTrait.php
  6. 30
      src/Installer/InteractiveConsole.php
  7. 348
      src/Installer/LibPhpInstaller.php
  8. 97
      src/Installer/LinuxPackageManager.php
  9. 42
      src/Installer/PhpBuildConfiguration.php

@ -24,6 +24,8 @@ vendor/bin/tpc.php project.yml
bin/tpc.php project.yml
```
Linux 环境缺少 `libphp.so` 时,`tpc.php` 可以交互式下载 PHP 源码并自动构建,详见 [自动构建 libphp.so](docs/LIBPHP_INSTALLER.md)。
```shell
# Ubuntu/Debian
sudo apt install libgmp-dev libmpfr-dev libmpdec-dev

12
composer.lock generated

@ -342,16 +342,16 @@
},
{
"name": "swoole/phpx",
"version": "v2.3.9",
"version": "v2.3.10",
"source": {
"type": "git",
"url": "https://github.com/swoole/phpx.git",
"reference": "98f96dead31b36510a4d3323a2d1a1bcfcd07a5e"
"reference": "e0f948db951b2d127f7bea644466f72ba2d58d6c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swoole/phpx/zipball/98f96dead31b36510a4d3323a2d1a1bcfcd07a5e",
"reference": "98f96dead31b36510a4d3323a2d1a1bcfcd07a5e",
"url": "https://api.github.com/repos/swoole/phpx/zipball/e0f948db951b2d127f7bea644466f72ba2d58d6c",
"reference": "e0f948db951b2d127f7bea644466f72ba2d58d6c",
"shasum": ""
},
"require": {
@ -381,9 +381,9 @@
],
"support": {
"issues": "https://github.com/swoole/phpx/issues",
"source": "https://github.com/swoole/phpx/tree/v2.3.9"
"source": "https://github.com/swoole/phpx/tree/v2.3.10"
},
"time": "2026-06-23T05:59:31+00:00"
"time": "2026-06-26T11:19:07+00:00"
},
{
"name": "symfony/polyfill-ctype",

@ -0,0 +1,51 @@
# 自动构建 libphp.so
TypePHP 的可执行文件和共享库模式需要 PHP Embed SAPI 提供的 `libphp.so`。许多 Linux 发行版的 PHP 包只包含 CLI 或 FPM,因此 `tpc.php` 在找不到 `libphp.so` 时会询问是否自动构建一份私有 PHP。
该功能只在 Linux 和交互式终端中启用。扩展模式(`-m ext`)不需要 `libphp.so`,不会触发安装器;CI 等非交互环境也不会自动下载、安装软件包或执行 `sudo`
## 使用流程
正常执行编译命令即可:
```bash
vendor/bin/tpc.php project.yml
```
缺少 `libphp.so` 时,安装器会依次询问:
1. 是否自动构建 PHP Embed 库;
2. PHP 版本,默认从 PHP.net 获取最新稳定版 PHP 8.4,也可输入 PHP 8.4/8.5 的具体稳定版本;
3. 安装目录,默认为 `~/.typephp`
4. 是否通过检测到的 `apt-get`、`dnf` 或 `yum` 安装缺失的开发包。
安装器读取当前 `php-config --configure-options`,保留当前 PHP 的扩展配置,替换安装路径并加入 `--enable-embed=shared`。PHP 源码只从 PHP.net 下载,并使用官方发布信息中的 SHA-256 校验。
编译完成后主要文件如下:
```text
~/.typephp/bin/php
~/.typephp/bin/php-config
~/.typephp/lib/libphp.so
~/.typephp/lib/php.ini
~/.typephp/lib/loaded-extensions.txt
```
当前 ini 主文件和扫描目录中的配置会被合并。使用相同 PHP 主次版本时,当前 ini 中加载的共享扩展会复制到新扩展目录;跨主次版本时不会复制二进制扩展,不可用的扩展配置会被注释,避免生成的 PHP 无法启动。
安装成功后,当前 `tpc.php` 进程会自动将新目录作为 `PHP_HOME` 并继续原来的编译任务。后续也可以显式指定:
```bash
export PHP_HOME="$HOME/.typephp"
vendor/bin/tpc.php project.yml
```
再次选择同一目录和版本时,安装器会询问是否直接复用已有的 `libphp.so`,不会重复执行完整构建。
## 非交互环境
安装器不会在 CI 中自动确认权限操作。请提前准备 `libphp.so`,然后设置:
```bash
PHP_HOME=/path/to/php vendor/bin/tpc.php project.yml
```

@ -0,0 +1,66 @@
<?php
namespace TypePhp\Tests\Installer;
use PHPUnit\Framework\TestCase;
use TypePhp\Installer\LinuxPackageManager;
use TypePhp\Installer\LibPhpInstaller;
use TypePhp\Installer\PhpBuildConfiguration;
final class PhpBuildConfigurationTest extends TestCase
{
public function testDerivePreservesExtensionsAndReplacesInstallationOptions(): void
{
$options = PhpBuildConfiguration::derive(
"'--prefix=/usr' '--with-curl' '--enable-mbstring' '--with-apxs2=/usr/bin/apxs' " .
"'--with-config-file-path=/etc/php/8.4/cli' '--enable-fpm'",
'/home/test/.typephp'
);
self::assertContains('--prefix=/home/test/.typephp', $options);
self::assertContains('--enable-embed=shared', $options);
self::assertContains('--with-curl', $options);
self::assertContains('--enable-mbstring', $options);
self::assertNotContains('--with-apxs2=/usr/bin/apxs', $options);
self::assertNotContains('--enable-fpm', $options);
}
public function testDetectPackageManagerUsesSupportedPriority(): void
{
$manager = LinuxPackageManager::detect(static fn(string $command): bool => in_array($command, ['dnf', 'yum'], true));
self::assertSame('dnf', $manager?->command);
}
public function testPackagesFollowEnabledConfigureOptions(): void
{
$manager = new LinuxPackageManager('apt-get');
$packages = $manager->packagesForConfigureOptions(['--with-curl', '--enable-mbstring']);
self::assertContains('build-essential', $packages);
self::assertContains('libcurl4-openssl-dev', $packages);
self::assertContains('libonig-dev', $packages);
self::assertNotContains('libzip-dev', $packages);
}
public function testMissingPackagesFiltersInstalledPackages(): void
{
$manager = new LinuxPackageManager('apt-get');
$missing = $manager->missingPackages(
['make', 're2c'],
static fn(string $package): bool => $package === 'make'
);
self::assertSame(['re2c'], $missing);
}
public function testExtensionNamesWithoutSuffixAlsoResolveSharedObjects(): void
{
self::assertSame(
['/php/extensions/swoole', '/php/extensions/swoole.so'],
LibPhpInstaller::extensionFileCandidates('/php/extensions/swoole')
);
self::assertSame(
['/php/extensions/opcache.so'],
LibPhpInstaller::extensionFileCandidates('/php/extensions/opcache.so')
);
}
}

@ -11,6 +11,8 @@ namespace TypePhp\Build;
use TypePhp\Backend\CompilerFactory;
use TypePhp\Exception\SyntaxError;
use TypePhp\Exception\Unsupported;
use TypePhp\Installer\LibPhpInstaller;
use TypePhp\Platform\Linux;
trait SourcePipelineTrait
{
@ -59,6 +61,15 @@ trait SourcePipelineTrait
public function prepare(string $path): array
{
$files = $this->getFiles($path);
if ($this->isBuildModeEmbed() && $this->getPlatform() instanceof Linux) {
try {
(new LibPhpInstaller())->ensure($this->getPhpDir());
} catch (\Throwable $e) {
$this->error('Unable to install libphp.so: ' . $e->getMessage());
}
}
$this->validateCompilerToolchain();
// shell_exec 和 define 已通过 php::fn:: 直接调用,无需动态符号表

@ -0,0 +1,30 @@
<?php
namespace TypePhp\Installer;
final class InteractiveConsole
{
public function isInteractive(): bool
{
return defined('STDIN') && function_exists('stream_isatty') && stream_isatty(STDIN);
}
public function confirm(string $question, bool $default = true): bool
{
$suffix = $default ? ' [Y/n] ' : ' [y/N] ';
$answer = strtolower(trim($this->ask($question . $suffix, '')));
return $answer === '' ? $default : in_array($answer, ['y', 'yes'], true);
}
public function ask(string $question, string $default): string
{
fwrite(STDERR, $question);
$value = trim((string) fgets(STDIN));
return $value === '' ? $default : $value;
}
public function write(string $message): void
{
fwrite(STDERR, $message . PHP_EOL);
}
}

@ -0,0 +1,348 @@
<?php
namespace TypePhp\Installer;
final class LibPhpInstaller
{
private const string RELEASE_API = 'https://www.php.net/releases/index.php?json=1&version=%s&max=100';
private ?string $sourcePhpDir = null;
public function __construct(private readonly InteractiveConsole $console = new InteractiveConsole())
{
}
public function ensure(string $currentPhpDir): ?string
{
$this->sourcePhpDir = rtrim($currentPhpDir, '/');
if (PHP_OS_FAMILY !== 'Linux' || $this->hasLibPhp($currentPhpDir)) {
return $currentPhpDir;
}
if (!$this->console->isInteractive()) {
$this->console->write('libphp.so is missing. Run tpc.php in an interactive terminal to build it automatically, or set PHP_HOME.');
return null;
}
$this->console->write("The current PHP installation does not provide libphp.so: {$currentPhpDir}");
if (!$this->console->confirm('Build a private PHP embed library now?', true)) {
return null;
}
$release = $this->latestRelease();
$version = $this->console->ask("PHP version [{$release['version']}]: ", $release['version']);
if (!preg_match('/^8\.[45]\.\d+$/', $version)) {
throw new \RuntimeException('Only stable PHP 8.4.x and 8.5.x versions are supported by the automatic installer');
}
if ($version !== $release['version']) {
$release = $this->release($version);
}
$home = getenv('HOME') ?: (string) ($_SERVER['HOME'] ?? '');
$defaultPrefix = rtrim($home, '/') . '/.typephp';
$prefix = $this->expandHome($this->console->ask("Install directory [{$defaultPrefix}]: ", $defaultPrefix), $home);
if ($this->hasLibPhp($prefix) && $this->installedVersion($prefix) === $version
&& $this->console->confirm("PHP {$version} with libphp.so already exists in {$prefix}; use it?", true)) {
putenv('PHP_HOME=' . $prefix);
$_ENV['PHP_HOME'] = $prefix;
return $prefix;
}
$this->install($release, $prefix);
putenv('PHP_HOME=' . $prefix);
$_ENV['PHP_HOME'] = $prefix;
return $prefix;
}
public function hasLibPhp(string $prefix): bool
{
return is_file($prefix . '/lib/libphp.so') || is_file($prefix . '/lib/libphp.a');
}
public function installedVersion(string $prefix): ?string
{
$phpConfig = $prefix . '/bin/php-config';
if (!is_executable($phpConfig)) {
return null;
}
$version = trim((string) shell_exec(escapeshellarg($phpConfig) . ' --version 2>/dev/null'));
return preg_match('/^\d+\.\d+\.\d+/', $version, $match) ? $match[0] : null;
}
/** @return array{version:string,filename:string,sha256:string,url:string} */
public function latestRelease(): array
{
$releases = $this->fetchReleaseList('8.4');
uksort($releases, static fn(string $a, string $b): int => version_compare($b, $a));
foreach ($releases as $version => $info) {
if (preg_match('/^8\.4\.\d+$/', $version)) {
return $this->normalizeRelease($version, $info);
}
}
throw new \RuntimeException('PHP.net did not return a stable PHP 8.4 release');
}
/** @return array{version:string,filename:string,sha256:string,url:string} */
public function release(string $version): array
{
$branch = implode('.', array_slice(explode('.', $version), 0, 2));
$releases = $this->fetchReleaseList($branch);
if (!isset($releases[$version])) {
throw new \RuntimeException("PHP {$version} was not found in the official release list");
}
return $this->normalizeRelease($version, $releases[$version]);
}
private function fetchReleaseList(string $branch): array
{
$json = $this->downloadText(sprintf(self::RELEASE_API, rawurlencode($branch)));
$data = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($data)) {
throw new \RuntimeException('Invalid release list returned by PHP.net');
}
return $data;
}
private function normalizeRelease(string $version, array $info): array
{
foreach ($info['source'] ?? [] as $source) {
$filename = (string) ($source['filename'] ?? '');
if (str_ends_with($filename, '.tar.xz') && !empty($source['sha256'])) {
return [
'version' => $version,
'filename' => $filename,
'sha256' => (string) $source['sha256'],
'url' => 'https://www.php.net/distributions/' . rawurlencode($filename),
];
}
}
throw new \RuntimeException("PHP {$version} has no verified tar.xz source archive");
}
private function install(array $release, string $prefix): void
{
$workDir = $prefix . '/var/build';
$archive = $workDir . '/' . $release['filename'];
$sourceDir = $workDir . '/php-' . $release['version'];
$this->mkdir($workDir);
$this->mkdir($prefix . '/lib/conf.d');
$configureOptions = $this->currentConfigureOptions();
$options = PhpBuildConfiguration::derive($configureOptions, $prefix);
$manager = LinuxPackageManager::detect();
if ($manager !== null) {
$packages = $manager->missingPackages($manager->packagesForConfigureOptions($options));
$this->console->write('Detected package manager: ' . $manager->command);
if ($packages !== [] && $this->console->confirm('Install missing development packages (' . implode(', ', $packages) . ')?', true)) {
$useSudo = function_exists('posix_geteuid') && posix_geteuid() !== 0;
$refresh = $manager->refreshCommand($useSudo);
if ($refresh !== null) {
$this->run($refresh);
}
$this->run($manager->installCommand($packages, $useSudo));
} elseif ($packages === []) {
$this->console->write('All detected build dependencies are already installed.');
}
} else {
$this->console->write('No supported package manager (apt-get/dnf/yum) was found; continuing with existing libraries.');
}
if (!is_file($archive) || hash_file('sha256', $archive) !== $release['sha256']) {
$this->console->write('Downloading ' . $release['url']);
$this->downloadFile($release['url'], $archive);
}
if (hash_file('sha256', $archive) !== $release['sha256']) {
throw new \RuntimeException('PHP source archive SHA-256 verification failed');
}
if (!is_dir($sourceDir)) {
$this->run(['tar', '-xJf', $archive, '-C', $workDir]);
}
$this->console->write('Configuring PHP with the current installation options plus --enable-embed=shared');
$this->run([$sourceDir . '/configure', ...$options], $sourceDir);
// PHP is a large build; capping parallelism avoids exhausting memory on
// hosts that expose many CPUs (especially containers and CI runners).
$jobs = min(8, max(1, $this->cpuCount()));
$this->run(['make', '-j' . $jobs], $sourceDir);
$this->run(['make', 'install'], $sourceDir);
if (!$this->hasLibPhp($prefix)) {
throw new \RuntimeException("Build completed but {$prefix}/lib/libphp.so was not created");
}
$this->writePhpIni($prefix, $sourceDir, $release['version']);
$this->console->write("libphp.so installed successfully in {$prefix}/lib");
}
private function currentConfigureOptions(): string
{
$phpConfig = $this->sourcePhpDir !== null && is_executable($this->sourcePhpDir . '/bin/php-config')
? $this->sourcePhpDir . '/bin/php-config'
: trim((string) shell_exec('command -v php-config 2>/dev/null'));
if ($phpConfig !== '') {
return trim($this->capture([$phpConfig, '--configure-options']));
}
$info = $this->capture([PHP_BINARY, '-n', '-i']);
if (preg_match('/^Configure Command =>\s*(.+)$/mi', $info, $match)) {
$words = PhpBuildConfiguration::parseShellWords(trim($match[1]));
if (($words[0] ?? null) === './configure') {
array_shift($words);
}
return implode(' ', array_map('escapeshellarg', $words));
}
throw new \RuntimeException('Unable to determine the current PHP configure options from php-config or php -i');
}
private function writePhpIni(string $prefix, string $sourceDir, string $version): void
{
$chunks = [];
$loaded = php_ini_loaded_file();
if (is_string($loaded) && is_file($loaded)) {
$chunks[] = file_get_contents($loaded);
} elseif (is_file($sourceDir . '/php.ini-development')) {
$chunks[] = file_get_contents($sourceDir . '/php.ini-development');
}
$scanned = php_ini_scanned_files();
if (is_string($scanned) && trim($scanned) !== '') {
foreach (preg_split('/,\s*/', trim($scanned)) as $file) {
if (is_file($file)) {
$chunks[] = PHP_EOL . '; imported from ' . $file . PHP_EOL . file_get_contents($file);
}
}
}
$ini = implode(PHP_EOL, $chunks);
$extensionDir = trim($this->capture([$prefix . '/bin/php-config', '--extension-dir']));
$branch = implode('.', array_slice(explode('.', $version), 0, 2));
if ($branch === PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION) {
$this->copyConfiguredExtensions($prefix, $extensionDir, $ini);
} else {
$this->console->write('Shared extensions cannot be copied across PHP minor versions; only extensions built from php-src will be enabled.');
}
$ini = $this->disableUnavailableExtensions($ini, $extensionDir);
if (preg_match('/^\s*extension_dir\s*=.*$/mi', $ini)) {
$ini = preg_replace('/^\s*extension_dir\s*=.*$/mi', 'extension_dir=' . $extensionDir, $ini, 1);
} else {
$ini .= PHP_EOL . 'extension_dir=' . $extensionDir . PHP_EOL;
}
file_put_contents($prefix . '/lib/php.ini', $ini);
}
private function disableUnavailableExtensions(string $ini, string $extensionDir): string
{
return preg_replace_callback(
'/^(\s*(?:zend_)?extension\s*=\s*["\']?)([^"\'\s;]+)(["\']?.*)$/mi',
static function (array $match) use ($extensionDir): string {
$module = basename($match[2]);
$basePath = rtrim($extensionDir, '/') . '/' . $module;
return self::firstExistingFile(self::extensionFileCandidates($basePath)) !== null
? $match[1] . $module . $match[3]
: '; disabled by TypePHP (module was not built): ' . $match[0];
},
$ini
);
}
private function copyConfiguredExtensions(string $prefix, string $targetDirectory, string $ini): void
{
$this->mkdir($targetDirectory);
$manifest = [];
preg_match_all('/^\s*(?:zend_)?extension\s*=\s*["\']?([^"\'\s;]+)["\']?/mi', $ini, $matches);
$currentExtensionDir = (string) ini_get('extension_dir');
foreach (array_unique($matches[1]) as $configuredPath) {
$sourceBase = str_starts_with($configuredPath, '/')
? $configuredPath
: rtrim($currentExtensionDir, '/') . '/' . $configuredPath;
$source = self::firstExistingFile(self::extensionFileCandidates($sourceBase));
if ($source === null) {
continue;
}
$target = $targetDirectory . '/' . basename($source);
if (!is_file($target) && !copy($source, $target)) {
throw new \RuntimeException("Unable to copy loaded extension {$source}");
}
$manifest[] = $configuredPath . '=' . basename($source);
}
file_put_contents($prefix . '/lib/loaded-extensions.txt', implode(PHP_EOL, $manifest) . PHP_EOL);
}
public static function extensionFileCandidates(string $path): array
{
return str_ends_with($path, '.so') ? [$path] : [$path, $path . '.so'];
}
private static function firstExistingFile(array $paths): ?string
{
foreach ($paths as $path) {
if (is_file($path)) {
return $path;
}
}
return null;
}
private function downloadText(string $url): string
{
$context = stream_context_create(['http' => ['timeout' => 30, 'user_agent' => 'TypePHP/tpc']]);
$data = @file_get_contents($url, false, $context);
if ($data === false) {
$curl = trim((string) shell_exec('command -v curl 2>/dev/null'));
if ($curl !== '') {
return $this->capture([$curl, '--fail', '--location', '--retry', '3', $url]);
}
throw new \RuntimeException("Unable to download {$url}; enable allow_url_fopen or install curl");
}
return $data;
}
private function downloadFile(string $url, string $target): void
{
$curl = trim((string) shell_exec('command -v curl 2>/dev/null'));
if ($curl !== '') {
$this->run([$curl, '--fail', '--location', '--retry', '3', '--output', $target, $url]);
return;
}
$data = $this->downloadText($url);
if (file_put_contents($target, $data) === false) {
throw new \RuntimeException("Unable to write {$target}");
}
}
private function run(array $command, ?string $cwd = null): void
{
$this->console->write('$ ' . implode(' ', array_map('escapeshellarg', $command)));
$process = proc_open($command, [STDIN, STDOUT, STDERR], $pipes, $cwd);
if (!is_resource($process) || proc_close($process) !== 0) {
throw new \RuntimeException('Command failed: ' . implode(' ', $command));
}
}
private function capture(array $command): string
{
$process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
if (!is_resource($process)) {
throw new \RuntimeException('Unable to run command: ' . implode(' ', $command));
}
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
if (proc_close($process) !== 0) {
throw new \RuntimeException(trim($stderr));
}
return $stdout;
}
private function mkdir(string $directory): void
{
if (!is_dir($directory) && !mkdir($directory, 0755, true) && !is_dir($directory)) {
throw new \RuntimeException("Unable to create directory {$directory}");
}
}
private function expandHome(string $path, string $home): string
{
return str_starts_with($path, '~/') ? rtrim($home, '/') . substr($path, 1) : rtrim($path, '/');
}
private function cpuCount(): int
{
$count = (int) trim((string) shell_exec('getconf _NPROCESSORS_ONLN 2>/dev/null'));
return $count > 0 ? $count : 1;
}
}

@ -0,0 +1,97 @@
<?php
namespace TypePhp\Installer;
final readonly class LinuxPackageManager
{
private const array COMMANDS = ['apt-get', 'dnf', 'yum'];
public function __construct(public string $command)
{
if (!in_array($command, self::COMMANDS, true)) {
throw new \InvalidArgumentException("Unsupported package manager: {$command}");
}
}
public static function detect(?callable $commandExists = null): ?self
{
$commandExists ??= static fn(string $command): bool => trim((string) shell_exec('command -v ' . escapeshellarg($command) . ' 2>/dev/null')) !== '';
foreach (self::COMMANDS as $command) {
if ($commandExists($command)) {
return new self($command);
}
}
return null;
}
public function packagesForConfigureOptions(array $options): array
{
$text = implode(' ', $options);
$groups = [
'base' => [
'apt-get' => ['build-essential', 'pkg-config', 'autoconf', 'bison', 're2c', 'libxml2-dev', 'libsqlite3-dev'],
'dnf' => ['gcc', 'gcc-c++', 'make', 'pkgconf-pkg-config', 'autoconf', 'bison', 're2c', 'libxml2-devel', 'sqlite-devel'],
'yum' => ['gcc', 'gcc-c++', 'make', 'pkgconfig', 'autoconf', 'bison', 're2c', 'libxml2-devel', 'sqlite-devel'],
],
'curl' => ['needle' => '--with-curl', 'apt-get' => ['libcurl4-openssl-dev'], 'dnf' => ['libcurl-devel'], 'yum' => ['libcurl-devel']],
'openssl' => ['needle' => '--with-openssl', 'apt-get' => ['libssl-dev'], 'dnf' => ['openssl-devel'], 'yum' => ['openssl-devel']],
'zlib' => ['needle' => '--with-zlib', 'apt-get' => ['zlib1g-dev'], 'dnf' => ['zlib-devel'], 'yum' => ['zlib-devel']],
'bz2' => ['needle' => '--with-bz2', 'apt-get' => ['libbz2-dev'], 'dnf' => ['bzip2-devel'], 'yum' => ['bzip2-devel']],
'mbstring' => ['needle' => '--enable-mbstring', 'apt-get' => ['libonig-dev'], 'dnf' => ['oniguruma-devel'], 'yum' => ['oniguruma-devel']],
'zip' => ['needle' => '--with-zip', 'apt-get' => ['libzip-dev'], 'dnf' => ['libzip-devel'], 'yum' => ['libzip-devel']],
'readline' => ['needle' => '--with-readline', 'apt-get' => ['libreadline-dev'], 'dnf' => ['readline-devel'], 'yum' => ['readline-devel']],
'libedit' => ['needle' => '--with-libedit', 'apt-get' => ['libedit-dev'], 'dnf' => ['libedit-devel'], 'yum' => ['libedit-devel']],
'icu' => ['needle' => '--enable-intl', 'apt-get' => ['libicu-dev'], 'dnf' => ['libicu-devel'], 'yum' => ['libicu-devel']],
'xslt' => ['needle' => '--with-xsl', 'apt-get' => ['libxslt1-dev'], 'dnf' => ['libxslt-devel'], 'yum' => ['libxslt-devel']],
'gmp' => ['needle' => '--with-gmp', 'apt-get' => ['libgmp-dev'], 'dnf' => ['gmp-devel'], 'yum' => ['gmp-devel']],
'sodium' => ['needle' => '--with-sodium', 'apt-get' => ['libsodium-dev'], 'dnf' => ['libsodium-devel'], 'yum' => ['libsodium-devel']],
'ffi' => ['needle' => '--with-ffi', 'apt-get' => ['libffi-dev'], 'dnf' => ['libffi-devel'], 'yum' => ['libffi-devel']],
'pgsql' => ['needle' => '--with-pgsql', 'apt-get' => ['libpq-dev'], 'dnf' => ['libpq-devel'], 'yum' => ['libpq-devel']],
'pdo_pgsql' => ['needle' => '--with-pdo-pgsql', 'apt-get' => ['libpq-dev'], 'dnf' => ['libpq-devel'], 'yum' => ['libpq-devel']],
'ldap' => ['needle' => '--with-ldap', 'apt-get' => ['libldap2-dev'], 'dnf' => ['openldap-devel'], 'yum' => ['openldap-devel']],
'gd' => ['needle' => '--enable-gd', 'apt-get' => ['libpng-dev', 'libjpeg-dev', 'libwebp-dev', 'libfreetype6-dev'], 'dnf' => ['libpng-devel', 'libjpeg-turbo-devel', 'libwebp-devel', 'freetype-devel'], 'yum' => ['libpng-devel', 'libjpeg-turbo-devel', 'libwebp-devel', 'freetype-devel']],
];
$packages = $groups['base'][$this->command];
unset($groups['base']);
foreach ($groups as $group) {
if (str_contains($text, $group['needle'])) {
array_push($packages, ...$group[$this->command]);
}
}
return array_values(array_unique($packages));
}
public function installCommand(array $packages, bool $useSudo): array
{
$prefix = $useSudo ? ['sudo'] : [];
$args = match ($this->command) {
'apt-get' => ['apt-get', 'install', '-y'],
'dnf' => ['dnf', 'install', '-y'],
'yum' => ['yum', 'install', '-y'],
};
return [...$prefix, ...$args, ...$packages];
}
public function refreshCommand(bool $useSudo): ?array
{
if ($this->command !== 'apt-get') {
return null;
}
return [...($useSudo ? ['sudo'] : []), 'apt-get', 'update'];
}
public function missingPackages(array $packages, ?callable $isInstalled = null): array
{
$isInstalled ??= function (string $package): bool {
$command = $this->command === 'apt-get'
? "dpkg-query -W -f='\${Status}' " . escapeshellarg($package) . ' 2>/dev/null'
: 'rpm -q ' . escapeshellarg($package) . ' 2>/dev/null';
$output = trim((string) shell_exec($command));
return $this->command === 'apt-get'
? str_contains($output, 'install ok installed')
: $output !== '';
};
return array_values(array_filter($packages, static fn(string $package): bool => !$isInstalled($package)));
}
}

@ -0,0 +1,42 @@
<?php
namespace TypePhp\Installer;
final class PhpBuildConfiguration
{
public static function parseShellWords(string $value): array
{
preg_match_all('/(?:[^\s"\']+|"[^"]*"|\'[^\']*\')+/', $value, $matches);
return array_map(static function (string $word): string {
if (strlen($word) >= 2 && (($word[0] === "'" && $word[-1] === "'") || ($word[0] === '"' && $word[-1] === '"'))) {
return substr($word, 1, -1);
}
return $word;
}, $matches[0]);
}
public static function derive(string $configureOptions, string $prefix): array
{
$replace = [
'--prefix', '--with-config-file-path', '--with-config-file-scan-dir',
'--enable-embed', '--enable-cli', '--disable-cli', '--with-libdir',
];
$drop = ['--with-apxs', '--with-apxs2', '--enable-fpm', '--with-fpm-systemd'];
$result = [];
foreach (self::parseShellWords($configureOptions) as $option) {
$name = explode('=', $option, 2)[0];
if (in_array($name, $replace, true) || in_array($name, $drop, true)) {
continue;
}
$result[] = $option;
}
return [
'--prefix=' . $prefix,
'--with-config-file-path=' . $prefix . '/lib',
'--with-config-file-scan-dir=' . $prefix . '/lib/conf.d',
'--enable-embed=shared',
'--enable-cli',
...$result,
];
}
}
Loading…
Cancel
Save