Compare commits

..

3 Commits

Author SHA1 Message Date
crazywhalecc
d600530813 Merge branch 'feat/pgo-v3-split' into fix/spcconfig 2026-07-16 14:40:25 +08:00
Jerry Ma
a3d34df152 Merge branch 'v3' into fix/spcconfig 2026-07-16 14:37:29 +08:00
crazywhalecc
01a1db85b1 Add getResolvedPackageClosure method to DependencyResolver for improved package resolution 2026-07-16 14:36:35 +08:00
18 changed files with 665 additions and 147 deletions

View File

@@ -100,6 +100,7 @@ SPC_TARGET=${GNU_ARCH}-linux-musl
CC=${SPC_DEFAULT_CC} CC=${SPC_DEFAULT_CC}
CXX=${SPC_DEFAULT_CXX} CXX=${SPC_DEFAULT_CXX}
AR=${SPC_DEFAULT_AR} AR=${SPC_DEFAULT_AR}
RANLIB=${SPC_DEFAULT_RANLIB}
LD=${SPC_DEFAULT_LD} LD=${SPC_DEFAULT_LD}
; default compiler flags, used in CMake toolchain file, openssl and pkg-config build ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build
SPC_DEFAULT_CFLAGS="-fPIC -O3 -pipe -fno-plt -fno-semantic-interposition -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections -Wno-unused-command-line-argument" SPC_DEFAULT_CFLAGS="-fPIC -O3 -pipe -fno-plt -fno-semantic-interposition -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections -Wno-unused-command-line-argument"
@@ -142,6 +143,7 @@ SPC_USE_LLVM=system
CC=${SPC_DEFAULT_CC} CC=${SPC_DEFAULT_CC}
CXX=${SPC_DEFAULT_CXX} CXX=${SPC_DEFAULT_CXX}
AR=${SPC_DEFAULT_AR} AR=${SPC_DEFAULT_AR}
RANLIB=${SPC_DEFAULT_RANLIB}
LD=${SPC_DEFAULT_LD} LD=${SPC_DEFAULT_LD}
; default compiler flags, used in CMake toolchain file, openssl and pkg-config build ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build
SPC_DEFAULT_CFLAGS="--target=${MAC_ARCH}-apple-darwin -O3 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections" SPC_DEFAULT_CFLAGS="--target=${MAC_ARCH}-apple-darwin -O3 -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections"

View File

@@ -35,12 +35,37 @@ class event extends PhpExtensionPackage
#[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-event')] #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-event')]
#[PatchDescription('Prevent event extension compile error on macOS')] #[PatchDescription('Prevent event extension compile error on macOS')]
#[PatchDescription('Patch libevent http_connection.c to use a const peer address')]
public function patchBeforeMake(PackageInstaller $installer): void public function patchBeforeMake(PackageInstaller $installer): void
{ {
$php_src = $installer->getTargetPackage('php')->getSourceDir();
// Prevent event extension compile error on macOS // Prevent event extension compile error on macOS
if (SystemTarget::getTargetOS() === 'Darwin') { if (SystemTarget::getTargetOS() === 'Darwin') {
$php_src = $installer->getTargetPackage('php')->getSourceDir();
FileSystem::replaceFileRegex("{$php_src}/main/php_config.h", '/^#define HAVE_OPENPTY 1$/m', ''); FileSystem::replaceFileRegex("{$php_src}/main/php_config.h", '/^#define HAVE_OPENPTY 1$/m', '');
} }
$this->patchLibeventConstPeer("{$php_src}/ext/event");
}
#[BeforeStage('ext-event', [PhpExtensionPackage::class, 'makeForUnix'])]
#[PatchDescription('Patch libevent http_connection.c to use a const peer address')]
public function patchBeforeSharedMake(PhpExtensionPackage $pkg): void
{
$this->patchLibeventConstPeer($pkg->getSourceDir());
}
private function patchLibeventConstPeer(string $event_source_dir): bool
{
$file = "{$event_source_dir}/php8/classes/http_connection.c";
if (!is_file($file)) {
return false;
}
// libevent >= 2.2 changed evhttp_connection_get_peer()'s
// address arg to `const char **`
$header = $this->getBuilder()->getBuildRootPath() . '/include/event2/http.h';
$isConst = is_file($header)
&& preg_match('/evhttp_connection_get_peer\s*\([^;]*\bconst\s+char\s*\*\*\s*address/s', file_get_contents($header)) === 1;
return (bool) ($isConst
? FileSystem::replaceFileRegex($file, '/^\tchar \*address;$/m', "\tconst char *address;")
: FileSystem::replaceFileRegex($file, '/^\tconst char \*address;$/m', "\tchar *address;"));
} }
} }

View File

@@ -49,7 +49,8 @@ class rdkafka extends PhpExtensionPackage
#[CustomPhpConfigureArg('Linux')] #[CustomPhpConfigureArg('Linux')]
public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): string public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): string
{ {
$pkgconf_libs = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->getExtensionConfig($this); $pkgconf_libs = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])
->configForResolvedBuild([$this->getName()], $this->getInstaller());
return '--with-rdkafka=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath() . " RDKAFKA_LIBS=\"{$pkgconf_libs['libs']}\""; return '--with-rdkafka=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath() . " RDKAFKA_LIBS=\"{$pkgconf_libs['libs']}\"";
} }
} }

View File

