Refactor, supports shared extension build now !

This commit is contained in:
crazywhalecc 2025-03-24 23:50:12 +08:00
parent 76c353e790
commit aa4d4db11f
No known key found for this signature in database
GPG Key ID: 1F4BDD59391F2680
19 changed files with 218 additions and 103 deletions

View File

@ -924,8 +924,8 @@
"support": {
"Windows": "wip",
"BSD": "no",
"Darwin": "no",
"Linux": "wip"
"Darwin": "partial",
"Linux": "partial"
},
"notes": true
},

View File

@ -281,8 +281,7 @@
"headers-unix": [
"ares.h",
"ares_dns.h",
"ares_nameser.h",
"ares_rules.h"
"ares_nameser.h"
]
},
"libde265": {

View File

@ -122,9 +122,12 @@ abstract class BuilderBase
*
* @return Extension[]
*/
public function getExts(): array
public function getExts(bool $including_dynamic = true): array
{
return $this->exts;
if ($including_dynamic) {
return $this->exts;
}
return array_filter($this->exts, fn ($ext) => !$ext->isBuildShared());
}
/**
@ -136,7 +139,7 @@ abstract class BuilderBase
public function hasCpp(): bool
{
// judge cpp-extension
$exts = array_keys($this->getExts());
$exts = array_keys($this->getExts(false));
foreach ($exts as $ext) {
if (Config::getExt($ext, 'cpp-extension', false) === true) {
return true;
@ -170,9 +173,20 @@ abstract class BuilderBase
* @throws \Throwable|WrongUsageException
* @internal
*/
public function proveExts(array $extensions, bool $skip_check_deps = false): void
public function proveExts(array $extensions, bool $skip_check_deps = false, array $shared_build_extensions = []): void
{
CustomExt::loadCustomExt();
// judge ext
foreach ($extensions as $ext) {
// if extension does not support static && no shared build, throw exception
if (!in_array('static', Config::getExtTarget($ext)) && !in_array($ext, $shared_build_extensions)) {
throw new WrongUsageException('Extension [' . $ext . '] does not support static build !');
}
// if extension does not support shared && no static build, throw exception
if (!in_array('shared', Config::getExtTarget($ext)) && !in_array($ext, $shared_build_extensions)) {
throw new WrongUsageException('Extension [' . $ext . '] does not support shared build !');
}
}
$this->emitPatchPoint('before-php-extract');
SourceManager::initSource(sources: ['php-src']);
$this->emitPatchPoint('after-php-extract');
@ -186,15 +200,21 @@ abstract class BuilderBase
$this->emitPatchPoint('after-exts-extract');
foreach ($extensions as $extension) {
$class = CustomExt::getExtClass($extension);
/** @var Extension $ext */
$ext = new $class($extension, $this);
$this->addExt($ext);
if (in_array($extension, $shared_build_extensions)) {
$ext->setBuildShared();
} else {
$ext->setBuildStatic();
}
}
if ($skip_check_deps) {
return;
}
foreach ($this->exts as $ext) {
foreach ($this->getExts() as $ext) {
$ext->checkDependency();
}
$this->ext_list = $extensions;
@ -207,6 +227,17 @@ abstract class BuilderBase
*/
abstract public function buildPHP(int $build_target = BUILD_TARGET_NONE);
public function buildDynamicExts(): void
{
foreach ($this->getExts() as $ext) {
if (!$ext->isBuildShared()) {
continue;
}
logger()->info('Building extension [' . $ext->getName() . '] as shared extension (' . $ext->getName() . '.so)');
$ext->buildShared();
}
}
/**
* Generate extension enable arguments for configure.
* e.g. --enable-mbstring
@ -214,10 +245,10 @@ abstract class BuilderBase
* @throws FileSystemException
* @throws WrongUsageException
*/
public function makeExtensionArgs(): string
public function makeStaticExtensionArgs(): string
{
$ret = [];
foreach ($this->exts as $ext) {
foreach ($this->getExts(false) as $ext) {
logger()->info($ext->getName() . ' is using ' . $ext->getConfigureArg());
$ret[] = trim($ext->getConfigureArg());
}
@ -396,7 +427,7 @@ abstract class BuilderBase
foreach ($this->libs as $lib) {
$lib->validate();
}
foreach ($this->exts as $ext) {
foreach ($this->getExts() as $ext) {
$ext->validate();
}
}
@ -441,7 +472,7 @@ abstract class BuilderBase
{
$php = "<?php\n\necho '[micro-test-start]' . PHP_EOL;\n";
foreach ($this->getExts() as $ext) {
foreach ($this->getExts(false) as $ext) {
$ext_name = $ext->getDistName();
if (!empty($ext_name)) {
$php .= "echo 'Running micro with {$ext_name} test' . PHP_EOL;\n";

View File

@ -14,6 +14,12 @@ class Extension
{
protected array $dependencies = [];
protected bool $build_shared = false;
protected bool $build_static = false;
protected string $source_dir;
/**
* @throws FileSystemException
* @throws RuntimeException
@ -30,6 +36,18 @@ class Extension
if (PHP_OS_FAMILY === 'Windows' && $unix_only) {
throw new RuntimeException("{$ext_type} extension {$name} is not supported on Windows platform");
}
// set source_dir for builtin
if ($ext_type === 'builtin') {
$this->source_dir = SOURCE_PATH . '/php-src/ext/' . $this->name;
} else {
$source = Config::getExt($this->name, 'source');
if ($source === null) {
throw new RuntimeException("{$ext_type} extension {$name} source not found");
}
$source_path = Config::getSource($source)['path'] ?? null;
$source_path = $source_path === null ? SOURCE_PATH . '/' . $source : SOURCE_PATH . '/' . $source_path;
$this->source_dir = $source_path;
}
}
/**
@ -167,6 +185,11 @@ class Extension
return false;
}
/**
* Run shared extension check when cli is enabled
*/
public function runSharedExtensionCheckUnix(): void {}
/**
* @throws RuntimeException
*/
@ -231,6 +254,32 @@ class Extension
// do nothing, just throw wrong usage exception if not valid
}
public function buildShared(): void
{
match (PHP_OS_FAMILY) {
'Darwin', 'Linux' => $this->buildUnixShared(),
default => throw new WrongUsageException(PHP_OS_FAMILY . ' build shared extensions is not supported yet'),
};
}
public function buildUnixShared(): void
{
// prepare configure args
shell()->cd($this->source_dir)
->setEnv(['CFLAGS' => $this->builder->arch_c_flags])
->execWithEnv(BUILD_BIN_PATH . '/phpize')
->execWithEnv('./configure ' . $this->getUnixConfigureArg() . ' --with-php-config=' . BUILD_BIN_PATH . '/php-config --enable-static --disable-shared')
->execWithEnv('make clean')
->execWithEnv('make -j' . $this->builder->concurrency);
// copy shared library
copy($this->source_dir . '/modules/' . $this->getDistName() . '.so', BUILD_LIB_PATH . '/' . $this->getDistName() . '.so');
// check shared extension with php-cli
if (file_exists(BUILD_BIN_PATH . '/php')) {
$this->runSharedExtensionCheckUnix();
}
}
/**
* Get current extension version
*
@ -241,6 +290,29 @@ class Extension
return null;
}
public function setBuildStatic(): void
{
$this->build_static = true;
}
public function setBuildShared(): void
{
if (!in_array('shared', Config::getExtTarget($this->name)) || Config::getExt($this->name, 'type') === 'builtin') {
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;
}
/**
* @throws RuntimeException
*/

View File

@ -5,7 +5,17 @@ declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\util\DynamicExt;
use SPC\exception\RuntimeException;
use SPC\util\CustomExt;
#[DynamicExt]
class xdebug extends Extension {}
#[CustomExt('xdebug')]
class xdebug extends Extension
{
public function runSharedExtensionCheckUnix(): void
{
[$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n -d "zend_extension=' . BUILD_LIB_PATH . '/xdebug.so" --ri xdebug');
if ($ret !== 0) {
throw new RuntimeException('xdebug.so not found');
}
}
}

View File

@ -118,7 +118,7 @@ class BSDBuilder extends UnixBuilderBase
$config_file_scan_dir .
$json_74 .
$zts .
$this->makeExtensionArgs()
$this->makeStaticExtensionArgs()
);
$this->emitPatchPoint('before-php-make');

View File

@ -149,6 +149,7 @@ class LinuxBuilder extends UnixBuilderBase
$enable_fpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM;
$enable_micro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
$enable_embed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
$emable_dev = ($build_target & BUILD_TARGET_DEV) === BUILD_TARGET_DEV;
$mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : '';
// prepare build php envs
@ -182,7 +183,7 @@ class LinuxBuilder extends UnixBuilderBase
$json_74 .
$zts .
$maxExecutionTimers .
$this->makeExtensionArgs() .
$this->makeStaticExtensionArgs() .
' ' . $envs_build_php . ' '
);
@ -203,6 +204,11 @@ class LinuxBuilder extends UnixBuilderBase
logger()->info('building micro');
$this->buildMicro();
}
if ($emable_dev && !$enable_embed) {
// install dynamic php extension building
logger()->info('building dynamic php extension dev dependencies');
$this->buildPhpDev();
}
if ($enable_embed) {
logger()->info('building embed');
if ($enable_micro) {
@ -311,15 +317,15 @@ class LinuxBuilder extends UnixBuilderBase
shell()->cd(SOURCE_PATH . '/php-src')
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . ' INSTALL_ROOT=' . BUILD_ROOT_PATH . " {$vars} install");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
// move mimalloc to the beginning of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
// move lstdc++ to the end of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
$this->patchPhpScripts();
}
protected function buildPhpDev(): void
{
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
shell()->cd(SOURCE_PATH . '/php-src')
->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . ' INSTALL_ROOT=' . BUILD_ROOT_PATH . " {$vars} install-build install-programs install-headers");
$this->patchPhpScripts();
}
private function getMakeExtraVars(): array

View File

@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\builder\linux\library;
use SPC\util\DynamicExt;
#[DynamicExt]
class xdebug extends LinuxLibraryBase
{
use \SPC\builder\unix\library\xdebug;
public const NAME = 'xdebug';
}

View File

@ -146,6 +146,7 @@ class MacOSBuilder extends UnixBuilderBase
$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;
$enableDev = ($build_target & BUILD_TARGET_DEV) === BUILD_TARGET_DEV;
// prepare build php envs
$mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : '';
@ -176,7 +177,7 @@ class MacOSBuilder extends UnixBuilderBase
$config_file_scan_dir .
$json_74 .
$zts .
$this->makeExtensionArgs() . ' ' .
$this->makeStaticExtensionArgs() . ' ' .
$envs_build_php
);
@ -197,6 +198,11 @@ class MacOSBuilder extends UnixBuilderBase
logger()->info('building micro');
$this->buildMicro();
}
if ($enableDev && !$enableEmbed) {
// install dynamic php extension building
logger()->info('building dynamic php extension dev dependencies');
$this->buildPhpDev();
}
if ($enableEmbed) {
logger()->info('building embed');
if ($enableMicro) {
@ -300,13 +306,15 @@ class MacOSBuilder extends UnixBuilderBase
->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');
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
// move mimalloc to the beginning of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
$this->patchPhpScripts();
}
protected function buildPhpDev(): void
{
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
shell()->cd(SOURCE_PATH . '/php-src')
->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . ' INSTALL_ROOT=' . BUILD_ROOT_PATH . " {$vars} install-build install-programs install-headers");
$this->patchPhpScripts();
}
private function getMakeExtraVars(): array

View File

@ -146,7 +146,7 @@ abstract class UnixBuilderBase extends BuilderBase
throw new RuntimeException("cli failed sanity check: ret[{$ret}]. out[{$raw_output}]");
}
foreach ($this->exts as $ext) {
foreach ($this->getExts(false) as $ext) {
logger()->debug('testing ext: ' . $ext->getName());
$ext->runCliCheckUnix();
}
@ -238,4 +238,29 @@ abstract class UnixBuilderBase extends BuilderBase
logger()->info('cleaning up');
shell()->cd(SOURCE_PATH . '/php-src')->exec('make clean');
}
/**
* Patch phpize and php-config if needed
* @throws FileSystemException
*/
protected function patchPhpScripts(): void
{
// patch phpize
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
logger()->debug('Patching phpize prefix');
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
}
// patch php-config
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
logger()->debug('Patching php-config prefix and libs order');
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
// move mimalloc to the beginning of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
// move lstdc++ to the end of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
}
}
}

