Merge branch 'main' into frankenphp/mbed

This commit is contained in:
Marc
2025-10-24 16:50:28 +02:00
committed by GitHub
76 changed files with 1844 additions and 1385 deletions

View File

@@ -34,7 +34,7 @@ use Symfony\Component\Console\Application;
*/
final class ConsoleApplication extends Application
{
public const string VERSION = '2.7.3';
public const string VERSION = '2.7.5';
public function __construct()
{

View File

@@ -10,6 +10,7 @@ use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\LockFile;
use SPC\store\pkg\GoXcaddy;
use SPC\store\SourceManager;
use SPC\store\SourcePatcher;
use SPC\util\AttributeMapper;
@@ -127,27 +128,6 @@ abstract class BuilderBase
return array_filter($this->exts, fn ($ext) => $ext->isBuildStatic());
}
/**
* Check if there is a cpp extensions or libraries.
*/
public function hasCpp(): bool
{
// judge cpp-extension
$exts = array_keys($this->getExts(false));
foreach ($exts as $ext) {
if (Config::getExt($ext, 'cpp-extension', false) === true) {
return true;
}
}
$libs = array_keys($this->getLibs());
foreach ($libs as $lib) {
if (Config::getLib($lib, 'cpp-library', false) === true) {
return true;
}
}
return false;
}
/**
* Set libs only mode.
*
@@ -507,8 +487,7 @@ abstract class BuilderBase
throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!');
}
// frankenphp needs package go-xcaddy installed
$pkg_dir = PKG_ROOT_PATH . '/go-xcaddy-' . arch2gnu(php_uname('m')) . '-' . osfamily2shortname();
if (!file_exists("{$pkg_dir}/bin/go") || !file_exists("{$pkg_dir}/bin/xcaddy")) {
if (!GoXcaddy::isInstalled()) {
global $argv;
throw new WrongUsageException("FrankenPHP SAPI requires the go-xcaddy package, please install it first: {$argv[0]} install-pkg go-xcaddy");
}

View File

@@ -10,8 +10,6 @@ use SPC\exception\ValidationException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\toolchain\ToolchainManager;
use SPC\toolchain\ZigToolchain;
use SPC\util\SPCConfigUtil;
use SPC\util\SPCTarget;
@@ -223,12 +221,24 @@ class Extension
public function patchBeforeSharedMake(): bool
{
$config = (new SPCConfigUtil($this->builder))->config([$this->getName()], array_map(fn ($l) => $l->getName(), $this->builder->getLibs()));
[$staticLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
FileSystem::replaceFileRegex(
$this->source_dir . '/Makefile',
'/^(.*_SHARED_LIBADD\s*=.*)$/m',
'$1 ' . trim($staticLibs)
);
[$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
$lstdcpp = str_contains($sharedLibs, '-l:libstdc++.a') ? '-l:libstdc++.a' : null;
$lstdcpp ??= str_contains($sharedLibs, '-lstdc++') ? '-lstdc++' : '';
$makefileContent = file_get_contents($this->source_dir . '/Makefile');
if (preg_match('/^(.*_SHARED_LIBADD\s*=\s*)(.*)$/m', $makefileContent, $matches)) {
$prefix = $matches[1];
$currentLibs = trim($matches[2]);
$newLibs = trim("{$currentLibs} {$staticLibs} {$lstdcpp}");
$deduplicatedLibs = deduplicate_flags($newLibs);
FileSystem::replaceFileRegex(
$this->source_dir . '/Makefile',
'/^(.*_SHARED_LIBADD\s*=.*)$/m',
$prefix . $deduplicatedLibs
);
}
if ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS')) {
FileSystem::replaceFileRegex(
$this->source_dir . '/Makefile',
@@ -295,7 +305,7 @@ class Extension
// 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
$sharedExtensions = $this->getSharedExtensionLoadString();
[$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"');
[$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"');
if ($ret !== 0) {
throw new ValidationException(
"extension {$this->getName()} failed compile check: php-cli returned {$ret}",
@@ -325,7 +335,7 @@ class Extension
{
// 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);
[$ret] = cmd()->execWithResult(BUILD_BIN_PATH . '/php.exe -n --ri "' . $this->getDistName() . '"', false);
if ($ret !== 0) {
throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: "Extension {$this->getName()} sanity check");
}
@@ -402,26 +412,7 @@ class Extension
*/
public function buildUnixShared(): void
{
$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'),
);
[$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
$preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group ';
$postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group ';
$env = [
'CFLAGS' => $config['cflags'],
'CXXFLAGS' => $config['cflags'],
'LDFLAGS' => $config['ldflags'],
'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"),
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
];
if (ToolchainManager::getToolchainClass() === ZigToolchain::class && SPCTarget::getTargetOS() === 'Linux') {
$env['SPC_COMPILER_EXTRA'] = '-lstdc++';
}
$env = $this->getSharedExtensionEnv();
if ($this->patchBeforeSharedPhpize()) {
logger()->info("Extension [{$this->getName()}] patched before shared phpize");
}
@@ -436,13 +427,15 @@ class Extension
logger()->info("Extension [{$this->getName()}] patched before shared configure");
}
$phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: '';
shell()->cd($this->source_dir)
->setEnv($env)
->appendEnv($this->getExtraEnv())
->exec(
'./configure ' . $this->getUnixConfigureArg(true) .
' --with-php-config=' . BUILD_BIN_PATH . '/php-config ' .
'--enable-shared --disable-static'
"--enable-shared --disable-static {$phpvars}"
);
if ($this->patchBeforeSharedMake()) {
@@ -493,6 +486,30 @@ class Extension
return $this->build_static;
}
/**
* Returns the environment variables a shared extension needs to be built.
* CFLAGS, CXXFLAGS, LDFLAGS and so on.
*/
protected function getSharedExtensionEnv(): array
{
$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'),
);
[$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
$preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group ';
$postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group ';
return [
'CFLAGS' => $config['cflags'],
'CXXFLAGS' => $config['cflags'],
'LDFLAGS' => $config['ldflags'],
'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"),
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
];
}
protected function addLibraryDependency(string $name, bool $optional = false): void
{
$depLib = $this->builder->getLib($name);
@@ -565,12 +582,12 @@ class Extension
$added = 0;
foreach ($ret as $depName => $dep) {
foreach ($dep->getDependencies(true) as $depdepName => $depdep) {
if (!in_array($depdepName, array_keys($deps), true)) {
if (!array_key_exists($depdepName, $deps)) {
$deps[$depdepName] = $depdep;
++$added;
}
}
if (!in_array($depName, array_keys($deps), true)) {
if (!array_key_exists($depName, $deps)) {
$deps[$depName] = $dep;
}
}

View File

@@ -12,7 +12,7 @@ class dba extends Extension
{
public function getUnixConfigureArg(bool $shared = false): string
{
$qdbm = $this->builder->getLib('qdbm') ? (' --with-qdbm=' . ($shared ? 'shared,' : '') . BUILD_ROOT_PATH) : '';
$qdbm = $this->builder->getLib('qdbm') ? (' --with-qdbm=' . BUILD_ROOT_PATH) : '';
return '--enable-dba' . ($shared ? '=shared' : '') . $qdbm;
}

View File

@@ -15,7 +15,11 @@ class gettext extends Extension
public function patchBeforeBuildconf(): bool
{
if ($this->builder instanceof MacOSBuilder) {
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/gettext/config.m4', 'AC_CHECK_LIB($GETTEXT_CHECK_IN_LIB', 'AC_CHECK_LIB(intl');
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/ext/gettext/config.m4',
['AC_CHECK_LIB($GETTEXT_CHECK_IN_LIB', 'AC_CHECK_LIB([$GETTEXT_CHECK_IN_LIB'],
['AC_CHECK_LIB(intl', 'AC_CHECK_LIB([intl'] // new php versions use a bracket
);
}
return true;
}

View File

@@ -56,4 +56,11 @@ class grpc extends Extension
GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes');
return true;
}
protected function getSharedExtensionEnv(): array
{
$env = parent::getSharedExtensionEnv();
$env['CPPFLAGS'] = $env['CXXFLAGS'] . ' -Wno-attributes';
return $env;
}
}

View File

@@ -17,6 +17,10 @@ class memcached extends Extension
'--with-libmemcached-dir=' . BUILD_ROOT_PATH . ' ' .
'--disable-memcached-sasl ' .
'--enable-memcached-json ' .
($this->builder->getLib('zstd') ? '--with-zstd ' : '') .
($this->builder->getExt('igbinary') ? '--enable-memcached-igbinary ' : '') .
($this->builder->getExt('session') ? '--enable-memcached-session ' : '') .
($this->builder->getExt('msgpack') ? '--enable-memcached-msgpack ' : '') .
'--with-system-fastlz';
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\exception\ValidationException;
use SPC\store\FileSystem;
use SPC\util\CustomExt;
@@ -23,12 +24,7 @@ class readline extends Extension
public function getUnixConfigureArg(bool $shared = false): string
{
$enable = '--without-libedit --with-readline=' . BUILD_ROOT_PATH;
if ($this->builder->getPHPVersionID() < 84000) {
// the check uses `char rl_pending_input()` instead of `extern int rl_pending_input`, which makes LTO fail
$enable .= ' ac_cv_lib_readline_rl_pending_input=yes';
}
return $enable;
return '--with-libedit --without-readline';
}
public function buildUnixShared(): void
@@ -39,4 +35,13 @@ class readline extends Extension
}
parent::buildUnixShared();
}
public function runCliCheckUnix(): void
{
parent::runCliCheckUnix();
[$ret, $out] = shell()->execWithResult('printf "exit\n" | ' . BUILD_BIN_PATH . '/php -a');
if ($ret !== 0 || !str_contains(implode("\n", $out), 'Interactive shell')) {
throw new ValidationException("readline extension failed sanity check. Code: {$ret}, output: " . implode("\n", $out));
}
}
}

View File

@@ -6,6 +6,8 @@ namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\store\FileSystem;
use SPC\toolchain\ToolchainManager;
use SPC\toolchain\ZigToolchain;
use SPC\util\CustomExt;
#[CustomExt('simdjson')]
@@ -17,7 +19,7 @@ class simdjson extends Extension
FileSystem::replaceFileRegex(
SOURCE_PATH . '/php-src/ext/simdjson/config.m4',
'/php_version=(`.*`)$/m',
'php_version=' . strval($php_ver)
'php_version=' . $php_ver
);
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/ext/simdjson/config.m4',
@@ -31,4 +33,18 @@ class simdjson extends Extension
);
return true;
}
public function getSharedExtensionEnv(): array
{
$env = parent::getSharedExtensionEnv();
if (ToolchainManager::getToolchainClass() === ZigToolchain::class) {
$extra = getenv('SPC_COMPILER_EXTRA');
if (!str_contains((string) $extra, '-lstdc++')) {
f_putenv('SPC_COMPILER_EXTRA=' . clean_spaces($extra . ' -lstdc++'));
}
$env['CFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512';
$env['CXXFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512';
}
return $env;
}
}

View File

@@ -13,7 +13,7 @@ class spx extends Extension
{
public function getUnixConfigureArg(bool $shared = false): string
{
$arg = '--enable-spx' . ($shared ? '=shared' : '');
$arg = '--enable-SPX' . ($shared ? '=shared' : '');
if ($this->builder->getLib('zlib') !== null) {
$arg .= ' --with-zlib-dir=' . BUILD_ROOT_PATH;
}
@@ -29,4 +29,20 @@ class spx extends Extension
);
return true;
}
public function patchBeforeBuildconf(): bool
{
FileSystem::replaceFileStr(
$this->source_dir . '/config.m4',
'CFLAGS="$CFLAGS -Werror -Wall -O3 -pthread -std=gnu90"',
'CFLAGS="$CFLAGS -pthread"'
);
FileSystem::replaceFileStr(
$this->source_dir . '/src/php_spx.h',
"extern zend_module_entry spx_module_entry;\n",
"extern zend_module_entry spx_module_entry;;\n#define phpext_spx_ptr &spx_module_entry\n"
);
FileSystem::copy($this->source_dir . '/src/php_spx.h', $this->source_dir . '/php_spx.h');
return true;
}
}

View File