@@ -93,7 +93,8 @@ class swoole extends PhpExtensionPackage
$arg .= $installer->getPhpExtensionPackage('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd'; $arg .= $installer->getPhpExtensionPackage('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd';
$arg .= $installer->getPhpExtensionPackage('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite'; $arg .= $installer->getPhpExtensionPackage('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite';
if ($installer->getPhpExtensionPackage('swoole-hook-odbc')) { if ($installer->getPhpExtensionPackage('swoole-hook-odbc')) {
$config = new SPCConfigUtil()->getLibraryConfig($installer->getLibraryPackage('unixodbc')); $config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])
->configForResolvedBuild(['unixodbc'], $installer);
$arg .= " --with-swoole-odbc=unixODBC,{$builder->getBuildRootPath()} SWOOLE_ODBC_LIBS=\"{$config['libs']}\""; $arg .= " --with-swoole-odbc=unixODBC,{$builder->getBuildRootPath()} SWOOLE_ODBC_LIBS=\"{$config['libs']}\"";
} }

View File

@@ -17,8 +17,8 @@ class bzip2
#[PatchBeforeBuild] #[PatchBeforeBuild]
public function patchBeforeBuild(LibraryPackage $lib): void public function patchBeforeBuild(LibraryPackage $lib): void
{ {
// Inject SPC_DEFAULT_CFLAGS into the Makefile // Makefile pins -O2 -fPIC; inject SPC_DEFAULT_CFLAGS
$extra = deduplicate_flags(trim((string) getenv('SPC_DEFAULT_CFLAGS')) . ' -Wall'); $extra = deduplicate_flags(trim((string) getenv('SPC_DEFAULT_CFLAGS')) . ' -fPIC -Wall');
FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', "CFLAGS={$extra}"); FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', "CFLAGS={$extra}");
} }

View File

@@ -27,7 +27,7 @@ class krb5
$resolved = array_keys($installer->getResolvedPackages()); $resolved = array_keys($installer->getResolvedPackages());
$spc = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); $spc = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]);
$config = $spc->getPackageDepsConfig($lib->getName(), $resolved, include_suggests: true); $config = $spc->getPackageDepsConfig($lib->getName(), $resolved);
$extraEnv = [ $extraEnv = [
'CFLAGS' => '-fcommon', 'CFLAGS' => '-fcommon',
'LIBS' => $config['libs'], 'LIBS' => $config['libs'],

View File

@@ -129,7 +129,7 @@ class postgresql extends LibraryPackage
public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): void public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): void
{ {
$spc_config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); $spc_config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]);
$config = $spc_config->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()), include_suggests: $builder->getOption('with-suggests', false)); $config = $spc_config->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()));
$env_vars = [ $env_vars = [
'CFLAGS' => $config['cflags'] . ' -std=c17', 'CFLAGS' => $config['cflags'] . ' -std=c17',

View File

@@ -122,10 +122,7 @@ trait frankenphp
$staticFlags = ''; $staticFlags = '';
} }
$resolved = array_keys($installer->getResolvedPackages()); $config = new SPCConfigUtil()->configForResolvedBuild([$package->getName()], $installer);
// remove self from deps
$resolved = array_filter($resolved, fn ($pkg_name) => $pkg_name !== $package->getName());
$config = new SPCConfigUtil()->config($resolved);
$cflags = "{$package->getLibExtraCFlags()} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . " -DFRANKENPHP_VERSION={$frankenphp_version}"; $cflags = "{$package->getLibExtraCFlags()} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . " -DFRANKENPHP_VERSION={$frankenphp_version}";
$libs = $config['libs']; $libs = $config['libs'];

View File