View File

@ -26,9 +26,9 @@ trait libcares
{
shell()->cd($this->source_dir)
->setEnv(['CFLAGS' => $this->getLibExtraCFlags(), 'LDFLAGS' => $this->getLibExtraLdFlags(), 'LIBS' => $this->getLibExtraLibs()])
->execWithEnv('./configure --prefix= --enable-static --disable-shared --disable-tests')
->execWithEnv('./configure --prefix= --enable-static --disable-shared --disable-tests --with-pic')
->execWithEnv("make -j {$this->builder->concurrency}")
->exec('make install DESTDIR=' . BUILD_ROOT_PATH);
->execWithEnv('make install DESTDIR=' . BUILD_ROOT_PATH);
$this->patchPkgconfPrefix(['libcares.pc'], PKGCONF_PATCH_PREFIX);
}

View File

@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
trait xdebug
{
/**
* @throws RuntimeException
* @throws FileSystemException
*/
public function build(): void
{
shell()->cd($this->source_dir)
->exec(BUILD_BIN_PATH . '/phpize')
->exec('./configure --with-php-config=' . BUILD_BIN_PATH . '/php-config')
->exec('make clean')
->exec("make -j{$this->builder->concurrency}");
copy($this->source_dir . '/modules/xdebug.so', BUILD_LIB_PATH . '/xdebug.so');
copy($this->source_dir . '/modules/xdebug.la', BUILD_LIB_PATH . '/xdebug.la');
}
}