@@ -69,12 +69,15 @@ class swoole extends Extension
$arg .= $this->builder->getExt('swoole-hook-pgsql') ? ' --enable-swoole-pgsql' : ' --disable-swoole-pgsql';
$arg .= $this->builder->getExt('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd';
$arg .= $this->builder->getExt('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite';
if ($this->builder->getExt('swoole-hook-odbc')) {
$config = (new SPCConfigUtil($this->builder, ['libs_only_deps' => true]))->config([], ['unixodbc']);
$arg .= ' --with-swoole-odbc=unixODBC,' . BUILD_ROOT_PATH . ' SWOOLE_ODBC_LIBS="' . $config['libs'] . '"';
}
if ($this->getExtVersion() >= '6.1.0') {
$arg .= ' --enable-swoole-stdext';
}
if (SPCTarget::getTargetOS() === 'Darwin') {
$arg .= ' ac_cv_lib_pthread_pthread_barrier_init=no';
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\util\CustomExt;
#[CustomExt('zip')]
class zip extends Extension
{
public function getUnixConfigureArg(bool $shared = false): string
{
return !$shared ? '--with-zip=' . BUILD_ROOT_PATH : '--enable-zip=shared';
}
}

View File

@@ -7,6 +7,7 @@ namespace SPC\builder\linux;
use SPC\builder\unix\UnixBuilderBase;
use SPC\exception\PatchException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\SourcePatcher;
use SPC\util\GlobalEnvManager;
@@ -95,6 +96,8 @@ class LinuxBuilder extends UnixBuilderBase
// 'LIBS' => SPCTarget::getRuntimeLibs(), // do not pass static libraries here yet, they may contain polyfills for libc functions!
]);
$phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: '';
$embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static';
if ($embed_type !== 'static' && SPCTarget::isStatic()) {
throw new WrongUsageException(
@@ -103,22 +106,22 @@ class LinuxBuilder extends UnixBuilderBase
);
}
shell()->cd(SOURCE_PATH . '/php-src')
->exec(
$php_configure_env . ' ' .
getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' .
($enableCli ? '--enable-cli ' : '--disable-cli ') .
($enableFpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') .
($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') .
($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') .
($enableCgi ? '--enable-cgi ' : '--disable-cgi ') .
$config_file_path .
$config_file_scan_dir .
$json_74 .
$zts .
$maxExecutionTimers .
$this->makeStaticExtensionArgs() . ' '
);
$this->seekPhpSrcLogFileOnException(fn () => shell()->cd(SOURCE_PATH . '/php-src')->exec(
$php_configure_env . ' ' .
getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' .
($enableCli ? '--enable-cli ' : '--disable-cli ') .
($enableFpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') .
($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') .
($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') .
($enableCgi ? '--enable-cgi ' : '--disable-cgi ') .
$config_file_path .
$config_file_scan_dir .
$json_74 .
$zts .
$maxExecutionTimers .
$phpvars . ' ' .
$this->makeStaticExtensionArgs() . ' '
));
$this->emitPatchPoint('before-php-make');
SourcePatcher::patchBeforeMake($this);
@@ -148,16 +151,22 @@ class LinuxBuilder extends UnixBuilderBase
}
$this->buildEmbed();
}
// build dynamic extensions if needed, must happen before building FrankenPHP to make sure we export all necessary, undefined symbols
$shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared'))));
if (!empty($shared_extensions)) {
logger()->info('Building shared extensions ...');
$this->buildSharedExts();
}
if ($enableFrankenphp) {
logger()->info('building frankenphp');
$this->buildFrankenphp();
}
$shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared'))));
if (!empty($shared_extensions)) {
if (SPCTarget::isStatic()) {
throw new WrongUsageException(
"You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" .
'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" .
'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.'
);
}
logger()->info('Building shared extensions...');
$this->buildSharedExts();
}
}
public function testPHP(int $build_target = BUILD_TARGET_NONE)
@@ -171,11 +180,19 @@ class LinuxBuilder extends UnixBuilderBase
*/
protected function buildCli(): void
{
if ($this->getExt('readline') && SPCTarget::isStatic()) {
SourcePatcher::patchFile('musl_static_readline.patch', SOURCE_PATH . '/php-src');
}
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
$SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make';
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
shell()->cd(SOURCE_PATH . '/php-src')
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cli");
->exec("make {$concurrency} {$vars} cli");
if ($this->getExt('readline') && SPCTarget::isStatic()) {
SourcePatcher::patchFile('musl_static_readline.patch', SOURCE_PATH . '/php-src', true);
}
if (!$this->getOption('no-strip', false)) {
shell()->cd(SOURCE_PATH . '/php-src/sapi/cli')->exec('strip --strip-unneeded php');
@@ -191,10 +208,10 @@ class LinuxBuilder extends UnixBuilderBase
protected function buildCgi(): void
{
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
$SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make';
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
shell()->cd(SOURCE_PATH . '/php-src')
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cgi");
->exec("make {$concurrency} {$vars} cgi");
if (!$this->getOption('no-strip', false)) {
shell()->cd(SOURCE_PATH . '/php-src/sapi/cgi')->exec('strip --strip-unneeded php-cgi');
@@ -226,11 +243,11 @@ class LinuxBuilder extends UnixBuilderBase
// patch fake cli for micro
$vars['EXTRA_CFLAGS'] .= $enable_fake_cli;
$vars = SystemUtil::makeEnvVarString($vars);
$SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make';
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
shell()->cd(SOURCE_PATH . '/php-src')
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} micro");
->exec("make {$concurrency} {$vars} micro");
$this->processMicroUPX();
@@ -247,10 +264,10 @@ class LinuxBuilder extends UnixBuilderBase
protected function buildFpm(): void
{
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
$SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make';
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
shell()->cd(SOURCE_PATH . '/php-src')
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} fpm");
->exec("make {$concurrency} {$vars} fpm");
if (!$this->getOption('no-strip', false)) {
shell()->cd(SOURCE_PATH . '/php-src/sapi/fpm')->exec('strip --strip-unneeded php-fpm');
@@ -267,12 +284,17 @@ class LinuxBuilder extends UnixBuilderBase
*/
protected function buildEmbed(): void
{
$sharedExts = array_filter($this->exts, static fn ($ext) => $ext->isBuildShared());
$sharedExts = array_filter($sharedExts, static function ($ext) {
return Config::getExt($ext->getName(), 'build-with-php') === true;
});
$install_modules = $sharedExts ? 'install-modules' : '';
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
shell()->cd(SOURCE_PATH . '/php-src')
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile')
->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . ' INSTALL_ROOT=' . BUILD_ROOT_PATH . " {$vars} install");
->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi {$install_modules} install-build install-headers install-programs");
$ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: '';
$libDir = BUILD_LIB_PATH;
@@ -327,7 +349,7 @@ class LinuxBuilder extends UnixBuilderBase
$target = "{$libDir}/{$realLibName}";
if (file_exists($target)) {
[, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target));
$output = join("\n", $output);
$output = implode("\n", $output);
if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) {
$currentSoname = $sonameMatch[1];
if ($currentSoname !== basename($target)) {
@@ -361,12 +383,12 @@ class LinuxBuilder extends UnixBuilderBase
$config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
$static = SPCTarget::isStatic() ? '-all-static' : '';
$lib = BUILD_LIB_PATH;
return [
return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_LIBS' => $config['libs'],
'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie",
];
]);
}
/**

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SPC\builder\linux\library;
class libedit extends LinuxLibraryBase
{
use \SPC\builder\unix\library\libedit;
public const NAME = 'libedit';
}

View File

@@ -4,9 +4,6 @@ declare(strict_types=1);
namespace SPC\builder\linux\library;
/**
* gmp is a template library class for unix
*/
class readline extends LinuxLibraryBase
{
use \SPC\builder\unix\library\readline;

View File

@@ -7,6 +7,7 @@ namespace SPC\builder\macos;
use SPC\builder\macos\library\MacOSLibraryBase;
use SPC\builder\unix\UnixBuilderBase;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\SourcePatcher;
use SPC\util\GlobalEnvManager;
@@ -118,9 +119,8 @@ class MacOSBuilder extends UnixBuilderBase
}
$embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static';
shell()->cd(SOURCE_PATH . '/php-src')
->exec(
getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' .
$this->seekPhpSrcLogFileOnException(fn () => shell()->cd(SOURCE_PATH . '/php-src')->exec(
getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' .
($enableCli ? '--enable-cli ' : '--disable-cli ') .
($enableFpm ? '--enable-fpm ' : '--disable-fpm ') .
($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') .
@@ -132,7 +132,7 @@ class MacOSBuilder extends UnixBuilderBase
$zts .
$this->makeStaticExtensionArgs() . ' ' .
$envs_build_php
);
));
$this->emitPatchPoint('before-php-make');
SourcePatcher::patchBeforeMake($this);
@@ -162,15 +162,15 @@ class MacOSBuilder extends UnixBuilderBase
}
$this->buildEmbed();
}
if ($enableFrankenphp) {
logger()->info('building frankenphp');
$this->buildFrankenphp();
}
$shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared'))));
if (!empty($shared_extensions)) {
logger()->info('Building shared extensions ...');
$this->buildSharedExts();
}
if ($enableFrankenphp) {
logger()->info('building frankenphp');
$this->buildFrankenphp();
}
}
public function testPHP(int $build_target = BUILD_TARGET_NONE)
@@ -187,8 +187,8 @@ class MacOSBuilder extends UnixBuilderBase
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
$shell = shell()->cd(SOURCE_PATH . '/php-src');
$SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make';
$shell->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cli");
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
$shell->exec("make {$concurrency} {$vars} cli");
if (!$this->getOption('no-strip', false)) {
$shell->exec('dsymutil -f sapi/cli/php')->exec('strip -S sapi/cli/php');
}
@@ -200,8 +200,8 @@ class MacOSBuilder extends UnixBuilderBase
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
$shell = shell()->cd(SOURCE_PATH . '/php-src');
$SPC_CMD_PREFIX_PHP_MAKE = getenv('SPC_CMD_PREFIX_PHP_MAKE') ?: 'make';
$shell->exec("{$SPC_CMD_PREFIX_PHP_MAKE} {$vars} cgi");
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
$shell->exec("make {$concurrency} {$vars} cgi");
if (!$this->getOption('no-strip', false)) {
$shell->exec('dsymutil -f sapi/cgi/php-cgi')->exec('strip -S sapi/cgi/php-cgi');
}
@@ -230,7 +230,8 @@ class MacOSBuilder extends UnixBuilderBase
$shell = shell()->cd(SOURCE_PATH . '/php-src');
// build
$shell->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . " {$vars} micro");
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
$shell->exec("make {$concurrency} {$vars} micro");
// strip
if (!$this->getOption('no-strip', false)) {
$shell->exec('dsymutil -f sapi/micro/micro.sfx')->exec('strip -S sapi/micro/micro.sfx');
@@ -251,7 +252,8 @@ class MacOSBuilder extends UnixBuilderBase
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
$shell = shell()->cd(SOURCE_PATH . '/php-src');
$shell->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . " {$vars} fpm");
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
$shell->exec("make {$concurrency} {$vars} fpm");
if (!$this->getOption('no-strip', false)) {
$shell->exec('dsymutil -f sapi/fpm/php-fpm')->exec('strip -S sapi/fpm/php-fpm');
}
@@ -263,10 +265,15 @@ class MacOSBuilder extends UnixBuilderBase
*/
protected function buildEmbed(): void
{
$sharedExts = array_filter($this->exts, static fn ($ext) => $ext->isBuildShared());
$sharedExts = array_filter($sharedExts, static function ($ext) {
return Config::getExt($ext->getName(), 'build-with-php') === true;
});
$install_modules = $sharedExts ? 'install-modules' : '';
$vars = SystemUtil::makeEnvVarString($this->getMakeExtraVars());
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
shell()->cd(SOURCE_PATH . '/php-src')
->exec(getenv('SPC_CMD_PREFIX_PHP_MAKE') . ' INSTALL_ROOT=' . BUILD_ROOT_PATH . " {$vars} install");
->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi {$install_modules} install-build install-headers install-programs");
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') {
$AR = getenv('AR') ?: 'ar';
@@ -280,10 +287,10 @@ class MacOSBuilder extends UnixBuilderBase
private function getMakeExtraVars(): array
{
$config = (new SPCConfigUtil($this, ['libs_only_deps' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
return [
return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_LDFLAGS_PROGRAM' => '-L' . BUILD_LIB_PATH,
'EXTRA_LIBS' => $config['libs'],
];
]);
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace SPC\builder\macos\library;
class libedit extends MacOSLibraryBase
{
use \SPC\builder\unix\library\libedit;
public const NAME = 'libedit';
}

View File

@@ -4,9 +4,6 @@ declare(strict_types=1);
namespace SPC\builder\macos\library;
/**
* gmp is a template library class for unix
*/
class readline extends MacOSLibraryBase
{
use \SPC\builder\unix\library\readline;

View File

@@ -6,6 +6,7 @@ namespace SPC\builder\unix;
use SPC\builder\BuilderBase;
use SPC\builder\linux\SystemUtil as LinuxSystemUtil;
use SPC\exception\SPCException;
use SPC\exception\SPCInternalException;
use SPC\exception\ValidationException;
use SPC\exception\WrongUsageException;
@@ -312,7 +313,7 @@ abstract class UnixBuilderBase extends BuilderBase
protected function buildFrankenphp(): void
{
GlobalEnvManager::addPathIfNotExists(GoXcaddy::getEnvironment()['PATH']);
GlobalEnvManager::addPathIfNotExists(GoXcaddy::getPath());
$this->processFrankenphpApp();
$nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : '';
$nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : '';
@@ -339,11 +340,11 @@ abstract class UnixBuilderBase extends BuilderBase
}
}
$debugFlags = $this->getOption('no-strip') ? '-w -s ' : '';
$extLdFlags = "-extldflags '-pie{$dynamic_exports}'";
$extLdFlags = "-extldflags '-pie{$dynamic_exports} {$this->arch_ld_flags}'";
$muslTags = '';
$staticFlags = '';
if (SPCTarget::isStatic()) {
$extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports}'";
$extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports} {$this->arch_ld_flags}'";
$muslTags = 'static_build,';
$staticFlags = '-static-pie';
}
@@ -351,7 +352,6 @@ abstract class UnixBuilderBase extends BuilderBase
$config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list);
$cflags = "{$this->arch_c_flags} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS');
$libs = $config['libs'];
$libs .= PHP_OS_FAMILY === 'Linux' ? ' -lrt' : '';
// Go's gcc driver doesn't automatically link against -lgcov or -lrt. Ugly, but necessary fix.
if ((str_contains((string) getenv('SPC_DEFAULT_C_FLAGS'), '-fprofile') ||
str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), '-fprofile')) &&
@@ -359,7 +359,7 @@ abstract class UnixBuilderBase extends BuilderBase
$cflags .= ' -Wno-error=missing-profile';
$libs .= ' -lgcov';
}
$env = [
$env = [...[
'CGO_ENABLED' => '1',
'CGO_CFLAGS' => clean_spaces($cflags),
'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}",
@@ -369,12 +369,7 @@ abstract class UnixBuilderBase extends BuilderBase
"v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " .
"-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}",
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
];
foreach (GoXcaddy::getEnvironment() as $key => $value) {
if ($key !== 'PATH') {
$env[$key] = $value;
}
}
], ...GoXcaddy::getEnvironment()];
shell()->cd(BUILD_BIN_PATH)
->setEnv($env)
->exec("xcaddy build --output frankenphp {$xcaddyModules}");
@@ -387,4 +382,20 @@ abstract class UnixBuilderBase extends BuilderBase
}
}
}
/**
* Seek php-src/config.log when building PHP, add it to exception.
*/
protected function seekPhpSrcLogFileOnException(callable $callback): void
{
try {
$callback();
} catch (SPCException $e) {
if (file_exists(SOURCE_PATH . '/php-src/config.log')) {
$e->addExtraLogFile('php-src config.log', 'php-src.config.log');
copy(SOURCE_PATH . '/php-src/config.log', SPC_LOGS_DIR . '/php-src.config.log');
}
throw $e;
}
}
}

View File

@@ -17,7 +17,7 @@ trait attr
->exec('libtoolize --force --copy')
->exec('./autogen.sh || autoreconf -if')
->configure('--disable-nls')
->make();
->make('install-attributes_h install-data install-libattr_h install-libLTLIBRARIES install-pkgincludeHEADERS install-pkgconfDATA', with_install: false);
$this->patchPkgconfPrefix(['libattr.pc'], PKGCONF_PATCH_PREFIX);
}
}