@@ -590,7 +590,7 @@ trait unix
copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c'); copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c');
copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php'); copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php');
$config = new SPCConfigUtil()->config($installer->getAvailableResolvedPackageNames()); $config = new SPCConfigUtil()->configForResolvedBuild(['php'], $installer);
$lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}"; $lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}";
if ($toolchain->isStatic()) { if ($toolchain->isStatic()) {
$lens .= ' -static'; $lens .= ' -static';
@@ -785,7 +785,7 @@ trait unix
*/ */
private function makeVars(PackageInstaller $installer): array private function makeVars(PackageInstaller $installer): array
{ {
$config = new SPCConfigUtil(['libs_only_deps' => true])->config($installer->getAvailableResolvedPackageNames()); $config = new SPCConfigUtil(['libs_only_deps' => true])->configForResolvedBuild(['php'], $installer);
$static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : ''; $static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : '';
$pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : ''; $pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : '';

View File

@@ -708,7 +708,7 @@ C_CODE;
// Get build configuration using spc-config // Get build configuration using spc-config
$util = new \StaticPHP\Util\SPCConfigUtil(); $util = new \StaticPHP\Util\SPCConfigUtil();
$config = $util->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); $config = $util->configForResolvedBuild(['php'], $installer);
// Build the embed test executable using cl.exe // Build the embed test executable using cl.exe
// Note: MSVCToolchain already initialized the VC environment, no need for vcvarsall // Note: MSVCToolchain already initialized the VC environment, no need for vcvarsall

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Doctor\Item;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\ArtifactExtractor;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;
use StaticPHP\Attribute\Doctor\OptionalCheck;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Doctor\CheckResult;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\MuslToolchain;
use StaticPHP\Toolchain\ZigToolchain;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\SourcePatcher;
use StaticPHP\Util\System\LinuxUtil;
#[OptionalCheck([self::class, 'optionalCheck'])]
class LinuxMuslCheck
{
public static function optionalCheck(): bool
{
$toolchain = ApplicationContext::get(ToolchainInterface::class);
return PHP_OS_FAMILY === 'Linux' && (
$toolchain instanceof MuslToolchain || (
$toolchain instanceof ZigToolchain
&& !LinuxUtil::isMuslDist()
&& SystemTarget::getLibc() === 'musl'
&& !$toolchain->isStatic()
)
);
}
/** @noinspection PhpUnused */
#[CheckItem('if musl-wrapper is installed', limit_os: 'Linux', level: 800)]
public function checkMusl(): ?CheckResult
{
$musl_wrapper_lib = sprintf('/lib/ld-musl-%s.so.1', php_uname('m'));
if (file_exists($musl_wrapper_lib) && (file_exists('/usr/local/musl/lib/libc.a') || getenv('SPC_TOOLCHAIN') === ZigToolchain::class)) {
return null;
}
return CheckResult::fail('musl-wrapper is not installed on your system', 'fix-musl-wrapper');
}
#[CheckItem('if musl-cross-make is installed', limit_os: 'Linux', level: 799)]
public function checkMuslCrossMake(): ?CheckResult
{
if (getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !LinuxUtil::isMuslDist()) {
return null;
}
$arch = arch2gnu(php_uname('m'));
$cross_compile_lib = "/usr/local/musl/{$arch}-linux-musl/lib/libc.a";
$cross_compile_gcc = "/usr/local/musl/bin/{$arch}-linux-musl-gcc";
if (file_exists($cross_compile_lib) && file_exists($cross_compile_gcc)) {
return CheckResult::ok();
}
return CheckResult::fail('musl-cross-make is not installed on your system', 'fix-musl-cross-make');
}
#[FixItem('fix-musl-wrapper')]
public function fixMusl(): bool
{
$downloader = new ArtifactDownloader(interactive: false);
$downloader->add('musl-wrapper')->download();
$extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class));
$extractor->extract('musl-wrapper');
// Apply CVE-2025-26519 patch and install musl wrapper
SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0001.patch', SOURCE_PATH . '/musl-wrapper');
SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0002.patch', SOURCE_PATH . '/musl-wrapper');
$prefix = '';
if (get_current_user() !== 'root') {
$prefix = 'sudo ';
logger()->warning('Current user is not root, using sudo for running command');
}
$sysEnv = ['CC' => 'gcc', 'CXX' => 'g++', 'AR' => 'ar', 'LD' => 'ld', 'RANLIB' => 'ranlib'];
$envFlags = '';
foreach ($sysEnv as $k => $v) {
$envFlags .= "{$k}={$v} ";
}
$envFlags = rtrim($envFlags);
$shell = shell()->cd(SOURCE_PATH . '/musl-wrapper')
->setEnv($sysEnv)
->exec('./configure --disable-gcc-wrapper')
->exec('make -j');
if ($prefix !== '') {
f_passthru('cd ' . SOURCE_PATH . "/musl-wrapper && {$envFlags} {$prefix}make install");
} else {
$shell->exec("{$prefix}make install");
}
return true;
}
#[FixItem('fix-musl-cross-make')]
public function fixMuslCrossMake(): bool
{
// sudo
$prefix = '';
if (get_current_user() !== 'root') {
$prefix = 'sudo ';
logger()->warning('Current user is not root, using sudo for running command');
}
Shell::passthruCallback(function () {
InteractiveTerm::advance();
});
$downloader = new ArtifactDownloader(interactive: false);
$extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class));
$downloader->add('musl-toolchain')->download();
$extractor->extract('musl-toolchain');
$pkg_root = PKG_ROOT_PATH . '/musl-toolchain';
f_passthru("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl");
FileSystem::removeDir($pkg_root);
return true;
}
}

View File

@@ -305,8 +305,8 @@ class LibraryPackage extends Package
*/ */
public function getStaticLibFiles(): string public function getStaticLibFiles(): string
{ {
$config = new SPCConfigUtil(['libs_only_deps' => true, 'absolute_libs' => true]); $config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true, 'absolute_libs' => true]);
$res = $config->config([$this->getName()]); $res = $config->configForResolvedBuild([$this->getName()], $this->getInstaller());
return $res['libs']; return $res['libs'];
} }

View File

