Merge branch 'main' into feat/gnu-static

# Conflicts:
#	src/SPC/builder/linux/LinuxBuilder.php
This commit is contained in:
crazywhalecc
2025-03-09 17:44:13 +08:00
66 changed files with 2320 additions and 1270 deletions

View File

@@ -18,6 +18,7 @@ use SPC\command\dev\PhpVerCommand;
use SPC\command\dev\SortConfigCommand;
use SPC\command\DoctorCommand;
use SPC\command\DownloadCommand;
use SPC\command\DumpExtensionsCommand;
use SPC\command\DumpLicenseCommand;
use SPC\command\ExtractCommand;
use SPC\command\InstallPkgCommand;
@@ -31,7 +32,7 @@ use Symfony\Component\Console\Application;
*/
final class ConsoleApplication extends Application
{
public const VERSION = '2.4.4';
public const VERSION = '2.5.0';
public function __construct()
{
@@ -54,6 +55,7 @@ final class ConsoleApplication extends Application
new MicroCombineCommand(),
new SwitchPhpVersionCommand(),
new SPCConfigCommand(),
new DumpExtensionsCommand(),
// Dev commands
new AllExtCommand(),

View File

@@ -172,7 +172,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
// If check failed, throw RuntimeException
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "' . $this->getDistName() . '"', false);
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "' . $this->getDistName() . '"', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);
}
@@ -185,7 +185,7 @@ class Extension
file_get_contents(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php')
);
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "' . trim($test) . '"');
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "' . trim($test) . '"');
if ($ret !== 0) {
if ($this->builder->getOption('debug')) {
var_dump($out);
@@ -203,7 +203,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
// If check failed, throw RuntimeException
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe --ri "' . $this->getDistName() . '"', false);
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -n --ri "' . $this->getDistName() . '"', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);
}
@@ -216,7 +216,7 @@ class Extension
file_get_contents(FileSystem::convertPath(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php'))
);
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -r "' . trim($test) . '"');
[$ret] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php.exe -n -r "' . trim($test) . '"');
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed sanity check');
}

View File

@@ -146,6 +146,17 @@ abstract class LibraryBase
return Config::getLib(static::NAME, 'headers', []);
}
/**
* Get binary files.
*
* @throws FileSystemException
* @throws WrongUsageException
*/
public function getBinaryFiles(): array
{
return Config::getLib(static::NAME, 'bin', []);
}
/**
* @throws WrongUsageException
* @throws FileSystemException
@@ -203,7 +214,8 @@ abstract class LibraryBase
}
// force means just build
if ($force_build) {
logger()->info('Building required library [' . static::NAME . ']');
$type = Config::getLib(static::NAME, 'type', 'lib');
logger()->info('Building required ' . $type . ' [' . static::NAME . ']');
// extract first if not exists
if (!is_dir($this->source_dir)) {
@@ -236,10 +248,14 @@ abstract class LibraryBase
return LIB_STATUS_OK;
}
}
// pkg-config is treated specially. If it is pkg-config, check if the pkg-config binary exists
if (static::NAME === 'pkg-config' && !file_exists(BUILD_ROOT_PATH . '/bin/pkg-config')) {
$this->tryBuild(true);
return LIB_STATUS_OK;
// current library is package and binary file is not exists
if (Config::getLib(static::NAME, 'type', 'lib') === 'package') {
foreach ($this->getBinaryFiles() as $name) {
if (!file_exists(BUILD_BIN_PATH . "/{$name}")) {
$this->tryBuild(true);
return LIB_STATUS_OK;
}
}
}
// if all the files exist at this point, skip the compilation process
return LIB_STATUS_ALREADY;

View File

@@ -26,7 +26,7 @@ class mbregex extends Extension
*/
public function runCliCheckUnix(): void
{
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "mbstring" | grep regex', false);
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "mbstring" | grep regex', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: compiled php-cli mbstring extension does not contain regex !');
}
@@ -34,7 +34,7 @@ class mbregex extends Extension
public function runCliCheckWindows(): void
{
[$ret, $out] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "mbstring"', false);
[$ret, $out] = cmd()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "mbstring"', false);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: compiled php-cli does not contain mbstring !');
}

View File