View File

@@ -31,7 +31,7 @@ trait gettext
$autoconf->addConfigureArgs('--disable-threads');
}
$autoconf->configure()->make();
$autoconf->configure()->make(dir: $this->getSourceDir() . '/gettext-runtime/intl');
$this->patchLaDependencyPrefix();
}
}

View File

@@ -12,6 +12,11 @@ trait imagemagick
{
protected function build(): void
{
$original_ldflags = $this->builder->arch_ld_flags;
if (str_contains($this->builder->arch_ld_flags, '-Wl,--as-needed')) {
$this->builder->arch_ld_flags = str_replace('-Wl,--as-needed', '', $original_ldflags);
}
$ac = UnixAutoconfExecutor::create($this)
->optionalLib('libzip', ...ac_with_args('zip'))
->optionalLib('libjpeg', ...ac_with_args('jpeg'))
@@ -32,7 +37,7 @@ trait imagemagick
);
// special: linux-static target needs `-static`
$ldflags = SPCTarget::isStatic() ? ('-static -ldl') : '-ldl';
$ldflags = SPCTarget::isStatic() ? '-static -ldl' : '-ldl';
// special: macOS needs -iconv
$libs = SPCTarget::getTargetOS() === 'Darwin' ? '-liconv' : '';
@@ -45,6 +50,8 @@ trait imagemagick
$ac->configure()->make();
$this->builder->arch_ld_flags = $original_ldflags;
$filelist = [
'ImageMagick.pc',
'ImageMagick-7.Q16HDRI.pc',

View File

@@ -26,7 +26,7 @@ trait libacl
->exec('libtoolize --force --copy')
->exec('./autogen.sh || autoreconf -if')
->configure('--disable-nls', '--disable-tests')
->make();
->make('install-acl_h install-libacl_h install-data install-libLTLIBRARIES install-pkgincludeHEADERS install-sysincludeHEADERS install-pkgconfDATA', with_install: false);
$this->patchPkgconfPrefix(['libacl.pc'], PKGCONF_PATCH_PREFIX);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\store\FileSystem;
use SPC\util\executor\UnixAutoconfExecutor;
trait libedit
{
public function patchBeforeBuild(): bool
{
FileSystem::replaceFileRegex(
$this->source_dir . '/src/sys.h',
'|//#define\s+strl|',
'#define strl'
);
return true;
}
protected function build(): void
{
UnixAutoconfExecutor::create($this)
->appendEnv(['CFLAGS' => '-D__STDC_ISO_10646__=201103L'])
->configure()
->make();
$this->patchPkgconfPrefix(['libedit.pc']);
}
}

View File

@@ -10,7 +10,13 @@ trait libiconv
{
protected function build(): void
{
UnixAutoconfExecutor::create($this)->configure('--enable-extra-encodings')->make();
UnixAutoconfExecutor::create($this)
->configure(
'--enable-extra-encodings',
'--enable-year2038',
)
->make('install-lib', with_install: false)
->make('install-lib', with_install: false, dir: $this->getSourceDir() . '/libcharset');
$this->patchLaDependencyPrefix();
}
}

View File

@@ -20,17 +20,17 @@ trait libxml2
"-DZLIB_INCLUDE_DIR={$this->getIncludeDir()}",
'-DLIBXML2_WITH_ZLIB=OFF',
)
->optionalLib('icu', ...cmake_boolean_args('LIBXML2_WITH_ICU'))
->optionalLib('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA'))
->addConfigureArgs(
'-DLIBXML2_WITH_ICONV=ON',
'-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md
'-DLIBXML2_WITH_PYTHON=OFF',
'-DLIBXML2_WITH_PROGRAMS=OFF',
'-DLIBXML2_WITH_TESTS=OFF',
);
if ($this instanceof LinuxLibraryBase) {
$cmake->addConfigureArgs('-DIconv_IS_BUILD_IN=OFF');
$cmake->addConfigureArgs('-DIconv_IS_BUILT_IN=OFF');
}
$cmake->build();

View File

@@ -22,6 +22,7 @@ trait libzip
'-DBUILD_EXAMPLES=OFF',
'-DBUILD_REGRESS=OFF',
'-DBUILD_TOOLS=OFF',
'-DBUILD_OSSFUZZ=OFF',
)
->build();
$this->patchPkgconfPrefix(['libzip.pc'], PKGCONF_PATCH_PREFIX);

View File

@@ -38,7 +38,7 @@ trait ncurses
->make();
$final = FileSystem::scanDirFiles(BUILD_BIN_PATH, relative: true);
// Remove the new files
$new_files = array_diff($final, $filelist);
$new_files = array_diff($final, $filelist ?: []);
foreach ($new_files as $file) {
@unlink(BUILD_BIN_PATH . '/' . $file);
}

View File

