static-php-cli/src/SPC/builder/Extension.php

586 lines
21 KiB
PHP
Raw Normal View History

2023-03-18 17:32:21 +08:00
<?php
declare(strict_types=1);
namespace SPC\builder;
use SPC\exception\EnvironmentException;
use SPC\exception\SPCException;
use SPC\exception\ValidationException;
2023-03-29 21:39:36 +08:00
use SPC\exception\WrongUsageException;
2023-03-18 17:32:21 +08:00
use SPC\store\Config;
2024-01-10 21:08:25 +08:00
use SPC\store\FileSystem;
2025-07-24 21:57:56 +07:00
use SPC\toolchain\ToolchainManager;
use SPC\toolchain\ZigToolchain;
use SPC\util\SPCConfigUtil;
2025-07-22 13:06:48 +07:00
use SPC\util\SPCTarget;
2023-03-18 17:32:21 +08:00
class Extension
{
protected array $dependencies = [];
protected bool $build_shared = false;
protected bool $build_static = false;
protected string $source_dir;
2023-03-18 17:32:21 +08:00
public function __construct(protected string $name, protected BuilderBase $builder)
{
$ext_type = Config::getExt($this->name, 'type');
$unix_only = Config::getExt($this->name, 'unix-only', false);
$windows_only = Config::getExt($this->name, 'windows-only', false);
if (PHP_OS_FAMILY !== 'Windows' && $windows_only) {
throw new EnvironmentException("{$ext_type} extension {$name} is not supported on Linux and macOS platform");
2023-03-18 17:32:21 +08:00
}
if (PHP_OS_FAMILY === 'Windows' && $unix_only) {
throw new EnvironmentException("{$ext_type} extension {$name} is not supported on Windows platform");
2023-03-18 17:32:21 +08:00
}
// set source_dir for builtin
if ($ext_type === 'builtin') {
$this->source_dir = SOURCE_PATH . '/php-src/ext/' . $this->name;
} elseif ($ext_type === 'external') {
$source = Config::getExt($this->name, 'source');
if ($source === null) {
throw new ValidationException("{$ext_type} extension {$name} source not found", validation_module: "Extension [{$name}] loader");
}
$source_path = Config::getSource($source)['path'] ?? null;
$source_path = $source_path === null ? SOURCE_PATH . '/' . $source : SOURCE_PATH . '/' . $source_path;
$this->source_dir = $source_path;
} else {
$this->source_dir = SOURCE_PATH . '/php-src';
}
2023-03-18 17:32:21 +08:00
}
2025-06-12 01:16:57 +08:00
public function getFrameworks(): array
{
return Config::getExt($this->getName(), 'frameworks', []);
}
2023-03-18 17:32:21 +08:00
/**
* 获取开启该扩展的 PHP 编译添加的参数
*/
public function getConfigureArg(bool $shared = false): string
2023-03-18 17:32:21 +08:00
{
return match (PHP_OS_FAMILY) {
'Windows' => $this->getWindowsConfigureArg($shared),
'Darwin',
'Linux',
'BSD' => $this->getUnixConfigureArg($shared),
default => throw new WrongUsageException(PHP_OS_FAMILY . ' build is not supported yet'),
};
2023-03-18 17:32:21 +08:00
}
/**
* 根据 ext arg-type 获取对应开启的参数,一般都是 --enable-xxx --with-xxx
*/
public function getEnableArg(bool $shared = false): string
2023-03-18 17:32:21 +08:00
{
2025-07-22 19:03:16 +07:00
$escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH;
2023-03-18 17:32:21 +08:00
$_name = str_replace('_', '-', $this->name);
return match ($arg_type = Config::getExt($this->name, 'arg-type', 'enable')) {
2025-05-21 14:10:56 +07:00
'enable' => '--enable-' . $_name . ($shared ? '=shared' : '') . ' ',
2025-07-22 18:38:17 +07:00
'enable-path' => '--enable-' . $_name . '=' . ($shared ? 'shared,' : '') . $escapedPath . ' ',
'with' => '--with-' . $_name . ($shared ? '=shared' : '') . ' ',
2025-07-22 18:38:17 +07:00
'with-path' => '--with-' . $_name . '=' . ($shared ? 'shared,' : '') . $escapedPath . ' ',
2023-03-18 17:32:21 +08:00
'none', 'custom' => '',
default => throw new WrongUsageException("argType does not accept {$arg_type}, use [enable/with/with-path] ."),
2023-03-18 17:32:21 +08:00
};
}
/**
* 导出当前扩展依赖的所有 lib 库生成的 .a 静态编译库文件,以字符串形式导出,用空格分割
*/
public function getLibFilesString(): string
{
$ret = array_map(
fn ($x) => $x->getStaticLibFiles(),
$this->getLibraryDependencies(recursive: true)
);
return implode(' ', $ret);
}
/**
* 检查下依赖就行了,作用是导入依赖给 Extension 对象,今后可以对库依赖进行选择性处理
*/
public function checkDependency(): static
{
foreach (Config::getExt($this->name, 'lib-depends', []) as $name) {
$this->addLibraryDependency($name);
}
foreach (Config::getExt($this->name, 'lib-suggests', []) as $name) {
$this->addLibraryDependency($name, true);
}
foreach (Config::getExt($this->name, 'ext-depends', []) as $name) {
$this->addExtensionDependency($name);
}
foreach (Config::getExt($this->name, 'ext-suggests', []) as $name) {
$this->addExtensionDependency($name, true);
}
return $this;
}
public function getExtensionDependency(): array
{
return array_filter($this->dependencies, fn ($x) => $x instanceof Extension);
}
public function getName(): string
{
return $this->name;
}
2023-04-08 11:49:06 +08:00
/**
* returns extension dist name
*/
public function getDistName(): string
{
2023-04-15 18:45:11 +08:00
return $this->name;
}
public function getWindowsConfigureArg(bool $shared = false): string
2023-04-15 18:45:11 +08:00
{
return $this->getEnableArg();
2023-04-15 18:45:11 +08:00
// Windows is not supported yet
}
public function getUnixConfigureArg(bool $shared = false): string
2023-04-15 18:45:11 +08:00
{
return $this->getEnableArg($shared);
2023-04-08 11:49:06 +08:00
}
/**
* Patch code before ./buildconf
2025-03-11 07:44:31 +01:00
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
public function patchBeforeBuildconf(): bool
{
return false;
}
/**
* Patch code before ./configure
2025-03-11 07:44:31 +01:00
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
public function patchBeforeConfigure(): bool
{
return false;
}
/**
* Patch code before ./configure.bat for Windows
*/
public function patchBeforeWindowsConfigure(): bool
{
return false;
}
/**
* Patch code before make
2025-03-11 07:44:31 +01:00
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
public function patchBeforeMake(): bool
{
if (SPCTarget::getTargetOS() === 'Linux' && $this->isBuildShared() && ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS'))) {
FileSystem::replaceFileRegex(
2025-07-19 15:12:15 +07:00
SOURCE_PATH . '/php-src/Makefile',
"/^(shared_objects_{$this->getName()}\\s*=.*)$/m",
"$1 {$objs}",
);
return true;
}
return false;
}
2025-05-18 08:57:07 +07:00
/**
2025-05-20 20:00:37 +07:00
* Patch code before shared extension phpize
2025-05-18 08:57:07 +07:00
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
2025-06-20 17:11:52 +07:00
public function patchBeforeSharedPhpize(): bool
2025-05-18 08:57:07 +07:00
{
return false;
}
2025-05-20 20:00:37 +07:00
/**
* Patch code before shared extension ./configure
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
public function patchBeforeSharedConfigure(): bool
{
return false;
}
2025-06-20 15:25:07 +07:00
/**
* Patch code before shared extension make
* If you need to patch some code, overwrite this
* return true if you patched something, false if not
*/
public function patchBeforeSharedMake(): bool
{
2025-07-25 11:12:49 +07:00
$config = (new SPCConfigUtil($this->builder))->config([$this->getName()], array_map(fn ($l) => $l->getName(), $this->builder->getLibs()));
2025-07-25 16:18:04 +07:00
[$staticLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
2025-07-25 11:02:01 +07:00
FileSystem::replaceFileRegex(
$this->source_dir . '/Makefile',
'/^(.*_SHARED_LIBADD\s*=.*)$/m',
'$1 ' . trim($staticLibs)
);
2025-07-22 13:06:48 +07:00
if ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS')) {
FileSystem::replaceFileRegex(
$this->source_dir . '/Makefile',
"/^(shared_objects_{$this->getName()}\\s*=.*)$/m",
"$1 {$objs}",
);
}
2025-07-25 11:02:01 +07:00
return true;
2025-06-20 15:25:07 +07:00
}
2025-05-25 10:52:46 +07:00
/**
* @return string
2025-05-25 11:00:38 +07:00
* returns a command line string with all required shared extensions to load
* i.e.; pdo_pgsql would return:
2025-05-25 10:52:46 +07:00
*
* `-d "extension=pgsql" -d "extension=pdo_pgsql"`
*/
2025-05-25 11:07:44 +07:00
public function getSharedExtensionLoadString(): string
2025-05-21 17:57:53 +07:00
{
$loaded = [];
$order = [];
$resolve = function ($extension) use (&$resolve, &$loaded, &$order) {
2025-08-25 14:55:30 +07:00
if (!$extension instanceof Extension) {
return;
}
2025-05-21 17:57:53 +07:00
if (isset($loaded[$extension->getName()])) {
return;
}
$loaded[$extension->getName()] = true;
2025-08-25 14:55:30 +07:00
foreach ($extension->dependencies as $dependency) {
2025-05-21 17:57:53 +07:00
$resolve($dependency);
}
$order[] = $extension;
};
$resolve($this);
$ret = '';
foreach ($order as $ext) {
2025-07-18 11:53:36 +07:00
if ($ext instanceof self && $ext->isBuildShared()) {
2025-08-25 12:57:49 +07:00
if (Config::getExt($ext->getName(), 'type', false) === 'addon') {
continue;
}
2025-06-06 23:49:58 +07:00
if (Config::getExt($ext->getName(), 'zend-extension', false) === true) {
$ret .= " -d \"zend_extension={$ext->getName()}\"";
2025-05-21 18:35:48 +07:00
} else {
$ret .= " -d \"extension={$ext->getName()}\"";
2025-05-21 18:35:48 +07:00
}
2025-05-21 17:57:53 +07:00
}
}
if ($ret !== '') {
$ret = ' -d "extension_dir=' . BUILD_MODULES_PATH . '"' . $ret;
}
2025-05-21 17:57:53 +07:00
return $ret;
}
2024-01-10 21:08:25 +08:00
public function runCliCheckUnix(): void
2023-11-01 01:46:21 +08:00
{
2024-01-10 21:08:25 +08:00
// Run compile check if build target is cli
// If you need to run some check, overwrite this or add your assert in src/globals/ext-tests/{extension_name}.php
2025-05-25 11:07:44 +07:00
$sharedExtensions = $this->getSharedExtensionLoadString();
2025-06-17 14:01:53 +07:00
[$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"');
2023-11-01 01:46:21 +08:00
if ($ret !== 0) {
throw new ValidationException(
"extension {$this->getName()} failed compile check: php-cli returned {$ret}",
validation_module: 'Extension ' . $this->getName() . ' sanity check'
2025-06-17 14:01:53 +07:00
);
2023-11-01 01:46:21 +08:00
}
if (file_exists(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php')) {
2023-11-01 01:46:21 +08:00
// 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/ext-tests/' . $this->getName() . '.php')
2023-11-01 01:46:21 +08:00
);
2025-05-21 18:35:48 +07:00
[$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' -r "' . trim($test) . '"');
2024-01-10 21:08:25 +08:00
if ($ret !== 0) {
throw new ValidationException(
"extension {$this->getName()} failed sanity check. Code: {$ret}, output: " . implode("\n", $out),
validation_module: 'Extension ' . $this->getName() . ' function check'
);
2024-01-10 21:08:25 +08:00
}
}
}
public function runCliCheckWindows(): void
{
// Run compile check if build target is cli
// If you need to run some check, overwrite this or add your assert in src/globals/ext-tests/{extension_name}.php
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -n --ri "' . $this->getDistName() . '"', false);
2024-01-10 21:08:25 +08:00
if ($ret !== 0) {
throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: "Extension {$this->getName()} sanity check");
2024-01-10 21:08:25 +08:00
}
if (file_exists(FileSystem::convertPath(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php'))) {
2024-01-10 21:08:25 +08:00
// Trim additional content & escape special characters to allow inline usage
$test = str_replace(
['<?php', 'declare(strict_types=1);', "\n", '"', '$'],
['', '', '', '\"', '$'],
file_get_contents(FileSystem::convertPath(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php'))
2024-01-10 21:08:25 +08:00
);
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -n -r "' . trim($test) . '"');
2023-11-01 01:46:21 +08:00
if ($ret !== 0) {
throw new ValidationException(
"extension {$this->getName()} failed function check",
validation_module: "Extension {$this->getName()} function check"
);
2023-11-01 01:46:21 +08:00
}
}
}
public function validate(): void
{
// do nothing, just throw wrong usage exception if not valid
}
/**
* Build shared extension
*/
public function buildShared(): void
{
2025-08-25 12:57:49 +07:00
if (Config::getExt($this->getName(), 'type') === 'addon') {
return;
}
try {
if (Config::getExt($this->getName(), 'type') === 'builtin' || Config::getExt($this->getName(), 'build-with-php') === true) {
if (file_exists(BUILD_MODULES_PATH . '/' . $this->getName() . '.so')) {
logger()->info('Shared extension [' . $this->getName() . '] was already built by php-src/configure (' . $this->getName() . '.so)');
return;
}
if (Config::getExt($this->getName(), 'build-with-php') === true) {
logger()->warning('Shared extension [' . $this->getName() . '] did not build with php-src/configure (' . $this->getName() . '.so)');
logger()->warning('Try deleting your build and source folders and running `spc build`` again.');
return;
}
}
if (file_exists(BUILD_MODULES_PATH . '/' . $this->getName() . '.so')) {
logger()->info('Shared extension [' . $this->getName() . '] was already built, skipping (' . $this->getName() . '.so)');
}
logger()->info('Building extension [' . $this->getName() . '] as shared extension (' . $this->getName() . '.so)');
foreach ($this->dependencies as $dependency) {
if (!$dependency instanceof Extension) {
continue;
}
if (!$dependency->isBuildStatic()) {
logger()->info('extension ' . $this->getName() . ' requires extension ' . $dependency->getName());
$dependency->buildShared();
}
2025-05-22 12:27:41 +07:00
}
match (PHP_OS_FAMILY) {
'Darwin', 'Linux' => $this->buildUnixShared(),
default => throw new WrongUsageException(PHP_OS_FAMILY . ' build shared extensions is not supported yet'),
};
} catch (SPCException $e) {
$e->bindExtensionInfo(['extension_name' => $this->getName()]);
throw $e;
}
}
/**
* Build shared extension for Unix
*/
public function buildUnixShared(): void
{
2025-07-26 11:13:29 +07:00
$config = (new SPCConfigUtil($this->builder))->config(
[$this->getName()],
array_map(fn ($l) => $l->getName(), $this->getLibraryDependencies(recursive: true)),
$this->builder->getOption('with-suggested-exts'),
$this->builder->getOption('with-suggested-libs'),
);
2025-07-25 16:18:04 +07:00
[$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
2025-07-31 21:11:15 +07:00
$preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group ';
$postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group ';
$env = [
'CFLAGS' => $config['cflags'],
2025-06-06 23:49:58 +07:00
'CXXFLAGS' => $config['cflags'],
2025-06-11 22:43:41 +07:00
'LDFLAGS' => $config['ldflags'],
2025-07-31 21:11:15 +07:00
'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"),
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
];
2025-07-25 16:17:13 +07:00
if (ToolchainManager::getToolchainClass() === ZigToolchain::class && SPCTarget::getTargetOS() === 'Linux') {
2025-07-24 21:57:56 +07:00
$env['SPC_COMPILER_EXTRA'] = '-lstdc++';
}
2025-06-11 21:54:59 +07:00
2025-06-20 17:11:52 +07:00
if ($this->patchBeforeSharedPhpize()) {
logger()->info("Extension [{$this->getName()}] patched before shared phpize");
}
// prepare configure args
shell()->cd($this->source_dir)
->setEnv($env)
2025-06-27 00:11:21 +07:00
->appendEnv($this->getExtraEnv())
->exec(BUILD_BIN_PATH . '/phpize');
2025-05-20 20:00:37 +07:00
if ($this->patchBeforeSharedConfigure()) {
2025-06-20 17:11:52 +07:00
logger()->info("Extension [{$this->getName()}] patched before shared configure");
2025-05-20 20:00:37 +07:00
}
2025-05-21 14:15:58 +07:00
2025-05-20 20:00:37 +07:00
shell()->cd($this->source_dir)
->setEnv($env)
2025-06-27 00:11:21 +07:00
->appendEnv($this->getExtraEnv())
->exec(
'./configure ' . $this->getUnixConfigureArg(true) .
' --with-php-config=' . BUILD_BIN_PATH . '/php-config ' .
'--enable-shared --disable-static'
);
2025-06-20 15:25:07 +07:00
if ($this->patchBeforeSharedMake()) {
2025-06-20 17:11:52 +07:00
logger()->info("Extension [{$this->getName()}] patched before shared make");
2025-06-20 15:25:07 +07:00
}
shell()->cd($this->source_dir)
->setEnv($env)
2025-06-27 00:11:21 +07:00
->appendEnv($this->getExtraEnv())
2025-06-11 22:45:08 +07:00
->exec('make clean')
->exec('make -j' . $this->builder->concurrency)
->exec('make install');
}
/**
* Get current extension version
*
* @return null|string Version string or null
*/
public function getExtVersion(): ?string
{
return null;
}
public function setBuildStatic(): void
{
2025-03-26 12:39:15 +08:00
if (!in_array('static', Config::getExtTarget($this->name))) {
throw new WrongUsageException("Extension [{$this->name}] does not support static build!");
2025-03-26 12:39:15 +08:00
}
$this->build_static = true;
}
public function setBuildShared(): void
{
if (!in_array('shared', Config::getExtTarget($this->name))) {
throw new WrongUsageException("Extension [{$this->name}] does not support shared build!");
}
$this->build_shared = true;
}
public function isBuildShared(): bool
{
return $this->build_shared;
}
public function isBuildStatic(): bool
{
return $this->build_static;
}
2023-03-18 17:32:21 +08:00
protected function addLibraryDependency(string $name, bool $optional = false): void
{
$depLib = $this->builder->getLib($name);
if (!$depLib) {
if (!$optional) {
throw new WrongUsageException("extension {$this->name} requires library {$name}");
2023-03-18 17:32:21 +08:00
}
logger()->info("enabling {$this->name} without library {$name}");
} else {
$this->dependencies[] = $depLib;
}
}
protected function addExtensionDependency(string $name, bool $optional = false): void
{
$depExt = $this->builder->getExt($name);
if (!$depExt) {
if (!$optional) {
throw new WrongUsageException("{$this->name} requires extension {$name} which is not included");
2023-03-18 17:32:21 +08:00
}
logger()->info("enabling {$this->name} without extension {$name}");
} else {
$this->dependencies[] = $depExt;
}
}
2025-06-27 00:17:24 +07:00
protected function getExtraEnv(): array
{
return [];
}
2025-07-25 16:22:42 +07:00
/**
* Splits a given string of library flags into static and shared libraries.
*
2025-07-25 16:26:34 +07:00
* @param string $allLibs A space-separated string of library flags (e.g., -lxyz).
* @return array an array containing two elements: the first is a space-separated string
* of static library flags, and the second is a space-separated string
* of shared library flags
2025-07-25 16:22:42 +07:00
*/
2025-07-25 16:18:04 +07:00
protected function splitLibsIntoStaticAndShared(string $allLibs): array
{
$staticLibString = '';
$sharedLibString = '';
$libs = explode(' ', $allLibs);
foreach ($libs as $lib) {
$staticLib = BUILD_LIB_PATH . '/lib' . str_replace('-l', '', $lib) . '.a';
if (str_starts_with($lib, BUILD_LIB_PATH . '/lib') && str_ends_with($lib, '.a')) {
$staticLib = $lib;
}
2025-07-25 16:22:42 +07:00
if ($lib === '-lphp' || !file_exists($staticLib)) {
$sharedLibString .= " {$lib}";
2025-07-24 23:47:01 +07:00
} else {
$staticLibString .= " {$lib}";
}
}
2025-07-24 23:47:01 +07:00
return [trim($staticLibString), trim($sharedLibString)];
}
2023-03-18 17:32:21 +08:00
private function getLibraryDependencies(bool $recursive = false): array
{
$ret = array_filter($this->dependencies, fn ($x) => $x instanceof LibraryBase);
if (!$recursive) {
return $ret;
}
$deps = [];
$added = 1;
while ($added !== 0) {
$added = 0;
foreach ($ret as $depName => $dep) {
foreach ($dep->getDependencies(true) as $depdepName => $depdep) {
if (!in_array($depdepName, array_keys($deps), true)) {
$deps[$depdepName] = $depdep;
++$added;
}
}
if (!in_array($depName, array_keys($deps), true)) {
$deps[$depName] = $dep;
}
}
}
2025-06-06 23:49:58 +07:00
if (array_key_exists(0, $deps)) {
$zero = [0 => $deps[0]];
unset($deps[0]);
return $zero + $deps;
}
2023-03-18 17:32:21 +08:00
return $deps;
}
}