@@ -13,6 +13,7 @@ class memcached extends Extension
public function getUnixConfigureArg(): string
{
$rootdir = BUILD_ROOT_PATH;
return "--enable-memcached --with-zlib-dir={$rootdir} --with-libmemcached-dir={$rootdir} --disable-memcached-sasl --enable-memcached-json";
$zlib_dir = $this->builder->getPHPVersionID() >= 80400 ? '' : "--with-zlib-dir={$rootdir}";
return "--enable-memcached {$zlib_dir} --with-libmemcached-dir={$rootdir} --disable-memcached-sasl --enable-memcached-json";
}
}

View File

@@ -25,6 +25,7 @@ class openssl extends Extension
public function getUnixConfigureArg(): string
{
return '--with-openssl=' . BUILD_ROOT_PATH . ' --with-openssl-dir=' . BUILD_ROOT_PATH;
$openssl_dir = $this->builder->getPHPVersionID() >= 80400 ? '' : ' --with-openssl-dir=' . BUILD_ROOT_PATH;
return '--with-openssl=' . BUILD_ROOT_PATH . $openssl_dir;
}
}

View File

@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace SPC\builder\extension;
use SPC\builder\Extension;
use SPC\store\FileSystem;
use SPC\util\CustomExt;
use SPC\util\GlobalEnvManager;
#[CustomExt('opentelemetry')]
class opentelemetry extends Extension
{
public function validate(): void
{
if ($this->builder->getPHPVersionID() < 80000 && getenv('SPC_SKIP_PHP_VERSION_CHECK') !== 'yes') {
throw new \RuntimeException('The opentelemetry extension requires PHP 8.0 or later');
}
}
public function patchBeforeBuildconf(): bool
{
if (PHP_OS_FAMILY === 'Windows') {
FileSystem::replaceFileStr(
SOURCE_PATH . '/php-src/ext/opentelemetry/config.w32',
"EXTENSION('opentelemetry', 'opentelemetry.c otel_observer.c', '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');",
"EXTENSION('opentelemetry', 'opentelemetry.c otel_observer.c', PHP_OPENTELEMETRY_SHARED, '/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1');"
);
return true;
}
return false;
}
public function patchBeforeMake(): bool
{
// add -Wno-strict-prototypes
GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes');
return true;
}
}

View File

@@ -18,7 +18,7 @@ class password_argon2 extends Extension
public function runCliCheckUnix(): void
{
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "assert(defined(\'PASSWORD_ARGON2I\'));"');
[$ret] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "assert(defined(\'PASSWORD_ARGON2I\'));"');
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed sanity check');
}

View File

@@ -29,7 +29,7 @@ class swoole_hook_mysql extends Extension
if ($this->builder->getExt('swoole') === null) {
return;
}
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "swoole"', false);
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "swoole"', false);
$out = implode('', $out);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);

View File

@@ -37,7 +37,7 @@ class swoole_hook_pgsql extends Extension
if ($this->builder->getExt('swoole') === null) {
return;
}
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "swoole"', false);
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "swoole"', false);
$out = implode('', $out);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);

View File