@@ -4,93 +4,83 @@ declare(strict_types=1);
namespace SPC\builder\unix\library;
use SPC\builder\linux\library\LinuxLibraryBase;
use SPC\exception\BuildFailureException;
use SPC\exception\FileSystemException;
use SPC\store\FileSystem;
use SPC\util\PkgConfigUtil;
use SPC\util\SPCConfigUtil;
use SPC\util\SPCTarget;
trait postgresql
{
public function patchBeforeBuild(): bool
{
// fix aarch64 build on glibc 2.17 (e.g. CentOS 7)
if (SPCTarget::getLibcVersion() === '2.17' && GNU_ARCH === 'aarch64') {
try {
FileSystem::replaceFileStr("{$this->source_dir}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0');
FileSystem::replaceFileStr(
"{$this->source_dir}/src/port/pg_crc32c_armv8_choose.c",
'#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32)',
'#if defined(__linux__) && !defined(HWCAP_CRC32)'
);
} catch (FileSystemException) {
// allow file not-existence to make it compatible with old and new version
}
}
// skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries
FileSystem::replaceFileStr("{$this->source_dir}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';');
// disable shared libs build
FileSystem::replaceFileStr(
"{$this->source_dir}/src/Makefile.shlib",
[
'$(LINK.shared) -o $@ $(OBJS) $(LDFLAGS) $(LDFLAGS_SL) $(SHLIB_LINK)',
'$(INSTALL_SHLIB) $< \'$(DESTDIR)$(pkglibdir)/$(shlib)\'',
'$(INSTALL_SHLIB) $< \'$(DESTDIR)$(libdir)/$(shlib)\'',
'$(INSTALL_SHLIB) $< \'$(DESTDIR)$(bindir)/$(shlib)\'',
],
''
);
return true;
}
protected function build(): void
{
$builddir = BUILD_ROOT_PATH;
$envs = '';
$packages = 'zlib openssl readline libxml-2.0';
$optional_packages = [
'zstd' => 'libzstd',
'ldap' => 'ldap',
'libxslt' => 'libxslt',
'icu' => 'icu-i18n',
$libs = array_map(fn ($x) => $x->getName(), $this->getDependencies());
$spc = new SPCConfigUtil($this->getBuilder(), ['no_php' => true, 'libs_only_deps' => true]);
$config = $spc->config(libraries: $libs, include_suggest_lib: $this->builder->getOption('with-suggested-libs'));
$env_vars = [
'CFLAGS' => $config['cflags'],
'CPPFLAGS' => '-DPIC',
'LDFLAGS' => $config['ldflags'],
'LIBS' => $config['libs'],
];
$error_exec_cnt = 0;
foreach ($optional_packages as $lib => $pkg) {
if ($this->getBuilder()->getLib($lib)) {
$packages .= ' ' . $pkg;
$output = shell()->execWithResult("pkg-config --static {$pkg}");
$error_exec_cnt += $output[0] === 0 ? 0 : 1;
logger()->info(var_export($output[1], true));
}
}
$output = shell()->execWithResult("pkg-config --cflags-only-I --static {$packages}");
$error_exec_cnt += $output[0] === 0 ? 0 : 1;
$macos_15_bug_cflags = PHP_OS_FAMILY === 'Darwin' ? ' -Wno-unguarded-availability-new' : '';
$cflags = '';
if (!empty($output[1][0])) {
$cflags = $output[1][0];
$envs .= ' CPPFLAGS="-DPIC"';
$cflags = "{$cflags} -fno-ident{$macos_15_bug_cflags}";
}
$output = shell()->execWithResult("pkg-config --libs-only-L --static {$packages}");
$error_exec_cnt += $output[0] === 0 ? 0 : 1;
if (!empty($output[1][0])) {
$ldflags = $output[1][0];
$envs .= SPCTarget::isStatic() ? " LDFLAGS=\"{$ldflags} -static\" " : " LDFLAGS=\"{$ldflags}\" ";
}
$output = shell()->execWithResult("pkg-config --libs-only-l --static {$packages}");
$error_exec_cnt += $output[0] === 0 ? 0 : 1;
if (!empty($output[1][0])) {
$libs = $output[1][0];
$libcpp = '';
if ($this->builder->getLib('icu')) {
$libcpp = $this instanceof LinuxLibraryBase ? ' -lstdc++' : ' -lc++';
}
$envs .= " LIBS=\"{$libs}{$libcpp}\" ";
}
if ($error_exec_cnt > 0) {
throw new BuildFailureException('Failed to get pkg-config information!');
if ($ldLibraryPath = getenv('SPC_LD_LIBRARY_PATH')) {
$env_vars['LD_LIBRARY_PATH'] = $ldLibraryPath;
}
FileSystem::resetDir($this->source_dir . '/build');
$version = $this->getVersion();
// 16.1 workaround
if (version_compare($version, '16.1') >= 0) {
# 有静态链接配置 参考文件: src/interfaces/libpq/Makefile
shell()->cd($this->source_dir . '/build')
->exec('sed -i.backup "s/invokes exit\'; exit 1;/invokes exit\';/" ../src/interfaces/libpq/Makefile')
->exec('sed -i.backup "278 s/^/# /" ../src/Makefile.shlib')
->exec('sed -i.backup "402 s/^/# /" ../src/Makefile.shlib');
} else {
throw new BuildFailureException('Unsupported version for postgresql: ' . $version . ' !');
}
// php source relies on the non-private encoding functions in libpgcommon.a
FileSystem::replaceFileStr(
"{$this->source_dir}/src/common/Makefile",
'$(OBJS_FRONTEND): CPPFLAGS += -DUSE_PRIVATE_ENCODING_FUNCS',
'$(OBJS_FRONTEND): CPPFLAGS += -UUSE_PRIVATE_ENCODING_FUNCS -DFRONTEND',
);
// configure
shell()->cd($this->source_dir . '/build')->initializeEnv($this)
->appendEnv(['CFLAGS' => $cflags])
$shell = shell()->cd("{$this->source_dir}/build")->initializeEnv($this)
->appendEnv($env_vars)
->exec(
"{$envs} ../configure " .
"--prefix={$builddir} " .
($this->builder->getOption('enable-zts') ? '--enable-thread-safety ' : '--disable-thread-safety ') .
'../configure ' .
"--prefix={$this->getBuildRootPath()} " .
'--enable-coverage=no ' .
'--with-ssl=openssl ' .
'--with-readline ' .
'--with-libxml ' .
($this->builder->getLib('icu') ? '--with-icu ' : '--without-icu ') .
($this->builder->getLib('ldap') ? '--with-ldap ' : '--without-ldap ') .
// '--without-ldap ' .
($this->builder->getLib('libxslt') ? '--with-libxslt ' : '--without-libxslt ') .
($this->builder->getLib('zstd') ? '--with-zstd ' : '--without-zstd ') .
'--without-lz4 ' .
@@ -99,32 +89,29 @@ trait postgresql
'--without-pam ' .
'--without-bonjour ' .
'--without-tcl '
)
->exec($envs . ' make -C src/bin/pg_config install')
->exec($envs . ' make -C src/include install')
->exec($envs . ' make -C src/common install')
->exec($envs . ' make -C src/port install')
->exec($envs . ' make -C src/interfaces/libpq install');
);
// patch ldap lib
if ($this->builder->getLib('ldap')) {
$libs = PkgConfigUtil::getLibsArray('ldap');
$libs = clean_spaces(implode(' ', $libs));
FileSystem::replaceFileStr($this->source_dir . '/build/config.status', '-lldap', $libs);
FileSystem::replaceFileStr($this->source_dir . '/build/src/Makefile.global', '-lldap', $libs);
}
$shell
->exec('make -C src/bin/pg_config install')
->exec('make -C src/include install')
->exec('make -C src/common install')
->exec('make -C src/port install')
->exec('make -C src/interfaces/libpq install');
// remove dynamic libs
shell()->cd($this->source_dir . '/build')
->exec("rm -rf {$builddir}/lib/*.so.*")
->exec("rm -rf {$builddir}/lib/*.so")
->exec("rm -rf {$builddir}/lib/*.dylib");
->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so.*")
->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so")
->exec("rm -rf {$this->getBuildRootPath()}/lib/*.dylib");
FileSystem::replaceFileStr(BUILD_LIB_PATH . '/pkgconfig/libpq.pc', '-lldap', '-lldap -llber');
}
private function getVersion(): string
{
try {
$file = FileSystem::readFile($this->source_dir . '/meson.build');
if (preg_match("/^\\s+version:\\s?'(.*)'/m", $file, $match)) {
return $match[1];
}
return 'unknown';
} catch (FileSystemException) {
return 'unknown';
}
FileSystem::replaceFileStr("{$this->getLibDir()}/pkgconfig/libpq.pc", '-lldap', '-lldap -llber');
}
}

View File

@@ -223,11 +223,9 @@ class BuildPHPCommand extends BuildCommand
// ---------- When using bin/spc-alpine-docker, the build root path is different from the host system ----------
$build_root_path = BUILD_ROOT_PATH;
$cwd = getcwd();
$fixed = '';
$build_root_path = get_display_path($build_root_path);
if (!empty(getenv('SPC_FIX_DEPLOY_ROOT'))) {
str_replace($cwd, '', $build_root_path);
$build_root_path = getenv('SPC_FIX_DEPLOY_ROOT') . '/' . basename($build_root_path);
$fixed = ' (host system)';
}
if (($rule & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace SPC\command;
use SPC\exception\ValidationException;
use SPC\store\pkg\GoXcaddy;
use SPC\store\pkg\Zig;
use SPC\toolchain\ToolchainManager;
use SPC\toolchain\ZigToolchain;
use SPC\util\ConfigValidator;
@@ -63,7 +65,7 @@ class CraftCommand extends BuildCommand
}
}
// install go and xcaddy for frankenphp
if (in_array('frankenphp', $craft['sapi'])) {
if (in_array('frankenphp', $craft['sapi']) && !GoXcaddy::isInstalled()) {
$retcode = $this->runCommand('install-pkg', 'go-xcaddy');
if ($retcode !== 0) {
$this->output->writeln('<error>craft go-xcaddy failed</error>');
@@ -71,7 +73,7 @@ class CraftCommand extends BuildCommand
}
}
// install zig if requested
if (ToolchainManager::getToolchainClass() === ZigToolchain::class) {
if (ToolchainManager::getToolchainClass() === ZigToolchain::class && !Zig::isInstalled()) {
$retcode = $this->runCommand('install-pkg', 'zig');
if ($retcode !== 0) {
$this->output->writeln('<error>craft zig failed</error>');

View File

@@ -106,7 +106,7 @@ class DownloadCommand extends BaseCommand
}
// retry
$retry = intval($this->getOption('retry'));
$retry = (int) $this->getOption('retry');
f_putenv('SPC_DOWNLOAD_RETRIES=' . $retry);
// Use shallow-clone can reduce git resource download
@@ -265,7 +265,7 @@ class DownloadCommand extends BaseCommand
f_passthru((PHP_OS_FAMILY === 'Windows' ? 'rmdir /s /q ' : 'rm -rf ') . DOWNLOAD_PATH);
}
// unzip command check
if (PHP_OS_FAMILY !== 'Windows' && !$this->findCommand('unzip')) {
if (PHP_OS_FAMILY !== 'Windows' && !self::findCommand('unzip')) {
$this->output->writeln('Missing unzip command, you need to install it first !');
$this->output->writeln('You can use "bin/spc doctor" command to check and install required tools');
return static::FAILURE;

View File

@@ -28,7 +28,7 @@ class LinuxToolCheckList
public const TOOLS_DEBIAN = [
'make', 'bison', 're2c', 'flex',
'git', 'autoconf', 'automake', 'autopoint',
'tar', 'unzip', 'gzip',
'tar', 'unzip', 'gzip', 'gcc', 'g++',
'bzip2', 'cmake', 'patch',
'xz', 'libtoolize', 'which',
'patchelf',
@@ -37,7 +37,7 @@ class LinuxToolCheckList
public const TOOLS_RHEL = [
'perl', 'make', 'bison', 're2c', 'flex',
'git', 'autoconf', 'automake',
'tar', 'unzip', 'gzip', 'gcc',
'tar', 'unzip', 'gzip', 'gcc', 'g++',
'bzip2', 'cmake', 'patch', 'which',
'xz', 'libtool', 'gettext-devel',
'patchelf',
@@ -53,7 +53,8 @@ class LinuxToolCheckList
'base-devel' => 'automake',
'gettext-devel' => 'gettextize',
'gettext-dev' => 'gettextize',
'perl-IPC-Cmd' => '/usr/share/doc/perl-IPC-Cmd',
'perl-IPC-Cmd' => '/usr/share/perl5/vendor_perl/IPC/Cmd.pm',
'perl-Time-Piece' => '/usr/lib64/perl5/Time/Piece.pm',
];
/** @noinspection PhpUnused */
@@ -65,7 +66,7 @@ class LinuxToolCheckList
$required = match ($distro['dist']) {
'alpine' => self::TOOLS_ALPINE,
'redhat' => self::TOOLS_RHEL,
'centos' => array_merge(self::TOOLS_RHEL, ['perl-IPC-Cmd']),
'centos' => array_merge(self::TOOLS_RHEL, ['perl-IPC-Cmd', 'perl-Time-Piece']),
'arch' => self::TOOLS_ARCH,
default => self::TOOLS_DEBIAN,
};
@@ -81,14 +82,14 @@ class LinuxToolCheckList
return CheckResult::ok();
}
#[AsCheckItem('if cmake version >= 3.18', limit_os: 'Linux')]
#[AsCheckItem('if cmake version >= 3.22', limit_os: 'Linux')]
public function checkCMakeVersion(): ?CheckResult
{
$ver = get_cmake_version();
if ($ver === null) {
return CheckResult::fail('Failed to get cmake version');
}
if (version_compare($ver, '3.18.0') < 0) {
if (version_compare($ver, '3.22.0') < 0) {
return CheckResult::fail('cmake version is too low (' . $ver . '), please update it manually!');
}
return CheckResult::ok($ver);

View File

@@ -137,13 +137,18 @@ class ExceptionHandler
self::logError("\n----------------------------------------\n");
self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_OUTPUT_LOG));
// convert log file path if in docker
$spc_log_convert = get_display_path(SPC_OUTPUT_LOG);
$shell_log_convert = get_display_path(SPC_SHELL_LOG);
$spc_logs_dir_convert = get_display_path(SPC_LOGS_DIR);
self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($spc_log_convert));
if (file_exists(SPC_SHELL_LOG)) {
self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none(SPC_SHELL_LOG));
self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::none($shell_log_convert));
}
if ($e->getExtraLogFiles() !== []) {
foreach ($e->getExtraLogFiles() as $key => $file) {
self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none(SPC_LOGS_DIR . "/{$file}"));
self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::none("{$spc_logs_dir_convert}/{$file}"));
}
}
if (!defined('DEBUG_MODE')) {

View File

@@ -16,6 +16,42 @@ use SPC\util\SPCTarget;
*/
class Downloader
{
/**
* Get latest version from PIE config (Packagist)
*
* @param string $name Source name
* @param array $source Source meta info: [repo]
* @return array<int, string> [url, filename]
*/
public static function getPIEInfo(string $name, array $source): array
{
$packagist_url = "https://repo.packagist.org/p2/{$source['repo']}.json";
logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}");
$data = json_decode(self::curlExec(
url: $packagist_url,
retries: self::getRetryAttempts()
), true);
if (!isset($data['packages'][$source['repo']]) || !is_array($data['packages'][$source['repo']])) {
throw new DownloaderException("failed to find {$name} repo info from packagist");
}
// get the first version
$first = $data['packages'][$source['repo']][0] ?? [];
// check 'type' => 'php-ext' or contains 'php-ext' key
if (!isset($first['php-ext'])) {
throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package");
}
// get download link from dist
$dist_url = $first['dist']['url'] ?? null;
$dist_type = $first['dist']['type'] ?? null;
if (!$dist_url || !$dist_type) {
throw new DownloaderException("failed to find {$name} dist info from packagist");
}
$name = str_replace('/', '_', $source['repo']);
$version = $first['version'] ?? 'unknown';
// file name use: $name-$version.$dist_type
return [$dist_url, "{$name}-{$version}.{$dist_type}"];
}
/**
* Get latest version from BitBucket tag
*
@@ -65,19 +101,19 @@ class Downloader
url: "https://api.github.com/repos/{$source['repo']}/{$type}",
hooks: [[CurlHook::class, 'setupGithubToken']],
retries: self::getRetryAttempts()
), true);
), true, 512, JSON_THROW_ON_ERROR);
$url = null;
for ($i = 0; $i < count($data); ++$i) {
if (($data[$i]['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) {
foreach ($data as $rel) {
if (($rel['prerelease'] ?? false) === true && ($source['prefer-stable'] ?? false)) {
continue;
}
if (!($source['match'] ?? null)) {
$url = $data[$i]['tarball_url'] ?? null;
$url = $rel['tarball_url'] ?? null;
break;
}
if (preg_match('|' . $source['match'] . '|', $data[$i]['tarball_url'])) {
$url = $data[$i]['tarball_url'];
if (preg_match('|' . $source['match'] . '|', $rel['tarball_url'])) {
$url = $rel['tarball_url'];
break;
}
}
@@ -232,7 +268,8 @@ class Downloader
$quiet = !defined('DEBUG_MODE') ? '-q --quiet' : '';
$git = SPC_GIT_EXEC;
$shallow = defined('GIT_SHALLOW_CLONE') ? '--depth 1 --single-branch' : '';
$recursive = ($submodules === null) ? '--recursive' : '';
$recursive = ($submodules === null && defined('GIT_SHALLOW_CLONE')) ? '--recursive --shallow-submodules' : null;
$recursive ??= $submodules === null ? '--recursive' : '';
try {
self::registerCancelEvent(function () use ($download_path) {
@@ -243,8 +280,9 @@ class Downloader
});
f_passthru("{$git} clone {$quiet} --config core.autocrlf=false --branch \"{$branch}\" {$shallow} {$recursive} \"{$url}\" \"{$download_path}\"");
if ($submodules !== null) {
$depth_flag = defined('GIT_SHALLOW_CLONE') ? '--depth 1' : '';
foreach ($submodules as $submodule) {
f_passthru("cd \"{$download_path}\" && {$git} submodule update --init " . escapeshellarg($submodule));
f_passthru("cd \"{$download_path}\" && {$git} submodule update --init {$depth_flag} " . escapeshellarg($submodule));
}
}
} catch (SPCException $e) {
@@ -315,91 +353,14 @@ class Downloader
if (self::isAlreadyDownloaded($name, $force, SPC_DOWNLOAD_PACKAGE)) {
return;
}
try {
switch ($pkg['type']) {
case 'bitbuckettag': // BitBucket Tag
[$url, $filename] = self::getLatestBitbucketTag($name, $pkg);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE);
break;
case 'ghtar': // GitHub Release (tar)
[$url, $filename] = self::getLatestGithubTarball($name, $pkg);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]);
break;
case 'ghtagtar': // GitHub Tag (tar)
[$url, $filename] = self::getLatestGithubTarball($name, $pkg, 'tags');
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, hooks: [[CurlHook::class, 'setupGithubToken']]);
break;
case 'ghrel': // GitHub Release (uploaded)
[$url, $filename] = self::getLatestGithubRelease($name, $pkg);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]);
break;
case 'filelist': // Basic File List (regex based crawler)
[$url, $filename] = self::getFromFileList($name, $pkg);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE);
break;
case 'url': // Direct download URL
$url = $pkg['url'];
$filename = $pkg['filename'] ?? basename($pkg['url']);
self::downloadFile($name, $url, $filename, $pkg['extract'] ?? null, SPC_DOWNLOAD_PACKAGE);
break;
case 'git': // Git repo
self::downloadGit(
$name,
$pkg['url'],
$pkg['rev'],
$pkg['submodules'] ?? null,
$pkg['extract'] ?? null,
self::getRetryAttempts(),
SPC_DOWNLOAD_PRE_BUILT
);
break;
case 'local':
// Local directory, do nothing, just lock it
logger()->debug("Locking local source {$name}");
LockFile::lockSource($name, [
'source_type' => SPC_SOURCE_LOCAL,
'dirname' => $pkg['dirname'],
'move_path' => $pkg['extract'] ?? null,
'lock_as' => SPC_DOWNLOAD_PACKAGE,
]);
break;
case 'custom': // Custom download method, like API-based download or other
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg');
if (isset($pkg['func']) && is_callable($pkg['func'])) {
$pkg['name'] = $name;
$pkg['func']($force, $pkg, SPC_DOWNLOAD_PACKAGE);
break;
}
foreach ($classes as $class) {
if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) {
$cls = new $class();
if (in_array($name, $cls->getSupportName())) {
(new $class())->fetch($name, $force, $pkg);
break;
}
}
}
break;
default:
throw new DownloaderException('unknown source type: ' . $pkg['type']);
}
} catch (\Throwable $e) {
// Because sometimes files downloaded through the command line are not automatically deleted after a failure.
// Here we need to manually delete the file if it is detected to exist.
if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) {
logger()->warning('Deleting download file: ' . $filename);
unlink(DOWNLOAD_PATH . '/' . $filename);
}
throw new DownloaderException('Download failed! ' . $e->getMessage());
}
self::downloadByType($pkg['type'], $name, $pkg, $force, SPC_DOWNLOAD_PACKAGE);
}
/**
* Download source
*
* @param string $name source name
* @param null|array{
* @param null|array{
* type: string,
* repo: ?string,
* url: ?string,
@@ -414,7 +375,7 @@ class Downloader
* path: ?string,
* text: ?string
* }
* } $source source meta info: [type, path, rev, url, filename, regex, license]
* } $source source meta info: [type, path, rev, url, filename, regex, license]
* @param bool $force Whether to force download (default: false)
* @param int $download_as Lock source type (default: SPC_LOCK_SOURCE)
*/
@@ -437,80 +398,7 @@ class Downloader
return;
}
try {
switch ($source['type']) {
case 'bitbuckettag': // BitBucket Tag
[$url, $filename] = self::getLatestBitbucketTag($name, $source);
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as);
break;
case 'ghtar': // GitHub Release (tar)
[$url, $filename] = self::getLatestGithubTarball($name, $source);
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]);
break;
case 'ghtagtar': // GitHub Tag (tar)
[$url, $filename] = self::getLatestGithubTarball($name, $source, 'tags');
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]);
break;
case 'ghrel': // GitHub Release (uploaded)
[$url, $filename] = self::getLatestGithubRelease($name, $source);
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]);
break;
case 'filelist': // Basic File List (regex based crawler)
[$url, $filename] = self::getFromFileList($name, $source);
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as);
break;
case 'url': // Direct download URL
$url = $source['url'];
$filename = $source['filename'] ?? basename($source['url']);
self::downloadFile($name, $url, $filename, $source['path'] ?? null, $download_as);
break;
case 'git': // Git repo
self::downloadGit(
$name,
$source['url'],
$source['rev'],
$source['submodules'] ?? null,
$source['path'] ?? null,
self::getRetryAttempts(),
$download_as
);
break;
case 'local':
// Local directory, do nothing, just lock it
logger()->debug("Locking local source {$name}");
LockFile::lockSource($name, [
'source_type' => SPC_SOURCE_LOCAL,
'dirname' => $source['dirname'],
'move_path' => $source['extract'] ?? null,
'lock_as' => $download_as,
]);
break;
case 'custom': // Custom download method, like API-based download or other
if (isset($source['func']) && is_callable($source['func'])) {
$source['name'] = $name;
$source['func']($force, $source, $download_as);
break;
}
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source');
foreach ($classes as $class) {
if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) {
(new $class())->fetch($force, $source, $download_as);
break;
}
}
break;
default:
throw new DownloaderException('unknown source type: ' . $source['type']);
}
} catch (\Throwable $e) {
// Because sometimes files downloaded through the command line are not automatically deleted after a failure.
// Here we need to manually delete the file if it is detected to exist.
if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) {
logger()->warning('Deleting download file: ' . $filename);
unlink(DOWNLOAD_PATH . '/' . $filename);
}
throw new DownloaderException('Download failed! ' . $e->getMessage());
}
self::downloadByType($source['type'], $name, $source, $force, $download_as);
}
/**
@@ -711,4 +599,109 @@ class Downloader
}
return false;
}
/**
* Download by type.
*
* @param string $type Types
* @param string $name Download item name
* @param array{
* url?: string,
* repo?: string,
* rev?: string,
* path?: string,
* filename?: string,
* dirname?: string,
* match?: string,
* prefer-stable?: bool,
* extract?: string,
* submodules?: array<string>,
* provide-pre-built?: bool,
* func?: ?callable,
* license?: array
* } $conf Download item config
* @param bool $force Force download
* @param int $download_as Lock source type
*/
private static function downloadByType(string $type, string $name, array $conf, bool $force, int $download_as): void
{
try {
switch ($type) {
case 'pie': // Packagist
[$url, $filename] = self::getPIEInfo($name, $conf);
self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]);
break;
case 'bitbuckettag': // BitBucket Tag
[$url, $filename] = self::getLatestBitbucketTag($name, $conf);
self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as);
break;
case 'ghtar': // GitHub Release (tar)
[$url, $filename] = self::getLatestGithubTarball($name, $conf);
self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]);
break;
case 'ghtagtar': // GitHub Tag (tar)
[$url, $filename] = self::getLatestGithubTarball($name, $conf, 'tags');
self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, hooks: [[CurlHook::class, 'setupGithubToken']]);
break;
case 'ghrel': // GitHub Release (uploaded)
[$url, $filename] = self::getLatestGithubRelease($name, $conf);
self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as, ['Accept: application/octet-stream'], [[CurlHook::class, 'setupGithubToken']]);
break;
case 'filelist': // Basic File List (regex based crawler)
[$url, $filename] = self::getFromFileList($name, $conf);
self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as);
break;
case 'url': // Direct download URL
$url = $conf['url'];
$filename = $conf['filename'] ?? basename($conf['url']);
self::downloadFile($name, $url, $filename, $conf['path'] ?? $conf['extract'] ?? null, $download_as);
break;
case 'git': // Git repo
self::downloadGit($name, $conf['url'], $conf['rev'], $conf['submodules'] ?? null, $conf['path'] ?? $conf['extract'] ?? null, self::getRetryAttempts(), $download_as);
break;
case 'local': // Local directory, do nothing, just lock it
LockFile::lockSource($name, [
'source_type' => SPC_SOURCE_LOCAL,
'dirname' => $conf['dirname'],
'move_path' => $conf['path'] ?? $conf['extract'] ?? null,
'lock_as' => $download_as,
]);
break;
case 'custom': // Custom download method, like API-based download or other
if (isset($conf['func'])) {
$conf['name'] = $name;
$conf['func']($force, $conf, $download_as);
break;
}
$classes = [
...FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source'),
...FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg'),
];
foreach ($classes as $class) {
if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) {
(new $class())->fetch($force, $conf, $download_as);
break;
}
if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) {
$cls = new $class();
if (in_array($name, $cls->getSupportName())) {
(new $class())->fetch($name, $force, $conf);
break;
}
}
}
break;
default:
throw new DownloaderException("Unknown download type: {$type}");
}
} catch (\Throwable $e) {
// Because sometimes files downloaded through the command line are not automatically deleted after a failure.
// Here we need to manually delete the file if it is detected to exist.
if (isset($filename) && file_exists(DOWNLOAD_PATH . '/' . $filename)) {
logger()->warning("Deleting download file: {$filename}");
unlink(DOWNLOAD_PATH . '/' . $filename);
}
throw new DownloaderException("Download failed: {$e->getMessage()}");
}
}
}