@@ -274,13 +274,13 @@ class PhpExtensionPackage extends Package
$compiler_extra = trim($compiler_extra . ' -lcompiler_rt'); $compiler_extra = trim($compiler_extra . ' -lcompiler_rt');
GlobalEnvManager::putenv("SPC_COMPILER_EXTRA={$compiler_extra}"); GlobalEnvManager::putenv("SPC_COMPILER_EXTRA={$compiler_extra}");
} }
$config = (new SPCConfigUtil())->getExtensionConfig($this); $config = (new SPCConfigUtil())->configForResolvedBuild([$this->getName()], $this->getInstaller());
[$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']);
$preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group ';
$postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group ';
return [ return [
'CFLAGS' => $config['cflags'], 'CFLAGS' => $config['cflags'],
'CXXFLAGS' => $config['cflags'], 'CXXFLAGS' => $config['cxxflags'],
'LDFLAGS' => $config['ldflags'], 'LDFLAGS' => $config['ldflags'],
'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"),
'LD_LIBRARY_PATH' => BUILD_LIB_PATH, 'LD_LIBRARY_PATH' => BUILD_LIB_PATH,

View File

@@ -136,6 +136,52 @@ class DependencyResolver
return $sorted; return $sorted;
} }
/**
* Get the link closure for one or more roots within an already-resolved package set.
*
* Both depends and suggests may be traversed, but only packages present in the
* resolved set are returned. Dependency overrides add build-specific graph edges,
* such as PHP's resolved static extensions and virtual SAPI targets.
*
* @param array<Package|string> $packages
* @param string[] $resolved_packages
* @param array<string, string[]> $dependency_overrides
* @return string[] Packages in dependency order, including the roots
*/
public static function getResolvedPackageClosure(
array $packages,
array $resolved_packages,
array $dependency_overrides = [],
bool $include_suggests = true,
): array {
$resolved_set = array_flip($resolved_packages);
$dep_map = [];
foreach ($resolved_packages as $package) {
$dep_map[$package] = [
'depends' => array_merge(
PackageConfig::get($package, 'depends', []),
$dependency_overrides[$package] ?? [],
),
'suggests' => PackageConfig::get($package, 'suggests', []),
];
}
$visited = [];
$sorted = [];
foreach ($packages as $package) {
$name = is_string($package) ? $package : $package->getName();
if (!PackageConfig::isPackageExists($name)) {
throw new WrongUsageException("Package '{$name}' does not exist in config, please check your package name !");
}
if (!isset($resolved_set[$name])) {
throw new WrongUsageException("Package '{$name}' is not part of the resolved package set.");
}
self::visitSubDeps($name, $dep_map, $resolved_set, $include_suggests, $visited, $sorted);
}
return $sorted;
}
/** /**
* Build a reverse dependency map for the resolved packages. * Build a reverse dependency map for the resolved packages.
* For each package that is depended upon, list which packages depend on it. * For each package that is depended upon, list which packages depend on it.

View File

@@ -7,7 +7,10 @@ namespace StaticPHP\Util;
use StaticPHP\Config\PackageConfig; use StaticPHP\Config\PackageConfig;
use StaticPHP\Exception\WrongUsageException; use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\Package;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Runtime\SystemTarget; use StaticPHP\Runtime\SystemTarget;
class SPCConfigUtil class SPCConfigUtil
@@ -32,95 +35,72 @@ class SPCConfigUtil
$this->absolute_libs = $options['absolute_libs'] ?? false; $this->absolute_libs = $options['absolute_libs'] ?? false;
} }
/**
* Resolve a standalone package request and generate its configuration.
*
* This is the entry point for spc-config and callers that do not already have
* a PackageInstaller resolution result.
*
* @param array<Package|string> $packages
* @return array{cflags: string, cxxflags: string, ldflags: string, libs: string}
*/
public function config(array $packages = [], bool $include_suggests = false): array public function config(array $packages = [], bool $include_suggests = false): array
{ {
// if have php, make php as all extension's dependency // if have php, make php as all extension's dependency
$package_names = array_map(fn ($package) => is_string($package) ? $package : $package->getName(), $packages);
$dep_override = $this->no_php
? []
: ['php' => array_values(array_filter($package_names, fn ($name) => str_starts_with($name, 'ext-')))];
return $this->configWithResolvedPackages(
DependencyResolver::resolve($packages, $dep_override, $include_suggests)
);
}
/**
* Build configuration for roots within the installer's resolved package graph.
*
* The installer decides whether suggested packages are enabled. This method always
* walks depends and suggests, then filters every edge through that resolved set.
*
* @param string[] $package_names Root package names whose link closure is required
* @return array{cflags: string, cxxflags: string, ldflags: string, libs: string}
*/
public function configForResolvedBuild(array $package_names, PackageInstaller $installer): array
{
$dependency_overrides = [];
if (!$this->no_php) { if (!$this->no_php) {
$dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))]; $php_link_packages = [];
} else { foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $extension) {
$dep_override = []; if ($extension->isBuildStatic()) {
$php_link_packages[] = $extension->getName();
} }
$resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests); }
foreach ($installer->getResolvedPackages(TargetPackage::class) as $target) {
$ldflags = $this->getLdflagsString(); if ($target->isVirtualTarget()) {
$cflags = $this->getIncludesString($resolved); $php_link_packages[] = $target->getName();
$libs = $this->getLibsString($resolved, !$this->absolute_libs); }
}
// additional OS-specific libraries (e.g. macOS -lresolv) $dependency_overrides['php'] = $php_link_packages;
// embed
if ($extra_libs = SystemTarget::getRuntimeLibs()) {
$libs .= " {$extra_libs}";
} }
$extra_env = getenv('SPC_EXTRA_LIBS'); return $this->configWithResolvedPackages(
if (is_string($extra_env) && !empty($extra_env)) { DependencyResolver::getResolvedPackageClosure(
$libs .= " {$extra_env}"; $package_names,
} array_keys($installer->getResolvedPackages()),
// package frameworks $dependency_overrides,
if (SystemTarget::getTargetOS() === 'Darwin') { )
$libs .= " {$this->getFrameworksString($resolved)}"; );
}
// C++
if ($this->hasCpp($resolved)) {
$target_os = SystemTarget::getTargetOS();
if ($target_os === 'Darwin') {
$libcpp = '-lc++';
$libs = str_replace($libcpp, '', $libs) . " {$libcpp}";
} elseif ($target_os !== 'Windows') {
// Linux and other Unix-like systems use libstdc++
$libcpp = '-lstdc++';
$libs = str_replace($libcpp, '', $libs) . " {$libcpp}";
}
// Windows (MSVC): C++ runtime is linked automatically, no explicit lib needed
}
if ($this->libs_only_deps) {
// mimalloc must come first
if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
$libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs);
}
return [
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs),
];
}
// embed
if (!$this->no_php) {
if (SystemTarget::getTargetOS() === 'Windows') {
// Windows: use php8embed.lib directly (either full path or short name)
$major = intdiv(PHP_VERSION_ID, 10000);
$php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.lib";
// Windows system libs required by PHP
// Use same system libs as PHP Makefile: LIBS=kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib Dnsapi.lib psapi.lib bcrypt.lib
$libs = "{$php_lib} {$libs} kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib";
} else {
$libs = "-lphp {$libs} -lc";
}
}
$allLibs = getenv('LIBS') . ' ' . $libs;
// mimalloc must come first
if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
$allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs);
}
return [
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags),
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags),
'libs' => clean_spaces($allLibs),
];
} }
/** /**
* [Helper function] * [Helper function]
* Get configuration for a specific extension(s) dependencies. * Get configuration for a specific extension(s) dependencies.
* *
* @param array|PhpExtensionPackage $extension_packages Extension instance or list * @param PhpExtensionPackage|PhpExtensionPackage[] $extension_packages Extension instance or list
* @return array{ * @return array{
* cflags: string, * cflags: string,
* cxxflags: string,
* ldflags: string, * ldflags: string,
* libs: string * libs: string
* } * }
@@ -131,7 +111,7 @@ class SPCConfigUtil
$extension_packages = [$extension_packages]; $extension_packages = [$extension_packages];
} }
return $this->config( return $this->config(
packages: array_map(fn ($y) => $y->getName(), $extension_packages), packages: array_map(fn ($extension_package) => $extension_package->getName(), $extension_packages),
include_suggests: $include_suggests, include_suggests: $include_suggests,
); );
} }
@@ -140,30 +120,33 @@ class SPCConfigUtil
* [Helper function] * [Helper function]
* Get configuration for a specific library(s) dependencies. * Get configuration for a specific library(s) dependencies.
* *
* @param array|LibraryPackage $lib Library instance or list * @param LibraryPackage|LibraryPackage[] $library_packages Library instance or list
* @param bool $include_suggests Whether to include suggested libraries * @param bool $include_suggests Whether to include suggested libraries
* @return array{ * @return array{
* cflags: string, * cflags: string,
* cxxflags: string,
* ldflags: string, * ldflags: string,
* libs: string * libs: string
* } * }
*/ */
public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array public function getLibraryConfig(array|LibraryPackage $library_packages, bool $include_suggests = false): array
{ {
if (!is_array($lib)) { if (!is_array($library_packages)) {
$lib = [$lib]; $library_packages = [$library_packages];
} }
$save_no_php = $this->no_php; $save_no_php = $this->no_php;
$this->no_php = true; $this->no_php = true;
$save_libs_only_deps = $this->libs_only_deps; $save_libs_only_deps = $this->libs_only_deps;
$this->libs_only_deps = true; $this->libs_only_deps = true;
$ret = $this->config( try {
packages: array_map(fn ($y) => $y->getName(), $lib), return $this->config(
packages: array_map(fn ($library_package) => $library_package->getName(), $library_packages),
include_suggests: $include_suggests, include_suggests: $include_suggests,
); );
} finally {
$this->no_php = $save_no_php; $this->no_php = $save_no_php;
$this->libs_only_deps = $save_libs_only_deps; $this->libs_only_deps = $save_libs_only_deps;
return $ret; }
} }
/** /**
@@ -174,22 +157,24 @@ class SPCConfigUtil
* dependencies by only including those that were actually resolved. * dependencies by only including those that were actually resolved.
* *
* @param string $package_name The package to get config for * @param string $package_name The package to get config for
* @param string[] $resolved_packages The full resolved package list * @param string[] $resolved_package_names The full resolved package name list
* @param bool $include_suggests Whether to include resolved suggests * @param bool $include_suggests Whether to include resolved suggests
* @return array{ * @return array{
* cflags: string, * cflags: string,
* cxxflags: string,
* ldflags: string, * ldflags: string,
* libs: string * libs: string
* } * }
*/ */
public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array public function getPackageDepsConfig(string $package_name, array $resolved_package_names, bool $include_suggests = true): array
{ {
// Get sub-dependencies within the resolved set // Get sub-dependencies within the resolved set
$sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_package_names, $include_suggests);
if (empty($sub_deps)) { if (empty($sub_deps)) {
return [ return [
'cflags' => '', 'cflags' => '',
'cxxflags' => '',
'ldflags' => '', 'ldflags' => '',
'libs' => '', 'libs' => '',
]; ];
@@ -201,29 +186,36 @@ class SPCConfigUtil
$this->no_php = true; $this->no_php = true;
$this->libs_only_deps = true; $this->libs_only_deps = true;
$ret = $this->configWithResolvedPackages($sub_deps); try {
return $this->configWithResolvedPackages($sub_deps);
} finally {
$this->no_php = $save_no_php; $this->no_php = $save_no_php;
$this->libs_only_deps = $save_libs_only_deps; $this->libs_only_deps = $save_libs_only_deps;
}
return $ret;
} }
/** /**
* Get configuration using already-resolved packages (skip dependency resolution). * Generate configuration from an exact, dependency-ordered package list.
* *
* @param string[] $resolved_packages Already resolved package names in build order * This low-level method does not resolve dependencies or filter packages.
*
* @param string[] $resolved_package_names Already resolved package names in build order
* @return array{ * @return array{
* cflags: string, * cflags: string,
* cxxflags: string,
* ldflags: string, * ldflags: string,
* libs: string * libs: string
* } * }
*/ */
public function configWithResolvedPackages(array $resolved_packages): array public function configWithResolvedPackages(array $resolved_package_names): array
{ {
$ldflags = $this->getLdflagsString(); $ldflags = $this->getLdflagsString();
$cflags = $this->getIncludesString($resolved_packages); $includes = $this->getIncludesString($resolved_package_names);
$libs = $this->getLibsString($resolved_packages, !$this->absolute_libs); $libs = $this->getLibsString($resolved_package_names, !$this->absolute_libs);
$cflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_CFLAGS') ?: '') . ' ' . getenv('CFLAGS') . ' ' . $includes));
$cxxflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_CXXFLAGS') ?: '') . ' ' . getenv('CXXFLAGS') . ' ' . $includes));
$ldflags = deduplicate_flags(clean_spaces((getenv('SPC_DEFAULT_LDFLAGS') ?: '') . ' ' . getenv('LDFLAGS') . ' ' . $ldflags));
// additional OS-specific libraries (e.g. macOS -lresolv) // additional OS-specific libraries (e.g. macOS -lresolv)
if ($extra_libs = SystemTarget::getRuntimeLibs()) { if ($extra_libs = SystemTarget::getRuntimeLibs()) {
@@ -237,11 +229,11 @@ class SPCConfigUtil
// package frameworks // package frameworks
if (SystemTarget::getTargetOS() === 'Darwin') { if (SystemTarget::getTargetOS() === 'Darwin') {
$libs .= " {$this->getFrameworksString($resolved_packages)}"; $libs .= " {$this->getFrameworksString($resolved_package_names)}";
} }
// C++ // C++
if ($this->hasCpp($resolved_packages)) { if ($this->hasCpp($resolved_package_names)) {
$target_os = SystemTarget::getTargetOS(); $target_os = SystemTarget::getTargetOS();
if ($target_os === 'Darwin') { if ($target_os === 'Darwin') {
$libcpp = '-lc++'; $libcpp = '-lc++';
@@ -256,40 +248,49 @@ class SPCConfigUtil
if ($this->libs_only_deps) { if ($this->libs_only_deps) {
// mimalloc must come first // mimalloc must come first
if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { if (in_array('mimalloc', $resolved_package_names) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
$libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs);
} }
return [ return [
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), 'cflags' => $cflags,
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), 'cxxflags' => $cxxflags,
'ldflags' => $ldflags,
'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs),
]; ];
} }
// embed // embed
if (!$this->no_php) { if (!$this->no_php) {
if (SystemTarget::getTargetOS() === 'Windows') {
$major = intdiv(PHP_VERSION_ID, 10000);
$php_lib = $this->absolute_libs ? BUILD_LIB_PATH . "\\php{$major}embed.lib" : "php{$major}embed.lib";
$libs = "{$php_lib} {$libs} kernel32.lib ole32.lib user32.lib advapi32.lib shell32.lib ws2_32.lib dnsapi.lib psapi.lib bcrypt.lib";
} else {
$libs = "-lphp {$libs} -lc"; $libs = "-lphp {$libs} -lc";
} }
}
$allLibs = getenv('LIBS') . ' ' . $libs; $allLibs = getenv('LIBS') . ' ' . $libs;
// mimalloc must come first // mimalloc must come first
if (in_array('mimalloc', $resolved_packages) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { if (in_array('mimalloc', $resolved_package_names) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
$allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs);
} }
return [ return [
'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), 'cflags' => $cflags,
'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), 'cxxflags' => $cxxflags,
'ldflags' => $ldflags,
'libs' => clean_spaces($allLibs), 'libs' => clean_spaces($allLibs),
]; ];
} }
public function getFrameworksString(array $extensions): string /** @param string[] $package_names */
public function getFrameworksString(array $package_names): string
{ {
$list = []; $list = [];
foreach ($extensions as $extension) { foreach ($package_names as $package_name) {
foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { foreach (PackageConfig::get($package_name, 'frameworks', []) as $fw) {
$ks = '-framework ' . $fw; $ks = '-framework ' . $fw;
if (!in_array($ks, $list)) { if (!in_array($ks, $list)) {
$list[] = $ks; $list[] = $ks;
@@ -299,10 +300,11 @@ class SPCConfigUtil
return implode(' ', $list); return implode(' ', $list);
} }
private function hasCpp(array $packages): bool /** @param string[] $package_names */
private function hasCpp(array $package_names): bool
{ {
foreach ($packages as $package) { foreach ($package_names as $package_name) {
$lang = PackageConfig::get($package, 'lang', 'c'); $lang = PackageConfig::get($package_name, 'lang', 'c');
if ($lang === 'cpp') { if ($lang === 'cpp') {
return true; return true;
} }
@@ -310,7 +312,8 @@ class SPCConfigUtil
return false; return false;
} }
private function getIncludesString(array $packages): string /** @param string[] $package_names */
private function getIncludesString(array $package_names): string
{ {
$base = BUILD_INCLUDE_PATH; $base = BUILD_INCLUDE_PATH;
@@ -347,8 +350,8 @@ class SPCConfigUtil
// parse pkg-configs (only for Unix) // parse pkg-configs (only for Unix)
if (SystemTarget::isUnix()) { if (SystemTarget::isUnix()) {
foreach ($packages as $package) { foreach ($package_names as $package_name) {
$pc = PackageConfig::get($package, 'pkg-configs', []); $pc = PackageConfig::get($package_name, 'pkg-configs', []);
$pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: '';
$search_paths = array_filter(explode(':', $pkg_config_path)); $search_paths = array_filter(explode(':', $pkg_config_path));
foreach ($pc as $file) { foreach ($pc as $file) {
@@ -360,7 +363,7 @@ class SPCConfigUtil
} }
} }
if (!$found) { if (!$found) {
throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist. Please build it first."); throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package_name}] does not exist. Please build it first.");
} }
} }
$pc_cflags = implode(' ', $pc); $pc_cflags = implode(' ', $pc);
@@ -386,16 +389,17 @@ class SPCConfigUtil
return '-L' . BUILD_LIB_PATH; return '-L' . BUILD_LIB_PATH;
} }
private function getLibsString(array $packages, bool $use_short_libs = true): string /** @param string[] $package_names */
private function getLibsString(array $package_names, bool $use_short_libs = true): string
{ {
$lib_names = []; $lib_names = [];
$frameworks = []; $frameworks = [];
foreach ($packages as $package) { foreach ($package_names as $package_name) {
// parse pkg-configs only for unix systems // parse pkg-configs only for unix systems
if (SystemTarget::isUnix()) { if (SystemTarget::isUnix()) {
// add pkg-configs libs // add pkg-configs libs
$pkg_configs = PackageConfig::get($package, 'pkg-configs', []); $pkg_configs = PackageConfig::get($package_name, 'pkg-configs', []);
$pkg_config_path = getenv('PKG_CONFIG_PATH') ?: ''; $pkg_config_path = getenv('PKG_CONFIG_PATH') ?: '';
$search_paths = array_filter(explode(':', $pkg_config_path)); $search_paths = array_filter(explode(':', $pkg_config_path));
foreach ($pkg_configs as $pkg_config) { foreach ($pkg_configs as $pkg_config) {
@@ -407,7 +411,7 @@ class SPCConfigUtil
} }
} }
if (!$found) { if (!$found) {
throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist. Please build it first."); throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package_name}] does not exist. Please build it first.");
} }
} }
$pkg_configs = implode(' ', $pkg_configs); $pkg_configs = implode(' ', $pkg_configs);
@@ -418,12 +422,12 @@ class SPCConfigUtil
} }
} }
// convert all static-libs to short names // convert all static-libs to short names
$libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); $libs = array_reverse(PackageConfig::get($package_name, 'static-libs', []));
foreach ($libs as $lib) { foreach ($libs as $lib) {
if (FileSystem::isRelativePath($lib)) { if (FileSystem::isRelativePath($lib)) {
// check file existence // check file existence
if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) { if (!file_exists(BUILD_LIB_PATH . "/{$lib}")) {
throw new WrongUsageException("Library file '{$lib}' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first."); throw new WrongUsageException("Library file '{$lib}' for lib [{$package_name}] does not exist in '" . BUILD_LIB_PATH . "'. Please build it first.");
} }
$lib_names[] = $this->getShortLibName($lib); $lib_names[] = $this->getShortLibName($lib);
} else { } else {
@@ -432,7 +436,7 @@ class SPCConfigUtil
} }
// add frameworks for macOS // add frameworks for macOS
if (SystemTarget::getTargetOS() === 'Darwin') { if (SystemTarget::getTargetOS() === 'Darwin') {
$frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); $frameworks = array_merge($frameworks, PackageConfig::get($package_name, 'frameworks', []));
} }
} }
@@ -451,7 +455,7 @@ class SPCConfigUtil
} }
} }
if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { if (in_array('imap', $package_names) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') {
if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) {
$lib_names[] = '-lcrypt'; $lib_names[] = '-lcrypt';
} }

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Command;
use PHPUnit\Framework\TestCase;
use Psr\Log\LogLevel;
use StaticPHP\Command\SPCConfigCommand;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
/**
* @internal
*/
class SPCConfigCommandTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
ApplicationContext::reset();
$this->setPackageConfig([
'root' => [
'type' => 'library',
'suggests' => ['optional'],
'static-libs' => ['/fixtures/libroot.a'],
],
'optional' => [
'type' => 'library',
'static-libs' => ['/fixtures/liboptional.a'],
],
]);
}
protected function tearDown(): void
{
$this->setPackageConfig([]);
ApplicationContext::reset();
logger()->setLevel(LogLevel::ERROR);
parent::tearDown();
}
public function testWithSuggestsOptionStillAffectsStandaloneCommand(): void
{
$withoutSuggests = $this->runCommand(false);
$withSuggests = $this->runCommand(true);
$this->assertStringNotContainsString('/fixtures/liboptional.a', $withoutSuggests);
$this->assertStringContainsString('/fixtures/liboptional.a', $withSuggests);
}
private function runCommand(bool $withSuggests): string
{
$application = new Application();
$application->setAutoExit(false);
$application->add(new SPCConfigCommand());
$tester = new CommandTester($application->find('spc-config'));
$input = [
'--with-libs' => 'root',
'--libs-only-deps' => true,
'--no-php' => true,
'--no-ansi' => true,
];
if ($withSuggests) {
$input['--with-suggests'] = true;
}
$tester->execute($input);
return $tester->getDisplay();
}
private function setPackageConfig(array $config): void
{
$reflection = new \ReflectionClass(PackageConfig::class);
$reflection->getProperty('package_configs')->setValue(null, $config);
}
}