@@ -37,7 +37,7 @@ class swoole_hook_sqlite extends Extension
if ($this->builder->getExt('swoole') === null) {
return;
}
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php --ri "swoole"', false);
[$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n --ri "swoole"', false);
$out = implode('', $out);
if ($ret !== 0) {
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);

View File

@@ -12,9 +12,7 @@ class zlib extends Extension
{
public function getUnixConfigureArg(): string
{
if ($this->builder->getPHPVersionID() >= 80400) {
return '--with-zlib';
}
return '--with-zlib --with-zlib-dir="' . BUILD_ROOT_PATH . '"';
$zlib_dir = $this->builder->getPHPVersionID() >= 80400 ? '' : ' --with-zlib-dir=' . BUILD_ROOT_PATH;
return '--with-zlib' . $zlib_dir;
}
}

View File

@@ -34,14 +34,14 @@ class LinuxBuilder extends UnixBuilderBase
// check musl-cross make installed if we use musl-cross-make
$arch = arch2gnu(php_uname('m'));
if ($this->libc !== LIBC_GLIBC) {
// set library path, some libraries need it. (We cannot use `putenv` here, because cmake will be confused)
$this->setOptionIfNotExist('library_path', "LIBRARY_PATH=/usr/local/musl/{$arch}-linux-musl/lib");
$this->setOptionIfNotExist('ld_library_path', "LD_LIBRARY_PATH=/usr/local/musl/{$arch}-linux-musl/lib");
}
GlobalEnvManager::init($this);
// set library path, some libraries need it. (We cannot use `putenv` here, because cmake will be confused)
if (!filter_var(getenv('SPC_NO_MUSL_PATH'), FILTER_VALIDATE_BOOLEAN)) {
$this->setOptionIfNotExist('library_path', "LIBRARY_PATH=\"/usr/local/musl/{$arch}-linux-musl/lib\"");
$this->setOptionIfNotExist('ld_library_path', "LD_LIBRARY_PATH=\"/usr/local/musl/{$arch}-linux-musl/lib\"");
}
if (str_ends_with(getenv('CC'), 'linux-musl-gcc') && !file_exists("/usr/local/musl/bin/{$arch}-linux-musl-gcc") && (getenv('SPC_NO_MUSL_PATH') !== 'yes')) {
throw new WrongUsageException('musl-cross-make not installed, please install it first. (You can use `doctor` command to install it)');
}

View File

@@ -110,13 +110,11 @@ abstract class UnixBuilderBase extends BuilderBase
$sorted_libraries = DependencyUtil::getLibs($libraries);
}
// pkg-config must be compiled first, whether it is specified or not
if (!in_array('pkg-config', $sorted_libraries)) {
array_unshift($sorted_libraries, 'pkg-config');
}
// add lib object for builder
foreach ($sorted_libraries as $library) {
if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) {
continue;
}
// if some libs are not supported (but in config "lib.json", throw exception)
if (!isset($support_lib_list[$library])) {
throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!');
@@ -142,9 +140,10 @@ abstract class UnixBuilderBase extends BuilderBase
// sanity check for php-cli
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
logger()->info('running cli sanity check');
[$ret, $output] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -r "echo \"hello\";"');
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new RuntimeException('cli failed sanity check');
[$ret, $output] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "echo \"hello\";"');
$raw_output = implode('', $output);
if ($ret !== 0 || trim($raw_output) !== 'hello') {
throw new RuntimeException("cli failed sanity check: ret[{$ret}]. out[{$raw_output}]");
}
foreach ($this->exts as $ext) {

View File

@@ -10,14 +10,21 @@ trait gettext
{
$extra = $this->builder->getLib('ncurses') ? ('--with-libncurses-prefix=' . BUILD_ROOT_PATH . ' ') : '';
$extra .= $this->builder->getLib('libxml2') ? ('--with-libxml2-prefix=' . BUILD_ROOT_PATH . ' ') : '';
$zts = $this->builder->getOption('enable-zts') ? '--enable-threads=isoc+posix ' : '--disable-threads ';
$cflags = $this->builder->getOption('enable-zts') ? '-lpthread -D_REENTRANT' : '';
$ldflags = $this->builder->getOption('enable-zts') ? '-lpthread' : '';
shell()->cd($this->source_dir)
->setEnv(['CFLAGS' => $this->getLibExtraCFlags(), 'LDFLAGS' => $this->getLibExtraLdFlags(), 'LIBS' => $this->getLibExtraLibs()])
->setEnv(['CFLAGS' => $this->getLibExtraCFlags() ?: $cflags, 'LDFLAGS' => $this->getLibExtraLdFlags() ?: $ldflags, 'LIBS' => $this->getLibExtraLibs()])
->execWithEnv(
'./configure ' .
'--enable-static ' .
'--disable-shared ' .
'--disable-java ' .
'--disable-c+ ' .
$zts .
$extra .
'--with-included-gettext ' .
'--with-libiconv-prefix=' . BUILD_ROOT_PATH . ' ' .

View File

@@ -236,6 +236,9 @@ class WindowsBuilder extends BuilderBase
// add lib object for builder
foreach ($sorted_libraries as $library) {
if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) {
continue;
}
// if some libs are not supported (but in config "lib.json", throw exception)
if (!isset($support_lib_list[$library])) {
throw new WrongUsageException('library [' . $library . '] is in the lib.json list but not supported to compile, but in the future I will support it!');
@@ -277,7 +280,7 @@ class WindowsBuilder extends BuilderBase
// sanity check for php-cli
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
logger()->info('running cli sanity check');
[$ret, $output] = cmd()->execWithResult(BUILD_ROOT_PATH . '\bin\php.exe -r "echo \"hello\";"');
[$ret, $output] = cmd()->execWithResult(BUILD_ROOT_PATH . '\bin\php.exe -n -r "echo \"hello\";"');
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new RuntimeException('cli failed sanity check');
}

View File

@@ -12,14 +12,41 @@ class curl extends WindowsLibraryBase
protected function build(): void
{
FileSystem::createDir(BUILD_BIN_PATH);
cmd()->cd($this->source_dir . '\winbuild')
// reset cmake
FileSystem::resetDir($this->source_dir . '\cmakebuild');
// lib:zstd
$alt = $this->builder->getLib('zstd') ? '' : '-DCURL_ZSTD=OFF';
// lib:brotli
$alt .= $this->builder->getLib('brotli') ? '' : ' -DCURL_BROTLI=OFF';
// start build
cmd()->cd($this->source_dir)
->execWithWrapper(
$this->builder->makeSimpleWrapper('nmake'),
'/f Makefile.vc WITH_DEVEL=' . BUILD_ROOT_PATH . ' ' .
'WITH_PREFIX=' . BUILD_ROOT_PATH . ' ' .
'mode=static RTLIBCFG=static WITH_SSL=static WITH_NGHTTP2=static WITH_SSH2=static ENABLE_IPV6=yes WITH_ZLIB=static MACHINE=x64 DEBUG=no'
$this->builder->makeSimpleWrapper('cmake'),
'-B cmakebuild ' .
'-A x64 ' .
"-DCMAKE_TOOLCHAIN_FILE={$this->builder->cmake_toolchain_file} " .
'-DCMAKE_BUILD_TYPE=Release ' .
'-DBUILD_SHARED_LIBS=OFF ' .
'-DBUILD_STATIC_LIBS=ON ' .
'-DCURL_STATICLIB=ON ' .
'-DCMAKE_INSTALL_PREFIX=' . BUILD_ROOT_PATH . ' ' .
'-DBUILD_CURL_EXE=OFF ' . // disable curl.exe
'-DBUILD_TESTING=OFF ' . // disable tests
'-DBUILD_EXAMPLES=OFF ' . // disable examples
'-DUSE_LIBIDN2=OFF ' . // disable libidn2
'-DCURL_USE_LIBPSL=OFF ' . // disable libpsl
'-DCURL_ENABLE_SSL=ON ' .
'-DUSE_NGHTTP2=ON ' . // enable nghttp2
'-DCURL_USE_LIBSSH2=ON ' . // enable libssh2
'-DENABLE_IPV6=ON ' . // enable ipv6
'-DNGHTTP2_CFLAGS="/DNGHTTP2_STATICLIB" ' .
$alt
)
->execWithWrapper(
$this->builder->makeSimpleWrapper('cmake'),
"--build cmakebuild --config Release --target install -j{$this->builder->concurrency}"
);
FileSystem::copyDir($this->source_dir . '\include\curl', BUILD_INCLUDE_PATH . '\curl');
}
}

View File

@@ -46,7 +46,6 @@ abstract class BaseCommand extends Command
E_USER_ERROR => ['PHP Error: ', 'error'],
E_USER_WARNING => ['PHP Warning: ', 'warning'],
E_USER_NOTICE => ['PHP Notice: ', 'notice'],
E_STRICT => ['PHP Strict: ', 'notice'],
E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'],
E_DEPRECATED => ['PHP Deprecated: ', 'notice'],
E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'],
@@ -56,7 +55,7 @@ abstract class BaseCommand extends Command
logger()->{$level_tip[1]}($error);
// 如果 return false 则错误会继续递交给 PHP 标准错误处理
return true;
}, E_ALL | E_STRICT);
});
$version = ConsoleApplication::VERSION;
if (!$this->no_motd) {
echo " _ _ _ _
@@ -154,24 +153,24 @@ abstract class BaseCommand extends Command
/**
* Parse extension list from string, replace alias and filter internal extensions.
*
* @param string $ext_list Extension string list, e.g. "mbstring,posix,sockets"
* @param array|string $ext_list Extension string list, e.g. "mbstring,posix,sockets" or array
*/
protected function parseExtensionList(string $ext_list): array
protected function parseExtensionList(array|string $ext_list): array
{
// replace alias
$ls = array_map(function ($x) {
$lower = strtolower(trim($x));
if (isset(SPC_EXTENSION_ALIAS[$lower])) {
logger()->notice("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.');
logger()->debug("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.');
return SPC_EXTENSION_ALIAS[$lower];
}
return $lower;
}, explode(',', $ext_list));
}, is_array($ext_list) ? $ext_list : explode(',', $ext_list));
// filter internals
return array_values(array_filter($ls, function ($x) {
if (in_array($x, SPC_INTERNAL_EXTENSIONS)) {
logger()->warning("Extension [{$x}] is an builtin extension, it will be ignored.");
logger()->debug("Extension [{$x}] is an builtin extension, it will be ignored.");
return false;
}
return true;

View File

@@ -7,6 +7,7 @@ namespace SPC\command;
use SPC\builder\BuilderProvider;
use SPC\exception\ExceptionHandler;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\SourcePatcher;
use SPC\util\DependencyUtil;
@@ -32,7 +33,6 @@ class BuildCliCommand extends BuildCommand
$this->addOption('build-embed', null, null, 'Build embed SAPI');
$this->addOption('build-all', null, null, 'Build all SAPI');
$this->addOption('no-strip', null, null, 'build without strip, in order to debug and load external extensions');
$this->addOption('enable-zts', null, null, 'enable ZTS support');
$this->addOption('disable-opcache-jit', null, null, 'disable opcache jit');
$this->addOption('with-config-file-path', null, InputOption::VALUE_REQUIRED, 'Set the path in which to look for php.ini', $isWindows ? null : '/usr/local/etc/php');
$this->addOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', $isWindows ? null : '/usr/local/etc/php/conf.d');
@@ -108,13 +108,14 @@ class BuildCliCommand extends BuildCommand
$include_suggest_ext = $this->getOption('with-suggested-exts');
$include_suggest_lib = $this->getOption('with-suggested-libs');
[$extensions, $libraries, $not_included] = DependencyUtil::getExtsAndLibs($extensions, $libraries, $include_suggest_ext, $include_suggest_lib);
$display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package']));
// print info
$indent_texts = [
'Build OS' => PHP_OS_FAMILY . ' (' . php_uname('m') . ')',
'Build SAPI' => $builder->getBuildTypeName($rule),
'Extensions (' . count($extensions) . ')' => implode(',', $extensions),
'Libraries (' . count($libraries) . ')' => implode(',', $libraries),
'Libraries (' . count($libraries) . ')' => implode(',', $display_libs),
'Strip Binaries' => $builder->getOption('no-strip') ? 'no' : 'yes',
'Enable ZTS' => $builder->getOption('enable-zts') ? 'yes' : 'no',
];

View File

@@ -33,5 +33,6 @@ abstract class BuildCommand extends BaseCommand
$this->addOption('with-clean', null, null, 'fresh build, remove `source` dir before `make`');
$this->addOption('bloat', null, null, 'add all libraries into binary');
$this->addOption('rebuild', 'r', null, 'Delete old build and rebuild');
$this->addOption('enable-zts', null, null, 'enable ZTS support');
}
}

View File

@@ -7,6 +7,7 @@ namespace SPC\command;
use SPC\builder\BuilderProvider;
use SPC\exception\ExceptionHandler;
use SPC\exception\RuntimeException;
use SPC\store\Config;
use SPC\util\DependencyUtil;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
@@ -61,7 +62,9 @@ class BuildLibsCommand extends BuildCommand
$builder->setLibsOnly();
// 编译和检查库完整
$libraries = DependencyUtil::getLibs($libraries);
logger()->info('Building libraries: ' . implode(',', $libraries));
$display_libs = array_filter($libraries, fn ($lib) => in_array(Config::getLib($lib, 'type', 'lib'), ['lib', 'package']));
logger()->info('Building libraries: ' . implode(',', $display_libs));
sleep(2);
$builder->proveLibs($libraries);
$builder->validateLibsAndExts();

View File

@@ -72,10 +72,6 @@ class DownloadCommand extends BaseCommand
if ($for_ext = $input->getOption('for-extensions')) {
$ext = $this->parseExtensionList($for_ext);
$sources = $this->calculateSourcesByExt($ext, !$input->getOption('without-suggestions'));
if (PHP_OS_FAMILY !== 'Windows') {
array_unshift($sources, 'pkg-config');
}
array_unshift($sources, 'php-src', 'micro');
$final_sources = array_merge($final_sources, array_diff($sources, $final_sources));
}
// mode: --for-libs
@@ -323,7 +319,10 @@ class DownloadCommand extends BaseCommand
}
}
foreach ($libraries as $library) {
$sources[] = Config::getLib($library, 'source');
$source = Config::getLib($library, 'source');
if ($source !== null) {
$sources[] = $source;
}
}
return array_values(array_unique($sources));
}