View File

@@ -274,7 +274,7 @@ class FileSystem
public static function convertWinPathToMinGW(string $path): string
{
if (preg_match('/^[A-Za-z]:/', $path)) {
$path = '/' . strtolower(substr($path, 0, 1)) . '/' . str_replace('\\', '/', substr($path, 2));
$path = '/' . strtolower($path[0]) . '/' . str_replace('\\', '/', substr($path, 2));
}
return $path;
}
@@ -314,8 +314,13 @@ class FileSystem
$sub_file = self::convertPath($dir . '/' . $v);
if (is_dir($sub_file) && $recursive) {
# 如果是 目录 且 递推 , 则递推添加下级文件
$list = array_merge($list, self::scanDirFiles($sub_file, $recursive, $relative));
} elseif (is_file($sub_file) || is_dir($sub_file) && !$recursive && $include_dir) {
$sub_list = self::scanDirFiles($sub_file, $recursive, $relative);
if (is_array($sub_list)) {
foreach ($sub_list as $item) {
$list[] = $item;
}
}
} elseif (is_file($sub_file) || (is_dir($sub_file) && !$recursive && $include_dir)) {
# 如果是 文件 或 (是 目录 且 不递推 且 包含目录)
if (is_string($relative) && mb_strpos($sub_file, $relative) === 0) {
$list[] = ltrim(mb_substr($sub_file, mb_strlen($relative)), '/\\');
@@ -440,7 +445,7 @@ class FileSystem
public static function writeFile(string $path, mixed $content, ...$args): bool|int|string
{
$dir = pathinfo(self::convertPath($path), PATHINFO_DIRNAME);
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) {
throw new FileSystemException('Write file failed, cannot create parent directory: ' . $dir);
}
return file_put_contents($path, $content, ...$args);
@@ -579,7 +584,7 @@ class FileSystem
'tar', 'xz', 'txz' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"),
'tgz', 'gz' => f_passthru("tar -xzf {$filename} -C {$target} --strip-components 1"),
'bz2' => f_passthru("tar -xjf {$filename} -C {$target} --strip-components 1"),
'zip' => f_passthru("unzip {$filename} -d {$target}"),
'zip' => self::unzipWithStrip($filename, $target),
default => throw new FileSystemException('unknown archive format: ' . $filename),
};
} elseif (PHP_OS_FAMILY === 'Windows') {
@@ -594,7 +599,7 @@ class FileSystem
match (self::extname($filename)) {
'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"),
'xz', 'txz', 'gz', 'tgz', 'bz2' => cmd()->execWithResult("\"{$_7z}\" x -so {$filename} | tar -f - -x -C \"{$target}\" --strip-components 1"),
'zip' => f_passthru("\"{$_7z}\" x {$filename} -o{$target} -y"),
'zip' => self::unzipWithStrip($filename, $target),
default => throw new FileSystemException("unknown archive format: {$filename}"),
};
}
@@ -630,7 +635,7 @@ class FileSystem
private static function extractWithType(string $source_type, string $filename, string $extract_path): void
{
logger()->debug('Extracting source [' . $source_type . ']: ' . $filename);
logger()->debug("Extracting source [{$source_type}]: {$filename}");
/* @phpstan-ignore-next-line */
match ($source_type) {
SPC_SOURCE_ARCHIVE => self::extractArchive($filename, $extract_path),
@@ -639,4 +644,107 @@ class FileSystem
SPC_SOURCE_LOCAL => symlink(self::convertPath($filename), $extract_path),
};
}
/**
* Move file or directory, handling cross-device scenarios
* Uses rename() if possible, falls back to copy+delete for cross-device moves
*
* @param string $source Source path
* @param string $dest Destination path
*/
private static function moveFileOrDir(string $source, string $dest): void
{
$source = self::convertPath($source);
$dest = self::convertPath($dest);
// Try rename first (fast, atomic)
if (@rename($source, $dest)) {
return;
}
if (is_dir($source)) {
self::copyDir($source, $dest);
self::removeDir($source);
} else {
if (!copy($source, $dest)) {
throw new FileSystemException("Failed to copy file from {$source} to {$dest}");
}
if (!unlink($source)) {
throw new FileSystemException("Failed to remove source file: {$source}");
}
}
}
/**
* Unzip file with stripping top-level directory
*/
private static function unzipWithStrip(string $zip_file, string $extract_path): void
{
$temp_dir = self::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16)));
$zip_file = self::convertPath($zip_file);
$extract_path = self::convertPath($extract_path);
// extract to temp dir
self::createDir($temp_dir);
if (PHP_OS_FAMILY === 'Windows') {
$mute = defined('DEBUG_MODE') ? '' : ' > NUL';
// use php-sdk-binary-tools/bin/7za.exe
$_7z = self::convertPath(getenv('PHP_SDK_PATH') . '/bin/7za.exe');
f_passthru("\"{$_7z}\" x {$zip_file} -o{$temp_dir} -y{$mute}");
} else {
$mute = defined('DEBUG_MODE') ? '' : ' > /dev/null';
f_passthru("unzip \"{$zip_file}\" -d \"{$temp_dir}\"{$mute}");
}
// scan first level dirs (relative, not recursive, include dirs)
$contents = self::scanDirFiles($temp_dir, false, true, true);
if ($contents === false) {
throw new FileSystemException('Cannot scan unzip temp dir: ' . $temp_dir);
}
// if extract path already exists, remove it
if (is_dir($extract_path)) {
self::removeDir($extract_path);
}
// if only one dir, move its contents to extract_path
$subdir = self::convertPath("{$temp_dir}/{$contents[0]}");
if (count($contents) === 1 && is_dir($subdir)) {
self::moveFileOrDir($subdir, $extract_path);
} else {
// else, if it contains only one dir, strip dir and copy other files
$dircount = 0;
$dir = [];
$top_files = [];
foreach ($contents as $item) {
if (is_dir(self::convertPath("{$temp_dir}/{$item}"))) {
++$dircount;
$dir[] = $item;
} else {
$top_files[] = $item;
}
}
// extract dir contents to extract_path
self::createDir($extract_path);
// extract move dir
if ($dircount === 1) {
$sub_contents = self::scanDirFiles("{$temp_dir}/{$dir[0]}", false, true, true);
if ($sub_contents === false) {
throw new FileSystemException("Cannot scan unzip temp sub-dir: {$dir[0]}");
}
foreach ($sub_contents as $sub_item) {
self::moveFileOrDir(self::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"), self::convertPath("{$extract_path}/{$sub_item}"));
}
} else {
foreach ($dir as $item) {
self::moveFileOrDir(self::convertPath("{$temp_dir}/{$item}"), self::convertPath("{$extract_path}/{$item}"));
}
}
// move top-level files to extract_path
foreach ($top_files as $top_file) {
self::moveFileOrDir(self::convertPath("{$temp_dir}/{$top_file}"), self::convertPath("{$extract_path}/{$top_file}"));
}
}
// Clean up temp directory
self::removeDir($temp_dir);
}
}

View File

@@ -155,8 +155,8 @@ class LockFile
* @param string $name Source name
* @param array{
* source_type: string,
* dirname: ?string,
* filename: ?string,
* dirname?: ?string,
* filename?: ?string,
* move_path: ?string,
* lock_as: int
* } $data Source data

View File

@@ -44,7 +44,7 @@ class SourcePatcher
}
foreach ($builder->getLibs() as $lib) {
if ($lib->patchBeforeBuildconf() === true) {
logger()->info("Library [{$lib->getName()}]patched before buildconf");
logger()->info("Library [{$lib->getName()}] patched before buildconf");
}
}
// patch windows php 8.1 bug

View File