View File

@ -119,7 +119,7 @@ class WindowsBuilder extends BuilderBase
($enableMicro ? ('--enable-micro=yes ' . $micro_logo . $micro_w32) : '--enable-micro=no ') .
($enableEmbed ? '--enable-embed=yes ' : '--enable-embed=no ') .
$config_file_scan_dir .
"{$this->makeExtensionArgs()} " .
"{$this->makeStaticExtensionArgs()} " .
$zts .
'"'
);
@ -286,7 +286,7 @@ class WindowsBuilder extends BuilderBase
throw new RuntimeException('cli failed sanity check');
}
foreach ($this->exts as $ext) {
foreach ($this->getExts(false) as $ext) {
logger()->debug('testing ext: ' . $ext->getName());
$ext->runCliCheckWindows();
}

View File

@ -61,6 +61,18 @@ class BuildPHPCommand extends BuildCommand
// parse rule with options
$rule = $this->parseRules($dynamic_extensions);
// check dynamic extension build env
// macOS must use --no-strip option
if (!empty($dynamic_extensions) && PHP_OS_FAMILY === 'Darwin' && !$this->getOption('no-strip')) {
$this->output->writeln('MacOS does not support dynamic extension loading with stripped binary, please use --no-strip option!');
return static::FAILURE;
}
// linux must build with glibc
if (!empty($dynamic_extensions) && PHP_OS_FAMILY === 'Linux' && getenv('SPC_LIBC') !== 'glibc') {
$this->output->writeln('Linux does not support dynamic extension loading with musl-libc full-static build, please build with glibc!');
return static::FAILURE;
}
if ($rule === BUILD_TARGET_NONE || $rule === BUILD_TARGET_DEV) {
$this->output->writeln('<error>Please add at least one build SAPI!</error>');
$this->output->writeln("<comment>\t--build-cli\tBuild php-cli SAPI</comment>");
@ -124,7 +136,7 @@ class BuildPHPCommand extends BuildCommand
'Strip Binaries' => $builder->getOption('no-strip') ? 'no' : 'yes',
'Enable ZTS' => $builder->getOption('enable-zts') ? 'yes' : 'no',
];
if (!empty($dynamic_exts) || ($rule & BUILD_TARGET_EMBED) || ($rule & BUILD_TARGET_DEV)) {
if (!empty($dynamic_extensions) || ($rule & BUILD_TARGET_EMBED) || ($rule & BUILD_TARGET_DEV)) {
$indent_texts['Build Dev'] = 'yes';
}
if (!empty($this->input->getOption('with-config-file-path'))) {
@ -192,7 +204,8 @@ class BuildPHPCommand extends BuildCommand
$builder->buildPHP($rule);
// build dynamic extensions if needed
if (!empty($dynamic_exts)) {
if (!empty($dynamic_extensions)) {
logger()->info('Building dynamic extensions ...');
$builder->buildDynamicExts();
}
@ -224,6 +237,12 @@ class BuildPHPCommand extends BuildCommand
$path = FileSystem::convertPath("{$build_root_path}/bin/php-fpm");
logger()->info("Static php-fpm binary path{$fixed}: {$path}");
}
if (!empty($dynamic_extensions)) {
foreach ($dynamic_extensions as $ext) {
$path = FileSystem::convertPath("{$build_root_path}/ext/{$ext}.so");
logger()->info("Dynamic extension path{$fixed}: {$path}");
}
}
// export metadata
file_put_contents(BUILD_ROOT_PATH . '/build-extensions.json', json_encode($extensions, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

View File

@ -127,10 +127,6 @@ class Downloader
if (!$match_result) {
return $release['assets'];
}
if ($source['match'] === 'Source code') {
$url = $release['tarball_url'];
break;
}
foreach ($release['assets'] as $asset) {
if (preg_match('|' . $source['match'] . '|', $asset['name'])) {
$url = $asset['browser_download_url'];
@ -143,9 +139,6 @@ class Downloader
throw new DownloaderException("failed to find {$name} release metadata");
}
$filename = basename($url);
if ($source['match'] === 'Source code') {
$filename = $name . $filename . '.tar.gz';
}
return [$url, $filename];
}

View File

@ -43,7 +43,7 @@ class SourcePatcher
*/
public static function patchBeforeBuildconf(BuilderBase $builder): void
{
foreach ($builder->getExts() as $ext) {
foreach ($builder->getExts(false) as $ext) {
if ($ext->patchBeforeBuildconf() === true) {
logger()->info('Extension [' . $ext->getName() . '] patched before buildconf');
}
@ -86,7 +86,7 @@ class SourcePatcher
*/
public static function patchBeforeConfigure(BuilderBase $builder): void
{
foreach ($builder->getExts() as $ext) {
foreach ($builder->getExts(false) as $ext) {
if ($ext->patchBeforeConfigure() === true) {
logger()->info('Extension [' . $ext->getName() . '] patched before configure');
}
@ -253,7 +253,7 @@ class SourcePatcher
// }
// call extension patch before make
foreach ($builder->getExts() as $ext) {
foreach ($builder->getExts(false) as $ext) {
if ($ext->patchBeforeMake() === true) {
logger()->info('Extension [' . $ext->getName() . '] patched before make');
}

View File

@ -1,8 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\util;
#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)]
class DynamicExt {}

View File

@ -61,7 +61,8 @@ const BUILD_TARGET_CLI = 1; // build cli
const BUILD_TARGET_MICRO = 2; // build micro
const BUILD_TARGET_FPM = 4; // build fpm
const BUILD_TARGET_EMBED = 8; // build embed
const BUILD_TARGET_ALL = 15; // build all
const BUILD_TARGET_DEV = 16; // build dev
const BUILD_TARGET_ALL = 31; // build all
// doctor error fix policy
const FIX_POLICY_DIE = 1; // die directly

View File

@ -72,7 +72,7 @@ class BuilderTest extends TestCase
public function testMakeExtensionArgs()
{
$this->assertStringContainsString('--enable-mbstring', $this->builder->makeExtensionArgs());
$this->assertStringContainsString('--enable-mbstring', $this->builder->makeStaticExtensionArgs());
}
public function testIsLibsOnly()