View File

@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace SPC\command;
use SPC\store\FileSystem;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'dump-extensions', description: 'Determines the required php extensions')]
class DumpExtensionsCommand extends BaseCommand
{
protected bool $no_motd = true;
public function configure(): void
{
// path to project files or specific composer file
$this->addArgument('path', InputArgument::OPTIONAL, 'Path to project root', '.');
$this->addOption('format', 'F', InputOption::VALUE_REQUIRED, 'Parsed output format', 'default');
// output zero extension replacement rather than exit as failure
$this->addOption('no-ext-output', 'N', InputOption::VALUE_REQUIRED, 'When no extensions found, output default combination (comma separated)');
// no dev
$this->addOption('no-dev', null, null, 'Do not include dev dependencies');
// no spc filter
$this->addOption('no-spc-filter', 'S', null, 'Do not use SPC filter to determine the required extensions');
}
public function handle(): int
{
$path = FileSystem::convertPath($this->getArgument('path'));
$path_installed = FileSystem::convertPath(rtrim($path, '/\\') . '/vendor/composer/installed.json');
$path_lock = FileSystem::convertPath(rtrim($path, '/\\') . '/composer.lock');
$ext_installed = $this->extractFromInstalledJson($path_installed, !$this->getOption('no-dev'));
if ($ext_installed === null) {
if ($this->getOption('format') === 'default') {
$this->output->writeln('<comment>vendor/composer/installed.json load failed, skipped</comment>');
}
$ext_installed = [];
}
$ext_lock = $this->extractFromComposerLock($path_lock, !$this->getOption('no-dev'));
if ($ext_lock === null) {
$this->output->writeln('<error>composer.lock load failed</error>');
return static::FAILURE;
}
$extensions = array_unique(array_merge($ext_installed, $ext_lock));
sort($extensions);
if (empty($extensions)) {
if ($this->getOption('no-ext-output')) {
$this->outputExtensions(explode(',', $this->getOption('no-ext-output')));
return static::SUCCESS;
}
$this->output->writeln('<error>No extensions found</error>');
return static::FAILURE;
}
$this->outputExtensions($extensions);
return static::SUCCESS;
}
private function filterExtensions(array $requirements): array
{
return array_map(
fn ($key) => substr($key, 4),
array_keys(
array_filter($requirements, function ($key) {
return str_starts_with($key, 'ext-');
}, ARRAY_FILTER_USE_KEY)
)
);
}
private function loadJson(string $file): array|bool
{
if (!file_exists($file)) {
return false;
}
$data = json_decode(file_get_contents($file), true);
if (!$data) {
return false;
}
return $data;
}
private function extractFromInstalledJson(string $file, bool $include_dev = true): ?array
{
if (!($data = $this->loadJson($file))) {
return null;
}
$packages = $data['packages'] ?? [];
if (!$include_dev) {
$packages = array_filter($packages, fn ($package) => !in_array($package['name'], $data['dev-package-names'] ?? []));
}
return array_merge(
...array_map(fn ($x) => isset($x['require']) ? $this->filterExtensions($x['require']) : [], $packages)
);
}
private function extractFromComposerLock(string $file, bool $include_dev = true): ?array
{
if (!($data = $this->loadJson($file))) {
return null;
}
// get packages ext
$packages = $data['packages'] ?? [];
$exts = array_merge(
...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages)
);
// get dev packages ext
if ($include_dev) {
$packages = $data['packages-dev'] ?? [];
$exts = array_merge(
$exts,
...array_map(fn ($package) => $this->filterExtensions($package['require'] ?? []), $packages)
);
}
// get require ext
$platform = $data['platform'] ?? [];
$exts = array_merge($exts, $this->filterExtensions($platform));
// get require-dev ext
if ($include_dev) {
$platform = $data['platform-dev'] ?? [];
$exts = array_merge($exts, $this->filterExtensions($platform));
}
return $exts;
}
private function outputExtensions(array $extensions): void
{
if (!$this->getOption('no-spc-filter')) {
$extensions = $this->parseExtensionList($extensions);
}
switch ($this->getOption('format')) {
case 'json':
$this->output->writeln(json_encode($extensions, JSON_PRETTY_PRINT));
break;
case 'text':
$this->output->writeln(implode(',', $extensions));
break;
default:
$this->output->writeln('<info>Required PHP extensions' . ($this->getOption('no-dev') ? ' (without dev)' : '') . ':</info>');
$this->output->writeln(implode(',', $extensions));
}
}
}