@@ -30,10 +30,14 @@ abstract class CustomPackage
/**
* Get the environment variables this package needs to be usable.
* PATH needs to be appended, rather than replaced.
*/
abstract public static function getEnvironment(): array;
/**
* Get the PATH required to use this package.
*/
abstract public static function getPath(): ?string;
abstract public static function isInstalled(): bool;
/**

View File

@@ -13,18 +13,7 @@ class GoXcaddy extends CustomPackage
{
public static function isInstalled(): bool
{
$arch = arch2gnu(php_uname('m'));
$os = match (PHP_OS_FAMILY) {
'Windows' => 'win',
'Darwin' => 'macos',
'BSD' => 'freebsd',
default => 'linux',
};
$packageName = "go-xcaddy-{$arch}-{$os}";
$pkgroot = PKG_ROOT_PATH;
$folder = "{$pkgroot}/{$packageName}";
$folder = PKG_ROOT_PATH . '/go-xcaddy';
return is_dir($folder) && is_file("{$folder}/bin/go") && is_file("{$folder}/bin/xcaddy");
}
@@ -59,7 +48,7 @@ class GoXcaddy extends CustomPackage
'macos' => 'darwin',
default => throw new \InvalidArgumentException('Unsupported OS: ' . $name),
};
$go_version = '1.24.4';
$go_version = '1.25.0';
$config = [
'type' => 'url',
'url' => "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz",
@@ -70,15 +59,15 @@ class GoXcaddy extends CustomPackage
public function extract(string $name): void
{
$pkgroot = PKG_ROOT_PATH;
$go_exec = "{$pkgroot}/{$name}/bin/go";
$xcaddy_exec = "{$pkgroot}/{$name}/bin/xcaddy";
$go_exec = "{$pkgroot}/go-xcaddy/bin/go";
$xcaddy_exec = "{$pkgroot}/go-xcaddy/bin/xcaddy";
if (file_exists($go_exec) && file_exists($xcaddy_exec)) {
return;
}
$lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true);
$source_type = $lock[$name]['source_type'];
$filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']);
$extract = $lock[$name]['move_path'] ?? "{$pkgroot}/{$name}";
$extract = $lock[$name]['move_path'] ?? "{$pkgroot}/go-xcaddy";
FileSystem::extractPackage($name, $source_type, $filename, $extract);
@@ -91,9 +80,9 @@ class GoXcaddy extends CustomPackage
// install xcaddy without using musl tools, xcaddy build requires dynamic linking
shell()
->appendEnv([
'PATH' => "{$pkgroot}/{$name}/bin:" . $sanitizedPath,
'GOROOT' => "{$pkgroot}/{$name}",
'GOBIN' => "{$pkgroot}/{$name}/bin",
'PATH' => "{$pkgroot}/go-xcaddy/bin:" . $sanitizedPath,
'GOROOT' => "{$pkgroot}/go-xcaddy",
'GOBIN' => "{$pkgroot}/go-xcaddy/bin",
'GOPATH' => "{$pkgroot}/go",
])
->exec('CC=cc go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest');
@@ -101,22 +90,17 @@ class GoXcaddy extends CustomPackage
public static function getEnvironment(): array
{
$arch = arch2gnu(php_uname('m'));
$os = match (PHP_OS_FAMILY) {
'Windows' => 'win',
'Darwin' => 'macos',
'BSD' => 'freebsd',
default => 'linux',
};
$packageName = "go-xcaddy-{$arch}-{$os}";
$packageName = 'go-xcaddy';
$pkgroot = PKG_ROOT_PATH;
return [
'PATH' => "{$pkgroot}/{$packageName}/bin",
'GOROOT' => "{$pkgroot}/{$packageName}",
'GOBIN' => "{$pkgroot}/{$packageName}/bin",
'GOPATH' => "{$pkgroot}/go",
];
}
public static function getPath(): ?string
{
return PKG_ROOT_PATH . '/go-xcaddy/bin';
}
}

View File

@@ -103,7 +103,7 @@ class Zig extends CustomPackage
public function extract(string $name): void
{
$pkgroot = PKG_ROOT_PATH;
$zig_bin_dir = "{$pkgroot}/{$name}";
$zig_bin_dir = "{$pkgroot}/zig";
$files = ['zig', 'zig-cc', 'zig-c++', 'zig-ar', 'zig-ld.lld', 'zig-ranlib', 'zig-objcopy'];
$all_exist = true;
@@ -120,7 +120,7 @@ class Zig extends CustomPackage
$lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true);
$source_type = $lock[$name]['source_type'];
$filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']);
$extract = "{$pkgroot}/{$name}";
$extract = "{$pkgroot}/zig";
FileSystem::extractPackage($name, $source_type, $filename, $extract);
@@ -129,34 +129,12 @@ class Zig extends CustomPackage
public static function getEnvironment(): array
{
$arch = arch2gnu(php_uname('m'));
$os = match (PHP_OS_FAMILY) {
'Windows' => 'win',
'Darwin' => 'macos',
'BSD' => 'freebsd',
default => 'linux',
};
$packageName = "zig-{$arch}-{$os}";
$path = PKG_ROOT_PATH . "/{$packageName}";
return [
'PATH' => $path,
];
return [];
}
private static function getPath(): string
public static function getPath(): ?string
{
$arch = arch2gnu(php_uname('m'));
$os = match (PHP_OS_FAMILY) {
'Windows' => 'win',
'Darwin' => 'macos',
'BSD' => 'freebsd',
default => 'linux',
};
$packageName = "zig-{$arch}-{$os}";
return PKG_ROOT_PATH . "/{$packageName}";
return PKG_ROOT_PATH . '/zig';
}
private function createZigCcScript(string $bin_dir): void

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
BUILDROOT_ABS="$(realpath "$SCRIPT_DIR/../../buildroot/include" 2>/dev/null || true)"
BUILDROOT_ABS="$(realpath "$SCRIPT_DIR/../../../buildroot/include" 2>/dev/null || true)"
PARSED_ARGS=()
while [[ $# -gt 0 ]]; do
@@ -19,9 +19,15 @@ while [[ $# -gt 0 ]]; do
ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)"
[[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG")
;;
-march=*|-mcpu=*) # replace -march=x86-64 with -march=x86_64
-march=*|-mcpu=*)
OPT_NAME="${1%%=*}"
OPT_VALUE="${1#*=}"
# Skip armv8- flags entirely as Zig doesn't support them
if [[ "$OPT_VALUE" == armv8-* ]]; then
shift
continue
fi
# replace -march=x86-64 with -march=x86_64
OPT_VALUE="${OPT_VALUE//-/_}"
PARSED_ARGS+=("${OPT_NAME}=${OPT_VALUE}")
shift

View File

@@ -16,11 +16,11 @@ class PhpSource extends CustomSourceBase
{
$major = defined('SPC_BUILD_PHP_VERSION') ? SPC_BUILD_PHP_VERSION : '8.4';
if ($major === '8.5') {
Downloader::downloadSource('php-src', ['type' => 'url', 'url' => 'https://downloads.php.net/~edorian/php-8.5.0beta1.tar.xz'], $force);
Downloader::downloadSource('php-src', ['type' => 'url', 'url' => 'https://downloads.php.net/~daniels/php-8.5.0RC3.tar.xz'], $force);
} elseif ($major === 'git') {
Downloader::downloadSource('php-src', ['type' => 'git', 'url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $force);
} else {
Downloader::downloadSource('php-src', self::getLatestPHPInfo($major), $force);
Downloader::downloadSource('php-src', $this->getLatestPHPInfo($major), $force);
}
}
@@ -33,7 +33,7 @@ class PhpSource extends CustomSourceBase
// 查找最新的小版本号
$info = json_decode(Downloader::curlExec(
url: "https://www.php.net/releases/index.php?json&version={$major_version}",
retries: intval(getenv('SPC_DOWNLOAD_RETRIES') ?: 0)
retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0
), true);
if (!isset($info['version'])) {
throw new DownloaderException("Version {$major_version} not found.");

View File

@@ -42,10 +42,10 @@ class ZigToolchain implements ToolchainInterface
public function afterInit(): void
{
if (!is_dir(Zig::getEnvironment()['PATH'])) {
if (!Zig::isInstalled()) {
throw new EnvironmentException('You are building with zig, but zig is not installed, please install zig first. (You can use `doctor` command to install it)');
}
GlobalEnvManager::addPathIfNotExists(Zig::getEnvironment()['PATH']);
GlobalEnvManager::addPathIfNotExists(Zig::getPath());
f_passthru('ulimit -n 2048'); // zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough
$cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: '';
$cxxflags = getenv('SPC_DEFAULT_CXX_FLAGS') ?: '';
@@ -64,6 +64,11 @@ class ZigToolchain implements ToolchainInterface
$extra_libs = trim($extra_libs . ' -lunwind');
GlobalEnvManager::putenv("SPC_EXTRA_LIBS={$extra_libs}");
}
$cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: '';
$has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4');
if (!$has_avx512) {
GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no');
}
}
public function getCompilerInfo(): ?string

View File

@@ -11,6 +11,150 @@ use Symfony\Component\Yaml\Yaml;
class ConfigValidator
{
/**
* Global field type definitions
* Maps field names to their expected types and validation rules
* Note: This only includes fields used in config files (source.json, lib.json, ext.json, pkg.json, pre-built.json)
*/
private const array FIELD_TYPES = [
// String fields
'url' => 'string', // url
'regex' => 'string', // regex pattern
'rev' => 'string', // revision/branch
'repo' => 'string', // repository name
'match' => 'string', // match pattern (aaa*bbb)
'filename' => 'string', // filename
'path' => 'string', // copy path
'extract' => 'string', // copy path (alias of path)
'dirname' => 'string', // directory name for local source
'source' => 'string', // the source name that this item uses
'match-pattern-linux' => 'string', // pre-built match pattern for linux
'match-pattern-macos' => 'string', // pre-built match pattern for macos
'match-pattern-windows' => 'string', // pre-built match pattern for windows
// Boolean fields
'prefer-stable' => 'bool', // prefer stable releases
'provide-pre-built' => 'bool', // provide pre-built binaries
'notes' => 'bool', // whether to show notes in docs
'cpp-library' => 'bool', // whether this is a C++ library
'cpp-extension' => 'bool', // whether this is a C++ extension
'build-with-php' => 'bool', // whether if this extension can be built to shared with PHP source together
'zend-extension' => 'bool', // whether this is a zend extension
'unix-only' => 'bool', // whether this extension is only for unix-like systems
// Array fields
'submodules' => 'array', // git submodules list (for git source type)
'lib-depends' => 'list',
'lib-suggests' => 'list',
'ext-depends' => 'list',
'ext-suggests' => 'list',
'static-libs' => 'list',
'pkg-configs' => 'list', // required pkg-config files without suffix (e.g. [libwebp])
'headers' => 'list', // required header files
'bin' => 'list', // required binary files
'frameworks' => 'list', // shared library frameworks (macOS)
// Object/assoc array fields
'support' => 'object', // extension OS support docs
'extract-files' => 'object', // pkg.json extract files mapping with match pattern
'alt' => 'object|bool', // alternative source/package
'license' => 'object|array', // license information
'target' => 'array', // extension build targets (default: [static], alternate: [shared] or both)
// Special/mixed fields
'func' => 'callable', // custom download function for custom source/package type
'type' => 'string', // type field (validated separately)
];
/**
* Source/Package download type validation rules
* Maps type names to [required_props, optional_props]
*/
private const array SOURCE_TYPE_FIELDS = [
'filelist' => [['url', 'regex'], []],
'git' => [['url', 'rev'], ['path', 'extract', 'submodules']],
'ghtagtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']],
'ghtar' => [['repo'], ['path', 'extract', 'prefer-stable', 'match']],
'ghrel' => [['repo', 'match'], ['path', 'extract', 'prefer-stable']],
'url' => [['url'], ['filename', 'path', 'extract']],
'bitbuckettag' => [['repo'], ['path', 'extract']],
'local' => [['dirname'], ['path', 'extract']],
'pie' => [['repo'], ['path']],
'custom' => [[], ['func']],
];
/**
* Source.json specific fields [field_name => required]
* Note: 'type' is validated separately in validateSourceTypeConfig
* Field types are defined in FIELD_TYPES constant
*/
private const array SOURCE_FIELDS = [
'type' => true, // source type (must be SOURCE_TYPE_FIELDS key)
'provide-pre-built' => false, // whether to provide pre-built binaries
'alt' => false, // alternative source configuration
'license' => false, // license information for source
// ... other fields are validated based on source type
];
/**
* Lib.json specific fields [field_name => required]
* Field types are defined in FIELD_TYPES constant
*/
private const array LIB_FIELDS = [
'type' => false, // lib type (lib/package/target/root)
'source' => false, // the source name that this lib uses
'lib-depends' => false, // required libraries
'lib-suggests' => false, // suggested libraries
'static-libs' => false, // Generated static libraries
'pkg-configs' => false, // Generated pkg-config files
'cpp-library' => false, // whether this is a C++ library
'headers' => false, // Generated header files
'bin' => false, // Generated binary files
'frameworks' => false, // Used shared library frameworks (macOS)
];
/**
* Ext.json specific fields [field_name => required]
* Field types are defined in FIELD_TYPES constant
*/
private const array EXT_FIELDS = [
'type' => true, // extension type (builtin/external/addon/wip)
'source' => false, // the source name that this extension uses
'support' => false, // extension OS support docs
'notes' => false, // whether to show notes in docs
'cpp-extension' => false, // whether this is a C++ extension
'build-with-php' => false, // whether if this extension can be built to shared with PHP source together
'target' => false, // extension build targets (default: [static], alternate: [shared] or both)
'lib-depends' => false,
'lib-suggests' => false,
'ext-depends' => false,
'ext-suggests' => false,
'frameworks' => false,
'zend-extension' => false, // whether this is a zend extension
'unix-only' => false, // whether this extension is only for unix-like systems
];
/**
* Pkg.json specific fields [field_name => required]
* Field types are defined in FIELD_TYPES constant
*/
private const array PKG_FIELDS = [
'type' => true, // package type (same as source type)
'extract-files' => false, // files to extract mapping (source pattern => target path)
];
/**
* Pre-built.json specific fields [field_name => required]
* Field types are defined in FIELD_TYPES constant
*/
private const array PRE_BUILT_FIELDS = [
'repo' => true, // repository name for pre-built binaries
'prefer-stable' => false, // prefer stable releases
'match-pattern-linux' => false, // pre-built match pattern for linux
'match-pattern-macos' => false, // pre-built match pattern for macos
'match-pattern-windows' => false, // pre-built match pattern for windows
];
/**
* Validate source.json
*
@@ -22,33 +166,20 @@ class ConfigValidator
// Validate basic source type configuration
self::validateSourceTypeConfig($src, $name, 'source');
// Check source-specific fields
// Validate all source-specific fields using unified method
self::validateConfigFields($src, $name, 'source', self::SOURCE_FIELDS);
// Check for unknown fields
self::validateAllowedFields($src, $name, 'source', self::SOURCE_FIELDS);
// check if alt is valid
if (isset($src['alt'])) {
if (!is_assoc_array($src['alt']) && !is_bool($src['alt'])) {
throw new ValidationException("source {$name} alt must be object or boolean");
}
if (is_assoc_array($src['alt'])) {
// validate alt source recursively
self::validateSource([$name . '_alt' => $src['alt']]);
}
}
// check if provide-pre-built is boolean
if (isset($src['provide-pre-built']) && !is_bool($src['provide-pre-built'])) {
throw new ValidationException("source {$name} provide-pre-built must be boolean");
}
// check if prefer-stable is boolean
if (isset($src['prefer-stable']) && !is_bool($src['prefer-stable'])) {
throw new ValidationException("source {$name} prefer-stable must be boolean");
if (isset($src['alt']) && is_assoc_array($src['alt'])) {
// validate alt source recursively
self::validateSource([$name . '_alt' => $src['alt']]);
}
// check if license is valid
if (isset($src['license'])) {
if (!is_array($src['license'])) {
throw new ValidationException("source {$name} license must be an object or array");
}
if (is_assoc_array($src['license'])) {
self::checkSingleLicense($src['license'], $name);
} elseif (is_list_array($src['license'])) {
@@ -58,8 +189,6 @@ class ConfigValidator
}
self::checkSingleLicense($license, $name);
}
} else {
throw new ValidationException("source {$name} license must be an object or array");
}
}
}
@@ -71,9 +200,8 @@ class ConfigValidator
if (!is_array($data)) {
throw new ValidationException('lib.json is broken');
}
// check each lib
foreach ($data as $name => $lib) {
// check if lib is an assoc array
if (!is_assoc_array($lib)) {
throw new ValidationException("lib {$name} is not an object");
}
@@ -89,36 +217,22 @@ class ConfigValidator
if (isset($lib['source']) && !empty($source_data) && !isset($source_data[$lib['source']])) {
throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}");
}
// check if source is string
if (isset($lib['source']) && !is_string($lib['source'])) {
throw new ValidationException("lib {$name} source must be string");
}
// check if [lib-depends|lib-suggests|static-libs|headers|bin][-windows|-unix|-macos|-linux] are valid list array
// Validate basic fields using unified method
self::validateConfigFields($lib, $name, 'lib', self::LIB_FIELDS);
// Validate list array fields with suffixes
$suffixes = ['', '-windows', '-unix', '-macos', '-linux'];
foreach ($suffixes as $suffix) {
if (isset($lib['lib-depends' . $suffix]) && !is_list_array($lib['lib-depends' . $suffix])) {
throw new ValidationException("lib {$name} lib-depends must be a list");
}
if (isset($lib['lib-suggests' . $suffix]) && !is_list_array($lib['lib-suggests' . $suffix])) {
throw new ValidationException("lib {$name} lib-suggests must be a list");
}
if (isset($lib['static-libs' . $suffix]) && !is_list_array($lib['static-libs' . $suffix])) {
throw new ValidationException("lib {$name} static-libs must be a list");
}
if (isset($lib['pkg-configs' . $suffix]) && !is_list_array($lib['pkg-configs' . $suffix])) {
throw new ValidationException("lib {$name} pkg-configs must be a list");
}
if (isset($lib['headers' . $suffix]) && !is_list_array($lib['headers' . $suffix])) {
throw new ValidationException("lib {$name} headers must be a list");
}
if (isset($lib['bin' . $suffix]) && !is_list_array($lib['bin' . $suffix])) {
throw new ValidationException("lib {$name} bin must be a list");
}
}
// check if frameworks is a list array
if (isset($lib['frameworks']) && !is_list_array($lib['frameworks'])) {
throw new ValidationException("lib {$name} frameworks must be a list");
$fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin'];
self::validateListArrayFields($lib, $name, 'lib', $fields, $suffixes);
// Validate frameworks (special case without suffix)
if (isset($lib['frameworks'])) {
self::validateFieldType('frameworks', $lib['frameworks'], $name, 'lib');
}
// Check for unknown fields
self::validateAllowedFields($lib, $name, 'lib', self::LIB_FIELDS);
}
}
@@ -127,61 +241,34 @@ class ConfigValidator
if (!is_array($data)) {
throw new ValidationException('ext.json is broken');
}
// check each extension
foreach ($data as $name => $ext) {
// check if ext is an assoc array
if (!is_assoc_array($ext)) {
throw new ValidationException("ext {$name} is not an object");
}
// check if ext has valid type
if (!in_array($ext['type'] ?? '', ['builtin', 'external', 'addon', 'wip'])) {
throw new ValidationException("ext {$name} type is invalid");
}
// check if external ext has source
// Check source field requirement
if (($ext['type'] ?? '') === 'external' && !isset($ext['source'])) {
throw new ValidationException("ext {$name} does not assign any source");
}
// check if source is string
if (isset($ext['source']) && !is_string($ext['source'])) {
throw new ValidationException("ext {$name} source must be string");
}
// check if support is valid
if (isset($ext['support']) && !is_assoc_array($ext['support'])) {
throw new ValidationException("ext {$name} support must be an object");
}
// check if notes is boolean
if (isset($ext['notes']) && !is_bool($ext['notes'])) {
throw new ValidationException("ext {$name} notes must be boolean");
}
// check if [lib-depends|lib-suggests|ext-depends][-windows|-unix|-macos|-linux] are valid list array
// Validate basic fields using unified method
self::validateConfigFields($ext, $name, 'ext', self::EXT_FIELDS);
// Validate list array fields with suffixes
$suffixes = ['', '-windows', '-unix', '-macos', '-linux'];
foreach ($suffixes as $suffix) {
if (isset($ext['lib-depends' . $suffix]) && !is_list_array($ext['lib-depends' . $suffix])) {
throw new ValidationException("ext {$name} lib-depends must be a list");
}
if (isset($ext['lib-suggests' . $suffix]) && !is_list_array($ext['lib-suggests' . $suffix])) {
throw new ValidationException("ext {$name} lib-suggests must be a list");
}
if (isset($ext['ext-depends' . $suffix]) && !is_list_array($ext['ext-depends' . $suffix])) {
throw new ValidationException("ext {$name} ext-depends must be a list");
}
}
// check if arg-type is valid
if (isset($ext['arg-type'])) {
$valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path'];
if (!in_array($ext['arg-type'], $valid_arg_types)) {
throw new ValidationException("ext {$name} arg-type is invalid");
}
}
// check if arg-type with suffix is valid
foreach ($suffixes as $suffix) {
if (isset($ext['arg-type' . $suffix])) {
$valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path'];
if (!in_array($ext['arg-type' . $suffix], $valid_arg_types)) {
throw new ValidationException("ext {$name} arg-type{$suffix} is invalid");
}
}
}
$fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests'];
self::validateListArrayFields($ext, $name, 'ext', $fields, $suffixes);
// Validate arg-type fields
self::validateArgTypeFields($ext, $name, $suffixes);
// Check for unknown fields
self::validateAllowedFields($ext, $name, 'ext', self::EXT_FIELDS);
}
}
@@ -200,12 +287,11 @@ class ConfigValidator
// Validate basic source type configuration (reuse from source validation)
self::validateSourceTypeConfig($pkg, $name, 'pkg');
// Check pkg-specific fields
// check if extract-files is valid
// Validate all pkg-specific fields using unified method
self::validateConfigFields($pkg, $name, 'pkg', self::PKG_FIELDS);
// Validate extract-files content (object validation is done by validateFieldType)
if (isset($pkg['extract-files'])) {
if (!is_assoc_array($pkg['extract-files'])) {
throw new ValidationException("pkg {$name} extract-files must be an object");
}
// check each extract file mapping
foreach ($pkg['extract-files'] as $source => $target) {
if (!is_string($source) || !is_string($target)) {
@@ -213,6 +299,9 @@ class ConfigValidator
}
}
}
// Check for unknown fields
self::validateAllowedFields($pkg, $name, 'pkg', self::PKG_FIELDS);
}
}
@@ -227,18 +316,11 @@ class ConfigValidator
throw new ValidationException('pre-built.json is broken');
}
// Check required fields
if (!isset($data['repo'])) {
throw new ValidationException('pre-built.json must have [repo] field');
}
if (!is_string($data['repo'])) {
throw new ValidationException('pre-built.json [repo] must be string');
}
// Validate all fields using unified method
self::validateConfigFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS);
// Check optional prefer-stable field
if (isset($data['prefer-stable']) && !is_bool($data['prefer-stable'])) {
throw new ValidationException('pre-built.json [prefer-stable] must be boolean');
}
// Check for unknown fields
self::validateAllowedFields($data, 'pre-built', 'pre-built', self::PRE_BUILT_FIELDS);
// Check match pattern fields (at least one must exist)
$pattern_fields = ['match-pattern-linux', 'match-pattern-macos', 'match-pattern-windows'];
@@ -247,9 +329,6 @@ class ConfigValidator
foreach ($pattern_fields as $field) {
if (isset($data[$field])) {
$has_pattern = true;
if (!is_string($data[$field])) {
throw new ValidationException("pre-built.json [{$field}] must be string");
}
// Validate pattern contains required placeholders
if (!str_contains($data[$field], '{name}')) {
throw new ValidationException("pre-built.json [{$field}] must contain {name} placeholder");
@@ -403,6 +482,52 @@ class ConfigValidator
return $craft;
}
/**
* Validate a field based on its global type definition
*
* @param string $field Field name
* @param mixed $value Field value
* @param string $name Item name (for error messages)
* @param string $type Item type (for error messages)
* @return bool Returns true if validation passes
*/
private static function validateFieldType(string $field, mixed $value, string $name, string $type): bool
{
// Check if field exists in FIELD_TYPES
if (!isset(self::FIELD_TYPES[$field])) {
// Try to strip suffix and check base field name
$suffixes = ['-windows', '-unix', '-macos', '-linux'];
$base_field = $field;
foreach ($suffixes as $suffix) {
if (str_ends_with($field, $suffix)) {
$base_field = substr($field, 0, -strlen($suffix));
break;
}
}
if (!isset(self::FIELD_TYPES[$base_field])) {
// Unknown field is not allowed - strict validation
throw new ValidationException("{$type} {$name} has unknown field [{$field}]");
}
// Use base field type for validation
$expected_type = self::FIELD_TYPES[$base_field];
} else {
$expected_type = self::FIELD_TYPES[$field];
}
return match ($expected_type) {
'string' => is_string($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be string"),
'bool' => is_bool($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be boolean"),
'array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be array"),
'list' => is_list_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be a list"),
'object' => is_assoc_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object"),
'object|bool' => (is_assoc_array($value) || is_bool($value)) ?: throw new ValidationException("{$type} {$name} [{$field}] must be object or boolean"),
'object|array' => is_array($value) ?: throw new ValidationException("{$type} {$name} [{$field}] must be an object or array"),
'callable' => true, // Skip validation for callable
};
}
private static function checkSingleLicense(array $license, string $name): void
{
if (!is_assoc_array($license)) {
@@ -414,9 +539,6 @@ class ConfigValidator
if (!in_array($license['type'], ['file', 'text'])) {
throw new ValidationException("source {$name} license type is invalid");
}
if (!in_array($license['type'], ['file', 'text'])) {
throw new ValidationException("source {$name} license type is invalid");
}
if ($license['type'] === 'file' && !isset($license['path'])) {
throw new ValidationException("source {$name} license file must have path");
}
@@ -440,68 +562,127 @@ class ConfigValidator
if (!is_string($item['type'])) {
throw new ValidationException("{$config_type} {$name} type prop must be string");
}
if (!in_array($item['type'], ['filelist', 'git', 'ghtagtar', 'ghtar', 'ghrel', 'url', 'custom'])) {
if (!isset(self::SOURCE_TYPE_FIELDS[$item['type']])) {
throw new ValidationException("{$config_type} {$name} type [{$item['type']}] is invalid");
}
// Validate type-specific requirements
switch ($item['type']) {
case 'filelist':
if (!isset($item['url'], $item['regex'])) {
throw new ValidationException("{$config_type} {$name} needs [url] and [regex] props");
[$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']];
// Check required fields exist
foreach ($required as $prop) {
if (!isset($item[$prop])) {
$props = implode('] and [', $required);
throw new ValidationException("{$config_type} {$name} needs [{$props}] props");
}
}
// Validate field types using global field type definitions
foreach (array_merge($required, $optional) as $prop) {
if (isset($item[$prop])) {
self::validateFieldType($prop, $item[$prop], $name, $config_type);
}
}
}
/**
* Validate that fields with suffixes are list arrays
*/
private static function validateListArrayFields(array $item, string $name, string $type, array $fields, array $suffixes): void
{
foreach ($fields as $field) {
foreach ($suffixes as $suffix) {
$key = $field . $suffix;
if (isset($item[$key])) {
self::validateFieldType($key, $item[$key], $name, $type);
}
if (!is_string($item['url']) || !is_string($item['regex'])) {
throw new ValidationException("{$config_type} {$name} [url] and [regex] must be string");
}
}
}
/**
* Validate arg-type fields with suffixes
*/
private static function validateArgTypeFields(array $item, string $name, array $suffixes): void
{
$valid_arg_types = ['enable', 'with', 'with-path', 'custom', 'none', 'enable-path'];
foreach (array_merge([''], $suffixes) as $suffix) {
$key = 'arg-type' . $suffix;
if (isset($item[$key]) && !in_array($item[$key], $valid_arg_types)) {
throw new ValidationException("ext {$name} {$key} is invalid");
}
}
}
/**
* Unified method to validate config fields based on field definitions
*
* @param array $item Item data to validate
* @param string $name Item name for error messages
* @param string $type Config type (source, lib, ext, pkg, pre-built)
* @param array $field_definitions Field definitions [field_name => required (bool)]
*/
private static function validateConfigFields(array $item, string $name, string $type, array $field_definitions): void
{
foreach ($field_definitions as $field => $required) {
if ($required && !isset($item[$field])) {
throw new ValidationException("{$type} {$name} must have [{$field}] field");
}
if (isset($item[$field])) {
self::validateFieldType($field, $item[$field], $name, $type);
}
}
}
/**
* Validate that item only contains allowed fields
* This method checks for unknown fields based on the config type
*
* @param array $item Item data to validate
* @param string $name Item name for error messages
* @param string $type Config type (source, lib, ext, pkg, pre-built)
* @param array $field_definitions Field definitions [field_name => required (bool)]
*/
private static function validateAllowedFields(array $item, string $name, string $type, array $field_definitions): void
{
// For source and pkg types, we need to check SOURCE_TYPE_FIELDS as well
$allowed_fields = array_keys($field_definitions);
// For source/pkg, add allowed fields from SOURCE_TYPE_FIELDS based on the type
if (in_array($type, ['source', 'pkg']) && isset($item['type'], self::SOURCE_TYPE_FIELDS[$item['type']])) {
[$required, $optional] = self::SOURCE_TYPE_FIELDS[$item['type']];
$allowed_fields = array_merge($allowed_fields, $required, $optional);
}
// For lib and ext types, add fields with suffixes
if (in_array($type, ['lib', 'ext'])) {
$suffixes = ['-windows', '-unix', '-macos', '-linux'];
$base_fields = ['lib-depends', 'lib-suggests', 'static-libs', 'pkg-configs', 'headers', 'bin'];
if ($type === 'ext') {
$base_fields = ['lib-depends', 'lib-suggests', 'ext-depends', 'ext-suggests'];
// Add arg-type fields
foreach (array_merge([''], $suffixes) as $suffix) {
$allowed_fields[] = 'arg-type' . $suffix;
}
break;
case 'git':
if (!isset($item['url'], $item['rev'])) {
throw new ValidationException("{$config_type} {$name} needs [url] and [rev] props");
}
foreach ($base_fields as $field) {
foreach ($suffixes as $suffix) {
$allowed_fields[] = $field . $suffix;
}
if (!is_string($item['url']) || !is_string($item['rev'])) {
throw new ValidationException("{$config_type} {$name} [url] and [rev] must be string");
}
if (isset($item['path']) && !is_string($item['path'])) {
throw new ValidationException("{$config_type} {$name} [path] must be string");
}
break;
case 'ghtagtar':
case 'ghtar':
if (!isset($item['repo'])) {
throw new ValidationException("{$config_type} {$name} needs [repo] prop");
}
if (!is_string($item['repo'])) {
throw new ValidationException("{$config_type} {$name} [repo] must be string");
}
if (isset($item['path']) && !is_string($item['path'])) {
throw new ValidationException("{$config_type} {$name} [path] must be string");
}
break;
case 'ghrel':
if (!isset($item['repo'], $item['match'])) {
throw new ValidationException("{$config_type} {$name} needs [repo] and [match] props");
}
if (!is_string($item['repo']) || !is_string($item['match'])) {
throw new ValidationException("{$config_type} {$name} [repo] and [match] must be string");
}
break;
case 'url':
if (!isset($item['url'])) {
throw new ValidationException("{$config_type} {$name} needs [url] prop");
}
if (!is_string($item['url'])) {
throw new ValidationException("{$config_type} {$name} [url] must be string");
}
if (isset($item['filename']) && !is_string($item['filename'])) {
throw new ValidationException("{$config_type} {$name} [filename] must be string");
}
if (isset($item['path']) && !is_string($item['path'])) {
throw new ValidationException("{$config_type} {$name} [path] must be string");
}
break;
case 'custom':
// custom type has no specific requirements
break;
}
// frameworks is lib-only
if ($type === 'lib') {
$allowed_fields[] = 'frameworks';
}
}
// Check each field in item
foreach (array_keys($item) as $field) {
if (!in_array($field, $allowed_fields)) {
throw new ValidationException("{$type} {$name} has unknown field [{$field}]");
}
}
}
}

View File

@@ -40,7 +40,12 @@ class GlobalEnvManager
if (is_unix()) {
self::addPathIfNotExists(BUILD_BIN_PATH);
self::addPathIfNotExists(PKG_ROOT_PATH . '/bin');
self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . '/pkgconfig');
$pkgConfigPath = getenv('PKG_CONFIG_PATH');
if ($pkgConfigPath !== false) {
self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . "/pkgconfig:{$pkgConfigPath}");
} else {
self::putenv('PKG_CONFIG_PATH=' . BUILD_LIB_PATH . '/pkgconfig');
}
}
$ini = self::readIniFile();

View File

@@ -6,6 +6,7 @@ namespace SPC\util;
use SPC\builder\BuilderBase;
use SPC\builder\BuilderProvider;
use SPC\builder\Extension;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use Symfony\Component\Console\Input\ArgvInput;
@@ -87,7 +88,7 @@ class SPCConfigUtil
if (SPCTarget::getTargetOS() === 'Darwin') {
$libs .= " {$this->getFrameworksString($extensions)}";
}
if ($this->builder->hasCpp()) {
if ($this->hasCpp($extensions, $libraries)) {
$libcpp = SPCTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++';
$libs = str_replace($libcpp, '', $libs) . " {$libcpp}";
}
@@ -123,6 +124,27 @@ class SPCConfigUtil
];
}
private function hasCpp(array $extensions, array $libraries): bool
{
// judge cpp-extension
$builderExtNames = array_keys($this->builder->getExts(false));
$exts = array_unique([...$builderExtNames, ...$extensions]);
foreach ($exts as $ext) {
if (Config::getExt($ext, 'cpp-extension', false) === true) {
return true;
}
}
$builderLibNames = array_keys($this->builder->getLibs());
$libs = array_unique([...$builderLibNames, ...$libraries]);
foreach ($libs as $lib) {
if (Config::getLib($lib, 'cpp-library', false) === true) {
return true;
}
}
return false;
}
private function getIncludesString(array $libraries): string
{
$base = BUILD_INCLUDE_PATH;

View File

@@ -50,18 +50,22 @@ class UnixAutoconfExecutor extends Executor
* @param bool $with_clean Whether to clean before building
* @param array $after_env_vars Environment variables postfix
*/
public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = []): static
public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = [], ?string $dir = null): static
{
return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars) {
return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars, $dir) {
$shell = $this->shell;
if ($dir) {
$shell = $shell->cd($dir);
}
if ($with_clean) {
$this->shell->exec('make clean');
$shell->exec('make clean');
}
$after_env_vars_str = $after_env_vars !== [] ? shell()->setEnv($after_env_vars)->getEnvString() : '';
$this->shell->exec("make -j{$this->library->getBuilder()->concurrency} {$target} {$after_env_vars_str}");
$shell->exec("make -j{$this->library->getBuilder()->concurrency} {$target} {$after_env_vars_str}");
if ($with_install !== false) {
$this->shell->exec("make {$with_install}");
$shell->exec("make {$with_install}");
}
return $this->shell;
return $shell;
});
}

View File

@@ -205,7 +205,7 @@ SET(CMAKE_INSTALL_PREFIX "{$root}")
SET(CMAKE_INSTALL_LIBDIR "lib")
set(PKG_CONFIG_EXECUTABLE "{$pkgConfigExecutable}")
list(APPEND PKG_CONFIG_EXECUTABLE "--static")
set(PKG_CONFIG_ARGN "--static" CACHE STRING "Extra arguments for pkg-config" FORCE)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

View File

@@ -8,6 +8,7 @@ use SPC\builder\freebsd\library\BSDLibraryBase;
use SPC\builder\linux\library\LinuxLibraryBase;
use SPC\builder\macos\library\MacOSLibraryBase;
use SPC\exception\SPCInternalException;
use SPC\util\SPCTarget;
use ZM\Logger\ConsoleColor;
/**
@@ -28,6 +29,7 @@ class UnixShell extends Shell
public function exec(string $cmd): static
{
$cmd = clean_spaces($cmd);
/* @phpstan-ignore-next-line */
logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd));
$original_command = $cmd;
@@ -48,7 +50,7 @@ class UnixShell extends Shell
'CFLAGS' => $library->getLibExtraCFlags(),
'CXXFLAGS' => $library->getLibExtraCXXFlags(),
'LDFLAGS' => $library->getLibExtraLdFlags(),
'LIBS' => $library->getLibExtraLibs(),
'LIBS' => $library->getLibExtraLibs() . SPCTarget::getRuntimeLibs(),
]);
return $this;
}