View File

@@ -412,6 +412,39 @@ class DependencyResolverTest extends TestCase
$this->assertNotContains('c', $subDeps, 'c is not in the resolved set, should be excluded'); $this->assertNotContains('c', $subDeps, 'c is not in the resolved set, should be excluded');
} }
public function testGetResolvedPackageClosureFiltersSuggestsThroughResolvedSet(): void
{
$this->loadConfig([
'a' => ['type' => 'target', 'depends' => ['b'], 'suggests' => ['c']],
'b' => ['type' => 'library'],
'c' => ['type' => 'library'],
]);
$withoutSuggested = DependencyResolver::getResolvedPackageClosure(['a'], ['b', 'a']);
$withSuggested = DependencyResolver::getResolvedPackageClosure(['a'], ['b', 'c', 'a']);
$this->assertSame(['b', 'a'], $withoutSuggested);
$this->assertSame(['b', 'c', 'a'], $withSuggested);
}
public function testGetResolvedPackageClosureAppliesDependencyOverrides(): void
{
$this->loadConfig([
'php' => ['type' => 'target', 'depends' => ['libxml2']],
'libxml2' => ['type' => 'library'],
'ext-demo' => ['type' => 'php-extension', 'depends' => ['demo-lib']],
'demo-lib' => ['type' => 'library'],
]);
$closure = DependencyResolver::getResolvedPackageClosure(
['php'],
['libxml2', 'demo-lib', 'ext-demo', 'php'],
['php' => ['ext-demo']],
);
$this->assertSame(['libxml2', 'demo-lib', 'ext-demo', 'php'], $closure);
}
// ────────────────────────────────────────────── // ──────────────────────────────────────────────
// Edge cases & defensive // Edge cases & defensive
// ────────────────────────────────────────────── // ──────────────────────────────────────────────

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Util;
use PHPUnit\Framework\TestCase;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\Package;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Util\SPCConfigUtil;
/**
* @internal
*/
class SPCConfigUtilTest extends TestCase
{
/** @var array<string, false|string> */
private array $savedEnv = [];
protected function setUp(): void
{
parent::setUp();
ApplicationContext::reset();
$this->resetPackageConfig();
foreach (['CFLAGS', 'CXXFLAGS', 'LDFLAGS', 'LIBS', 'SPC_DEFAULT_CFLAGS', 'SPC_DEFAULT_CXXFLAGS', 'SPC_DEFAULT_LDFLAGS', 'SPC_EXTRA_LIBS'] as $name) {
$this->savedEnv[$name] = getenv($name);
}
}
protected function tearDown(): void
{
foreach ($this->savedEnv as $name => $value) {
$value === false ? putenv($name) : putenv("{$name}={$value}");
}
$this->resetPackageConfig();
ApplicationContext::reset();
parent::tearDown();
}
public function testStandaloneConfigStillControlsSuggestsDuringResolution(): void
{
$this->loadLinkFixture();
$util = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]);
$withoutSuggests = $util->config(['root']);
$withSuggests = $util->config(['root'], include_suggests: true);
$this->assertStringContainsString('/fixtures/librequired.a', $withoutSuggests['libs']);
$this->assertStringNotContainsString('/fixtures/liboptional.a', $withoutSuggests['libs']);
$this->assertStringContainsString('/fixtures/liboptional.a', $withSuggests['libs']);
}
public function testResolvedConfigIncludesOnlySuggestedPackagesThatWereResolved(): void
{
$this->loadLinkFixture();
$util = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]);
$withoutOptional = $util->configForResolvedBuild(['root'], $this->createInstaller([
'required' => 'library',
'root' => 'library',
]));
$withOptional = $util->configForResolvedBuild(['root'], $this->createInstaller([
'required' => 'library',
'optional' => 'library',
'root' => 'library',
]));
$this->assertStringNotContainsString('/fixtures/liboptional.a', $withoutOptional['libs']);
$this->assertStringContainsString('/fixtures/liboptional.a', $withOptional['libs']);
}
public function testResolvedConfigSupportsExplicitPhpLinkClosureOverrides(): void
{
$this->loadConfig([
'php' => ['type' => 'target', 'depends' => ['core']],
'core' => ['type' => 'library', 'static-libs' => ['/fixtures/libcore.a']],
'ext-demo' => ['type' => 'php-extension', 'depends' => ['ext-lib']],
'ext-lib' => ['type' => 'library', 'static-libs' => ['/fixtures/libext.a']],
'php-fpm' => ['type' => 'virtual-target', 'depends' => ['php'], 'suggests' => ['fpm-lib']],
'fpm-lib' => ['type' => 'library', 'static-libs' => ['/fixtures/libfpm.a']],
]);
$util = new SPCConfigUtil(['libs_only_deps' => true]);
$config = $util->configForResolvedBuild(['php'], $this->createInstaller([
'core' => 'library',
'ext-lib' => 'library',
'ext-demo' => 'static-extension',
'fpm-lib' => 'library',
'php-fpm' => 'virtual-target',
'php' => 'target',
]));
$this->assertStringContainsString('/fixtures/libcore.a', $config['libs']);
$this->assertStringContainsString('/fixtures/libext.a', $config['libs']);
$this->assertStringContainsString('/fixtures/libfpm.a', $config['libs']);
}
public function testResolvedConfigRejectsRootOutsideResolvedSet(): void
{
$this->loadLinkFixture();
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage("Package 'root' is not part of the resolved package set");
new SPCConfigUtil(['no_php' => true])->configForResolvedBuild(['root'], $this->createInstaller([
'required' => 'library',
]));
}
public function testCAndCxxFlagsRemainIndependent(): void
{
putenv('SPC_DEFAULT_CFLAGS=-DDEFAULT_C');
putenv('SPC_DEFAULT_CXXFLAGS=-DDEFAULT_CXX');
putenv('SPC_DEFAULT_LDFLAGS=-Wl,default');
putenv('CFLAGS=-DUSER_C');
putenv('CXXFLAGS=-DUSER_CXX');
putenv('LDFLAGS=-Wl,user');
$config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->configWithResolvedPackages([]);
$this->assertStringContainsString('-DDEFAULT_C', $config['cflags']);
$this->assertStringContainsString('-DUSER_C', $config['cflags']);
$this->assertStringNotContainsString('-DDEFAULT_CXX', $config['cflags']);
$this->assertStringContainsString('-DDEFAULT_CXX', $config['cxxflags']);
$this->assertStringContainsString('-DUSER_CXX', $config['cxxflags']);
$this->assertStringContainsString('-Wl,default', $config['ldflags']);
$this->assertStringContainsString('-Wl,user', $config['ldflags']);
}
private function loadLinkFixture(): void
{
$this->loadConfig([
'root' => [
'type' => 'library',
'depends' => ['required'],
'suggests' => ['optional'],
'static-libs' => ['/fixtures/libroot.a'],
],
'required' => ['type' => 'library', 'static-libs' => ['/fixtures/librequired.a']],
'optional' => ['type' => 'library', 'static-libs' => ['/fixtures/liboptional.a']],
]);
}
private function loadConfig(array $configs): void
{
$reflection = new \ReflectionClass(PackageConfig::class);
$property = $reflection->getProperty('package_configs');
$property->setValue(null, $configs);
}
private function resetPackageConfig(): void
{
$this->loadConfig([]);
}
/**
* @param array<string, 'library'|'static-extension'|'target'|'virtual-target'> $packages
*/
private function createInstaller(array $packages): PackageInstaller
{
$installer = new PackageInstaller(['no-tracker' => true], false);
$resolved = [];
foreach ($packages as $name => $type) {
$package = match ($type) {
'static-extension' => $this->createStaticExtension($name),
'target' => $this->createTarget($name, false),
'virtual-target' => $this->createTarget($name, true),
default => $this->createNamedPackage(LibraryPackage::class, $name),
};
$resolved[$name] = $package;
}
$reflection = new \ReflectionClass(PackageInstaller::class);
$reflection->getProperty('packages')->setValue($installer, $resolved);
return $installer;
}
private function createStaticExtension(string $name): PhpExtensionPackage
{
$extension = $this->createMock(PhpExtensionPackage::class);
$extension->method('getName')->willReturn($name);
$extension->method('isBuildStatic')->willReturn(true);
return $extension;
}
private function createTarget(string $name, bool $virtual): TargetPackage
{
$target = $this->createMock(TargetPackage::class);
$target->method('getName')->willReturn($name);
$target->method('isVirtualTarget')->willReturn($virtual);
return $target;
}
/** @param class-string<Package> $class */
private function createNamedPackage(string $class, string $name): Package
{
$package = $this->createMock($class);
$package->method('getName')->willReturn($name);
return $package;
}
}