View File

@@ -33,7 +33,17 @@ class SortConfigCommand extends BaseCommand
case 'lib':
$file = json_decode(FileSystem::readFile(ROOT_DIR . '/config/lib.json'), true);
ConfigValidator::validateLibs($file);
ksort($file);
uksort($file, function ($a, $b) use ($file) {
$type_a = $file[$a]['type'] ?? 'lib';
$type_b = $file[$b]['type'] ?? 'lib';
$type_order = ['root', 'target', 'package', 'lib'];
// compare type first
if ($type_a !== $type_b) {
return array_search($type_a, $type_order) <=> array_search($type_b, $type_order);
}
// compare name
return $a <=> $b;
});
if (!file_put_contents(ROOT_DIR . '/config/lib.json', json_encode($file, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n")) {
$this->output->writeln('<error>Write file lib.json failed!</error>');
return static::FAILURE;

View File

@@ -73,7 +73,7 @@ class Config
if (!isset(self::$lib[$name])) {
throw new WrongUsageException('lib [' . $name . '] is not supported yet');
}
$supported_sys_based = ['static-libs', 'headers', 'lib-depends', 'lib-suggests', 'frameworks'];
$supported_sys_based = ['static-libs', 'headers', 'lib-depends', 'lib-suggests', 'frameworks', 'bin'];
if ($key !== null && in_array($key, $supported_sys_based)) {
$m_key = match (PHP_OS_FAMILY) {
'Windows' => ['-windows', '-win', ''],

View File

@@ -53,13 +53,45 @@ class ConfigValidator
*/
public static function validateLibs(mixed $data, array $source_data = []): void
{
is_array($data) || throw new ValidationException('lib.json is broken');
// check if it is an array
if (!is_array($data)) {
throw new ValidationException('lib.json is broken');
}
// check each lib
foreach ($data as $name => $lib) {
isset($lib['source']) || throw new ValidationException("lib {$name} does not assign any source");
is_string($lib['source']) || throw new ValidationException("lib {$name} source must be string");
empty($source_data) || isset($source_data[$lib['source']]) || throw new ValidationException("lib {$name} assigns an invalid source: {$lib['source']}");
!isset($lib['lib-depends']) || !is_assoc_array($lib['lib-depends']) || throw new ValidationException("lib {$name} dependencies must be a list");
!isset($lib['lib-suggests']) || !is_assoc_array($lib['lib-suggests']) || throw new ValidationException("lib {$name} suggested dependencies must be a list");
// check if lib is an assoc array
if (!is_assoc_array($lib)) {
throw new ValidationException("lib {$name} is not an object");
}
// check if lib has valid type
if (!in_array($lib['type'] ?? 'lib', ['lib', 'package', 'target', 'root'])) {
throw new ValidationException("lib {$name} type is invalid");
}
// check if lib and package has source
if (in_array($lib['type'] ?? 'lib', ['lib', 'package']) && !isset($lib['source'])) {
throw new ValidationException("lib {$name} does not assign any source");
}
// check if source is valid
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 [lib-depends|lib-suggests|static-libs][-windows|-unix|-macos|-linux] are valid list array
$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");
}
}
// 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");
}
}
}

View File

@@ -33,7 +33,7 @@ class DependencyUtil
$ext_suggests = array_map(fn ($x) => "ext@{$x}", $ext_suggests);
// merge ext-depends with lib-depends
$lib_depends = Config::getExt($ext_name, 'lib-depends', []);
$depends = array_merge($ext_depends, $lib_depends);
$depends = array_merge($ext_depends, $lib_depends, ['php']);
// merge ext-suggests with lib-suggests
$lib_suggests = Config::getExt($ext_name, 'lib-suggests', []);
$suggests = array_merge($ext_suggests, $lib_suggests);
@@ -44,7 +44,7 @@ class DependencyUtil
}
foreach ($libs as $lib_name => $lib) {
$dep_list[$lib_name] = [
'depends' => Config::getLib($lib_name, 'lib-depends', []),
'depends' => array_merge(Config::getLib($lib_name, 'lib-depends', []), ['lib-base']),
'suggests' => Config::getLib($lib_name, 'lib-suggests', []),
];
}
@@ -210,6 +210,9 @@ class DependencyUtil
}
$visited[$lib_name] = true;
// 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常)
if (!isset($dep_list[$lib_name])) {
throw new WrongUsageException("{$lib_name} not exist !");
}
foreach ($dep_list[$lib_name]['depends'] as $dep) {
self::visitPlatDeps($dep, $dep_list, $visited, $sorted);
}