View File

@@ -4,20 +4,40 @@ declare(strict_types=1);
assert(function_exists('gettext'));
assert(function_exists('bindtextdomain'));
assert(function_exists('bind_textdomain_codeset'));
assert(function_exists('textdomain'));
if (!is_dir('locale/en_US/LC_MESSAGES/')) {
mkdir('locale/en_US/LC_MESSAGES/', 0755, true);
foreach (['en_US', 'en_GB'] as $lc) {
$dir = "locale/{$lc}/LC_MESSAGES";
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$mo = '3hIElQAAAAACAAAAHAAAACwAAAAFAAAAPAAAAAAAAABQAAAABgAAAFEAAAAXAQAAWAAAAAcAAABwAQAAAQAAAAAAAAAAAAAAAgAAAAAAAAAA56S65L6LAFByb2plY3QtSWQtVmVyc2lvbjogUEFDS0FHRSBWRVJTSU9OClJlcG9ydC1Nc2dpZC1CdWdzLVRvOiAKUE8tUmV2aXNpb24tRGF0ZTogWUVBUi1NTy1EQSBITzpNSytaT05FCkxhc3QtVHJhbnNsYXRvcjogRlVMTCBOQU1FIDxFTUFJTEBBRERSRVNTPgpMYW5ndWFnZS1UZWFtOiBMQU5HVUFHRSA8TExAbGkub3JnPgpMYW5ndWFnZTogCk1JTUUtVmVyc2lvbjogMS4wCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD1VVEYtOApDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA4Yml0CgBFeGFtcGxlAA==';
$path = "{$dir}/test.mo";
if (!file_exists($path)) {
file_put_contents($path, base64_decode($mo));
}
}
if (!file_exists('locale/en_US/LC_MESSAGES/test.mo')) {
$mo = '3hIElQAAAAACAAAAHAAAACwAAAAFAAAAPAAAAAAAAABQAAAABgAAAFEAAAAXAQAAWAAAAAcAAABwAQAAAQAAAAAAAAAAAAAAAgAAAAAAAAAA56S65L6LAFByb2plY3QtSWQtVmVyc2lvbjogUEFDS0FHRSBWRVJTSU9OClJlcG9ydC1Nc2dpZC1CdWdzLVRvOiAKUE8tUmV2aXNpb24tRGF0ZTogWUVBUi1NTy1EQSBITzpNSStaT05FCkxhc3QtVHJhbnNsYXRvcjogRlVMTCBOQU1FIDxFTUFJTEBBRERSRVNTPgpMYW5ndWFnZS1UZWFtOiBMQU5HVUFHRSA8TExAbGkub3JnPgpMYW5ndWFnZTogCk1JTUUtVmVyc2lvbjogMS4wCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFpbjsgY2hhcnNldD1VVEYtOApDb250ZW50LVRyYW5zZmVyLUVuY29kaW5nOiA4Yml0CgBFeGFtcGxlAA==';
file_put_contents('locale/en_US/LC_MESSAGES/test.mo', base64_decode($mo));
}
putenv('LANG=en_US');
assert(setlocale(LC_ALL, 'en_US.utf-8') === 'en_US.utf-8');
// Probe for an available English locale
$candidates = [
'en_US.UTF-8', 'en_US.utf8', 'en_US.utf-8', 'en_US',
'en_GB.UTF-8', 'en_GB.utf8', 'en_GB.utf-8', 'en_GB',
'English_United States.65001', 'English_United States.1252',
'English_United Kingdom.65001', 'English_United Kingdom.1252',
];
$locale = setlocale(LC_ALL, $candidates);
assert($locale !== false);
putenv('LC_ALL=' . $locale);
putenv('LANG=' . $locale);
putenv('LANGUAGE=' . (stripos($locale, 'US') !== false ? 'en_US:en_GB' : 'en_GB:en_US'));
$domain = 'test';
bindtextdomain($domain, 'locale/');
bind_textdomain_codeset($domain, 'UTF-8');
textdomain($domain);
assert(gettext(json_decode('"\u793a\u4f8b"', true)) === 'Example');
$src = json_decode('"\u793a\u4f8b"', true);
assert(gettext($src) === 'Example');

View File

@@ -245,6 +245,23 @@ function clean_spaces(string $string): string
return trim(preg_replace('/\s+/', ' ', $string));
}
/**
* Deduplicate flags in a string. Only the last occurence of each flag will be kept.
* E.g. `-lintl -lstdc++ -lphp -lstdc++` becomes `-lintl -lphp -lstdc++`
*
* @param string $flags the string containing flags to deduplicate
* @return string the deduplicated string with no duplicate flags
*/
function deduplicate_flags(string $flags): string
{
$tokens = preg_split('/\s+/', trim($flags));
// Reverse, unique, reverse back - keeps last occurrence of duplicates
$deduplicated = array_reverse(array_unique(array_reverse($tokens)));
return implode(' ', $deduplicated);
}
/**
* Register a callback function to handle keyboard interrupts (Ctrl+C).
*
@@ -283,3 +300,20 @@ function strip_ansi_colors(string $text): string
// Including color codes, cursor control, clear screen and other control sequences
return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', $text);
}
/**
* Convert to a real path for display purposes, used in docker volumes.
*/
function get_display_path(string $path): string
{
$deploy_root = getenv('SPC_FIX_DEPLOY_ROOT');
if ($deploy_root === false) {
return $path;
}
$cwd = WORKING_DIR;
// replace build root with deploy root, only if path starts with build root
if (str_starts_with($path, $cwd)) {
return $deploy_root . substr($path, strlen($cwd));
}
throw new WrongUsageException("Cannot convert path: {$path}");
}

View File

@@ -0,0 +1,26 @@
diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c
index 31212999..d80a21c3 100644
--- a/ext/readline/readline_cli.c
+++ b/ext/readline/readline_cli.c
@@ -739,8 +739,8 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void);
} while(0)
#else
-/*
#ifdef COMPILE_DL_READLINE
+/*
This dlsym() is always used as even the CGI SAPI is linked against "CLI"-only
extensions. If that is being changed dlsym() should only be used when building
this extension sharedto offer compatibility.
@@ -754,9 +754,9 @@ this extension sharedto offer compatibility.
(cb) = get_callbacks(); \
} \
} while(0)
-/*#else
+#else
#define GET_SHELL_CB(cb) (cb) = php_cli_get_shell_callbacks()
-#endif*/
+#endif
#endif
PHP_MINIT_FUNCTION(cli_readline)

View File

@@ -13,29 +13,28 @@ declare(strict_types=1);
// test php version (8.1 ~ 8.4 available, multiple for matrix)
$test_php_version = [
// '8.1',
'8.1',
// '8.2',
// '8.3',
'8.4',
// '8.5',
// '8.4',
'8.5',
// 'git',
];
// test os (macos-13, macos-14, macos-15, ubuntu-latest, windows-latest are available)
// test os (macos-15-intel, macos-15, ubuntu-latest, windows-latest are available)
$test_os = [
'macos-13', // bin/spc for x86_64
// 'macos-14', // bin/spc for arm64
'macos-15-intel', // bin/spc for x86_64
'macos-15', // bin/spc for arm64
'ubuntu-latest', // bin/spc-alpine-docker for x86_64
'ubuntu-22.04', // bin/spc-gnu-docker for x86_64
// 'ubuntu-24.04', // bin/spc for x86_64
'ubuntu-24.04', // bin/spc for x86_64
'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64
// 'ubuntu-24.04-arm', // bin/spc for arm64
'ubuntu-24.04-arm', // bin/spc for arm64
// 'windows-latest', // .\bin\spc.ps1
];
// whether enable thread safe
$zts = true;
$zts = false;
$no_strip = false;
@@ -50,13 +49,13 @@ $prefer_pre_built = false;
// If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`).
$extensions = match (PHP_OS_FAMILY) {
'Linux', 'Darwin' => 'bcmath',
'Linux', 'Darwin' => 'gettext',
'Windows' => 'bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,ftp,iconv,xml,mbstring,mbregex,mysqlnd,openssl,pdo,pdo_mysql,pdo_sqlite,phar,session,simplexml,soap,sockets,sqlite3,tokenizer,xmlwriter,xmlreader,zlib,zip',
};
// If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`).
$shared_extensions = match (PHP_OS_FAMILY) {
'Linux' => 'zip',
'Linux' => '',
'Darwin' => '',
'Windows' => '',
};
@@ -156,17 +155,13 @@ if ($shared_extensions) {
switch ($argv[2] ?? null) {
case 'ubuntu-22.04':
case 'ubuntu-22.04-arm':
case 'macos-15':
case 'macos-15-intel':
$shared_cmd = ' --build-shared=' . quote2($shared_extensions) . ' ';
break;
case 'ubuntu-24.04':
case 'ubuntu-24.04-arm':
break;
case 'macos-13':
case 'macos-14':
case 'macos-15':
$shared_cmd = ' --build-shared=' . quote2($shared_extensions) . ' ';
$no_strip = true;
break;
default:
$shared_cmd = '';
break;