View File

@@ -57,10 +57,6 @@ class GlobalEnvManager
self::putenv("SPC_LINUX_DEFAULT_CXX={$arch}-linux-musl-g++");
self::putenv("SPC_LINUX_DEFAULT_AR={$arch}-linux-musl-ar");
}
self::putenv("SPC_PHP_DEFAULT_LD_LIBRARY_PATH_CMD=LD_LIBRARY_PATH=/usr/local/musl/{$arch}-linux-musl/lib");
if (getenv('SPC_NO_MUSL_PATH') !== 'yes') {
self::putenv("PATH=/usr/local/musl/bin:/usr/local/musl/{$arch}-linux-musl/bin:" . getenv('PATH'));
}
}
// Init env.ini file, read order:
@@ -112,6 +108,11 @@ class GlobalEnvManager
'BSD' => self::applyConfig($ini['freebsd']),
default => null,
};
if (PHP_OS_FAMILY === 'Linux' && !filter_var(getenv('SPC_NO_MUSL_PATH'), FILTER_VALIDATE_BOOLEAN)) {
self::putenv("SPC_PHP_DEFAULT_LD_LIBRARY_PATH_CMD=LD_LIBRARY_PATH=/usr/local/musl/{$arch}-linux-musl/lib");
self::putenv("PATH=/usr/local/musl/bin:/usr/local/musl/{$arch}-linux-musl/bin:" . getenv('PATH'));
}
}
public static function putenv(string $val): void

View File

@@ -70,6 +70,9 @@ class LicenseDumper
}
foreach ($this->libs as $lib) {
if (Config::getLib($lib, 'type', 'lib') !== 'lib') {
continue;
}
$source_name = Config::getLib($lib, 'source');
foreach ($this->getSourceLicenses($source_name) as $index => $license) {
$result = file_put_contents("{$target_dir}/lib_{$lib}_{$index}.txt", $license);

View File

@@ -0,0 +1,5 @@
<?php
declare(strict_types=1);
assert(function_exists('OpenTelemetry\Instrumentation\hook'));

View File

@@ -20,6 +20,14 @@ function is_assoc_array(mixed $array): bool
return is_array($array) && (!empty($array) && array_keys($array) !== range(0, count($array) - 1));
}
/**
* Judge if an array is a list
*/
function is_list_array(mixed $array): bool
{
return is_array($array) && (empty($array) || array_keys($array) === range(0, count($array) - 1));
}
/**
* Return a logger instance
*/

View File

@@ -13,14 +13,13 @@ declare(strict_types=1);
// test php version
$test_php_version = [
// '8.1',
// '8.2',
// '8.3',
'8.3',
'8.4',
];
// test os (macos-13, macos-14, ubuntu-latest, windows-latest are available)
$test_os = [
// 'macos-13',
'macos-14',
'ubuntu-latest',
// 'windows-latest',
@@ -35,12 +34,12 @@ $no_strip = false;
$upx = false;
// prefer downloading pre-built packages to speed up the build process
$prefer_pre_built = true;
$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' => 'dio',
'Windows' => 'dio',
'Linux', 'Darwin' => 'imap,openssl,zlib,memcache',
'Windows' => 'gettext',
};
// If you want to test lib-suggests feature with extension, add them below (comma separated, example `libwebp,libavif`).
@@ -162,6 +161,8 @@ if ($argv[1] === 'download_cmd') {
} else {
passthru('./bin/spc ' . $build_cmd . ' --build-embed', $retcode);
}
} else {
$retcode = 0;
}
exit($retcode);