Compare commits

...

32 Commits

Author SHA1 Message Date
crazywhalecc
020a30315d Fix error handling in loadFromFile methods and update exception types in tests 2025-12-10 11:33:39 +08:00
crazywhalecc
bde1440617 Refactor test structure and update paths for improved organization 2025-12-10 11:15:44 +08:00
Jerry Ma
78375632b4 V3 feat/re2c (#992)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 10:07:39 +08:00
Jerry Ma
f68adc3256 Update src/Package/Target/php.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 09:52:59 +08:00
Jerry Ma
4a968757ba Update src/Package/Library/ncurses.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-10 09:49:20 +08:00
crazywhalecc
bcaef59a15 Support full --no-ansi options 2025-12-09 16:54:29 +08:00
crazywhalecc
b0f630f95f Add package outputs, colorize motd 2025-12-09 16:34:43 +08:00
crazywhalecc
ac01867e9c Refactor stage execution to use method references for improved clarity 2025-12-09 15:01:41 +08:00
crazywhalecc
808aed2a66 Refactor package stage handling and update class structures for improved flexibility 2025-12-09 14:58:11 +08:00
crazywhalecc
e004d10861 Fix phpstan 2025-12-08 17:04:00 +08:00
crazywhalecc
0db26be826 Correct SAPI-packages to be installed 2025-12-08 17:02:12 +08:00
crazywhalecc
a4bd2a79a9 Add shared extension build support 2025-12-08 17:01:58 +08:00
crazywhalecc
7b16f683fc Allow package implementation using parent class functions 2025-12-08 17:01:29 +08:00
crazywhalecc
78234ef147 Add missing patchPkgconfPrefix function 2025-12-08 17:00:56 +08:00
crazywhalecc
80128edd39 Add patch description display 2025-12-08 17:00:34 +08:00
crazywhalecc
b384345723 Add php-micro patch for embed mode 2025-12-08 17:00:18 +08:00
crazywhalecc
f4bb0263f6 Fix ncurses static-libs 2025-12-08 17:00:03 +08:00
crazywhalecc
321f2e13e8 Allow all types of package can be built 2025-12-08 16:59:38 +08:00
crazywhalecc
11e7a590c8 Add ncurses package build 2025-12-08 16:58:47 +08:00
crazywhalecc
20e0711747 Add libedit package build 2025-12-08 16:58:42 +08:00
crazywhalecc
80d922ab3b Use patch for current package exclusively 2025-12-08 16:58:04 +08:00
crazywhalecc
a1cadecc54 Refactor re2c fix-item 2025-12-08 12:45:12 +08:00
Jerry Ma
127c935106 Refactor BUILDROOT_ABS initialization to provide a default path (#991) 2025-12-08 12:33:37 +08:00
crazywhalecc
eab105965d Remove redundant path 2025-12-08 11:06:50 +08:00
crazywhalecc
abd6c2fa3a Add PackageInstaller::isPackageInstalled() API 2025-12-08 11:06:50 +08:00
crazywhalecc
df6c27c98d Allow absolute paths for configs 2025-12-08 11:06:50 +08:00
crazywhalecc
3ff762c4c8 Fix wrong namespace in go-xcaddy package 2025-12-08 11:06:50 +08:00
crazywhalecc
6775cb4674 Fix pkg-config doctor fix using source bug 2025-12-08 11:06:50 +08:00
crazywhalecc
88b86d3eaf Fix artifact downloade does not accept boolean options bug 2025-12-08 11:06:50 +08:00
crazywhalecc
dbc6dbee53 Add Zig package support with downloader and installation checks 2025-12-08 11:06:50 +08:00
crazywhalecc
baddd60113 Add dev commands: is-installed, shell (for debugging package status) 2025-12-08 11:06:50 +08:00
crazywhalecc
2f09ace82f Add LinuxToolCheck 2025-12-08 11:06:50 +08:00
87 changed files with 5833 additions and 3152 deletions

View File

@@ -69,6 +69,6 @@ return (new PhpCsFixer\Config())
'php_unit_data_provider_method_order' => false,
])
->setFinder(
PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/SPC'])
PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/StaticPHP'])
)
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect());

View File

@@ -13,6 +13,7 @@
"ext-mbstring": "*",
"ext-zlib": "*",
"laravel/prompts": "~0.1",
"nette/php-generator": "^4.2",
"php-di/php-di": "^7.1",
"symfony/console": "^5.4 || ^6 || ^7",
"symfony/process": "^7.2",
@@ -40,7 +41,7 @@
},
"autoload-dev": {
"psr-4": {
"SPC\\Tests\\": "tests/SPC"
"Tests\\StaticPHP\\": "tests/StaticPHP"
}
},
"bin": [

163
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "14b3ad42c138807fa9288e6b510ac69f",
"content-hash": "edb3243ddaa8b05d8f6545266a146e93",
"packages": [
{
"name": "laravel/prompts",
@@ -126,6 +126,167 @@
},
"time": "2025-11-21T20:52:36+00:00"
},
{
"name": "nette/php-generator",
"version": "v4.2.0",
"source": {
"type": "git",
"url": "https://github.com/nette/php-generator.git",
"reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/php-generator/zipball/4707546a1f11badd72f5d82af4f8a6bc64bd56ac",
"reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac",
"shasum": ""
},
"require": {
"nette/utils": "^4.0.6",
"php": "8.1 - 8.5"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.4",
"nikic/php-parser": "^5.0",
"phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.8"
},
"suggest": {
"nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.2-dev"
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.5 features.",
"homepage": "https://nette.org",
"keywords": [
"code",
"nette",
"php",
"scaffolding"
],
"support": {
"issues": "https://github.com/nette/php-generator/issues",
"source": "https://github.com/nette/php-generator/tree/v4.2.0"
},
"time": "2025-08-06T18:24:31+00:00"
},
{
"name": "nette/utils",
"version": "v4.1.0",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0",
"reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0",
"shasum": ""
},
"require": {
"php": "8.2 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.5",
"phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.1-dev"
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
"core",
"datetime",
"images",
"json",
"nette",
"paginator",
"password",
"slugify",
"string",
"unicode",
"utf-8",
"utility",
"validation"
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.1.0"
},
"time": "2025-12-01T17:49:23+00:00"
},
{
"name": "php-di/invoker",
"version": "2.3.7",

View File

@@ -711,6 +711,9 @@
"ncurses": {
"type": "library",
"artifact": "ncurses",
"static-libs@unix": [
"libncurses.a"
],
"license": {
"type": "file",
"path": "COPYING"

View File

@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace Package\Artifact;
use SPC\util\GlobalEnvManager;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\GlobalEnvManager;
use StaticPHP\Util\System\LinuxUtil;
class go_xcaddy

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Package\Artifact;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Runtime\SystemTarget;
class zig
{
#[CustomBinary('zig', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function downBinary(ArtifactDownloader $downloader): DownloadResult
{
$index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry());
$index_json = json_decode($index_json ?: '', true);
$latest_version = null;
foreach ($index_json as $version => $data) {
$latest_version = $version;
break;
}
if (!$latest_version) {
throw new DownloaderException('Could not determine latest Zig version');
}
$zig_arch = SystemTarget::getTargetArch();
$zig_os = match (SystemTarget::getTargetOS()) {
'Windows' => 'win',
'Darwin' => 'macos',
'Linux' => 'linux',
default => throw new DownloaderException('Unsupported OS for Zig: ' . SystemTarget::getTargetOS()),
};
$platform_key = "{$zig_arch}-{$zig_os}";
if (!isset($index_json[$latest_version][$platform_key])) {
throw new DownloaderException("No download available for {$platform_key} in Zig version {$latest_version}");
}
$download_info = $index_json[$latest_version][$platform_key];
$url = $download_info['tarball'];
$sha256 = $download_info['shasum'];
$filename = basename($url);
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
// verify hash
$file_hash = hash_file('sha256', $path);
if ($file_hash !== $sha256) {
throw new DownloaderException("Hash mismatch for downloaded Zig binary. Expected {$sha256}, got {$file_hash}");
}
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version);
}
#[AfterBinaryExtract('zig', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function postExtractZig(string $target_path): void
{
$files = ['zig', 'zig-cc', 'zig-c++', 'zig-ar', 'zig-ld.lld', 'zig-ranlib', 'zig-objcopy'];
$all_exist = true;
foreach ($files as $file) {
if (!file_exists("{$target_path}/{$file}")) {
$all_exist = false;
break;
}
}
if ($all_exist) {
return;
}
$script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh';
$script_content = file_get_contents($script_path);
file_put_contents("{$target_path}/zig-cc", $script_content);
chmod("{$target_path}/zig-cc", 0755);
$script_content = str_replace('zig cc', 'zig c++', $script_content);
file_put_contents("{$target_path}/zig-c++", $script_content);
file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@");
file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@");
file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@");
file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@");
chmod("{$target_path}/zig-c++", 0755);
chmod("{$target_path}/zig-ar", 0755);
chmod("{$target_path}/zig-ld.lld", 0755);
chmod("{$target_path}/zig-ranlib", 0755);
chmod("{$target_path}/zig-objcopy", 0755);
}
}

View File

@@ -9,7 +9,7 @@ use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\DownloaderOptions;
use StaticPHP\Command\BaseCommand;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Attribute\AsCommand;

View File

@@ -4,9 +4,11 @@ declare(strict_types=1);
namespace Package\Extension;
use Package\Target\php;
use StaticPHP\Attribute\Package\AfterStage;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Util\SourcePatcher;
@@ -14,7 +16,8 @@ use StaticPHP\Util\SourcePatcher;
#[Extension('readline')]
class readline
{
#[BeforeStage('php', 'unix-make-cli')]
#[BeforeStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')]
#[PatchDescription('Fix readline static build with musl')]
public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
{
if ($toolchain->isStatic()) {
@@ -23,7 +26,7 @@ class readline
}
}
#[AfterStage('php', 'unix-make-cli')]
#[AfterStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')]
public function afterMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
{
if ($toolchain->isStatic()) {

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Package\Library;
use Package\Target\php;
use StaticPHP\Attribute\Package\AfterStage;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Attribute\PatchDescription;
@@ -13,7 +14,7 @@ use StaticPHP\Util\FileSystem;
#[Library('imap')]
class imap
{
#[AfterStage('php', 'patch-embed-scripts')]
#[AfterStage('php', [php::class, 'patchEmbedScripts'], 'imap')]
#[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')]
public function afterPatchScripts(): void
{

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Package\Library;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Util\FileSystem;
#[Library('libedit')]
class libedit extends LibraryPackage
{
#[BeforeStage(stage: 'build')]
public function patchBeforeBuild(): void
{
FileSystem::replaceFileRegex(
"{$this->getSourceDir()}/src/sys.h",
'|//#define\s+strl|',
'#define strl'
);
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(): void
{
UnixAutoconfExecutor::create($this)
->appendEnv(['CFLAGS' => '-D__STDC_ISO_10646__=201103L'])
->configure()
->make();
$this->patchPkgconfPrefix(['libedit.pc']);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Package\Library;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Util\DirDiff;
use StaticPHP\Util\FileSystem;
#[Library('ncurses')]
class ncurses
{
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(LibraryPackage $package, ToolchainInterface $toolchain): void
{
$dirdiff = new DirDiff(BUILD_BIN_PATH);
UnixAutoconfExecutor::create($package)
->appendEnv([
'LDFLAGS' => $toolchain->isStatic() ? '-static' : '',
])
->configure(
'--enable-overwrite',
'--with-curses-h',
'--enable-pc-files',
'--enable-echo',
'--disable-widec',
'--with-normal',
'--with-ticlib',
'--without-tests',
'--without-dlsym',
'--without-debug',
'--enable-symlinks',
"--bindir={$package->getBinDir()}",
"--includedir={$package->getIncludeDir()}",
"--libdir={$package->getLibDir()}",
"--prefix={$package->getBuildRootPath()}",
)
->make();
$new_files = $dirdiff->getIncrementFiles(true);
foreach ($new_files as $file) {
@unlink(BUILD_BIN_PATH . '/' . $file);
}
shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo');
shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo');
$pkgconf_list = ['form.pc', 'menu.pc', 'ncurses++.pc', 'ncurses.pc', 'panel.pc', 'tic.pc'];
$package->patchPkgconfPrefix($pkgconf_list);
foreach ($pkgconf_list as $pkgconf) {
FileSystem::replaceFileStr("{$package->getLibDir()}/pkgconfig/{$pkgconf}", "-L{$package->getLibDir()}", '-L${libdir}');
}
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Package\Library;
use Package\Target\php;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Attribute\PatchDescription;
@@ -12,7 +13,7 @@ use StaticPHP\Package\TargetPackage;
#[Library('postgresql')]
class postgresql
{
#[BeforeStage('php', 'unix-configure', 'postgresql')]
#[BeforeStage('php', [php::class, 'configureForUnix'], 'postgresql')]
#[PatchDescription('Patch to avoid explicit_bzero detection issues on some systems')]
public function patchBeforePHPConfigure(TargetPackage $package): void
{

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Package\Target;
use SPC\util\GlobalEnvManager;
use StaticPHP\Attribute\Package\InitPackage;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Util\GlobalEnvManager;
#[Target('go-xcaddy')]
class go_xcaddy

View File

@@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Package\Target;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Util\FileSystem;
#[Target('php-micro')]
class micro
{
#[BeforeStage('php', [php::class, 'makeEmbedForUnix'], 'php-micro')]
#[PatchDescription('Patch Makefile to build only libphp.la for embedding')]
public function patchBeforeEmbed(TargetPackage $package): void
{
FileSystem::replaceFileStr("{$package->getSourceDir()}/Makefile", 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la');
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Package\Target;
use StaticPHP\Artifact\ArtifactLoader;
use StaticPHP\Attribute\Package\BeforeStage;
use StaticPHP\Attribute\Package\BuildFor;
use StaticPHP\Attribute\Package\Info;
@@ -14,15 +13,17 @@ use StaticPHP\Attribute\Package\Stage;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Attribute\Package\Validate;
use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\Package;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ToolchainManager;
@@ -44,7 +45,7 @@ use ZM\Logger\ConsoleColor;
#[Target('php-cgi')]
#[Target('php-embed')]
#[Target('frankenphp')]
class php
class php extends TargetPackage
{
public static function getPHPVersionID(): int
{
@@ -111,7 +112,7 @@ class php
}
#[ResolveBuild]
public function resolveBuild(TargetPackage $package): array
public function resolveBuild(TargetPackage $package, PackageInstaller $installer): array
{
// Parse extensions and additional packages for all php-* targets
$static_extensions = parse_extension_list($package->getBuildArgument('extensions'));
@@ -128,6 +129,7 @@ class php
// get instances
foreach ($extensions_pkg as $extension) {
$extname = substr($extension, 4);
$config = PackageConfig::get($extension, 'php-extension', []);
if (!PackageLoader::hasPackage($extension)) {
throw new WrongUsageException("Extension [{$extname}] does not exist. Please check your extension name.");
}
@@ -137,13 +139,25 @@ class php
}
// set build static/shared
if (in_array($extname, $static_extensions)) {
if (($config['build-static'] ?? true) === false) {
throw new WrongUsageException("Extension [{$extname}] cannot be built as static extension.");
}
$instance->setBuildStatic();
}
if (in_array($extname, $shared_extensions)) {
if (($config['build-shared'] ?? true) === false) {
throw new WrongUsageException("Extension [{$extname}] cannot be built as shared extension, please remove it from --build-shared option.");
}
$instance->setBuildShared();
$instance->setBuildWithPhp($config['build-with-php'] ?? false);
}
}
// building shared extensions need embed SAPI
if (!empty($shared_extensions) && !$package->getBuildOption('build-embed', false) && $package->getName() === 'php') {
$installer->addBuildPackage('php-embed');
}
return [...$extensions_pkg, ...$additional_packages];
}
@@ -178,14 +192,14 @@ class php
return [];
}
$sapis = array_filter([
$installer->getBuildPackage('php-cli') ? 'cli' : null,
$installer->getBuildPackage('php-fpm') ? 'fpm' : null,
$installer->getBuildPackage('php-micro') ? 'micro' : null,
$installer->getBuildPackage('php-cgi') ? 'cgi' : null,
$installer->getBuildPackage('php-embed') ? 'embed' : null,
$installer->getBuildPackage('frankenphp') ? 'frankenphp' : null,
$installer->isPackageResolved('php-cli') ? 'cli' : null,
$installer->isPackageResolved('php-fpm') ? 'fpm' : null,
$installer->isPackageResolved('php-micro') ? 'micro' : null,
$installer->isPackageResolved('php-cgi') ? 'cgi' : null,
$installer->isPackageResolved('php-embed') ? 'embed' : null,
$installer->isPackageResolved('frankenphp') ? 'frankenphp' : null,
]);
$static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() === 'php-extension');
$static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildStatic());
$shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []);
$install_packages = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() !== 'php-extension' && $x->getName() !== 'php' && !str_starts_with($x->getName(), 'php-'));
return [
@@ -227,7 +241,7 @@ class php
FileSystem::removeDir(BUILD_MODULES_PATH);
}
#[BeforeStage('php', 'unix-buildconf')]
#[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')]
#[PatchDescription('Patch configure.ac for musl and musl-toolchain')]
#[PatchDescription('Let php m4 tools use static pkg-config')]
public function patchBeforeBuildconf(TargetPackage $package): void
@@ -245,7 +259,7 @@ class php
FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC(');
}
#[Stage('unix-buildconf')]
#[Stage]
public function buildconfForUnix(TargetPackage $package): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf'));
@@ -253,7 +267,7 @@ class php
shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF'));
}
#[Stage('unix-configure')]
#[Stage]
public function configureForUnix(TargetPackage $package, PackageInstaller $installer): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure'));
@@ -281,15 +295,15 @@ class php
$args[] = "--with-config-file-scan-dir={$option}";
}
// perform enable cli options
$args[] = $installer->isBuildPackage('php-cli') ? '--enable-cli' : '--disable-cli';
$args[] = $installer->isBuildPackage('php-fpm') ? '--enable-fpm' : '--disable-fpm';
$args[] = $installer->isBuildPackage('php-micro') ? match (SystemTarget::getTargetOS()) {
$args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli';
$args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm';
$args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) {
'Linux' => '--enable-micro=all-static',
default => '--enable-micro',
} : null;
$args[] = $installer->isBuildPackage('php-cgi') ? '--enable-cgi' : '--disable-cgi';
$args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi';
$embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static';
$args[] = $installer->isBuildPackage('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed';
$args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed';
$args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null;
$args = implode(' ', array_filter($args));
@@ -303,7 +317,7 @@ class php
])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir());
}
#[Stage('unix-make')]
#[Stage]
public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void
{
V2CompatLayer::emitPatchPoint('before-php-make');
@@ -311,24 +325,24 @@ class php
logger()->info('cleaning up php-src build files');
shell()->cd($package->getSourceDir())->exec('make clean');
if ($installer->isBuildPackage('php-cli')) {
$package->runStage('unix-make-cli');
if ($installer->isPackageResolved('php-cli')) {
$package->runStage([self::class, 'makeCliForUnix']);
}
if ($installer->isBuildPackage('php-cgi')) {
$package->runStage('unix-make-cgi');
if ($installer->isPackageResolved('php-cgi')) {
$package->runStage([self::class, 'makeCgiForUnix']);
}
if ($installer->isBuildPackage('php-fpm')) {
$package->runStage('unix-make-fpm');
if ($installer->isPackageResolved('php-fpm')) {
$package->runStage([self::class, 'makeFpmForUnix']);
}
if ($installer->isBuildPackage('php-micro')) {
$package->runStage('unix-make-micro');
if ($installer->isPackageResolved('php-micro')) {
$package->runStage([self::class, 'makeMicroForUnix']);
}
if ($installer->isBuildPackage('php-embed')) {
$package->runStage('unix-make-embed');
if ($installer->isPackageResolved('php-embed')) {
$package->runStage([self::class, 'makeEmbedForUnix']);
}
}
#[Stage('unix-make-cli')]
#[Stage]
public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli'));
@@ -336,9 +350,12 @@ class php
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} cli");
$builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php');
$package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php');
}
#[Stage('unix-make-cgi')]
#[Stage]
public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi'));
@@ -346,9 +363,12 @@ class php
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} cgi");
$builder->deployBinary("{$package->getSourceDir()}/sapi/cgi/php-cgi", BUILD_BIN_PATH . '/php-cgi');
$package->setOutput('Binary path for cgi SAPI', BUILD_BIN_PATH . '/php-cgi');
}
#[Stage('unix-make-fpm')]
#[Stage]
public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm'));
@@ -356,9 +376,13 @@ class php
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec("make -j{$concurrency} fpm");
$builder->deployBinary("{$package->getSourceDir()}/sapi/fpm/php-fpm", BUILD_BIN_PATH . '/php-fpm');
$package->setOutput('Binary path for fpm SAPI', BUILD_BIN_PATH . '/php-fpm');
}
#[Stage('unix-make-micro')]
#[Stage]
#[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')]
public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
$phar_patched = false;
@@ -375,6 +399,9 @@ class php
shell()->cd($package->getSourceDir())
->setEnv($vars)
->exec("make -j{$builder->concurrency} micro");
$builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', BUILD_BIN_PATH . '/micro.sfx');
$package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx');
} finally {
if ($phar_patched) {
SourcePatcher::unpatchMicroPhar();
@@ -382,9 +409,10 @@ class php
}
}
#[Stage('unix-make-embed')]
#[Stage]
public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed'));
$shared_exts = array_filter(
$installer->getResolvedPackages(),
static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp()
@@ -395,9 +423,11 @@ class php
$diff = new DirDiff(BUILD_MODULES_PATH, true);
$root = BUILD_ROOT_PATH;
$sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i';
shell()->cd($package->getSourceDir())
->setEnv($this->makeVars($installer))
->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile')
->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile')
->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs");
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared -------------
@@ -412,39 +442,38 @@ class php
}
// deploy
$builder->deployBinary($libphp_so, $libphp_so, false);
$package->setOutput('Library path for embed SAPI', $libphp_so);
}
// process shared extensions that built-with-php
$increment_files = $diff->getChangedFiles();
$files = [];
foreach ($increment_files as $increment_file) {
$builder->deployBinary($increment_file, $libphp_so, false);
$builder->deployBinary($increment_file, $increment_file, false);
$files[] = basename($increment_file);
}
if (!empty($files)) {
$package->setOutput('Built shared extensions', implode(', ', $files));
}
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static -------------
// process libphp.a for static embed
if (!file_exists("{$package->getLibDir()}/libphp.a")) {
return;
}
$ar = getenv('AR') ?: 'ar';
$libphp_a = "{$package->getLibDir()}/libphp.a";
shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}");
UnixUtil::exportDynamicSymbols($libphp_a);
// deploy embed php scripts
$package->runStage('patch-embed-scripts');
$package->runStage([$this, 'patchEmbedScripts']);
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain): void
#[Stage]
public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void
{
// virtual target, do nothing
if ($package->getName() !== 'php') {
return;
}
$package->runStage('unix-buildconf');
$package->runStage('unix-configure');
$package->runStage('unix-make');
// collect shared extensions
/** @var PhpExtensionPackage[] $shared_extensions */
$shared_extensions = array_filter(
@@ -470,9 +499,10 @@ class php
}
try {
logger()->debug('Building shared extensions...');
foreach ($shared_extensions as $extension) {
logger()->info('Building shared extensions...');
$extension->buildSharedExtension();
InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName()));
$extension->buildShared();
}
} finally {
// restore php-config
@@ -483,17 +513,34 @@ class php
}
}
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build(TargetPackage $package): void
{
// virtual target, do nothing
if ($package->getName() !== 'php') {
return;
}
$package->runStage([$this, 'buildconfForUnix']);
$package->runStage([$this, 'configureForUnix']);
$package->runStage([$this, 'makeForUnix']);
$package->runStage([$this, 'unixBuildSharedExt']);
}
/**
* Patch phpize and php-config if needed
*/
#[Stage('patch-embed-scripts')]
public function patchPhpScripts(): void
#[Stage]
public function patchEmbedScripts(): void
{
// patch phpize
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
logger()->debug('Patching phpize prefix');
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
$this->setOutput('phpize script path for embed SAPI', BUILD_BIN_PATH . '/phpize');
}
// patch php-config
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
@@ -505,6 +552,7 @@ class php
// move lstdc++ to the end of libs
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
$this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config');
}
}

View File

@@ -24,6 +24,7 @@ use StaticPHP\Exception\ExecutionException;
use StaticPHP\Exception\SPCException;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
@@ -113,7 +114,7 @@ class ArtifactDownloader
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_PREFER_SOURCE;
}
} elseif ($options['prefer-source'] === null) {
} elseif ($options['prefer-source'] === null || $options['prefer-source'] === true) {
$this->default_fetch_pref = Artifact::FETCH_PREFER_SOURCE;
}
}
@@ -124,7 +125,7 @@ class ArtifactDownloader
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY;
}
} elseif ($options['prefer-binary'] === null) {
} elseif ($options['prefer-binary'] === null || $options['prefer-binary'] === true) {
$this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY;
}
}
@@ -134,7 +135,7 @@ class ArtifactDownloader
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY;
}
} elseif ($options['prefer-pre-built'] === null) {
} elseif ($options['prefer-pre-built'] === null || $options['prefer-pre-built'] === true) {
$this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY;
}
}
@@ -145,7 +146,7 @@ class ArtifactDownloader
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_ONLY_SOURCE;
}
} elseif ($options['source-only'] === null) {
} elseif ($options['source-only'] === null || $options['source-only'] === true) {
$this->default_fetch_pref = Artifact::FETCH_ONLY_SOURCE;
}
}
@@ -156,7 +157,7 @@ class ArtifactDownloader
foreach ($ls as $name) {
$this->fetch_prefs[$name] = Artifact::FETCH_ONLY_BINARY;
}
} elseif ($options['binary-only'] === null) {
} elseif ($options['binary-only'] === null || $options['binary-only'] === true) {
$this->default_fetch_pref = Artifact::FETCH_ONLY_BINARY;
}
}
@@ -164,7 +165,7 @@ class ArtifactDownloader
if (array_key_exists('ignore-cache', $options)) {
if (is_string($options['ignore-cache'])) {
$this->ignore_cache = parse_comma_list($options['ignore-cache']);
} elseif ($options['ignore-cache'] === null) {
} elseif ($options['ignore-cache'] === null || $options['ignore-cache'] === true) {
$this->ignore_cache = true;
}
}
@@ -172,7 +173,7 @@ class ArtifactDownloader
if (array_key_exists('ignore-cache-sources', $options)) {
if (is_string($options['ignore-cache-sources'])) {
$this->ignore_cache = parse_comma_list($options['ignore-cache-sources']);
} elseif ($options['ignore-cache-sources'] === null) {
} elseif ($options['ignore-cache-sources'] === null || $options['ignore-cache-sources'] === true) {
$this->ignore_cache = true;
}
}

View File

@@ -9,6 +9,7 @@ use StaticPHP\Exception\FileSystemException;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\Package;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\FileSystem;
@@ -351,8 +352,6 @@ class ArtifactExtractor
* @param string $name Artifact name (for error messages)
* @param string $source_file Path to the source file or directory
* @param string $cache_type Cache type: archive, git, local
*
* @throws WrongUsageException if source file does not exist
*/
protected function validateSourceFile(string $name, string $source_file, string $cache_type): void
{

View File

@@ -10,5 +10,5 @@ namespace StaticPHP\Attribute\Package;
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
readonly class AfterStage
{
public function __construct(public string $package_name, public string $stage, public ?string $only_when_package_resolved = null) {}
public function __construct(public string $package_name, public array|string $stage, public ?string $only_when_package_resolved = null) {}
}

View File

@@ -8,7 +8,12 @@ namespace StaticPHP\Attribute\Package;
* Indicates that the annotated method should be executed before a specific stage of the build process for a given package.
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
readonly class BeforeStage
class BeforeStage
{
public function __construct(public string $package_name, public string $stage, public ?string $only_when_package_resolved = null) {}
public readonly array|string $stage;
public function __construct(public string $package_name = '', array|callable|string $stage = '', public ?string $only_when_package_resolved = null)
{
$this->stage = $stage;
}
}

View File

@@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Attribute\Package;
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
readonly class PatchBeforeBuild
{
public function __construct() {}
}

View File

@@ -10,5 +10,5 @@ namespace StaticPHP\Attribute\Package;
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
readonly class Stage
{
public function __construct(public string $name) {}
public function __construct(public ?string $function = null) {}
}

View File

@@ -10,6 +10,7 @@ use StaticPHP\Exception\SPCException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Logger\ConsoleColor;
abstract class BaseCommand extends Command
{
@@ -21,7 +22,7 @@ abstract class BaseCommand extends Command
/ ___|| |_ __ _| |_(_) ___| _ \| | | | _ \
\___ \| __/ _` | __| |/ __| |_) | |_| | |_) |
___) | || (_| | |_| | (__| __/| _ | __/
|____/ \__\__,_|\__|_|\___|_| |_| |_|_| v{version}
|____/ \__\__,_|\__|_|\___|_| |_| |_|_| {version}
';
@@ -69,7 +70,8 @@ abstract class BaseCommand extends Command
});
$version = $this->getVersionWithCommit();
if (!$this->no_motd) {
echo str_replace('{version}', $version, self::$motd);
$str = str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd));
echo $this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str;
}
}

View File

@@ -6,7 +6,7 @@ namespace StaticPHP\Command;
use StaticPHP\Artifact\DownloaderOptions;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Util\V2CompatLayer;
use Symfony\Component\Console\Input\InputOption;
@@ -51,6 +51,8 @@ class BuildTargetCommand extends BaseCommand
$this->output->writeln("<info>✔ BUILD SUCCESSFUL ({$usedtime} s)</info>");
$this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
$installer->printBuildPackageOutputs();
return static::SUCCESS;
}
}

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command\Dev;
use StaticPHP\Command\BaseCommand;
use StaticPHP\Package\PackageInstaller;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
#[AsCommand('dev:is-installed', 'Check if a package is installed correctly', ['is-installed'], true)]
class IsInstalledCommand extends BaseCommand
{
public function configure(): void
{
$this->no_motd = true;
$this->addArgument('package', InputArgument::REQUIRED, 'The package name to check installation status');
}
public function handle(): int
{
$installer = new PackageInstaller();
$package = $this->input->getArgument('package');
$installer->addInstallPackage($package);
$installed = $installer->isPackageInstalled($package);
if ($installed) {
$this->output->writeln("<info>Package [{$package}] is installed correctly.</info>");
return static::SUCCESS;
}
$this->output->writeln("<error>Package [{$package}] is not installed.</error>");
return static::FAILURE;
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Command\Dev;
use StaticPHP\Command\BaseCommand;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\GlobalEnvManager;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand('dev:shell')]
class ShellCommand extends BaseCommand
{
public function handle(): int
{
// need to init global env first
GlobalEnvManager::afterInit();
$this->output->writeln("Entering interactive shell. Type 'exit' to leave.");
if (SystemTarget::isUnix()) {
passthru('PS1=\'[StaticPHP] > \' /bin/bash', $code);
return $code;
}
if (SystemTarget::getTargetOS() === 'Windows') {
passthru('cmd.exe', $code);
return $code;
}
$this->output->writeln('<error>Unsupported OS for shell command.</error>');
return static::FAILURE;
}
}

View File

@@ -6,7 +6,7 @@ namespace StaticPHP\Command;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\DownloaderOptions;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;

View File

@@ -6,9 +6,9 @@ namespace StaticPHP\Command;
use StaticPHP\Artifact\ArtifactCache;
use StaticPHP\Artifact\ArtifactExtractor;
use StaticPHP\Artifact\ArtifactLoader;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Attribute\AsCommand;

View File

@@ -31,7 +31,7 @@ class ArtifactConfig
*/
public static function loadFromFile(string $file): void
{
$content = file_get_contents($file);
$content = @file_get_contents($file);
if ($content === false) {
throw new WrongUsageException("Failed to read artifact config file: {$file}");
}

View File

@@ -38,7 +38,7 @@ class PackageConfig
*/
public static function loadFromFile(string $file): void
{
$content = file_get_contents($file);
$content = @file_get_contents($file);
if ($content === false) {
throw new WrongUsageException("Failed to read package config file: {$file}");
}

View File

@@ -6,13 +6,16 @@ namespace StaticPHP;
use StaticPHP\Command\BuildLibsCommand;
use StaticPHP\Command\BuildTargetCommand;
use StaticPHP\Command\Dev\IsInstalledCommand;
use StaticPHP\Command\Dev\ShellCommand;
use StaticPHP\Command\DoctorCommand;
use StaticPHP\Command\DownloadCommand;
use StaticPHP\Command\ExtractCommand;
use StaticPHP\Command\InstallPackageCommand;
use StaticPHP\Command\SPCConfigCommand;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Registry\Registry;
use Symfony\Component\Console\Application;
class ConsoleApplication extends Application
@@ -27,6 +30,9 @@ class ConsoleApplication extends Application
require_once ROOT_DIR . '/src/bootstrap.php';
// check registry
Registry::checkLoadedRegistries();
/**
* @var string $name
* @var TargetPackage $package
@@ -47,6 +53,10 @@ class ConsoleApplication extends Application
new BuildLibsCommand(),
new ExtractCommand(),
new SPCConfigCommand(),
// dev commands
new ShellCommand(),
new IsInstalledCommand(),
]);
// add additional commands from registries

View File

@@ -7,8 +7,10 @@ namespace StaticPHP\DI;
use DI\Container;
use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
use StaticPHP\Attribute\PatchDescription;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Logger\ConsoleColor;
use function DI\factory;
@@ -33,8 +35,6 @@ class ApplicationContext
* @param array $options Initialization options
* - 'debug': Enable debug mode (disables compilation)
* - 'definitions': Additional container definitions
*
* @throws \RuntimeException If already initialized
*/
public static function initialize(array $options = []): Container
{
@@ -58,7 +58,8 @@ class ApplicationContext
self::$debug = $options['debug'] ?? false;
self::$container = $builder->build();
self::$invoker = new CallbackInvoker(self::$container);
// Get invoker from container to ensure singleton consistency
self::$invoker = self::$container->get(CallbackInvoker::class);
return self::$container;
}
@@ -124,7 +125,8 @@ class ApplicationContext
public static function getInvoker(): CallbackInvoker
{
if (self::$invoker === null) {
self::$invoker = new CallbackInvoker(self::getContainer());
// Get from container to ensure singleton consistency
self::$invoker = self::getContainer()->get(CallbackInvoker::class);
}
return self::$invoker;
}
@@ -137,7 +139,19 @@ class ApplicationContext
*/
public static function invoke(callable $callback, array $context = []): mixed
{
logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure')));
if (function_exists('logger')) {
logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure')));
}
// get if callback has attribute PatchDescription
$ref = new \ReflectionFunction(\Closure::fromCallable($callback));
$attributes = $ref->getAttributes(PatchDescription::class);
foreach ($attributes as $attribute) {
$attrInstance = $attribute->newInstance();
if (function_exists('logger')) {
logger()->info(ConsoleColor::magenta('[PATCH]') . ConsoleColor::green(" {$attrInstance->description}"));
}
}
return self::getInvoker()->invoke($callback, $context);
}

View File

@@ -5,12 +5,13 @@ declare(strict_types=1);
namespace StaticPHP\DI;
use DI\Container;
use StaticPHP\Exception\SPCInternalException;
/**
* CallbackInvoker is responsible for invoking callbacks with automatic dependency injection.
* It supports context-based parameter resolution, allowing temporary bindings without polluting the container.
*/
class CallbackInvoker
readonly class CallbackInvoker
{
public function __construct(
private Container $container
@@ -26,15 +27,20 @@ class CallbackInvoker
* 4. Default value
* 5. Null (if nullable)
*
* Note: For object values in context, the invoker automatically registers
* the object under all its parent classes and interfaces, allowing type hints
* to match any type in the inheritance hierarchy.
*
* @param callable $callback The callback to invoke
* @param array $context Context parameters (type => value or name => value)
*
* @return mixed The return value of the callback
*
* @throws \RuntimeException If a required parameter cannot be resolved
*/
public function invoke(callable $callback, array $context = []): mixed
{
// Expand context to include all parent classes and interfaces for objects
$context = $this->expandContextHierarchy($context);
$reflection = new \ReflectionFunction(\Closure::fromCallable($callback));
$args = [];
@@ -57,8 +63,13 @@ class CallbackInvoker
// 3. Look up in container by type
if ($typeName !== null && !$this->isBuiltinType($typeName) && $this->container->has($typeName)) {
$args[] = $this->container->get($typeName);
continue;
try {
$args[] = $this->container->get($typeName);
continue;
} catch (\Throwable $e) {
// Container failed to resolve (e.g., missing constructor params)
// Fall through to try default value or nullable
}
}
// 4. Use default value if available
@@ -74,7 +85,7 @@ class CallbackInvoker
}
// Cannot resolve parameter
throw new \RuntimeException(
throw new SPCInternalException(
"Cannot resolve parameter '{$paramName}'" .
($typeName ? " of type '{$typeName}'" : '') .
' for callback invocation'
@@ -95,4 +106,44 @@ class CallbackInvoker
'void', 'null', 'false', 'true', 'never',
], true);
}
/**
* Expand context to include all parent classes and interfaces for object values.
* This allows type hints to match any type in the object's inheritance hierarchy.
*
* @param array $context Original context array
* @return array Expanded context with all class hierarchy mappings
*/
private function expandContextHierarchy(array $context): array
{
$expanded = [];
foreach ($context as $key => $value) {
// Keep the original key-value pair
$expanded[$key] = $value;
// If value is an object, add mappings for all parent classes and interfaces
if (is_object($value)) {
$originalReflection = new \ReflectionClass($value);
// Add concrete class
$expanded[$originalReflection->getName()] = $value;
// Add all parent classes
$reflection = $originalReflection;
while ($parent = $reflection->getParentClass()) {
$expanded[$parent->getName()] = $value;
$reflection = $parent;
}
// Add all interfaces - reuse original reflection
$interfaces = $originalReflection->getInterfaceNames();
foreach ($interfaces as $interface) {
$expanded[$interface] = $value;
}
}
}
return $expanded;
}
}

View File

@@ -7,6 +7,7 @@ namespace StaticPHP\Doctor;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCException;
use StaticPHP\Registry\DoctorLoader;
use StaticPHP\Runtime\Shell\Shell;
use StaticPHP\Util\InteractiveTerm;
use Symfony\Component\Console\Output\OutputInterface;

View File

@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Doctor\Item;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;
use StaticPHP\Doctor\CheckResult;
use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Util\System\LinuxUtil;
class LinuxToolCheck
{
public const TOOLS_ALPINE = [
'make', 'bison', 're2c', 'flex',
'git', 'autoconf', 'automake', 'gettext-dev',
'tar', 'unzip', 'gzip',
'bzip2', 'cmake', 'gcc',
'g++', 'patch', 'binutils-gold',
'libtoolize', 'which',
'patchelf',
];
public const TOOLS_DEBIAN = [
'make', 'bison', 're2c', 'flex',
'git', 'autoconf', 'automake', 'autopoint',
'tar', 'unzip', 'gzip', 'gcc', 'g++',
'bzip2', 'cmake', 'patch',
'xz', 'libtoolize', 'which',
'patchelf',
];
public const TOOLS_RHEL = [
'perl', 'make', 'bison', 're2c', 'flex',
'git', 'autoconf', 'automake',
'tar', 'unzip', 'gzip', 'gcc', 'g++',
'bzip2', 'cmake', 'patch', 'which',
'xz', 'libtool', 'gettext-devel',
'patchelf', 'file',
];
public const TOOLS_ARCH = [
'base-devel', 'cmake',
];
private const PROVIDED_COMMAND = [
'perl' => '/usr/share/perl5/FindBin.pm',
'binutils-gold' => 'ld.gold',
'base-devel' => 'automake',
'gettext-devel' => 'gettextize',
'gettext-dev' => 'gettextize',
'perl-IPC-Cmd' => '/usr/share/perl5/vendor_perl/IPC/Cmd.pm',
'perl-Time-Piece' => '/usr/lib64/perl5/Time/Piece.pm',
];
/** @noinspection PhpUnused */
#[CheckItem('if necessary tools are installed', limit_os: 'Linux', level: 999)]
public function checkCliTools(): ?CheckResult
{
$distro = LinuxUtil::getOSRelease();
$required = match ($distro['dist']) {
'alpine' => self::TOOLS_ALPINE,
'redhat' => self::TOOLS_RHEL,
'centos' => array_merge(self::TOOLS_RHEL, ['perl-IPC-Cmd', 'perl-Time-Piece']),
'arch' => self::TOOLS_ARCH,
default => self::TOOLS_DEBIAN,
};
$missing = [];
foreach ($required as $package) {
if (LinuxUtil::findCommand(self::PROVIDED_COMMAND[$package] ?? $package) === null) {
$missing[] = $package;
}
}
if (!empty($missing)) {
return CheckResult::fail(implode(', ', $missing) . ' not installed on your system', 'install-linux-tools', [$distro, $missing]);
}
return CheckResult::ok();
}
#[CheckItem('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.22.0') < 0) {
return CheckResult::fail('cmake version is too low (' . $ver . '), please update it manually!');
}
return CheckResult::ok($ver);
}
/** @noinspection PhpUnused */
#[CheckItem('if necessary linux headers are installed', limit_os: 'Linux')]
public function checkSystemOSPackages(): ?CheckResult
{
if (LinuxUtil::isMuslDist()) {
// check linux-headers installation
if (!file_exists('/usr/include/linux/mman.h')) {
return CheckResult::fail('linux-headers not installed on your system', 'install-linux-tools', [LinuxUtil::getOSRelease(), ['linux-headers']]);
}
}
return CheckResult::ok();
}
#[FixItem('install-linux-tools')]
public function fixBuildTools(array $distro, array $missing): bool
{
$install_cmd = match ($distro['dist']) {
'ubuntu', 'debian', 'Deepin', 'neon' => 'apt-get install -y',
'alpine' => 'apk add',
'redhat' => 'dnf install -y',
'centos' => 'yum install -y',
'arch' => 'pacman -S --noconfirm',
default => null,
};
if ($install_cmd === null) {
// try family
$family = explode(' ', strtolower($distro['family']));
if (in_array('debian', $family)) {
$install_cmd = 'apt-get install -y';
} elseif (in_array('rhel', $family) || in_array('fedora', $family)) {
$install_cmd = 'dnf install -y';
} else {
throw new EnvironmentException(
"Current linux distro [{$distro['dist']}] does not have an auto-install script for packages yet.",
'You can submit an issue to request support: https://github.com/crazywhalecc/static-php-cli/issues'
);
}
}
$prefix = '';
if (($user = exec('whoami')) !== 'root') {
$prefix = 'sudo ';
logger()->warning("Current user ({$user}) is not root, using sudo for running command (may require password input)");
}
$is_debian = LinuxUtil::isDebianDist();
$to_install = $is_debian ? str_replace('xz', 'xz-utils', $missing) : $missing;
// debian, alpine libtool -> libtoolize
$to_install = str_replace('libtoolize', 'libtool', $to_install);
shell()->exec($prefix . $install_cmd . ' ' . implode(' ', $to_install));
return true;
}
}

View File

@@ -45,7 +45,7 @@ class PkgConfigCheck
public function fix(): bool
{
ApplicationContext::set('elephant', true);
$installer = new PackageInstaller(['dl-prefer-binary' => true]);
$installer = new PackageInstaller(['dl-binary-only' => true]);
$installer->addInstallPackage('pkg-config');
$installer->run(false, true);
return true;

View File

@@ -7,6 +7,7 @@ namespace StaticPHP\Doctor\Item;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;
use StaticPHP\Doctor\CheckResult;
use StaticPHP\Package\PackageInstaller;
class Re2cVersionCheck
{
@@ -29,7 +30,9 @@ class Re2cVersionCheck
#[FixItem('build-re2c')]
public function buildRe2c(): bool
{
// TODO: implement re2c build process
return false;
$installer = new PackageInstaller();
$installer->addInstallPackage('re2c');
$installer->run(false);
return true;
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Doctor\Item;
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\Package\PackageInstaller;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ZigToolchain;
#[OptionalCheck([self::class, 'optionalCheck'])]
class ZigCheck
{
public static function optionalCheck(): bool
{
return ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain;
}
/** @noinspection PhpUnused */
#[CheckItem('if zig is installed', level: 800)]
public function checkZig(): CheckResult
{
$installer = new PackageInstaller();
$package = 'zig';
$installer->addInstallPackage($package);
$installed = $installer->isPackageInstalled($package);
if ($installed) {
return CheckResult::ok();
}
return CheckResult::fail('zig is not installed', 'install-zig');
}
#[FixItem('install-zig')]
public function installZig(): bool
{
$installer = new PackageInstaller();
$installer->addInstallPackage('zig');
$installer->run(false);
return $installer->isPackageInstalled('zig');
}
}

View File

@@ -10,6 +10,7 @@ use SPC\builder\linux\LinuxBuilder;
use SPC\builder\macos\MacOSBuilder;
use SPC\builder\windows\WindowsBuilder;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Util\InteractiveTerm;
use ZM\Logger\ConsoleColor;
class ExceptionHandler
@@ -25,11 +26,13 @@ class ExceptionHandler
SPCInternalException::class,
ValidationException::class,
WrongUsageException::class,
RegistryException::class,
];
public const array MINOR_LOG_EXCEPTIONS = [
InterruptException::class,
WrongUsageException::class,
RegistryException::class,
];
/** @var null|BuilderBase Builder binding */
@@ -52,6 +55,7 @@ class ExceptionHandler
SPCInternalException::class => "✗ SPC internal error: {$e->getMessage()}",
ValidationException::class => "⚠ Validation failed: {$e->getMessage()}",
WrongUsageException::class => $e->getMessage(),
RegistryException::class => "✗ Registry parsing error: {$e->getMessage()}",
default => "✗ Unknown SPC exception {$class}: {$e->getMessage()}",
};
self::logError($head_msg);
@@ -186,7 +190,7 @@ class ExceptionHandler
$line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT);
fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL);
if ($output_log) {
echo ConsoleColor::red($line) . PHP_EOL;
InteractiveTerm::plain(ConsoleColor::red($line) . '');
}
}
}

View File

@@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Exception;
class RegistryException extends SPCException {}

View File

@@ -13,42 +13,31 @@ use StaticPHP\Util\FileSystem;
*/
class LibraryPackage extends Package
{
/** @var array<string, callable> $build_functions Build functions for different OS binding */
protected array $build_functions = [];
/**
* Add a build function for a specific platform.
*
* @param string $platform PHP_OS_FAMILY
* @param callable $func Function to build for the platform
*/
public function addBuildFunction(string $platform, callable $func): void
{
$this->build_functions[$platform] = $func;
if ($platform === PHP_OS_FAMILY) {
$this->addStage('build', $func);
}
}
public function isInstalled(): bool
{
foreach (PackageConfig::get($this->getName(), 'static-libs', []) as $lib) {
if (!file_exists("{$this->getLibDir()}/{$lib}")) {
$path = FileSystem::isRelativePath($lib) ? "{$this->getLibDir()}/{$lib}" : $lib;
if (!file_exists($path)) {
return false;
}
}
foreach (PackageConfig::get($this->getName(), 'headers', []) as $header) {
if (!file_exists("{$this->getIncludeDir()}/{$header}")) {
$path = FileSystem::isRelativePath($header) ? "{$this->getIncludeDir()}/{$header}" : $header;
if (!file_exists($path)) {
return false;
}
}
foreach (PackageConfig::get($this->getName(), 'pkg-configs', []) as $pc) {
if (!file_exists("{$this->getLibDir()}/pkgconfig/{$pc}.pc")) {
if (!str_ends_with($pc, '.pc')) {
$pc .= '.pc';
}
if (!file_exists("{$this->getLibDir()}/pkgconfig/{$pc}")) {
return false;
}
}
foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) {
if (!file_exists("{$this->getBinDir()}/{$bin}")) {
$path = FileSystem::isRelativePath($bin) ? "{$this->getBinDir()}/{$bin}" : $bin;
if (!file_exists($path)) {
return false;
}
}
@@ -140,6 +129,36 @@ class LibraryPackage extends Package
return trim($env);
}
/**
* Patch pkgconfig file prefix, exec_prefix, libdir, includedir for correct build.
*
* @param array $files File list to patch, if empty, will use pkg-configs from config (e.g. ['zlib.pc', 'openssl.pc'])
* @param int $patch_option Patch options
* @param null|array $custom_replace Custom replace rules, if provided, will be used to replace in the format [regex, replacement]
*/
public function patchPkgconfPrefix(array $files = [], int $patch_option = PKGCONF_PATCH_ALL, ?array $custom_replace = null): void
{
logger()->info("Patching library [{$this->getName()}] pkgconfig");
if ($files === [] && ($conf_pc = PackageConfig::get($this->getName(), 'pkg-configs', [])) !== []) {
$files = array_map(fn ($x) => "{$x}.pc", $conf_pc);
}
foreach ($files as $name) {
$realpath = realpath("{$this->getLibDir()}/pkgconfig/{$name}");
if ($realpath === false) {
throw new PatchException('pkg-config prefix patcher', "Cannot find library [{$this->getName()}] pkgconfig file [{$name}] in {$this->getLibDir()}/pkgconfig/ !");
}
logger()->debug("Patching {$realpath}");
// replace prefix
$file = FileSystem::readFile($realpath);
$file = ($patch_option & PKGCONF_PATCH_PREFIX) === PKGCONF_PATCH_PREFIX ? preg_replace('/^prefix\s*=.*$/m', 'prefix=' . BUILD_ROOT_PATH, $file) : $file;
$file = ($patch_option & PKGCONF_PATCH_EXEC_PREFIX) === PKGCONF_PATCH_EXEC_PREFIX ? preg_replace('/^exec_prefix\s*=.*$/m', 'exec_prefix=${prefix}', $file) : $file;
$file = ($patch_option & PKGCONF_PATCH_LIBDIR) === PKGCONF_PATCH_LIBDIR ? preg_replace('/^libdir\s*=.*$/m', 'libdir=${prefix}/lib', $file) : $file;
$file = ($patch_option & PKGCONF_PATCH_INCLUDEDIR) === PKGCONF_PATCH_INCLUDEDIR ? preg_replace('/^includedir\s*=.*$/m', 'includedir=${prefix}/include', $file) : $file;
$file = ($patch_option & PKGCONF_PATCH_CUSTOM) === PKGCONF_PATCH_CUSTOM && $custom_replace !== null ? preg_replace($custom_replace[0], $custom_replace[1], $file) : $file;
FileSystem::writeFile($realpath, $file);
}
}
/**
* Get extra LIBS for current package.
* You need to define the environment variable in the format of {LIBRARY_NAME}_LIBS

View File

@@ -5,10 +5,11 @@ declare(strict_types=1);
namespace StaticPHP\Package;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Artifact\ArtifactLoader;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Registry\ArtifactLoader;
use StaticPHP\Registry\PackageLoader;
abstract class Package
{
@@ -19,6 +20,12 @@ abstract class Package
*/
protected array $stages = [];
/** @var array<string, callable> $build_functions Build functions for different OS binding */
protected array $build_functions = [];
/** @var array<string, string> */
protected array $outputs = [];
/**
* @param string $name Name of the package
* @param string $type Type of the package
@@ -29,15 +36,25 @@ abstract class Package
* Run a defined stage of the package.
* If the stage is not defined, an exception should be thrown.
*
* @param string $name Name of the stage to run
* @param array $context Additional context to pass to the stage callback
* @return mixed Based on the stage definition, return the result of the stage
* @param array|callable|string $name Name of the stage to run (can be callable)
* @param array $context Additional context to pass to the stage callback
* @return mixed Based on the stage definition, return the result of the stage
*/
public function runStage(string $name, array $context = []): mixed
public function runStage(mixed $name, array $context = []): mixed
{
if (!isset($this->stages[$name])) {
if (!$this->hasStage($name)) {
$name = match (true) {
is_string($name) => $name,
is_array($name) && count($name) === 2 => $name[1], // use function name
default => '{' . gettype($name) . '}',
};
throw new SPCInternalException("Stage '{$name}' is not defined for package '{$this->name}'.");
}
$name = match (true) {
is_string($name) => $name,
is_array($name) && count($name) === 2 => $name[1], // use function name
default => throw new SPCInternalException('Invalid stage name type: ' . gettype($name)),
};
// Merge package context with provided context
/** @noinspection PhpDuplicateArrayKeysInspection */
@@ -55,6 +72,31 @@ abstract class Package
return $ret;
}
public function setOutput(string $key, string $value): static
{
$this->outputs[$key] = $value;
return $this;
}
public function getOutputs(): array
{
return $this->outputs;
}
/**
* Add a build function for a specific platform.
*
* @param string $os_family PHP_OS_FAMILY
* @param callable $func Function to build for the platform
*/
public function addBuildFunction(string $os_family, callable $func): void
{
$this->build_functions[$os_family] = $func;
if ($os_family === PHP_OS_FAMILY) {
$this->addStage('build', $func);
}
}
public function isInstalled(): bool
{
// By default, assume package is not installed.
@@ -63,9 +105,6 @@ abstract class Package
/**
* Add a stage to the package.
*
* @param string $name Stage name
* @param callable $stage Stage callable
*/
public function addStage(string $name, callable $stage): void
{
@@ -75,11 +114,17 @@ abstract class Package
/**
* Check if the package has a specific stage defined.
*
* @param string $name Stage name
* @param mixed $name Stage name
*/
public function hasStage(string $name): bool
public function hasStage(mixed $name): bool
{
return isset($this->stages[$name]);
if (is_array($name) && count($name) === 2) {
return isset($this->stages[$name[1]]); // use function name
}
if (is_string($name)) {
return isset($this->stages[$name]); // use defined name
}
return false;
}
/**

View File

@@ -11,6 +11,7 @@ use StaticPHP\Artifact\ArtifactExtractor;
use StaticPHP\Artifact\DownloaderOptions;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Registry\PackageLoader;
use StaticPHP\Util\DependencyResolver;
use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
@@ -103,13 +104,25 @@ class PackageInstaller
return $this;
}
public function printBuildPackageOutputs(): void
{
foreach ($this->build_packages as $package) {
if (($outputs = $package->getOutputs()) !== []) {
InteractiveTerm::notice('Package ' . ConsoleColor::green($package->getName()) . ' outputs');
$this->printArrayInfo(info: $outputs);
}
}
}
/**
* Run the package installation process.
*/
public function run(bool $interactive = true, bool $disable_delay_msg = false): void
{
// resolve input, make dependency graph
$this->resolvePackages();
if (empty($this->packages)) {
// resolve input, make dependency graph
$this->resolvePackages();
}
if ($interactive && !$disable_delay_msg) {
// show install or build options in terminal with beautiful output
@@ -148,24 +161,11 @@ class PackageInstaller
}
$builder = ApplicationContext::get(PackageBuilder::class);
foreach ($this->packages as $package) {
if ($this->isBuildPackage($package) || $package instanceof LibraryPackage && $package->hasStage('build')) {
if ($interactive) {
InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName()));
}
try {
/** @var LibraryPackage $package */
$status = $builder->buildPackage($package, $this->isBuildPackage($package));
} catch (\Throwable $e) {
if ($interactive) {
InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false);
echo PHP_EOL;
}
throw $e;
}
if ($interactive) {
InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : ''));
}
} elseif ($package instanceof LibraryPackage && $package->getArtifact()->shouldUseBinary()) {
$is_to_build = $this->isBuildPackage($package);
$has_build_stage = $package instanceof LibraryPackage && $package->hasStage('build');
$should_use_binary = $package instanceof LibraryPackage && ($package->getArtifact()?->shouldUseBinary() ?? false);
$has_source = $package->hasSource();
if (!$is_to_build && $should_use_binary) {
// install binary
if ($interactive) {
InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName()));
@@ -182,7 +182,24 @@ class PackageInstaller
if ($interactive) {
InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : ''));
}
} elseif ($package instanceof LibraryPackage) {
} elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) {
if ($interactive) {
InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName()));
}
try {
/** @var LibraryPackage $package */
$status = $builder->buildPackage($package, $this->isBuildPackage($package));
} catch (\Throwable $e) {
if ($interactive) {
InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false);
echo PHP_EOL;
}
throw $e;
}
if ($interactive) {
InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : ''));
}
} elseif ($package->getType() === 'library') {
throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS.");
}
}
@@ -213,6 +230,31 @@ class PackageInstaller
return isset($this->packages[$package_name]);
}
public function isPackageInstalled(Package|string $package_name): bool
{
if (empty($this->packages)) {
$this->resolvePackages();
}
if (is_string($package_name)) {
$package = $this->getPackage($package_name);
if ($package === null) {
throw new WrongUsageException("Package '{$package_name}' is not resolved.");
}
} else {
$package = $package_name;
}
// check if package is built/installed
if ($this->isBuildPackage($package)) {
return $package->isInstalled();
}
if ($package instanceof LibraryPackage && $package->getArtifact()->shouldUseBinary()) {
$artifact = $package->getArtifact();
return $artifact->isBinaryExtracted();
}
return false;
}
/**
* Returns the download status of all artifacts for the resolved packages.
*
@@ -362,8 +404,6 @@ class PackageInstaller
/**
* Validate that a package has required artifacts.
*
* @throws WrongUsageException if target/library package has no source or platform binary
*/
private function validatePackageArtifact(Package $package): void
{
@@ -412,32 +452,32 @@ class PackageInstaller
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cli')) {
$cli = PackageLoader::getPackage('php-cli');
$this->build_packages[$cli->getName()] = $cli;
$this->install_packages[$cli->getName()] = $cli;
$added = true;
}
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-fpm')) {
$fpm = PackageLoader::getPackage('php-fpm');
$this->build_packages[$fpm->getName()] = $fpm;
$this->install_packages[$fpm->getName()] = $fpm;
$added = true;
}
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-micro')) {
$micro = PackageLoader::getPackage('php-micro');
$this->build_packages[$micro->getName()] = $micro;
$this->install_packages[$micro->getName()] = $micro;
$added = true;
}
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cgi')) {
$cgi = PackageLoader::getPackage('php-cgi');
$this->build_packages[$cgi->getName()] = $cgi;
$this->install_packages[$cgi->getName()] = $cgi;
$added = true;
}
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-embed')) {
$embed = PackageLoader::getPackage('php-embed');
$this->build_packages[$embed->getName()] = $embed;
$this->install_packages[$embed->getName()] = $embed;
$added = true;
}
if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) {
$frankenphp = PackageLoader::getPackage('frankenphp');
$this->build_packages[$frankenphp->getName()] = $frankenphp;
$this->install_packages[$frankenphp->getName()] = $frankenphp;
$added = true;
}
$this->build_packages[$package->getName()] = $package;
@@ -451,7 +491,7 @@ class PackageInstaller
} else {
// process specific php sapi targets
$this->build_packages['php'] = PackageLoader::getPackage('php');
$this->build_packages[$package->getName()] = $package;
$this->install_packages[$package->getName()] = $package;
}
}

View File

@@ -4,10 +4,13 @@ declare(strict_types=1);
namespace StaticPHP\Package;
use StaticPHP\Attribute\Package\Stage;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
use StaticPHP\Util\SPCConfigUtil;
/**
* Represents a PHP extension package.
@@ -41,6 +44,23 @@ class PhpExtensionPackage extends Package
parent::__construct($name, $type);
}
public function getSourceDir(): string
{
if ($this->getArtifact() === null) {
$path = SOURCE_PATH . '/php-src/ext/' . $this->getExtensionName();
if (!is_dir($path)) {
throw new ValidationException("Extension source directory not found: {$path}", validation_module: "Extension {$this->getExtensionName()} source");
}
return $path;
}
return parent::getSourceDir();
}
public function getExtensionName(): string
{
return str_replace('ext-', '', $this->getName());
}
public function addCustomPhpConfigureArgCallback(string $os, callable $fn): void
{
if ($os === '') {
@@ -59,7 +79,7 @@ class PhpExtensionPackage extends Package
return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]);
}
$escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH;
$name = str_replace('_', '-', substr($this->getName(), 4));
$name = str_replace('_', '-', $this->getExtensionName());
$ext_config = PackageConfig::get($name, 'php-extension', []);
$arg_type = match (SystemTarget::getTargetOS()) {
@@ -108,8 +128,151 @@ class PhpExtensionPackage extends Package
return $this->build_with_php;
}
public function buildSharedExtension(): void
public function buildShared(): void
{
// TODO: build common shared extensions code here...
if ($this->hasStage('build')) {
$this->runStage('build');
} else {
throw new WrongUsageException("Extension [{$this->getExtensionName()}] cannot build shared target yet.");
}
}
/**
* Get shared extension build environment variables for Unix.
*
* @return array{
* CFLAGS: string,
* CXXFLAGS: string,
* LDFLAGS: string,
* LIBS: string,
* LD_LIBRARY_PATH: string
* }
*/
public function getSharedExtensionEnv(): array
{
$config = (new SPCConfigUtil())->getExtensionConfig($this);
[$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,
];
}
/**
* @internal
*/
#[Stage]
public function phpizeForUnix(array $env, PhpExtensionPackage $package): void
{
shell()->cd($package->getSourceDir())->setEnv($env)->exec(BUILD_BIN_PATH . '/phpize');
}
/**
* @internal
*/
#[Stage]
public function configureForUnix(array $env, PhpExtensionPackage $package): void
{
$phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: '';
shell()->cd($package->getSourceDir())
->setEnv($env)
->exec(
'./configure ' . $this->getPhpConfigureArg(SystemTarget::getCurrentPlatformString(), true) .
' --with-php-config=' . BUILD_BIN_PATH . '/php-config ' .
"--enable-shared --disable-static {$phpvars}"
);
}
/**
* @internal
*/
#[Stage]
public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void
{
shell()->cd($package->getSourceDir())
->setEnv($env)
->exec('make clean')
->exec("make -j{$builder->concurrency}")
->exec('make install');
}
/**
* Build shared extension on Unix-like systems.
* Only for internal calling. For external use, call buildShared() instead.
* @internal
* #[Stage('build')]
*/
public function buildSharedForUnix(PackageBuilder $builder): void
{
$env = $this->getSharedExtensionEnv();
$this->runStage([$this, 'phpizeForUnix'], ['env' => $env]);
$this->runStage([$this, 'configureForUnix'], ['env' => $env]);
$this->runStage([$this, 'makeForUnix'], ['env' => $env]);
// process *.so file
$soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so';
if (!file_exists($soFile)) {
throw new ValidationException("Extension {$this->getExtensionName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getExtensionName()} build");
}
$builder->deployBinary($soFile, $soFile, false);
}
/**
* Register default stages if not already defined by attributes.
* This is called after all attributes have been loaded.
*
* @internal Called by PackageLoader after loading attributes
*/
public function registerDefaultStages(): void
{
// Add build stages for shared build on Unix-like systems
// TODO: Windows shared build support
if ($this->build_shared && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) {
if (!$this->hasStage('build')) {
$this->addBuildFunction(SystemTarget::getTargetOS(), [$this, 'buildSharedForUnix']);
}
if (!$this->hasStage('phpizeForUnix')) {
$this->addStage('phpizeForUnix', [$this, 'phpizeForUnix']);
}
if (!$this->hasStage('configureForUnix')) {
$this->addStage('configureForUnix', [$this, 'configureForUnix']);
}
if (!$this->hasStage('makeForUnix')) {
$this->addStage('makeForUnix', [$this, 'makeForUnix']);
}
}
}
/**
* Splits a given string of library flags into static and shared libraries.
*
* @param string $allLibs A space-separated string of library flags (e.g., -lxyz).
* @return array an array containing two elements: the first is a space-separated string
* of static library flags, and the second is a space-separated string
* of shared library flags
*/
protected function splitLibsIntoStaticAndShared(string $allLibs): array
{
$staticLibString = '';
$sharedLibString = '';
$libs = explode(' ', $allLibs);
foreach ($libs as $lib) {
$staticLib = BUILD_LIB_PATH . '/lib' . str_replace('-l', '', $lib) . '.a';
if (str_starts_with($lib, BUILD_LIB_PATH . '/lib') && str_ends_with($lib, '.a')) {
$staticLib = $lib;
}
if ($lib === '-lphp' || !file_exists($staticLib)) {
$sharedLibString .= " {$lib}";
} else {
$staticLibString .= " {$lib}";
}
}
return [trim($staticLibString), trim($sharedLibString)];
}
}

View File

@@ -2,8 +2,9 @@
declare(strict_types=1);
namespace StaticPHP\Artifact;
namespace StaticPHP\Registry;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\AfterSourceExtract;
use StaticPHP\Attribute\Artifact\BinaryExtract;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace StaticPHP\Doctor;
namespace StaticPHP\Registry;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;

View File

@@ -2,7 +2,7 @@
declare(strict_types=1);
namespace StaticPHP\Package;
namespace StaticPHP\Registry;
use StaticPHP\Attribute\Package\AfterStage;
use StaticPHP\Attribute\Package\BeforeStage;
@@ -12,15 +12,19 @@ use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\Package\Info;
use StaticPHP\Attribute\Package\InitPackage;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Attribute\Package\PatchBeforeBuild;
use StaticPHP\Attribute\Package\ResolveBuild;
use StaticPHP\Attribute\Package\Stage;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Attribute\Package\Validate;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Exception\RegistryException;
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\FileSystem;
class PackageLoader
@@ -30,9 +34,7 @@ class PackageLoader
private static array $before_stages = [];
private static array $after_stage = [];
private static array $patch_before_builds = [];
private static array $after_stages = [];
/** @var array<string, true> Track loaded classes to prevent duplicates */
private static array $loaded_classes = [];
@@ -53,7 +55,7 @@ class PackageLoader
if ($pkg !== null) {
self::$packages[$name] = $pkg;
} else {
throw new WrongUsageException("Package [{$name}] has unknown type [{$item['type']}]");
throw new RegistryException("Package [{$name}] has unknown type [{$item['type']}]");
}
}
}
@@ -143,8 +145,6 @@ class PackageLoader
}
self::$loaded_classes[$class_name] = true;
$instance_class = $refClass->newInstance();
$attributes = $refClass->getAttributes();
foreach ($attributes as $attribute) {
$pkg = null;
@@ -158,10 +158,21 @@ class PackageLoader
}
$package_type = PackageConfig::get($attribute_instance->name, 'type');
if ($package_type === null) {
throw new WrongUsageException("Package [{$attribute_instance->name}] not defined in config, please check your config files.");
throw new RegistryException("Package [{$attribute_instance->name}] not defined in config, please check your config files.");
}
// if class has parent class and matches the attribute instance, use custom class
if ($refClass->getParentClass() !== false) {
if (is_a($class_name, Package::class, true)) {
self::$packages[$attribute_instance->name] = new $class_name($attribute_instance->name, $package_type);
}
}
$pkg = self::$packages[$attribute_instance->name];
// Use the package instance if it's a Package subclass, otherwise create a new instance
$instance_class = is_a($class_name, Package::class, true) ? $pkg : $refClass->newInstance();
// validate package type matches
$pkg_type_attr = match ($attribute->getName()) {
Target::class => ['target', 'virtual-target'],
@@ -170,10 +181,10 @@ class PackageLoader
default => null,
};
if (!in_array($package_type, $pkg_type_attr, true)) {
throw new ValidationException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].');
throw new RegistryException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].');
}
if ($pkg !== null && !PackageConfig::isPackageExists($pkg->getName())) {
throw new ValidationException("Package [{$pkg->getName()}] config not found for class {$class}");
throw new RegistryException("Package [{$pkg->getName()}] config not found for class {$class}");
}
// init method attributes
@@ -188,20 +199,15 @@ class PackageLoader
// #[CustomPhpConfigureArg(PHP_OS_FAMILY)]
CustomPhpConfigureArg::class => self::bindCustomPhpConfigureArg($pkg, $method_attribute->newInstance(), [$instance_class, $method->getName()]),
// #[Stage('stage_name')]
Stage::class => $pkg->addStage($method_attribute->newInstance()->name, [$instance_class, $method->getName()]),
Stage::class => self::addStage($method, $pkg, $instance_class, $method_instance),
// #[InitPackage] (run now with package context)
InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], [
Package::class => $pkg,
$pkg::class => $pkg,
]),
InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], ['package' => $pkg]),
// #[InitBuild]
ResolveBuild::class => $pkg instanceof TargetPackage ? $pkg->setResolveBuildCallback([$instance_class, $method->getName()]) : null,
// #[Info]
Info::class => $pkg->setInfoCallback([$instance_class, $method->getName()]),
// #[Validate]
Validate::class => $pkg->setValidateCallback([$instance_class, $method->getName()]),
// #[PatchBeforeBuild]
PatchBeforeBuild::class => $pkg->setPatchBeforeBuildCallback([$instance_class, $method->getName()]),
default => null,
};
}
@@ -210,6 +216,11 @@ class PackageLoader
self::$packages[$pkg->getName()] = $pkg;
}
// For classes without package attributes, create a simple instance for non-package stage callbacks
if (!isset($instance_class)) {
$instance_class = $refClass->newInstance();
}
// parse non-package available attributes
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
$method_attributes = $method->getAttributes();
@@ -217,9 +228,9 @@ class PackageLoader
$method_instance = $method_attribute->newInstance();
match ($method_attribute->getName()) {
// #[BeforeStage('package_name', 'stage')] and #[AfterStage('package_name', 'stage')]
BeforeStage::class => self::$before_stages[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved],
AfterStage::class => self::$after_stage[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved],
// #[PatchBeforeBuild()
BeforeStage::class => self::addBeforeStage($method, $pkg ?? null, $instance_class, $method_instance),
AfterStage::class => self::addAfterStage($method, $pkg ?? null, $instance_class, $method_instance),
default => null,
};
}
@@ -243,7 +254,7 @@ class PackageLoader
{
// match condition
$installer = ApplicationContext::get(PackageInstaller::class);
$stages = self::$after_stage[$package_name][$stage] ?? [];
$stages = self::$after_stages[$package_name][$stage] ?? [];
$result = [];
foreach ($stages as [$callback, $only_when_package_resolved]) {
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
@@ -254,9 +265,53 @@ class PackageLoader
return $result;
}
public static function getPatchBeforeBuildCallbacks(string $package_name): array
/**
* Register default stages for all PhpExtensionPackage instances.
* Should be called after all registries have been loaded.
*/
public static function registerAllDefaultStages(): void
{
return self::$patch_before_builds[$package_name] ?? [];
foreach (self::$packages as $pkg) {
if ($pkg instanceof PhpExtensionPackage) {
$pkg->registerDefaultStages();
}
}
}
/**
* Check loaded stage events for consistency.
*/
public static function checkLoadedStageEvents(): void
{
foreach (['BeforeStage' => self::$before_stages, 'AfterStage' => self::$after_stages] as $event_name => $ev_all) {
foreach ($ev_all as $package_name => $stages) {
// check package exists
if (!self::hasPackage($package_name)) {
throw new RegistryException(
"{$event_name} event registered for unknown package [{$package_name}]."
);
}
$pkg = self::getPackage($package_name);
foreach ($stages as $stage_name => $before_events) {
foreach ($before_events as [$event_callable, $only_when_package_resolved]) {
// check only_when_package_resolved package exists
if ($only_when_package_resolved !== null && !self::hasPackage($only_when_package_resolved)) {
throw new RegistryException("{$event_name} event in package [{$package_name}] for stage [{$stage_name}] has unknown only_when_package_resolved package [{$only_when_package_resolved}].");
}
// check callable is valid
if (!is_callable($event_callable)) {
throw new RegistryException(
"{$event_name} event in package [{$package_name}] for stage [{$stage_name}] has invalid callable.",
);
}
}
// check stage exists
if (!$pkg->hasStage($stage_name)) {
throw new RegistryException("Package stage [{$stage_name}] is not registered in package [{$package_name}].");
}
}
}
}
}
/**
@@ -265,16 +320,53 @@ class PackageLoader
private static function bindCustomPhpConfigureArg(Package $pkg, object $attr, callable $fn): void
{
if (!$pkg instanceof PhpExtensionPackage) {
throw new ValidationException("Class [{$pkg->getName()}] must implement PhpExtensionPackage for CustomPhpConfigureArg attribute.");
throw new RegistryException("Class [{$pkg->getName()}] must implement PhpExtensionPackage for CustomPhpConfigureArg attribute.");
}
$pkg->addCustomPhpConfigureArgCallback($attr->os, $fn);
}
private static function addBuildFunction(Package $pkg, object $attr, callable $fn): void
{
if (!$pkg instanceof LibraryPackage) {
throw new ValidationException("Class [{$pkg->getName()}] must implement LibraryPackage for BuildFor attribute.");
}
$pkg->addBuildFunction($attr->os, $fn);
}
private static function addStage(\ReflectionMethod $method, Package $pkg, object $instance_class, object $method_instance): void
{
$name = $method_instance->function;
if ($name === null) {
$name = $method->getName();
}
$pkg->addStage($name, [$instance_class, $method->getName()]);
}
private static function addBeforeStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void
{
/** @var BeforeStage $method_instance */
$stage = $method_instance->stage;
$stage = match (true) {
is_string($stage) => $stage,
is_array($stage) && count($stage) === 2 => $stage[1],
default => throw new RegistryException('Invalid stage definition in BeforeStage attribute.'),
};
if ($method_instance->package_name === '' && $pkg === null) {
throw new RegistryException('Package name must not be empty when no package context is available for BeforeStage attribute.');
}
$package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name;
self::$before_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved];
}
private static function addAfterStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void
{
$stage = $method_instance->stage;
$stage = match (true) {
is_string($stage) => $stage,
is_array($stage) && count($stage) === 2 => $stage[1],
default => throw new RegistryException('Invalid stage definition in AfterStage attribute.'),
};
if ($method_instance->package_name === '' && $pkg === null) {
throw new RegistryException('Package name must not be empty when no package context is available for AfterStage attribute.');
}
$package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name;
self::$after_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved];
}
}

View File

@@ -4,13 +4,10 @@ declare(strict_types=1);
namespace StaticPHP\Registry;
use StaticPHP\Artifact\ArtifactLoader;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\Config\PackageConfig;
use StaticPHP\ConsoleApplication;
use StaticPHP\Doctor\DoctorLoader;
use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Package\PackageLoader;
use StaticPHP\Exception\RegistryException;
use StaticPHP\Util\FileSystem;
use Symfony\Component\Yaml\Yaml;
@@ -28,21 +25,21 @@ class Registry
*/
public static function loadRegistry(string $registry_file, bool $auto_require = true): void
{
$yaml = file_get_contents($registry_file);
$yaml = @file_get_contents($registry_file);
if ($yaml === false) {
throw new EnvironmentException("Failed to read registry file: {$registry_file}");
throw new RegistryException("Failed to read registry file: {$registry_file}");
}
$data = match (pathinfo($registry_file, PATHINFO_EXTENSION)) {
'json' => json_decode($yaml, true),
'yaml', 'yml' => Yaml::parse($yaml),
default => throw new EnvironmentException("Unsupported registry file format: {$registry_file}"),
default => throw new RegistryException("Unsupported registry file format: {$registry_file}"),
};
if (!is_array($data)) {
throw new EnvironmentException("Invalid registry format in file: {$registry_file}");
throw new RegistryException("Invalid registry format in file: {$registry_file}");
}
$registry_name = $data['name'] ?? null;
if (!is_string($registry_name) || empty($registry_name)) {
throw new EnvironmentException("Registry 'name' is missing or invalid in file: {$registry_file}");
throw new RegistryException("Registry 'name' is missing or invalid in file: {$registry_file}");
}
// Prevent loading the same registry twice
@@ -190,6 +187,16 @@ class Registry
}
}
public static function checkLoadedRegistries(): void
{
// Register default stages for all PhpExtensionPackage instances
// This must be done after all registries are loaded to ensure custom stages take precedence
PackageLoader::registerAllDefaultStages();
// check BeforeStage, AfterStage is valid
PackageLoader::checkLoadedStageEvents();
}
/**
* Get list of loaded registry names.
*
@@ -252,7 +259,7 @@ class Registry
}
// Class not found and no file path provided
throw new EnvironmentException(
throw new RegistryException(
"Class '{$class}' not found. For external registries, either:\n" .
" 1. Add an 'autoload' entry pointing to your composer autoload file\n" .
" 2. Use 'psr-4' instead of 'classes' for auto-discovery\n" .
@@ -272,7 +279,7 @@ class Registry
$path = $relative_path_base . DIRECTORY_SEPARATOR . $path;
}
if (!file_exists($path)) {
throw new EnvironmentException("Path does not exist: {$path}");
throw new RegistryException("Path does not exist: {$path}");
}
return FileSystem::convertPath($path);
}

View File

@@ -6,6 +6,7 @@ namespace StaticPHP\Util;
use StaticPHP\DI\ApplicationContext;
use Symfony\Component\Console\Helper\ProgressIndicator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use ZM\Logger\ConsoleColor;
@@ -15,50 +16,55 @@ class InteractiveTerm
public static function notice(string $message, bool $indent = false): void
{
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->notice(strip_ansi_colors($message));
} else {
$output->writeln(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message);
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message));
}
}
public static function success(string $message, bool $indent = false): void
{
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->info(strip_ansi_colors($message));
} else {
$output->writeln(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message);
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message));
}
}
public static function plain(string $message): void
{
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->info(strip_ansi_colors($message));
} else {
$output->writeln($message);
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')($message));
}
}
public static function info(string $message): void
{
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
$output = ApplicationContext::get(OutputInterface::class);
if (!$output->isVerbose()) {
$output->writeln(ConsoleColor::green('▶ ') . $message);
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green('▶ ') . $message));
}
logger()->info(strip_ansi_colors($message));
}
public static function error(string $message, bool $indent = true): void
{
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->error(strip_ansi_colors($message));
} else {
$output->writeln('' . ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message));
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message)));
}
}
@@ -69,11 +75,14 @@ class InteractiveTerm
public static function setMessage(string $message): void
{
self::$indicator?->setMessage($message);
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
self::$indicator?->setMessage(($no_ansi ? 'strip_ansi_colors' : 'strval')($message));
}
public static function finish(string $message, bool $status = true): void
{
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
$message = $no_ansi ? strip_ansi_colors($message) : $message;
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
if ($status) {
@@ -85,9 +94,9 @@ class InteractiveTerm
}
if (self::$indicator !== null) {
if (!$status) {
self::$indicator->finish($message, '' . ConsoleColor::red(' ✘'));
self::$indicator->finish($message, ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(' ✘')));
} else {
self::$indicator->finish($message, '' . ConsoleColor::green(' ✔'));
self::$indicator->finish($message, ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(' ✔')));
}
self::$indicator = null;
}
@@ -95,6 +104,7 @@ class InteractiveTerm
public static function indicateProgress(string $message): void
{
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
$output = ApplicationContext::get(OutputInterface::class);
if ($output->isVerbose()) {
logger()->info(strip_ansi_colors($message));
@@ -106,6 +116,12 @@ class InteractiveTerm
self::$indicator->advance();
return;
}
// if no ansi, use a dot instead of spinner
if ($no_ansi) {
self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' •', ' •']);
self::$indicator->start(strip_ansi_colors($message));
return;
}
self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' ⠏', ' ⠛', ' ⠹', ' ⢸', ' ⣰', ' ⣤', ' ⣆', ' ⡇']);
self::$indicator->start($message);
}

View File

@@ -225,11 +225,15 @@ class SPCConfigUtil
// convert all static-libs to short names
$libs = array_reverse(PackageConfig::get($package, 'static-libs', []));
foreach ($libs as $lib) {
// check file existence
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.");
if (FileSystem::isRelativePath($lib)) {
// check file existence
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.");
}
$lib_names[] = $this->getShortLibName($lib);
} else {
$lib_names[] = $lib;
}
$lib_names[] = $this->getShortLibName($lib);
}
// add frameworks for macOS
if (SystemTarget::getTargetOS() === 'Darwin') {

View File

@@ -271,11 +271,11 @@ function keyboard_interrupt_unregister(): void
/**
* Strip ANSI color codes from a string.
*/
function strip_ansi_colors(string $text): string
function strip_ansi_colors(string|Stringable $text): string
{
// Regular expression to match ANSI escape sequences
// Including color codes, cursor control, clear screen and other control sequences
return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', $text);
return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', strval($text));
}
/**

60
src/globals/scripts/zig-cc.sh Executable file
View File

@@ -0,0 +1,60 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
BUILDROOT_ABS="${BUILD_ROOT_PATH:-$(realpath "$SCRIPT_DIR/../../../buildroot/include" 2>/dev/null || true)}"
PARSED_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
-isystem)
shift
ARG="$1"
shift
ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)"
[[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem" "$ARG")
;;
-isystem*)
ARG="${1#-isystem}"
shift
ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)"
[[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG")
;;
-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
;;
*)
PARSED_ARGS+=("$1")
shift
;;
esac
done
[[ -n "$SPC_TARGET" ]] && TARGET="-target $SPC_TARGET" || TARGET=""
if [[ "$SPC_TARGET" =~ \.[0-9]+\.[0-9]+ ]]; then
output=$(zig cc $TARGET $SPC_COMPILER_EXTRA "${PARSED_ARGS[@]}" 2>&1)
status=$?
if [[ $status -eq 0 ]]; then
echo "$output"
exit 0
fi
if echo "$output" | grep -qE "version '.*' in target triple"; then
filtered_output=$(echo "$output" | grep -vE "version '.*' in target triple")
echo "$filtered_output"
exit 0
fi
fi
exec zig cc $TARGET $SPC_COMPILER_EXTRA "${PARSED_ARGS[@]}"

View File

@@ -1,25 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests;
use PHPUnit\Framework\TestCase;
/**
* @internal
*/
class GlobalDefinesTest extends TestCase
{
public function testGlobalDefines(): void
{
require __DIR__ . '/../../src/globals/defines.php';
$this->assertTrue(defined('WORKING_DIR'));
}
public function testInternalEnv(): void
{
require __DIR__ . '/../../src/globals/internal-env.php';
$this->assertTrue(defined('GNU_ARCH'));
}
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests;
use PHPUnit\Framework\TestCase;
use SPC\exception\InterruptException;
/**
* @internal
*/
class GlobalFunctionsTest extends TestCase
{
public function testMatchPattern(): void
{
$this->assertEquals('abc', match_pattern('a*c', 'abc'));
$this->assertFalse(match_pattern('a*c', 'abcd'));
}
public function testFExec(): void
{
$this->assertEquals('abc', f_exec('echo abc', $out, $ret));
$this->assertEquals(0, $ret);
$this->assertEquals(['abc'], $out);
}
public function testPatchPointInterrupt(): void
{
$except = patch_point_interrupt(0);
$this->assertInstanceOf(InterruptException::class, $except);
}
}

View File

@@ -1,246 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\builder;
use PHPUnit\Framework\TestCase;
use SPC\builder\BuilderBase;
use SPC\builder\BuilderProvider;
use SPC\builder\Extension;
use SPC\builder\LibraryBase;
use SPC\exception\WrongUsageException;
use SPC\store\FileSystem;
use SPC\store\LockFile;
use SPC\util\AttributeMapper;
use SPC\util\DependencyUtil;
use Symfony\Component\Console\Input\ArgvInput;
/**
* @internal
*/
class BuilderTest extends TestCase
{
private BuilderBase $builder;
public static function setUpBeforeClass(): void
{
BuilderProvider::makeBuilderByInput(new ArgvInput());
BuilderProvider::getBuilder();
}
public function setUp(): void
{
$this->builder = BuilderProvider::makeBuilderByInput(new ArgvInput());
[$extensions, $libs] = DependencyUtil::getExtsAndLibs(['mbregex']);
$this->builder->proveLibs($libs);
foreach ($extensions as $extension) {
$class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class;
$ext = new $class($extension, $this->builder);
$this->builder->addExt($ext);
}
foreach ($this->builder->getExts() as $ext) {
$ext->checkDependency();
}
}
public function testMakeBuilderByInput(): void
{
$this->assertInstanceOf(BuilderBase::class, BuilderProvider::makeBuilderByInput(new ArgvInput()));
$this->assertInstanceOf(BuilderBase::class, BuilderProvider::getBuilder());
}
public function testGetLibAndGetLibs()
{
$this->assertIsArray($this->builder->getLibs());
$this->assertInstanceOf(LibraryBase::class, $this->builder->getLib('onig'));
}
public function testGetExtAndGetExts()
{
$this->assertIsArray($this->builder->getExts());
$this->assertInstanceOf(Extension::class, $this->builder->getExt('mbregex'));
}
public function testMakeExtensionArgs()
{
$this->assertStringContainsString('--enable-mbstring', $this->builder->makeStaticExtensionArgs());
}
public function testIsLibsOnly()
{
// mbregex is not libs only
$this->assertFalse($this->builder->isLibsOnly());
}
public function testGetPHPVersionID()
{
if (file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) {
$file = SOURCE_PATH . '/php-src/main/php_version.h';
$cnt = preg_match('/PHP_VERSION_ID (\d+)/m', file_get_contents($file), $match);
if ($cnt !== 0) {
$this->assertEquals(intval($match[1]), $this->builder->getPHPVersionID());
} else {
$this->expectException(WrongUsageException::class);
$this->builder->getPHPVersionID();
}
} else {
$this->expectException(WrongUsageException::class);
$this->builder->getPHPVersionID();
}
}
public function testGetPHPVersion()
{
if (file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) {
$file = SOURCE_PATH . '/php-src/main/php_version.h';
$cnt = preg_match('/PHP_VERSION "(\d+\.\d+\.\d+(?:-[^"]+)?)/', file_get_contents($file), $match);
if ($cnt !== 0) {
$this->assertEquals($match[1], $this->builder->getPHPVersion());
} else {
$this->expectException(WrongUsageException::class);
$this->builder->getPHPVersion();
}
} else {
$this->expectException(WrongUsageException::class);
$this->builder->getPHPVersion();
}
}
public function testGetPHPVersionFromArchive()
{
$lock = file_exists(LockFile::LOCK_FILE) ? file_get_contents(LockFile::LOCK_FILE) : false;
if ($lock === false) {
$this->assertFalse($this->builder->getPHPVersionFromArchive());
} else {
$lock = json_decode($lock, true);
$file = $lock['php-src']['filename'] ?? null;
if ($file === null) {
$this->assertFalse($this->builder->getPHPVersionFromArchive());
} else {
$cnt = preg_match('/php-(\d+\.\d+\.\d+)/', $file, $match);
if ($cnt !== 0) {
$this->assertEquals($match[1], $this->builder->getPHPVersionFromArchive());
} else {
$this->assertFalse($this->builder->getPHPVersionFromArchive());
}
}
}
}
public function testGetMicroVersion()
{
$file = FileSystem::convertPath(SOURCE_PATH . '/php-src/sapi/micro/php_micro.h');
if (!file_exists($file)) {
$this->assertFalse($this->builder->getMicroVersion());
} else {
$content = file_get_contents($file);
$ver = '';
preg_match('/#define PHP_MICRO_VER_MAJ (\d)/m', $content, $match);
$ver .= $match[1] . '.';
preg_match('/#define PHP_MICRO_VER_MIN (\d)/m', $content, $match);
$ver .= $match[1] . '.';
preg_match('/#define PHP_MICRO_VER_PAT (\d)/m', $content, $match);
$ver .= $match[1];
$this->assertEquals($ver, $this->builder->getMicroVersion());
}
}
public static function providerGetBuildTypeName(): array
{
return [
[BUILD_TARGET_CLI, 'cli'],
[BUILD_TARGET_FPM, 'fpm'],
[BUILD_TARGET_MICRO, 'micro'],
[BUILD_TARGET_EMBED, 'embed'],
[BUILD_TARGET_FRANKENPHP, 'frankenphp'],
[BUILD_TARGET_ALL, 'cli, micro, fpm, embed, frankenphp, cgi'],
[BUILD_TARGET_CLI | BUILD_TARGET_EMBED, 'cli, embed'],
];
}
/**
* @dataProvider providerGetBuildTypeName
*/
public function testGetBuildTypeName(int $target, string $name): void
{
$this->assertEquals($name, $this->builder->getBuildTypeName($target));
}
public function testGetOption()
{
// we cannot assure the option exists, so just tests default value
$this->assertEquals('foo', $this->builder->getOption('bar', 'foo'));
}
public function testGetOptions()
{
$this->assertIsArray($this->builder->getOptions());
}
public function testSetOptionIfNotExist()
{
$this->assertEquals(null, $this->builder->getOption('bar'));
$this->builder->setOptionIfNotExist('bar', 'foo');
$this->assertEquals('foo', $this->builder->getOption('bar'));
}
public function testSetOption()
{
$this->assertEquals(null, $this->builder->getOption('bar'));
$this->builder->setOption('bar', 'foo');
$this->assertEquals('foo', $this->builder->getOption('bar'));
}
public function testGetEnvString()
{
$this->assertIsString($this->builder->getEnvString());
putenv('TEST_SPC_BUILDER=foo');
$this->assertStringContainsString('TEST_SPC_BUILDER=foo', $this->builder->getEnvString(['TEST_SPC_BUILDER']));
}
public function testValidateLibsAndExts()
{
$this->builder->validateLibsAndExts();
$this->assertTrue(true);
}
public static function providerEmitPatchPoint(): array
{
return [
['before-libs-extract'],
['after-libs-extract'],
['before-php-extract'],
['after-php-extract'],
['before-micro-extract'],
['after-micro-extract'],
['before-exts-extract'],
['after-exts-extract'],
['before-php-buildconf'],
['before-php-configure'],
['before-php-make'],
['before-sanity-check'],
];
}
/**
* @dataProvider providerEmitPatchPoint
*/
public function testEmitPatchPoint(string $point)
{
$code = '<?php if (patch_point() === "' . $point . '") echo "GOOD:' . $point . '";';
// emulate patch point
$this->builder->setOption('with-added-patch', ['/tmp/patch-point.' . $point . '.php']);
FileSystem::writeFile('/tmp/patch-point.' . $point . '.php', $code);
$this->expectOutputString('GOOD:' . $point);
$this->builder->emitPatchPoint($point);
}
public function testEmitPatchPointNotExists()
{
$this->expectOutputRegex('/failed to run/');
$this->expectException(WrongUsageException::class);
$this->builder->setOption('with-added-patch', ['/tmp/patch-point.not_exsssists.php']);
$this->builder->emitPatchPoint('not-exists');
}
}

View File

@@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\builder;
use PHPUnit\Framework\TestCase;
use SPC\builder\BuilderProvider;
use SPC\builder\Extension;
use SPC\util\AttributeMapper;
use SPC\util\DependencyUtil;
use Symfony\Component\Console\Input\ArgvInput;
/**
* @internal
*/
class ExtensionTest extends TestCase
{
private Extension $extension;
protected function setUp(): void
{
$builder = BuilderProvider::makeBuilderByInput(new ArgvInput());
[$extensions, $libs] = DependencyUtil::getExtsAndLibs(['mbregex']);
$builder->proveLibs($libs);
foreach ($extensions as $extension) {
$class = AttributeMapper::getExtensionClassByName($extension) ?? Extension::class;
$ext = new $class($extension, $builder);
$builder->addExt($ext);
}
foreach ($builder->getExts() as $ext) {
$ext->checkDependency();
}
$this->extension = $builder->getExt('mbregex');
}
public function testPatches()
{
$this->assertFalse($this->extension->patchBeforeBuildconf());
$this->assertFalse($this->extension->patchBeforeConfigure());
$this->assertFalse($this->extension->patchBeforeMake());
}
public function testGetExtensionDependency()
{
$this->assertEquals('mbstring', current($this->extension->getExtensionDependency())->getName());
}
public function testGetWindowsConfigureArg()
{
$this->assertEquals('', $this->extension->getWindowsConfigureArg());
}
public function testGetConfigureArg()
{
$this->assertEquals('', $this->extension->getUnixConfigureArg());
}
public function testGetExtVersion()
{
// only swoole has version, we cannot test it
$this->assertEquals(null, $this->extension->getExtVersion());
}
public function testGetDistName()
{
$this->assertEquals('mbregex', $this->extension->getName());
}
public function testRunCliCheckWindows()
{
if (is_unix()) {
$this->markTestSkipped('This test is for Windows only');
} else {
$this->extension->runCliCheckWindows();
$this->assertTrue(true);
}
}
public function testGetName()
{
$this->assertEquals('mbregex', $this->extension->getName());
}
public function testGetUnixConfigureArg()
{
$this->assertEquals('', $this->extension->getUnixConfigureArg());
}
public function testGetEnableArg()
{
$this->assertEquals('', $this->extension->getEnableArg());
}
}

View File

@@ -1,60 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\builder\linux;
use PHPUnit\Framework\TestCase;
use SPC\builder\linux\SystemUtil;
/**
* @internal
*/
class SystemUtilTest extends TestCase
{
public static function setUpBeforeClass(): void
{
if (PHP_OS_FAMILY !== 'Linux') {
self::markTestSkipped('This test is only for Linux');
}
}
public function testIsMuslDistAndGetOSRelease()
{
$release = SystemUtil::getOSRelease();
// we cannot ensure what is the current distro, just test the key exists
$this->assertArrayHasKey('dist', $release);
$this->assertArrayHasKey('ver', $release);
$this->assertTrue($release['dist'] === 'alpine' && SystemUtil::isMuslDist() || $release['dist'] !== 'alpine' && !SystemUtil::isMuslDist());
}
public function testFindStaticLib()
{
$this->assertIsArray(SystemUtil::findStaticLib('ld-linux-x86-64.so.2'));
}
public function testGetCpuCount()
{
$this->assertIsInt(SystemUtil::getCpuCount());
}
public function testFindHeader()
{
$this->assertIsArray(SystemUtil::findHeader('elf.h'));
}
public function testGetSupportedDistros()
{
$this->assertIsArray(SystemUtil::getSupportedDistros());
}
public function testFindHeaders()
{
$this->assertIsArray(SystemUtil::findHeaders(['elf.h']));
}
public function testFindStaticLibs()
{
$this->assertIsArray(SystemUtil::findStaticLibs(['ld-linux-x86-64.so.2']));
}
}

View File

@@ -1,31 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\builder\macos;
use PHPUnit\Framework\TestCase;
use SPC\builder\macos\SystemUtil;
/**
* @internal
*/
class SystemUtilTest extends TestCase
{
public static function setUpBeforeClass(): void
{
if (PHP_OS_FAMILY !== 'Darwin') {
self::markTestSkipped('This test is only for macOS');
}
}
public function testGetCpuCount()
{
$this->assertIsInt(SystemUtil::getCpuCount());
}
public function testGetArchCFlags()
{
$this->assertEquals('--target=x86_64-apple-darwin', SystemUtil::getArchCFlags('x86_64'));
}
}

View File

@@ -1,42 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\builder\unix;
use PHPUnit\Framework\TestCase;
use SPC\builder\freebsd\SystemUtil as FreebsdSystemUtil;
use SPC\builder\linux\SystemUtil as LinuxSystemUtil;
use SPC\builder\macos\SystemUtil as MacosSystemUtil;
/**
* @internal
*/
class UnixSystemUtilTest extends TestCase
{
private FreebsdSystemUtil|LinuxSystemUtil|MacosSystemUtil $util;
public function setUp(): void
{
$util_class = match (PHP_OS_FAMILY) {
'Linux' => 'SPC\builder\linux\SystemUtil',
'Darwin' => 'SPC\builder\macos\SystemUtil',
'FreeBSD' => 'SPC\builder\freebsd\SystemUtil',
default => null,
};
if ($util_class === null) {
self::markTestSkipped('This test is only for Unix');
}
$this->util = new $util_class();
}
public function testFindCommand()
{
$this->assertIsString($this->util->findCommand('bash'));
}
public function testMakeEnvVarString()
{
$this->assertEquals("PATH='/usr/bin' PKG_CONFIG='/usr/bin/pkg-config'", $this->util->makeEnvVarString(['PATH' => '/usr/bin', 'PKG_CONFIG' => '/usr/bin/pkg-config']));
}
}

View File

@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\doctor;
use PHPUnit\Framework\TestCase;
use SPC\doctor\DoctorHandler;
/**
* @internal
*/
final class CheckListHandlerTest extends TestCase
{
public function testRunChecksReturnsListOfCheck(): void
{
$list = new DoctorHandler();
$id = $list->getValidCheckList();
foreach ($id as $item) {
$this->assertInstanceOf('SPC\doctor\AsCheckItem', $item);
}
}
}

View File

@@ -1,94 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\globals;
use PHPUnit\Framework\TestCase;
use Psr\Log\LogLevel;
use SPC\exception\ExecutionException;
use ZM\Logger\ConsoleLogger;
/**
* @internal
*/
class GlobalFunctionsTest extends TestCase
{
private static $logger_cache;
public static function setUpBeforeClass(): void
{
global $ob_logger;
self::$logger_cache = $ob_logger;
$ob_logger = new ConsoleLogger(LogLevel::ALERT);
}
public static function tearDownAfterClass(): void
{
global $ob_logger;
$ob_logger = self::$logger_cache;
}
public function testIsAssocArray(): void
{
$this->assertTrue(is_assoc_array(['a' => 1, 'b' => 2]));
$this->assertFalse(is_assoc_array([1, 2, 3]));
}
public function testLogger(): void
{
$this->assertInstanceOf('Psr\Log\LoggerInterface', logger());
}
public function testArch2Gnu(): void
{
$this->assertEquals('x86_64', arch2gnu('x86_64'));
$this->assertEquals('x86_64', arch2gnu('x64'));
$this->assertEquals('x86_64', arch2gnu('amd64'));
$this->assertEquals('aarch64', arch2gnu('arm64'));
$this->assertEquals('aarch64', arch2gnu('aarch64'));
$this->expectException('SPC\exception\WrongUsageException');
arch2gnu('armv7');
}
public function testQuote(): void
{
$this->assertEquals('"hello"', quote('hello'));
$this->assertEquals("'hello'", quote('hello', "'"));
}
public function testFPassthru(): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('Windows not support f_passthru');
}
$this->assertEquals(null, f_passthru('echo ""'));
$this->expectException(ExecutionException::class);
f_passthru('false');
}
public function testFPutenv(): void
{
$this->assertTrue(f_putenv('SPC_TEST_ENV=1'));
$this->assertEquals('1', getenv('SPC_TEST_ENV'));
}
public function testShell(): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('Windows not support shell');
}
$shell = shell();
$this->assertInstanceOf('SPC\util\shell\UnixShell', $shell);
$this->assertInstanceOf('SPC\util\shell\UnixShell', $shell->cd('/'));
$this->assertInstanceOf('SPC\util\shell\UnixShell', $shell->exec('echo ""'));
$this->assertInstanceOf('SPC\util\shell\UnixShell', $shell->setEnv(['SPC_TEST_ENV' => '1']));
[$code, $out] = $shell->execWithResult('echo "_"');
$this->assertEquals(0, $code);
$this->assertEquals('_', implode('', $out));
$this->expectException('SPC\exception\ExecutionException');
$shell->exec('false');
}
}

View File

@@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\store;
use PHPUnit\Framework\TestCase;
use SPC\store\Config;
use SPC\store\FileSystem;
/**
* @internal
*/
class ConfigTest extends TestCase
{
public static function setUpBeforeClass(): void
{
$testdir = WORKING_DIR . '/.configtest';
FileSystem::createDir($testdir);
FileSystem::writeFile($testdir . '/lib.json', file_get_contents(ROOT_DIR . '/config/lib.json'));
FileSystem::writeFile($testdir . '/ext.json', file_get_contents(ROOT_DIR . '/config/ext.json'));
FileSystem::writeFile($testdir . '/source.json', file_get_contents(ROOT_DIR . '/config/source.json'));
FileSystem::loadConfigArray('lib', $testdir);
FileSystem::loadConfigArray('ext', $testdir);
FileSystem::loadConfigArray('source', $testdir);
}
public static function tearDownAfterClass(): void
{
FileSystem::removeDir(WORKING_DIR . '/.configtest');
}
public function testGetExts()
{
$this->assertTrue(is_assoc_array(Config::getExts()));
}
public function testGetLib()
{
$this->assertIsArray(Config::getLib('zlib'));
match (PHP_OS_FAMILY) {
'FreeBSD', 'Darwin', 'Linux' => $this->assertStringEndsWith('.a', Config::getLib('zlib', 'static-libs', [])[0]),
'Windows' => $this->assertStringEndsWith('.lib', Config::getLib('zlib', 'static-libs', [])[0]),
default => null,
};
}
public function testGetExt()
{
$this->assertIsArray(Config::getExt('bcmath'));
$this->assertEquals('builtin', Config::getExt('bcmath', 'type'));
}
public function testGetSources()
{
$this->assertTrue(is_assoc_array(Config::getSources()));
}
public function testGetSource()
{
$this->assertIsArray(Config::getSource('php-src'));
}
public function testGetLibs()
{
$this->assertTrue(is_assoc_array(Config::getLibs()));
}
}

View File

@@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\store;
use PHPUnit\Framework\TestCase;
use SPC\store\CurlHook;
/**
* @internal
*/
class CurlHookTest extends TestCase
{
public function testSetupGithubToken()
{
$header = [];
CurlHook::setupGithubToken('GET', 'https://example.com', $header);
if (getenv('GITHUB_TOKEN') === false) {
$this->assertEmpty($header);
} else {
$this->assertEquals(['Authorization: Bearer ' . getenv('GITHUB_TOKEN')], $header);
}
$header = [];
putenv('GITHUB_TOKEN=token');
CurlHook::setupGithubToken('GET', 'https://example.com', $header);
$this->assertEquals(['Authorization: Bearer token'], $header);
}
}

View File

@@ -1,113 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\store;
use PHPUnit\Framework\TestCase;
use SPC\exception\InterruptException;
use SPC\store\Downloader;
use SPC\store\LockFile;
/**
* @internal
* TODO: Test all methods
*/
class DownloaderTest extends TestCase
{
public function testGetLatestGithubTarball()
{
$this->assertEquals(
'https://api.github.com/repos/AOMediaCodec/libavif/tarball/v1.1.1',
Downloader::getLatestGithubTarball('libavif', [
'type' => 'ghtar',
'repo' => 'AOMediaCodec/libavif',
])[0]
);
}
public function testDownloadGit()
{
Downloader::downloadGit('setup-static-php', 'https://github.com/static-php/setup-static-php.git', 'main');
$this->assertTrue(true);
// test keyboard interrupt
try {
Downloader::downloadGit('setup-static-php', 'https://github.com/static-php/setup-static-php.git', 'SIGINT');
} catch (InterruptException $e) {
$this->assertStringContainsString('interrupted', $e->getMessage());
return;
}
$this->fail('Expected exception not thrown');
}
public function testDownloadFile()
{
Downloader::downloadFile('fake-file', 'https://fakecmd.com/curlDown', 'curlDown.exe');
$this->assertTrue(true);
// test keyboard interrupt
try {
Downloader::downloadFile('fake-file', 'https://fakecmd.com/curlDown', 'SIGINT');
} catch (InterruptException $e) {
$this->assertStringContainsString('interrupted', $e->getMessage());
return;
}
$this->fail('Expected exception not thrown');
}
public function testLockSource()
{
LockFile::lockSource('fake-file', ['source_type' => SPC_SOURCE_ARCHIVE, 'filename' => 'fake-file-name', 'move_path' => 'fake-path', 'lock_as' => 'fake-lock-as']);
$this->assertFileExists(LockFile::LOCK_FILE);
$json = json_decode(file_get_contents(LockFile::LOCK_FILE), true);
$this->assertIsArray($json);
$this->assertArrayHasKey('fake-file', $json);
$this->assertArrayHasKey('source_type', $json['fake-file']);
$this->assertArrayHasKey('filename', $json['fake-file']);
$this->assertArrayHasKey('move_path', $json['fake-file']);
$this->assertArrayHasKey('lock_as', $json['fake-file']);
$this->assertEquals(SPC_SOURCE_ARCHIVE, $json['fake-file']['source_type']);
$this->assertEquals('fake-file-name', $json['fake-file']['filename']);
$this->assertEquals('fake-path', $json['fake-file']['move_path']);
$this->assertEquals('fake-lock-as', $json['fake-file']['lock_as']);
}
public function testGetLatestBitbucketTag()
{
$this->assertEquals(
'abc.tar.gz',
Downloader::getLatestBitbucketTag('abc', [
'repo' => 'MATCHED/def',
])[1]
);
$this->assertEquals(
'abc-1.0.0.tar.gz',
Downloader::getLatestBitbucketTag('abc', [
'repo' => 'abc/def',
])[1]
);
}
public function testGetLatestGithubRelease()
{
$this->assertEquals(
'ghreltest.tar.gz',
Downloader::getLatestGithubRelease('ghrel', [
'type' => 'ghrel',
'repo' => 'ghreltest/ghrel',
'match' => 'ghreltest.tar.gz',
])[1]
);
}
public function testGetFromFileList()
{
$filelist = Downloader::getFromFileList('fake-filelist', [
'url' => 'https://fakecmd.com/filelist',
'regex' => '/href="(?<file>filelist-(?<version>[^"]+)\.tar\.xz)"/',
]);
$this->assertIsArray($filelist);
$this->assertEquals('filelist-4.7.0.tar.xz', $filelist[1]);
}
}

View File

@@ -1,176 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\store;
use PHPUnit\Framework\TestCase;
use SPC\store\FileSystem;
/**
* @internal
*/
class FileSystemTest extends TestCase
{
private const TEST_FILE_CONTENT = 'Hello! Bye!';
public static function setUpBeforeClass(): void
{
if (file_put_contents(WORKING_DIR . '/.testfile', self::TEST_FILE_CONTENT) === false) {
static::markTestSkipped('Current environment or working dir is not writable!');
}
}
public static function tearDownAfterClass(): void
{
if (file_exists(WORKING_DIR . '/.testfile')) {
unlink(WORKING_DIR . '/.testfile');
}
}
public function testReplaceFileRegex()
{
$file = WORKING_DIR . '/.txt1';
file_put_contents($file, 'hello');
FileSystem::replaceFileRegex($file, '/ll/', '11');
$this->assertEquals('he11o', file_get_contents($file));
unlink($file);
}
public function testFindCommandPath()
{
$this->assertNull(FileSystem::findCommandPath('randomtestxxxxx'));
if (PHP_OS_FAMILY === 'Windows') {
$this->assertIsString(FileSystem::findCommandPath('explorer'));
} elseif (in_array(PHP_OS_FAMILY, ['Linux', 'Darwin', 'FreeBSD'])) {
$this->assertIsString(FileSystem::findCommandPath('uname'));
}
}
public function testReadFile()
{
$file = WORKING_DIR . '/.testread';
file_put_contents($file, 'haha');
$content = FileSystem::readFile($file);
$this->assertEquals('haha', $content);
@unlink($file);
}
public function testReplaceFileUser()
{
$file = WORKING_DIR . '/.txt1';
file_put_contents($file, 'hello');
FileSystem::replaceFileUser($file, function ($file) {
return str_replace('el', '55', $file);
});
$this->assertEquals('h55lo', file_get_contents($file));
unlink($file);
}
public function testExtname()
{
$this->assertEquals('exe', FileSystem::extname('/tmp/asd.exe'));
$this->assertEquals('', FileSystem::extname('/tmp/asd.'));
}
public function testGetClassesPsr4()
{
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/builder/extension', 'SPC\builder\extension');
foreach ($classes as $class) {
$this->assertIsString($class);
new \ReflectionClass($class);
}
}
public function testConvertPath()
{
$this->assertEquals('phar://C:/pharfile.phar', FileSystem::convertPath('phar://C:/pharfile.phar'));
if (DIRECTORY_SEPARATOR === '\\') {
$this->assertEquals('C:\Windows\win.ini', FileSystem::convertPath('C:\Windows/win.ini'));
}
}
public function testCreateDir()
{
FileSystem::createDir(WORKING_DIR . '/.testdir');
$this->assertDirectoryExists(WORKING_DIR . '/.testdir');
rmdir(WORKING_DIR . '/.testdir');
}
public function testReplaceFileStr()
{
$file = WORKING_DIR . '/.txt1';
file_put_contents($file, 'hello');
FileSystem::replaceFileStr($file, 'el', '55');
$this->assertEquals('h55lo', file_get_contents($file));
unlink($file);
}
public function testResetDir()
{
// prepare fake git dir to test
FileSystem::createDir(WORKING_DIR . '/.fake_down_test');
FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }');
FileSystem::resetDir(WORKING_DIR . '/.fake_down_test');
$this->assertFileDoesNotExist(WORKING_DIR . '/.fake_down_test/a.c');
FileSystem::removeDir(WORKING_DIR . '/.fake_down_test');
}
public function testCopyDir()
{
// prepare fake git dir to test
FileSystem::createDir(WORKING_DIR . '/.fake_down_test');
FileSystem::writeFile(WORKING_DIR . '/.fake_down_test/a.c', 'int main() { return 0; }');
FileSystem::copyDir(WORKING_DIR . '/.fake_down_test', WORKING_DIR . '/.fake_down_test2');
$this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test2');
$this->assertFileExists(WORKING_DIR . '/.fake_down_test2/a.c');
FileSystem::removeDir(WORKING_DIR . '/.fake_down_test');
FileSystem::removeDir(WORKING_DIR . '/.fake_down_test2');
}
public function testRemoveDir()
{
FileSystem::createDir(WORKING_DIR . '/.fake_down_test');
$this->assertDirectoryExists(WORKING_DIR . '/.fake_down_test');
FileSystem::removeDir(WORKING_DIR . '/.fake_down_test');
$this->assertDirectoryDoesNotExist(WORKING_DIR . '/.fake_down_test');
}
public function testLoadConfigArray()
{
$arr = FileSystem::loadConfigArray('lib');
$this->assertArrayHasKey('zlib', $arr);
}
public function testIsRelativePath()
{
$this->assertTrue(FileSystem::isRelativePath('.'));
$this->assertTrue(FileSystem::isRelativePath('.\sdf'));
if (DIRECTORY_SEPARATOR === '\\') {
$this->assertFalse(FileSystem::isRelativePath('C:\asdasd/fwe\asd'));
} else {
$this->assertFalse(FileSystem::isRelativePath('/fwefwefewf'));
}
}
public function testScanDirFiles()
{
$this->assertFalse(FileSystem::scanDirFiles('wfwefewfewf'));
$files = FileSystem::scanDirFiles(ROOT_DIR . '/config', true, true);
$this->assertContains('lib.json', $files);
}
public function testWriteFile()
{
FileSystem::writeFile(WORKING_DIR . '/.txt', 'txt');
$this->assertFileExists(WORKING_DIR . '/.txt');
$this->assertEquals('txt', FileSystem::readFile(WORKING_DIR . '/.txt'));
unlink(WORKING_DIR . '/.txt');
}
}

View File

@@ -1,767 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\exception\ValidationException;
use SPC\util\ConfigValidator;
/**
* @internal
*/
class ConfigValidatorTest extends TestCase
{
public function testValidateSourceGood(): void
{
$good_source = [
'source1' => [
'type' => 'filelist',
'url' => 'https://example.com',
'regex' => '.*',
],
'source2' => [
'type' => 'git',
'url' => 'https://example.com',
'rev' => 'master',
],
'source3' => [
'type' => 'ghtagtar',
'repo' => 'aaaa/bbbb',
],
'source4' => [
'type' => 'ghtar',
'repo' => 'aaa/bbb',
'path' => 'path/to/dir',
],
'source5' => [
'type' => 'ghrel',
'repo' => 'aaa/bbb',
'match' => '.*',
],
'source6' => [
'type' => 'url',
'url' => 'https://example.com',
],
'source7' => [
'type' => 'url',
'url' => 'https://example.com',
'filename' => 'test.tar.gz',
'path' => 'test/path',
'provide-pre-built' => true,
'license' => [
'type' => 'file',
'path' => 'LICENSE',
],
],
'source8' => [
'type' => 'url',
'url' => 'https://example.com',
'alt' => [
'type' => 'url',
'url' => 'https://alt.example.com',
],
],
'source9' => [
'type' => 'url',
'url' => 'https://example.com',
'alt' => false,
'license' => [
'type' => 'text',
'text' => 'MIT License',
],
],
];
try {
ConfigValidator::validateSource($good_source);
$this->assertTrue(true);
} catch (ValidationException $e) {
$this->fail($e->getMessage());
}
}
public function testValidateSourceBad(): void
{
$bad_source = [
'source1' => [
'type' => 'filelist',
'url' => 'https://example.com',
// no regex
],
'source2' => [
'type' => 'git',
'url' => true, // not string
'rev' => 'master',
],
'source3' => [
'type' => 'ghtagtar',
'url' => 'aaaa/bbbb', // not repo
],
'source4' => [
'type' => 'ghtar',
'repo' => 'aaa/bbb',
'path' => true, // not string
],
'source5' => [
'type' => 'ghrel',
'repo' => 'aaa/bbb',
'match' => 1, // not string
],
'source6' => [
'type' => 'url', // no url
],
'source7' => [
'type' => 'url',
'url' => 'https://example.com',
'provide-pre-built' => 'not boolean', // not boolean
],
'source8' => [
'type' => 'url',
'url' => 'https://example.com',
'prefer-stable' => 'not boolean', // not boolean
],
'source9' => [
'type' => 'url',
'url' => 'https://example.com',
'license' => 'not object', // not object
],
'source10' => [
'type' => 'url',
'url' => 'https://example.com',
'license' => [
'type' => 'invalid', // invalid type
],
],
'source11' => [
'type' => 'url',
'url' => 'https://example.com',
'license' => [
'type' => 'file', // missing path
],
],
'source12' => [
'type' => 'url',
'url' => 'https://example.com',
'license' => [
'type' => 'text', // missing text
],
],
'source13' => [
'type' => 'url',
'url' => 'https://example.com',
'alt' => 'not object or boolean', // not object or boolean
],
];
foreach ($bad_source as $name => $src) {
try {
ConfigValidator::validateSource([$name => $src]);
$this->fail("should throw ValidationException for source {$name}");
} catch (ValidationException) {
$this->assertTrue(true);
}
}
}
public function testValidateLibsGood(): void
{
$good_libs = [
'lib1' => [
'source' => 'source1',
],
'lib2' => [
'source' => 'source2',
'lib-depends' => [
'lib1',
],
],
'lib3' => [
'source' => 'source3',
'lib-suggests' => [
'lib1',
],
],
'lib4' => [
'source' => 'source4',
'headers' => [
'header1.h',
'header2.h',
],
'headers-windows' => [
'windows_header.h',
],
'bin-unix' => [
'binary1',
'binary2',
],
'frameworks' => [
'CoreFoundation',
'SystemConfiguration',
],
],
'lib5' => [
'type' => 'package',
'source' => 'source5',
'pkg-configs' => [
'pkg1',
'pkg2',
],
],
'lib6' => [
'type' => 'root',
],
];
try {
ConfigValidator::validateLibs($good_libs, ['source1' => [], 'source2' => [], 'source3' => [], 'source4' => [], 'source5' => []]);
$this->assertTrue(true);
} catch (ValidationException $e) {
$this->fail($e->getMessage());
}
}
public function testValidateLibsBad(): void
{
// lib.json is broken
try {
ConfigValidator::validateLibs('not array');
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib source not exists
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source3']], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib.json is broken by not assoc array
try {
ConfigValidator::validateLibs(['lib1', 'lib2'], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib.json lib is not one of "lib", "package", "root", "target"
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'type' => 'not one of']], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib.json lib if it is "lib" or "package", it must have "source"
try {
ConfigValidator::validateLibs(['lib1' => ['type' => 'lib']], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib.json static-libs must be a list
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'static-libs-windows' => 'not list']], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib.json frameworks must be a list
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'frameworks' => 'not list']], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// source must be string
try {
ConfigValidator::validateLibs(['lib1' => ['source' => true]], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib-depends must be list
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'lib-depends' => ['a' => 'not list']]], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// lib-suggests must be list
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'lib-suggests' => ['a' => 'not list']]], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// headers must be list
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'headers' => 'not list']], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// bin must be list
try {
ConfigValidator::validateLibs(['lib1' => ['source' => 'source1', 'bin-unix' => 'not list']], ['source1' => [], 'source2' => []]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
}
public function testValidateExts(): void
{
// Test valid extensions
$valid_exts = [
'ext1' => [
'type' => 'builtin',
],
'ext2' => [
'type' => 'external',
'source' => 'source1',
],
'ext3' => [
'type' => 'external',
'source' => 'source2',
'arg-type' => 'enable',
'lib-depends' => ['lib1'],
'lib-suggests' => ['lib2'],
'ext-depends-windows' => ['ext1'],
'support' => [
'Windows' => 'wip',
'BSD' => 'wip',
],
'notes' => true,
],
'ext4' => [
'type' => 'external',
'source' => 'source3',
'arg-type-unix' => 'with-path',
'arg-type-windows' => 'with',
],
];
ConfigValidator::validateExts($valid_exts);
// Test invalid data
$this->expectException(ValidationException::class);
ConfigValidator::validateExts(null);
}
public function testValidateExtsBad(): void
{
// Test invalid extension type
try {
ConfigValidator::validateExts(['ext1' => ['type' => 'invalid']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test external extension without source
try {
ConfigValidator::validateExts(['ext1' => ['type' => 'external']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test non-object extension
try {
ConfigValidator::validateExts(['ext1' => 'not object']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid source type
try {
ConfigValidator::validateExts(['ext1' => ['type' => 'external', 'source' => true]]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid support
try {
ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'support' => 'not object']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid notes
try {
ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'notes' => 'not boolean']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid lib-depends
try {
ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'lib-depends' => 'not list']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid arg-type
try {
ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'arg-type' => 'invalid']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid arg-type with suffix
try {
ConfigValidator::validateExts(['ext1' => ['type' => 'builtin', 'arg-type-unix' => 'invalid']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
}
public function testValidatePkgs(): void
{
// Test valid packages (all supported types)
$valid_pkgs = [
'pkg1' => [
'type' => 'url',
'url' => 'https://example.com/file.tar.gz',
],
'pkg2' => [
'type' => 'ghrel',
'repo' => 'owner/repo',
'match' => 'file.+\.tar\.gz',
],
'pkg3' => [
'type' => 'custom',
],
'pkg4' => [
'type' => 'url',
'url' => 'https://example.com/archive.zip',
'filename' => 'archive.zip',
'path' => 'extract/path',
'extract-files' => [
'source/file.exe' => '{pkg_root_path}/bin/file.exe',
'source/lib.dll' => '{pkg_root_path}/lib/lib.dll',
],
],
'pkg5' => [
'type' => 'ghrel',
'repo' => 'owner/repo',
'match' => 'release.+\.zip',
'extract-files' => [
'binary' => '{pkg_root_path}/bin/binary',
],
],
'pkg6' => [
'type' => 'filelist',
'url' => 'https://example.com/filelist',
'regex' => '/href="(?<file>.*\.tar\.gz)"/',
],
'pkg7' => [
'type' => 'git',
'url' => 'https://github.com/owner/repo.git',
'rev' => 'main',
],
'pkg8' => [
'type' => 'git',
'url' => 'https://github.com/owner/repo.git',
'rev' => 'v1.0.0',
'path' => 'subdir/path',
],
'pkg9' => [
'type' => 'ghtagtar',
'repo' => 'owner/repo',
],
'pkg10' => [
'type' => 'ghtar',
'repo' => 'owner/repo',
'path' => 'subdir',
],
];
ConfigValidator::validatePkgs($valid_pkgs);
// Test invalid data
$this->expectException(ValidationException::class);
ConfigValidator::validatePkgs(null);
}
public function testValidatePkgsBad(): void
{
// Test invalid package type
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'invalid']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test non-object package
try {
ConfigValidator::validatePkgs(['pkg1' => 'not object']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test filelist type without url
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'filelist', 'regex' => '.*']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test filelist type without regex
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'filelist', 'url' => 'https://example.com']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test git type without url
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'git', 'rev' => 'main']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test git type without rev
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'git', 'url' => 'https://github.com/owner/repo.git']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test ghtagtar type without repo
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghtagtar']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test ghtar type without repo
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghtar']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test url type without url
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test url type with non-string url
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => true]]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test ghrel type without repo
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'match' => 'pattern']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test ghrel type without match
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'repo' => 'owner/repo']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test ghrel type with non-string repo
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'repo' => true, 'match' => 'pattern']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test ghrel type with non-string match
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'ghrel', 'repo' => 'owner/repo', 'match' => 123]]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test git type with non-string path
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'git', 'url' => 'https://github.com/owner/repo.git', 'rev' => 'main', 'path' => 123]]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test url type with non-string filename
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'filename' => 123]]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid extract-files (not object)
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'extract-files' => 'not object']]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid extract-files mapping (non-string key)
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'extract-files' => [123 => 'target']]]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid extract-files mapping (non-string value)
try {
ConfigValidator::validatePkgs(['pkg1' => ['type' => 'url', 'url' => 'https://example.com', 'extract-files' => ['source' => 123]]]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
}
public function testValidatePreBuilt(): void
{
// Test valid pre-built configurations
$valid_prebuilt = [
'basic' => [
'repo' => 'static-php/static-php-cli-hosted',
'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz',
],
'full' => [
'repo' => 'static-php/static-php-cli-hosted',
'prefer-stable' => true,
'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz',
'match-pattern-macos' => '{name}-{arch}-{os}.txz',
'match-pattern-windows' => '{name}-{arch}-{os}.tgz',
],
'prefer-stable-false' => [
'repo' => 'owner/repo',
'prefer-stable' => false,
'match-pattern-macos' => '{name}-{arch}-{os}.tar.gz',
],
];
foreach ($valid_prebuilt as $name => $config) {
try {
ConfigValidator::validatePreBuilt($config);
$this->assertTrue(true, "Config {$name} should be valid");
} catch (ValidationException $e) {
$this->fail("Config {$name} should be valid but got: " . $e->getMessage());
}
}
}
public function testValidatePreBuiltBad(): void
{
// Test non-array data
try {
ConfigValidator::validatePreBuilt('invalid');
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test missing repo
try {
ConfigValidator::validatePreBuilt(['match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid repo type
try {
ConfigValidator::validatePreBuilt(['repo' => 123, 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid prefer-stable type
try {
ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'prefer-stable' => 'true', 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test no match patterns
try {
ConfigValidator::validatePreBuilt(['repo' => 'owner/repo']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test invalid match pattern type
try {
ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => 123]);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test missing {name} placeholder
try {
ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{arch}-{os}-{libc}-{libcver}.txz']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test missing {arch} placeholder
try {
ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{os}-{libc}-{libcver}.txz']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test missing {os} placeholder
try {
ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{arch}-{libc}-{libcver}.txz']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test linux pattern missing {libc} placeholder
try {
ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{arch}-{os}-{libcver}.txz']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
// Test linux pattern missing {libcver} placeholder
try {
ConfigValidator::validatePreBuilt(['repo' => 'owner/repo', 'match-pattern-linux' => '{name}-{arch}-{os}-{libc}.txz']);
$this->fail('should throw ValidationException');
} catch (ValidationException) {
$this->assertTrue(true);
}
}
}

View File

@@ -1,113 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\util\DependencyUtil;
/**
* @internal
*/
final class DependencyUtilTest extends TestCase
{
private array $originalConfig;
protected function setUp(): void
{
// Save original configuration
$this->originalConfig = [
'source' => Config::$source,
'lib' => Config::$lib,
'ext' => Config::$ext,
];
}
protected function tearDown(): void
{
// Restore original configuration
Config::$source = $this->originalConfig['source'];
Config::$lib = $this->originalConfig['lib'];
Config::$ext = $this->originalConfig['ext'];
}
public function testGetExtLibsByDeps(): void
{
// Set up test data
Config::$source = [
'test1' => [
'type' => 'url',
'url' => 'https://pecl.php.net/get/APCu',
'filename' => 'apcu.tgz',
'license' => [
'type' => 'file',
'path' => 'LICENSE',
],
],
];
Config::$lib = [
'lib-base' => ['type' => 'root'],
'php' => ['type' => 'root'],
'libaaa' => [
'source' => 'test1',
'static-libs' => ['libaaa.a'],
'lib-depends' => ['libbbb', 'libccc'],
'lib-suggests' => ['libeee'],
],
'libbbb' => [
'source' => 'test1',
'static-libs' => ['libbbb.a'],
'lib-suggests' => ['libccc'],
],
'libccc' => [
'source' => 'test1',
'static-libs' => ['libccc.a'],
],
'libeee' => [
'source' => 'test1',
'static-libs' => ['libeee.a'],
'lib-suggests' => ['libfff'],
],
'libfff' => [
'source' => 'test1',
'static-libs' => ['libfff.a'],
],
];
Config::$ext = [
'ext-a' => [
'type' => 'builtin',
'lib-depends' => ['libaaa'],
'ext-suggests' => ['ext-b'],
],
'ext-b' => [
'type' => 'builtin',
'lib-depends' => ['libeee'],
],
];
// Test dependency resolution
[$exts, $libs, $not_included] = DependencyUtil::getExtsAndLibs(['ext-a'], include_suggested_exts: true);
$this->assertContains('libbbb', $libs);
$this->assertContains('libccc', $libs);
$this->assertContains('ext-b', $exts);
$this->assertContains('ext-b', $not_included);
// Test dependency order
$this->assertIsInt($b = array_search('libbbb', $libs));
$this->assertIsInt($c = array_search('libccc', $libs));
$this->assertIsInt($a = array_search('libaaa', $libs));
// libbbb, libaaa
$this->assertTrue($b < $a);
$this->assertTrue($c < $a);
$this->assertTrue($c < $b);
}
public function testNotExistExtException(): void
{
$this->expectException(WrongUsageException::class);
DependencyUtil::getExtsAndLibs(['sdsd']);
}
}

View File

@@ -1,143 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\exception\SPCInternalException;
use SPC\util\GlobalEnvManager;
/**
* @internal
*/
final class GlobalEnvManagerTest extends TestCase
{
private array $originalEnv;
protected function setUp(): void
{
// Save original environment variables
$this->originalEnv = [
'BUILD_ROOT_PATH' => getenv('BUILD_ROOT_PATH'),
'SPC_TARGET' => getenv('SPC_TARGET'),
'SPC_LIBC' => getenv('SPC_LIBC'),
];
// Temporarily set private GlobalEnvManager::$initialized to false (use reflection)
$reflection = new \ReflectionClass(GlobalEnvManager::class);
$property = $reflection->getProperty('initialized');
$property->setValue(null, false);
}
protected function tearDown(): void
{
// Restore original environment variables
foreach ($this->originalEnv as $key => $value) {
if ($value === false) {
putenv($key);
} else {
putenv("{$key}={$value}");
}
}
// Temporarily set private GlobalEnvManager::$initialized to false (use reflection)
$reflection = new \ReflectionClass(GlobalEnvManager::class);
$property = $reflection->getProperty('initialized');
$property->setValue(null, true);
}
public function testGetInitializedEnv(): void
{
// Test that getInitializedEnv returns an array
$result = GlobalEnvManager::getInitializedEnv();
$this->assertIsArray($result);
}
/**
* @dataProvider envVariableProvider
*/
public function testPutenv(string $envVar): void
{
// Test putenv functionality
GlobalEnvManager::putenv($envVar);
$env = GlobalEnvManager::getInitializedEnv();
$this->assertContains($envVar, $env);
$this->assertEquals(explode('=', $envVar, 2)[1], getenv(explode('=', $envVar, 2)[0]));
}
/**
* @dataProvider pathProvider
*/
public function testAddPathIfNotExistsOnUnix(string $path): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
$originalPath = getenv('PATH');
GlobalEnvManager::addPathIfNotExists($path);
$newPath = getenv('PATH');
$this->assertStringContainsString($path, $newPath);
}
/**
* @dataProvider pathProvider
*/
public function testAddPathIfNotExistsWhenPathAlreadyExists(string $path): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
GlobalEnvManager::addPathIfNotExists($path);
$pathAfterFirstAdd = getenv('PATH');
GlobalEnvManager::addPathIfNotExists($path);
$pathAfterSecondAdd = getenv('PATH');
// Should not add the same path twice
$this->assertEquals($pathAfterFirstAdd, $pathAfterSecondAdd);
}
public function testInitWithoutBuildRootPath(): void
{
// Temporarily unset BUILD_ROOT_PATH
putenv('BUILD_ROOT_PATH');
$this->expectException(SPCInternalException::class);
GlobalEnvManager::init();
}
public function testAfterInit(): void
{
// Set required environment variable
putenv('BUILD_ROOT_PATH=/test/path');
putenv('SPC_SKIP_TOOLCHAIN_CHECK=true');
// Should not throw exception when SPC_SKIP_TOOLCHAIN_CHECK is true
GlobalEnvManager::afterInit();
$this->assertTrue(true); // Test passes if no exception is thrown
}
public function envVariableProvider(): array
{
return [
'simple-env' => ['TEST_VAR=test_value'],
'complex-env' => ['COMPLEX_VAR=complex_value_with_spaces'],
'numeric-env' => ['NUMERIC_VAR=123'],
'special-chars-env' => ['SPECIAL_VAR=test@#$%'],
];
}
public function pathProvider(): array
{
return [
'simple-path' => ['/test/path'],
'complex-path' => ['/usr/local/bin'],
'home-path' => ['/home/user/bin'],
'root-path' => ['/root/bin'],
];
}
}

View File

@@ -1,110 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\store\Config;
use SPC\util\LicenseDumper;
/**
* @internal
*/
final class LicenseDumperTest extends TestCase
{
private const DIRECTORY = __DIR__ . '/../../var/license-dump';
public static function tearDownAfterClass(): void
{
@rmdir(self::DIRECTORY);
@rmdir(dirname(self::DIRECTORY));
}
protected function setUp(): void
{
@rmdir(self::DIRECTORY);
}
protected function tearDown(): void
{
array_map('unlink', glob(self::DIRECTORY . '/*.txt'));
}
public function testDumpWithSingleLicense(): void
{
$bak = [
'source' => Config::$source,
'lib' => Config::$lib,
];
Config::$lib = [
'lib-base' => ['type' => 'root'],
'php' => ['type' => 'root'],
'fake_lib' => [
'source' => 'fake_lib',
],
];
Config::$source = [
'fake_lib' => [
'license' => [
'type' => 'text',
'text' => 'license',
],
],
];
$dumper = new LicenseDumper();
$dumper->addLibs(['fake_lib']);
$dumper->dump(self::DIRECTORY);
$this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_0.txt');
// restore
Config::$source = $bak['source'];
Config::$lib = $bak['lib'];
}
public function testDumpWithMultipleLicenses(): void
{
$bak = [
'source' => Config::$source,
'lib' => Config::$lib,
];
Config::$lib = [
'lib-base' => ['type' => 'root'],
'php' => ['type' => 'root'],
'fake_lib' => [
'source' => 'fake_lib',
],
];
Config::$source = [
'fake_lib' => [
'license' => [
[
'type' => 'text',
'text' => 'license',
],
[
'type' => 'text',
'text' => 'license',
],
[
'type' => 'text',
'text' => 'license',
],
],
],
];
$dumper = new LicenseDumper();
$dumper->addLibs(['fake_lib']);
$dumper->dump(self::DIRECTORY);
$this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_0.txt');
$this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_1.txt');
$this->assertFileExists(self::DIRECTORY . '/lib_fake_lib_2.txt');
// restore
Config::$source = $bak['source'];
Config::$lib = $bak['lib'];
}
}

View File

@@ -1,210 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\exception\ExecutionException;
use SPC\util\PkgConfigUtil;
/**
* @internal
*/
final class PkgConfigUtilTest extends TestCase
{
private static string $originalPath;
private static string $fakePkgConfigPath;
public static function setUpBeforeClass(): void
{
if (PHP_OS_FAMILY === 'Windows') {
// Skip tests on Windows as pkg-config is not typically available
self::markTestSkipped('PkgConfigUtil tests are not applicable on Windows.');
}
parent::setUpBeforeClass();
// Save original PATH
self::$originalPath = getenv('PATH');
// Create fake pkg-config directory
self::$fakePkgConfigPath = sys_get_temp_dir() . '/fake-pkg-config-' . uniqid();
mkdir(self::$fakePkgConfigPath, 0755, true);
// Create fake pkg-config executable
self::createFakePkgConfig();
// Add fake pkg-config to PATH
putenv('PATH=' . self::$fakePkgConfigPath . ':' . self::$originalPath);
}
public static function tearDownAfterClass(): void
{
// Restore original PATH
putenv('PATH=' . self::$originalPath);
// Clean up fake pkg-config
if (is_dir(self::$fakePkgConfigPath)) {
self::removeDirectory(self::$fakePkgConfigPath);
}
parent::tearDownAfterClass();
}
/**
* @dataProvider validPackageProvider
*/
public function testGetCflagsWithValidPackage(string $package, string $expectedCflags): void
{
$result = PkgConfigUtil::getCflags($package);
$this->assertEquals($expectedCflags, $result);
}
/**
* @dataProvider validPackageProvider
*/
public function testGetLibsArrayWithValidPackage(string $package, string $expectedCflags, array $expectedLibs): void
{
$result = PkgConfigUtil::getLibsArray($package);
$this->assertEquals($expectedLibs, $result);
}
/**
* @dataProvider invalidPackageProvider
*/
public function testGetCflagsWithInvalidPackage(string $package): void
{
$this->expectException(ExecutionException::class);
PkgConfigUtil::getCflags($package);
}
/**
* @dataProvider invalidPackageProvider
*/
public function testGetLibsArrayWithInvalidPackage(string $package): void
{
$this->expectException(ExecutionException::class);
PkgConfigUtil::getLibsArray($package);
}
public static function invalidPackageProvider(): array
{
return [
'invalid-package' => ['invalid-package'],
'empty-string' => [''],
'non-existent-package' => ['non-existent-package'],
];
}
public static function validPackageProvider(): array
{
return [
'libxml2' => ['libxml-2.0', '-I/usr/include/libxml2', ['-lxml2', '']],
'zlib' => ['zlib', '-I/usr/include', ['-lz', '']],
'openssl' => ['openssl', '-I/usr/include/openssl', ['-lssl', '-lcrypto', '']],
];
}
/**
* Create a fake pkg-config executable
*/
private static function createFakePkgConfig(): void
{
$pkgConfigScript = self::$fakePkgConfigPath . '/pkg-config';
$script = <<<'SCRIPT'
#!/bin/bash
# Fake pkg-config script for testing
# Shift arguments to get the package name
shift
case "$1" in
--cflags-only-other)
shift
case "$1" in
libxml-2.0)
echo "-I/usr/include/libxml2"
;;
zlib)
echo "-I/usr/include"
;;
openssl)
echo "-I/usr/include/openssl"
;;
*)
echo "Package '$1' was not found in the pkg-config search path." >&2
exit 1
;;
esac
;;
--libs-only-l)
shift
case "$1" in
libxml-2.0)
echo "-lxml2"
;;
zlib)
echo "-lz"
;;
openssl)
echo "-lssl -lcrypto"
;;
*)
echo "Package '$1' was not found in the pkg-config search path." >&2
exit 1
;;
esac
;;
--libs-only-other)
shift
case "$1" in
libxml-2.0)
echo ""
;;
zlib)
echo ""
;;
openssl)
echo ""
;;
*)
echo "Package '$1' was not found in the pkg-config search path." >&2
exit 1
;;
esac
;;
*)
echo "Usage: pkg-config [OPTION] [PACKAGE]" >&2
echo "Try 'pkg-config --help' for more information." >&2
exit 1
;;
esac
SCRIPT;
file_put_contents($pkgConfigScript, $script);
chmod($pkgConfigScript, 0755);
}
/**
* Remove directory recursively
*/
private static function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
self::removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
}

View File

@@ -1,74 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
use SPC\builder\BuilderProvider;
use SPC\store\FileSystem;
use SPC\util\SPCConfigUtil;
use Symfony\Component\Console\Input\ArgvInput;
/**
* @internal
*/
class SPCConfigUtilTest extends TestCase
{
public static function setUpBeforeClass(): void
{
if (PHP_OS_FAMILY === 'Windows') {
// Skip tests on Windows as SPCConfigUtil is not applicable
self::markTestSkipped('SPCConfigUtil tests are not applicable on Windows.');
}
$testdir = WORKING_DIR . '/.configtest';
FileSystem::createDir($testdir);
FileSystem::writeFile($testdir . '/lib.json', file_get_contents(ROOT_DIR . '/config/lib.json'));
FileSystem::writeFile($testdir . '/ext.json', file_get_contents(ROOT_DIR . '/config/ext.json'));
FileSystem::writeFile($testdir . '/source.json', file_get_contents(ROOT_DIR . '/config/source.json'));
FileSystem::loadConfigArray('lib', $testdir);
FileSystem::loadConfigArray('ext', $testdir);
FileSystem::loadConfigArray('source', $testdir);
}
public static function tearDownAfterClass(): void
{
FileSystem::removeDir(WORKING_DIR . '/.configtest');
}
public function testConstruct(): void
{
$this->assertInstanceOf(SPCConfigUtil::class, new SPCConfigUtil());
$this->assertInstanceOf(SPCConfigUtil::class, new SPCConfigUtil(BuilderProvider::makeBuilderByInput(new ArgvInput())));
}
public function testConfig(): void
{
if (PHP_OS_FAMILY !== 'Linux') {
$this->markTestSkipped('SPCConfigUtil tests are only applicable on Linux.');
}
// normal
$result = (new SPCConfigUtil())->config(['bcmath']);
$this->assertStringContainsString(BUILD_ROOT_PATH . '/include', $result['cflags']);
$this->assertStringContainsString(BUILD_ROOT_PATH . '/lib', $result['ldflags']);
$this->assertStringContainsString('-lphp', $result['libs']);
// has cpp
$result = (new SPCConfigUtil())->config(['rar']);
$this->assertStringContainsString(PHP_OS_FAMILY === 'Darwin' ? '-lc++' : '-lstdc++', $result['libs']);
// has libmimalloc.a in lib dir
// backup first
if (file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) {
$bak = file_get_contents(BUILD_LIB_PATH . '/libmimalloc.a');
@unlink(BUILD_LIB_PATH . '/libmimalloc.a');
}
file_put_contents(BUILD_LIB_PATH . '/libmimalloc.a', '');
$result = (new SPCConfigUtil())->config(['bcmath'], ['mimalloc']);
$this->assertStringStartsWith(BUILD_LIB_PATH . '/libmimalloc.a', $result['libs']);
@unlink(BUILD_LIB_PATH . '/libmimalloc.a');
if (isset($bak)) {
file_put_contents(BUILD_LIB_PATH . '/libmimalloc.a', $bak);
}
}
}

View File

@@ -1,106 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use SPC\util\SPCTarget;
/**
* @internal
*/
final class SPCTargetTest extends TestBase
{
private array $originalEnv;
protected function setUp(): void
{
// Save original environment variables
$this->originalEnv = [
'SPC_TARGET' => getenv('SPC_TARGET'),
'SPC_LIBC' => getenv('SPC_LIBC'),
];
}
protected function tearDown(): void
{
// Restore original environment variables
foreach ($this->originalEnv as $key => $value) {
if ($value === false) {
putenv($key);
} else {
putenv("{$key}={$value}");
}
}
}
/**
* @dataProvider libcProvider
*/
public function testGetLibc(string $libc, bool $expected): void
{
putenv("SPC_LIBC={$libc}");
$result = SPCTarget::getLibc();
if ($libc === '') {
// When SPC_LIBC is set to empty string, getenv returns empty string, not false
$this->assertEquals('', $result);
} else {
$this->assertEquals($libc, $result);
}
}
/**
* @dataProvider libcProvider
*/
public function testGetLibcVersion(string $libc): void
{
putenv("SPC_LIBC={$libc}");
$result = SPCTarget::getLibcVersion();
// The actual result depends on the system, but it could be null if libc is not available
$this->assertIsStringOrNull($result);
}
/**
* @dataProvider targetOSProvider
*/
public function testGetTargetOS(string $target, string $expected): void
{
putenv("SPC_TARGET={$target}");
$result = SPCTarget::getTargetOS();
$this->assertEquals($expected, $result);
}
public function testLibcListConstant(): void
{
$this->assertIsArray(SPCTarget::LIBC_LIST);
$this->assertContains('musl', SPCTarget::LIBC_LIST);
$this->assertContains('glibc', SPCTarget::LIBC_LIST);
}
public function libcProvider(): array
{
return [
'musl' => ['musl', true],
'glibc' => ['glibc', false],
'empty' => ['', false],
];
}
public function targetOSProvider(): array
{
return [
'linux-target' => ['native-linux', 'Linux'],
'macos-target' => ['native-macos', 'Darwin'],
'windows-target' => ['native-windows', 'Windows'],
'empty-target' => ['', PHP_OS_FAMILY],
];
}
private function assertIsStringOrNull($value): void
{
$this->assertTrue(is_string($value) || is_null($value), 'Value must be string or null');
}
}

View File

@@ -1,100 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use PHPUnit\Framework\TestCase;
/**
* Base test class for util tests with output suppression
*/
abstract class TestBase extends TestCase
{
protected $outputBuffer;
protected function setUp(): void
{
parent::setUp();
$this->suppressOutput();
}
protected function tearDown(): void
{
$this->restoreOutput();
parent::tearDown();
}
/**
* Suppress output during tests
*/
protected function suppressOutput(): void
{
// Start output buffering to capture PHP output
$this->outputBuffer = ob_start();
}
/**
* Restore output after tests
*/
protected function restoreOutput(): void
{
// Clean output buffer
if ($this->outputBuffer) {
ob_end_clean();
}
}
/**
* Create a UnixShell instance with debug disabled to suppress logs
*/
protected function createUnixShell(): \SPC\util\shell\UnixShell
{
return new \SPC\util\shell\UnixShell(false);
}
/**
* Create a WindowsCmd instance with debug disabled to suppress logs
*/
protected function createWindowsCmd(): \SPC\util\shell\WindowsCmd
{
return new \SPC\util\shell\WindowsCmd(false);
}
/**
* Run a test with output suppression
*/
protected function runWithOutputSuppression(callable $callback)
{
$this->suppressOutput();
try {
return $callback();
} finally {
$this->restoreOutput();
}
}
/**
* Execute a command with output suppression
*/
protected function execWithSuppression(string $command): array
{
$this->suppressOutput();
try {
exec($command, $output, $returnCode);
return [$returnCode, $output];
} finally {
$this->restoreOutput();
}
}
/**
* Execute a command with output redirected to /dev/null
*/
protected function execSilently(string $command): array
{
$command .= ' 2>/dev/null 1>/dev/null';
exec($command, $output, $returnCode);
return [$returnCode, $output];
}
}

View File

@@ -1,184 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use SPC\exception\EnvironmentException;
use SPC\util\shell\UnixShell;
/**
* @internal
*/
final class UnixShellTest extends TestBase
{
public function testConstructorOnWindows(): void
{
if (PHP_OS_FAMILY !== 'Windows') {
$this->markTestSkipped('This test is for Windows systems only');
}
$this->expectException(EnvironmentException::class);
$this->expectExceptionMessage('Windows cannot use UnixShell');
new UnixShell();
}
/**
* @dataProvider envProvider
*/
public function testSetEnv(array $env): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
$shell = $this->createUnixShell();
$result = $shell->setEnv($env);
$this->assertSame($shell, $result);
foreach ($env as $item) {
if (trim($item) !== '') {
$this->assertStringContainsString($item, $shell->getEnvString());
}
}
}
/**
* @dataProvider envProvider
*/
public function testAppendEnv(array $env): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
$shell = $this->createUnixShell();
$shell->setEnv(['CFLAGS' => '-O2']);
$shell->appendEnv($env);
$this->assertStringContainsString('-O2', $shell->getEnvString());
foreach ($env as $value) {
if (trim($value) !== '') {
$this->assertStringContainsString($value, $shell->getEnvString());
}
}
}
/**
* @dataProvider envProvider
*/
public function testGetEnvString(array $env): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
$shell = $this->createUnixShell();
$shell->setEnv($env);
$envString = $shell->getEnvString();
$hasNonEmptyValues = false;
foreach ($env as $key => $value) {
if (trim($value) !== '') {
$this->assertStringContainsString("{$key}=\"{$value}\"", $envString);
$hasNonEmptyValues = true;
}
}
// If all values are empty, ensure we still have a test assertion
if (!$hasNonEmptyValues) {
$this->assertIsString($envString);
}
}
public function testGetEnvStringWithEmptyEnv(): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
$shell = $this->createUnixShell();
$envString = $shell->getEnvString();
$this->assertEquals('', trim($envString));
}
/**
* @dataProvider commandProvider
*/
public function testExecWithResult(string $command): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
$shell = $this->createUnixShell();
[$code, $output] = $shell->execWithResult($command);
$this->assertIsInt($code);
$this->assertIsArray($output);
}
public function testExecWithResultWithLog(): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
$shell = $this->createUnixShell();
[$code, $output] = $shell->execWithResult('echo "test"', false);
$this->assertIsInt($code);
$this->assertIsArray($output);
$this->assertEquals(0, $code);
$this->assertEquals(['test'], $output);
}
public function testExecWithResultWithCd(): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
$shell = $this->createUnixShell();
$shell->cd('/tmp');
[$code, $output] = $shell->execWithResult('pwd');
$this->assertIsInt($code);
$this->assertEquals(0, $code);
$this->assertIsArray($output);
}
public static function directoryProvider(): array
{
return [
'simple-directory' => ['/test/directory'],
'home-directory' => ['/home/user'],
'root-directory' => ['/root'],
'tmp-directory' => ['/tmp'],
];
}
public static function envProvider(): array
{
return [
'simple-env' => [['CFLAGS' => '-O2', 'LDFLAGS' => '-L/usr/lib']],
'complex-env' => [['CXXFLAGS' => '-std=c++11', 'LIBS' => '-lz -lxml']],
'empty-env' => [['CFLAGS' => '', 'LDFLAGS' => ' ']],
'mixed-env' => [['CFLAGS' => '-O2', 'EMPTY_VAR' => '']],
];
}
public static function commandProvider(): array
{
return [
'echo-command' => ['echo "test"'],
'pwd-command' => ['pwd'],
'ls-command' => ['ls -la'],
];
}
}

View File

@@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\Tests\util;
use SPC\exception\SPCInternalException;
use SPC\util\shell\WindowsCmd;
/**
* @internal
*/
final class WindowsCmdTest extends TestBase
{
public function testConstructorOnUnix(): void
{
if (PHP_OS_FAMILY === 'Windows') {
$this->markTestSkipped('This test is for Unix systems only');
}
$this->expectException(SPCInternalException::class);
$this->expectExceptionMessage('Only windows can use WindowsCmd');
new WindowsCmd();
}
/**
* @dataProvider commandProvider
*/
public function testExecWithResult(string $command): void
{
if (PHP_OS_FAMILY !== 'Windows') {
$this->markTestSkipped('This test is for Windows systems only');
}
$cmd = $this->createWindowsCmd();
[$code, $output] = $cmd->execWithResult($command);
$this->assertIsInt($code);
$this->assertEquals(0, $code);
$this->assertIsArray($output);
$this->assertNotEmpty($output);
}
public function testExecWithResultWithLog(): void
{
if (PHP_OS_FAMILY !== 'Windows') {
$this->markTestSkipped('This test is for Windows systems only');
}
$cmd = $this->createWindowsCmd();
[$code, $output] = $cmd->execWithResult('echo test', false);
$this->assertIsInt($code);
$this->assertIsArray($output);
$this->assertEquals(0, $code);
$this->assertEquals(['test'], $output);
}
public static function commandProvider(): array
{
return [
'echo-command' => ['echo test'],
'dir-command' => ['dir'],
'cd-command' => ['cd'],
];
}
}

View File

@@ -0,0 +1,303 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Config;
use PHPUnit\Framework\TestCase;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\Exception\WrongUsageException;
/**
* @internal
*/
class ArtifactConfigTest extends TestCase
{
private string $tempDir;
/** @noinspection PhpExpressionResultUnusedInspection */
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/artifact_config_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset static state
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue(null, []);
}
/** @noinspection PhpExpressionResultUnusedInspection */
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset static state
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$property->setValue(null, []);
}
public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void
{
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Directory /nonexistent/path does not exist, cannot load artifact config.');
ArtifactConfig::loadFromDir('/nonexistent/path');
}
public function testLoadFromDirWithValidArtifactJson(): void
{
$artifactContent = json_encode([
'test-artifact' => [
'source' => 'https://example.com/file.tar.gz',
],
]);
file_put_contents($this->tempDir . '/artifact.json', $artifactContent);
ArtifactConfig::loadFromDir($this->tempDir);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config);
$this->assertArrayHasKey('source', $config);
}
public function testLoadFromDirWithMultipleArtifactFiles(): void
{
$artifact1Content = json_encode([
'artifact-1' => [
'source' => 'https://example.com/file1.tar.gz',
],
]);
$artifact2Content = json_encode([
'artifact-2' => [
'source' => 'https://example.com/file2.tar.gz',
],
]);
file_put_contents($this->tempDir . '/artifact.ext.json', $artifact1Content);
file_put_contents($this->tempDir . '/artifact.lib.json', $artifact2Content);
file_put_contents($this->tempDir . '/artifact.json', json_encode(['artifact-3' => ['source' => 'custom']]));
ArtifactConfig::loadFromDir($this->tempDir);
$this->assertNotNull(ArtifactConfig::get('artifact-1'));
$this->assertNotNull(ArtifactConfig::get('artifact-2'));
$this->assertNotNull(ArtifactConfig::get('artifact-3'));
}
public function testLoadFromFileThrowsExceptionWhenFileCannotBeRead(): void
{
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Failed to read artifact config file:');
ArtifactConfig::loadFromFile('/nonexistent/file.json');
}
public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void
{
$file = $this->tempDir . '/invalid.json';
file_put_contents($file, 'not valid json{');
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Invalid JSON format in artifact config file:');
ArtifactConfig::loadFromFile($file);
}
public function testLoadFromFileWithValidJson(): void
{
$file = $this->tempDir . '/valid.json';
$content = json_encode([
'my-artifact' => [
'source' => [
'type' => 'url',
'url' => 'https://example.com/file.tar.gz',
],
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('my-artifact');
$this->assertIsArray($config);
$this->assertArrayHasKey('source', $config);
}
public function testGetAllReturnsAllLoadedArtifacts(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'artifact-a' => ['source' => 'custom'],
'artifact-b' => ['source' => 'custom'],
'artifact-c' => ['source' => 'custom'],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$all = ArtifactConfig::getAll();
$this->assertIsArray($all);
$this->assertCount(3, $all);
$this->assertArrayHasKey('artifact-a', $all);
$this->assertArrayHasKey('artifact-b', $all);
$this->assertArrayHasKey('artifact-c', $all);
}
public function testGetReturnsNullWhenArtifactNotFound(): void
{
$this->assertNull(ArtifactConfig::get('non-existent-artifact'));
}
public function testGetReturnsConfigWhenArtifactExists(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'custom',
'binary' => 'custom',
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config);
$this->assertEquals('custom', $config['source']);
$this->assertIsArray($config['binary']);
}
public function testLoadFromFileWithExpandedUrlInSource(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'https://example.com/archive.tar.gz',
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config);
$this->assertIsArray($config['source']);
$this->assertEquals('url', $config['source']['type']);
$this->assertEquals('https://example.com/archive.tar.gz', $config['source']['url']);
}
public function testLoadFromFileWithBinaryCustom(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'custom',
'binary' => 'custom',
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config['binary']);
$this->assertArrayHasKey('linux-x86_64', $config['binary']);
$this->assertArrayHasKey('macos-aarch64', $config['binary']);
$this->assertEquals('custom', $config['binary']['linux-x86_64']['type']);
}
public function testLoadFromFileWithBinaryHosted(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'custom',
'binary' => 'hosted',
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config['binary']);
$this->assertEquals('hosted', $config['binary']['linux-x86_64']['type']);
$this->assertEquals('hosted', $config['binary']['macos-aarch64']['type']);
}
public function testLoadFromFileWithBinaryPlatformSpecific(): void
{
$file = $this->tempDir . '/artifacts.json';
$content = json_encode([
'test-artifact' => [
'source' => 'custom',
'binary' => [
'linux-x86_64' => 'https://example.com/linux.tar.gz',
'macos-aarch64' => [
'type' => 'url',
'url' => 'https://example.com/macos.tar.gz',
],
],
],
]);
file_put_contents($file, $content);
ArtifactConfig::loadFromFile($file);
$config = ArtifactConfig::get('test-artifact');
$this->assertIsArray($config['binary']);
$this->assertEquals('url', $config['binary']['linux-x86_64']['type']);
$this->assertEquals('https://example.com/linux.tar.gz', $config['binary']['linux-x86_64']['url']);
$this->assertEquals('url', $config['binary']['macos-aarch64']['type']);
$this->assertEquals('https://example.com/macos.tar.gz', $config['binary']['macos-aarch64']['url']);
}
public function testLoadFromDirWithEmptyDirectory(): void
{
// Empty directory should not throw exception
ArtifactConfig::loadFromDir($this->tempDir);
$this->assertEquals([], ArtifactConfig::getAll());
}
public function testMultipleLoadsAppendConfigs(): void
{
$file1 = $this->tempDir . '/artifact1.json';
$file2 = $this->tempDir . '/artifact2.json';
file_put_contents($file1, json_encode(['art1' => ['source' => 'custom']]));
file_put_contents($file2, json_encode(['art2' => ['source' => 'custom']]));
ArtifactConfig::loadFromFile($file1);
ArtifactConfig::loadFromFile($file2);
$all = ArtifactConfig::getAll();
$this->assertCount(2, $all);
$this->assertArrayHasKey('art1', $all);
$this->assertArrayHasKey('art2', $all);
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
}
rmdir($dir);
}
}

View File

@@ -0,0 +1,196 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Config;
use PHPUnit\Framework\TestCase;
use StaticPHP\Config\ConfigType;
/**
* @internal
*/
class ConfigTypeTest extends TestCase
{
public function testConstantValues(): void
{
$this->assertEquals('list_array', ConfigType::LIST_ARRAY);
$this->assertEquals('assoc_array', ConfigType::ASSOC_ARRAY);
$this->assertEquals('string', ConfigType::STRING);
$this->assertEquals('bool', ConfigType::BOOL);
}
public function testPackageTypesConstant(): void
{
$expectedTypes = [
'library',
'php-extension',
'target',
'virtual-target',
];
$this->assertEquals($expectedTypes, ConfigType::PACKAGE_TYPES);
}
public function testValidateLicenseFieldWithValidFileType(): void
{
$license = [
'type' => 'file',
'path' => 'LICENSE',
];
$this->assertTrue(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldWithValidFileTypeArrayPath(): void
{
$license = [
'type' => 'file',
'path' => ['LICENSE', 'COPYING'],
];
$this->assertTrue(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldWithValidTextType(): void
{
$license = [
'type' => 'text',
'text' => 'MIT License',
];
$this->assertTrue(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldWithListOfLicenses(): void
{
$licenses = [
[
'type' => 'file',
'path' => 'LICENSE',
],
[
'type' => 'text',
'text' => 'MIT',
],
];
$this->assertTrue(ConfigType::validateLicenseField($licenses));
}
public function testValidateLicenseFieldWithEmptyList(): void
{
$licenses = [];
$this->assertTrue(ConfigType::validateLicenseField($licenses));
}
public function testValidateLicenseFieldReturnsFalseWhenNotAssocArray(): void
{
$this->assertFalse(ConfigType::validateLicenseField('string'));
$this->assertFalse(ConfigType::validateLicenseField(123));
$this->assertFalse(ConfigType::validateLicenseField(true));
}
public function testValidateLicenseFieldReturnsFalseWhenMissingType(): void
{
$license = [
'path' => 'LICENSE',
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWithInvalidType(): void
{
$license = [
'type' => 'invalid',
'data' => 'something',
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWhenFileTypeMissingPath(): void
{
$license = [
'type' => 'file',
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWhenFileTypePathIsInvalid(): void
{
$license = [
'type' => 'file',
'path' => 123,
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWhenTextTypeMissingText(): void
{
$license = [
'type' => 'text',
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldReturnsFalseWhenTextTypeTextIsNotString(): void
{
$license = [
'type' => 'text',
'text' => ['array'],
];
$this->assertFalse(ConfigType::validateLicenseField($license));
}
public function testValidateLicenseFieldWithListContainingInvalidItem(): void
{
$licenses = [
[
'type' => 'file',
'path' => 'LICENSE',
],
[
'type' => 'text',
// missing 'text' field
],
];
$this->assertFalse(ConfigType::validateLicenseField($licenses));
}
public function testValidateLicenseFieldWithNestedListsOfLicenses(): void
{
$licenses = [
[
[
'type' => 'file',
'path' => 'LICENSE',
],
],
];
$this->assertTrue(ConfigType::validateLicenseField($licenses));
}
public function testValidateLicenseFieldWithNestedListContainingInvalidItem(): void
{
$licenses = [
[
[
'type' => 'file',
'path' => 'LICENSE',
],
'invalid-string-item',
],
];
$this->assertFalse(ConfigType::validateLicenseField($licenses));
}
}

View File

@@ -0,0 +1,627 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Config;
use PHPUnit\Framework\TestCase;
use StaticPHP\Config\ConfigValidator;
use StaticPHP\Exception\ValidationException;
/**
* @internal
*/
class ConfigValidatorTest extends TestCase
{
public function testValidateAndLintArtifactsThrowsExceptionWhenDataIsNotArray(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('test.json is broken');
$data = 'not an array';
ConfigValidator::validateAndLintArtifacts('test.json', $data);
}
public function testValidateAndLintArtifactsWithCustomSource(): void
{
$data = [
'test-artifact' => [
'source' => 'custom',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertEquals('custom', $data['test-artifact']['source']);
}
public function testValidateAndLintArtifactsExpandsUrlString(): void
{
$data = [
'test-artifact' => [
'source' => 'https://example.com/file.tar.gz',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['source']);
$this->assertEquals('url', $data['test-artifact']['source']['type']);
$this->assertEquals('https://example.com/file.tar.gz', $data['test-artifact']['source']['url']);
}
public function testValidateAndLintArtifactsExpandsHttpUrlString(): void
{
$data = [
'test-artifact' => [
'source' => 'http://example.com/file.tar.gz',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['source']);
$this->assertEquals('url', $data['test-artifact']['source']['type']);
$this->assertEquals('http://example.com/file.tar.gz', $data['test-artifact']['source']['url']);
}
public function testValidateAndLintArtifactsWithSourceObject(): void
{
$data = [
'test-artifact' => [
'source' => [
'type' => 'git',
'url' => 'https://github.com/example/repo.git',
'rev' => 'main',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['source']);
$this->assertEquals('git', $data['test-artifact']['source']['type']);
}
public function testValidateAndLintArtifactsWithBinaryCustom(): void
{
$data = [
'test-artifact' => [
'binary' => 'custom',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['binary']);
$this->assertArrayHasKey('linux-x86_64', $data['test-artifact']['binary']);
$this->assertEquals('custom', $data['test-artifact']['binary']['linux-x86_64']['type']);
}
public function testValidateAndLintArtifactsWithBinaryHosted(): void
{
$data = [
'test-artifact' => [
'binary' => 'hosted',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['binary']);
$this->assertArrayHasKey('macos-aarch64', $data['test-artifact']['binary']);
$this->assertEquals('hosted', $data['test-artifact']['binary']['macos-aarch64']['type']);
}
public function testValidateAndLintArtifactsWithBinaryPlatformObject(): void
{
$data = [
'test-artifact' => [
'binary' => [
'linux-x86_64' => [
'type' => 'url',
'url' => 'https://example.com/binary.tar.gz',
],
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertEquals('url', $data['test-artifact']['binary']['linux-x86_64']['type']);
}
public function testValidateAndLintArtifactsExpandsBinaryPlatformUrlString(): void
{
$data = [
'test-artifact' => [
'binary' => [
'linux-x86_64' => 'https://example.com/binary.tar.gz',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['binary']['linux-x86_64']);
$this->assertEquals('url', $data['test-artifact']['binary']['linux-x86_64']['type']);
$this->assertEquals('https://example.com/binary.tar.gz', $data['test-artifact']['binary']['linux-x86_64']['url']);
}
public function testValidateAndLintArtifactsWithSourceMirror(): void
{
$data = [
'test-artifact' => [
'source-mirror' => 'https://mirror.example.com/file.tar.gz',
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data['test-artifact']['source-mirror']);
$this->assertEquals('url', $data['test-artifact']['source-mirror']['type']);
}
public function testValidateAndLintPackagesThrowsExceptionWhenDataIsNotArray(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('pkg.json is broken');
$data = 'not an array';
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionWhenPackageIsNotAssocArray(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package [test-pkg] in pkg.json is not a valid associative array');
$data = [
'test-pkg' => ['list', 'array'],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionWhenTypeMissing(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Package [test-pkg] in pkg.json has invalid or missing 'type' field");
$data = [
'test-pkg' => [
'depends' => [],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionWhenTypeInvalid(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Package [test-pkg] in pkg.json has invalid or missing 'type' field");
$data = [
'test-pkg' => [
'type' => 'invalid-type',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesWithValidLibraryType(): void
{
$data = [
'test-lib' => [
'type' => 'library',
'artifact' => 'test-artifact',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('library', $data['test-lib']['type']);
}
public function testValidateAndLintPackagesWithValidPhpExtensionType(): void
{
$data = [
'test-ext' => [
'type' => 'php-extension',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('php-extension', $data['test-ext']['type']);
}
public function testValidateAndLintPackagesWithValidTargetType(): void
{
$data = [
'test-target' => [
'type' => 'target',
'artifact' => 'test-artifact',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('target', $data['test-target']['type']);
}
public function testValidateAndLintPackagesWithValidVirtualTargetType(): void
{
$data = [
'test-virtual' => [
'type' => 'virtual-target',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('virtual-target', $data['test-virtual']['type']);
}
public function testValidateAndLintPackagesThrowsExceptionWhenLibraryMissingArtifact(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Package [test-lib] in pkg.json of type 'library' must have an 'artifact' field");
$data = [
'test-lib' => [
'type' => 'library',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionWhenTargetMissingArtifact(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Package [test-target] in pkg.json of type 'target' must have an 'artifact' field");
$data = [
'test-target' => [
'type' => 'target',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesWithPhpExtensionFields(): void
{
$data = [
'test-ext' => [
'type' => 'php-extension',
'php-extension' => [
'zend-extension' => false,
'build-shared' => true,
],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertIsArray($data['test-ext']['php-extension']);
}
public function testValidateAndLintPackagesThrowsExceptionWhenPhpExtensionIsNotObject(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-ext [php-extension] must be an object');
$data = [
'test-ext' => [
'type' => 'php-extension',
'php-extension' => 'string',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesWithDependsField(): void
{
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => ['dep1', 'dep2'],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertIsArray($data['test-pkg']['depends']);
}
public function testValidateAndLintPackagesThrowsExceptionWhenDependsIsNotList(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-pkg [depends] must be a list');
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => 'not-a-list',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesWithSuffixFields(): void
{
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends@linux' => ['linux-dep'],
'depends@windows' => ['windows-dep'],
'headers@unix' => ['header.h'],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertIsArray($data['test-pkg']['depends@linux']);
}
public function testValidateAndLintPackagesThrowsExceptionForInvalidSuffixFieldType(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-pkg [headers@linux] must be a list');
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'headers@linux' => 'not-a-list',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionForUnknownField(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('package [test-pkg] has invalid field [unknown-field]');
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'unknown-field' => 'value',
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionForUnknownPhpExtensionField(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('php-extension [test-ext] has invalid field [unknown]');
$data = [
'test-ext' => [
'type' => 'php-extension',
'php-extension' => [
'unknown' => 'value',
],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidatePlatformStringWithValidPlatforms(): void
{
ConfigValidator::validatePlatformString('linux-x86_64');
ConfigValidator::validatePlatformString('linux-aarch64');
ConfigValidator::validatePlatformString('windows-x86_64');
ConfigValidator::validatePlatformString('windows-aarch64');
ConfigValidator::validatePlatformString('macos-x86_64');
ConfigValidator::validatePlatformString('macos-aarch64');
$this->assertTrue(true); // If no exception thrown, test passes
}
public function testValidatePlatformStringThrowsExceptionForInvalidFormat(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Invalid platform format 'invalid', expected format 'os-arch'");
ConfigValidator::validatePlatformString('invalid');
}
public function testValidatePlatformStringThrowsExceptionForTooManyParts(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Invalid platform format 'linux-x86_64-extra', expected format 'os-arch'");
ConfigValidator::validatePlatformString('linux-x86_64-extra');
}
public function testValidatePlatformStringThrowsExceptionForInvalidOS(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Invalid platform OS 'bsd' in platform 'bsd-x86_64'");
ConfigValidator::validatePlatformString('bsd-x86_64');
}
public function testValidatePlatformStringThrowsExceptionForInvalidArch(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Invalid platform architecture 'arm' in platform 'linux-arm'");
ConfigValidator::validatePlatformString('linux-arm');
}
public function testArtifactTypeFieldsConstant(): void
{
$this->assertArrayHasKey('filelist', ConfigValidator::ARTIFACT_TYPE_FIELDS);
$this->assertArrayHasKey('git', ConfigValidator::ARTIFACT_TYPE_FIELDS);
$this->assertArrayHasKey('ghtagtar', ConfigValidator::ARTIFACT_TYPE_FIELDS);
$this->assertArrayHasKey('url', ConfigValidator::ARTIFACT_TYPE_FIELDS);
$this->assertArrayHasKey('custom', ConfigValidator::ARTIFACT_TYPE_FIELDS);
}
public function testValidateAndLintArtifactsThrowsExceptionForInvalidArtifactType(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact source object has unknown type 'invalid-type'");
$data = [
'test-artifact' => [
'source' => [
'type' => 'invalid-type',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
}
public function testValidateAndLintArtifactsThrowsExceptionForMissingRequiredField(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact source object of type 'git' must have required field 'url'");
$data = [
'test-artifact' => [
'source' => [
'type' => 'git',
'rev' => 'main',
// missing 'url'
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
}
public function testValidateAndLintArtifactsThrowsExceptionForMissingTypeInSource(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact source object must have a valid 'type' field");
$data = [
'test-artifact' => [
'source' => [
'url' => 'https://example.com',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
}
public function testValidateAndLintArtifactsWithAllArtifactTypes(): void
{
$data = [
'filelist-artifact' => [
'source' => [
'type' => 'filelist',
'url' => 'https://example.com/list',
'regex' => '/pattern/',
],
],
'git-artifact' => [
'source' => [
'type' => 'git',
'url' => 'https://github.com/example/repo.git',
'rev' => 'main',
],
],
'ghtagtar-artifact' => [
'source' => [
'type' => 'ghtagtar',
'repo' => 'example/repo',
],
],
'url-artifact' => [
'source' => [
'type' => 'url',
'url' => 'https://example.com/file.tar.gz',
],
],
'custom-artifact' => [
'source' => [
'type' => 'custom',
],
],
];
ConfigValidator::validateAndLintArtifacts('test.json', $data);
$this->assertIsArray($data);
}
public function testValidateAndLintPackagesWithAllFieldTypes(): void
{
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => 'test-artifact',
'depends' => ['dep1'],
'suggests' => ['sug1'],
'license' => [
'type' => 'file',
'path' => 'LICENSE',
],
'lang' => 'c',
'frameworks' => ['framework1'],
'headers' => ['header.h'],
'static-libs' => ['lib.a'],
'pkg-configs' => ['pkg.pc'],
'static-bins' => ['bin'],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
$this->assertEquals('library', $data['test-pkg']['type']);
}
public function testValidateAndLintPackagesThrowsExceptionForWrongTypeString(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-pkg [artifact] must be string');
$data = [
'test-pkg' => [
'type' => 'library',
'artifact' => ['not', 'a', 'string'],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionForWrongTypeBool(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-ext [zend-extension] must be boolean');
$data = [
'test-ext' => [
'type' => 'php-extension',
'php-extension' => [
'zend-extension' => 'not-a-bool',
],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
public function testValidateAndLintPackagesThrowsExceptionForWrongTypeAssocArray(): void
{
$this->expectException(ValidationException::class);
$this->expectExceptionMessage('Package test-pkg [support] must be an object');
$data = [
'test-pkg' => [
'type' => 'php-extension',
'php-extension' => [
'support' => 'not-an-object',
],
],
];
ConfigValidator::validateAndLintPackages('pkg.json', $data);
}
}

View File

@@ -0,0 +1,434 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Config;
use PHPUnit\Framework\TestCase;
use StaticPHP\Config\PackageConfig;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Runtime\SystemTarget;
/**
* @internal
*/
class PackageConfigTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/package_config_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset static state
$reflection = new \ReflectionClass(PackageConfig::class);
$property = $reflection->getProperty('package_configs');
$property->setAccessible(true);
$property->setValue(null, []);
}
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset static state
$reflection = new \ReflectionClass(PackageConfig::class);
$property = $reflection->getProperty('package_configs');
$property->setAccessible(true);
$property->setValue(null, []);
}
public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void
{
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Directory /nonexistent/path does not exist, cannot load pkg.json config.');
PackageConfig::loadFromDir('/nonexistent/path');
}
public function testLoadFromDirWithValidPkgJson(): void
{
$packageContent = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test-artifact',
],
]);
file_put_contents($this->tempDir . '/pkg.json', $packageContent);
PackageConfig::loadFromDir($this->tempDir);
$this->assertTrue(PackageConfig::isPackageExists('test-pkg'));
}
public function testLoadFromDirWithMultiplePackageFiles(): void
{
$pkg1Content = json_encode([
'pkg-1' => [
'type' => 'library',
'artifact' => 'artifact-1',
],
]);
$pkg2Content = json_encode([
'pkg-2' => [
'type' => 'php-extension',
],
]);
file_put_contents($this->tempDir . '/pkg.ext.json', $pkg1Content);
file_put_contents($this->tempDir . '/pkg.lib.json', $pkg2Content);
file_put_contents($this->tempDir . '/pkg.json', json_encode(['pkg-3' => ['type' => 'virtual-target']]));
PackageConfig::loadFromDir($this->tempDir);
$this->assertTrue(PackageConfig::isPackageExists('pkg-1'));
$this->assertTrue(PackageConfig::isPackageExists('pkg-2'));
$this->assertTrue(PackageConfig::isPackageExists('pkg-3'));
}
public function testLoadFromFileThrowsExceptionWhenFileCannotBeRead(): void
{
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Failed to read package config file:');
PackageConfig::loadFromFile('/nonexistent/file.json');
}
public function testLoadFromFileThrowsExceptionWhenJsonIsInvalid(): void
{
$file = $this->tempDir . '/invalid.json';
file_put_contents($file, 'not valid json{');
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('Invalid JSON format in package config file:');
PackageConfig::loadFromFile($file);
}
public function testLoadFromFileWithValidJson(): void
{
$file = $this->tempDir . '/valid.json';
$content = json_encode([
'my-pkg' => [
'type' => 'library',
'artifact' => 'my-artifact',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$this->assertTrue(PackageConfig::isPackageExists('my-pkg'));
}
public function testIsPackageExistsReturnsFalseWhenPackageNotLoaded(): void
{
$this->assertFalse(PackageConfig::isPackageExists('non-existent'));
}
public function testIsPackageExistsReturnsTrueWhenPackageLoaded(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$this->assertTrue(PackageConfig::isPackageExists('test-pkg'));
}
public function testGetAllReturnsAllLoadedPackages(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'pkg-a' => ['type' => 'virtual-target'],
'pkg-b' => ['type' => 'virtual-target'],
'pkg-c' => ['type' => 'virtual-target'],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$all = PackageConfig::getAll();
$this->assertIsArray($all);
$this->assertCount(3, $all);
$this->assertArrayHasKey('pkg-a', $all);
$this->assertArrayHasKey('pkg-b', $all);
$this->assertArrayHasKey('pkg-c', $all);
}
public function testGetReturnsDefaultWhenPackageNotExists(): void
{
$result = PackageConfig::get('non-existent', 'field', 'default-value');
$this->assertEquals('default-value', $result);
}
public function testGetReturnsWholePackageWhenFieldNameIsNull(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => ['dep1'],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg');
$this->assertIsArray($result);
$this->assertEquals('library', $result['type']);
$this->assertEquals('test', $result['artifact']);
}
public function testGetReturnsFieldValueWhenFieldExists(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test-artifact',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg', 'artifact');
$this->assertEquals('test-artifact', $result);
}
public function testGetReturnsDefaultWhenFieldNotExists(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg', 'non-existent-field', 'default');
$this->assertEquals('default', $result);
}
public function testGetWithSuffixFieldsOnLinux(): void
{
// Mock SystemTarget to return Linux
$mockTarget = $this->getMockBuilder(SystemTarget::class)
->disableOriginalConstructor()
->getMock();
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => ['base-dep'],
'depends@linux' => ['linux-dep'],
'depends@unix' => ['unix-dep'],
'depends@windows' => ['windows-dep'],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
// The get method will check SystemTarget::getTargetOS()
// On real Linux systems, it should return 'depends@linux' first
$result = PackageConfig::get('test-pkg', 'depends', []);
// Result should be one of the suffixed versions or base version
$this->assertIsArray($result);
}
public function testGetWithSuffixFieldsReturnsBasicFieldWhenNoSuffixMatch(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends' => ['base-dep'],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg', 'depends');
$this->assertEquals(['base-dep'], $result);
}
public function testGetWithNonSuffixedFieldIgnoresSuffixes(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test-artifact',
'artifact@linux' => 'linux-artifact', // This should be ignored
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
// 'artifact' is not in SUFFIX_ALLOWED_FIELDS, so it won't check suffixes
$result = PackageConfig::get('test-pkg', 'artifact');
$this->assertEquals('test-artifact', $result);
}
public function testGetAllSuffixAllowedFields(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'library',
'artifact' => 'test',
'depends@linux' => ['dep1'],
'suggests@macos' => ['sug1'],
'headers@unix' => ['header.h'],
'static-libs@windows' => ['lib.a'],
'static-bins@linux' => ['bin'],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
// These are all suffix-allowed fields
$pkg = PackageConfig::get('test-pkg');
$this->assertArrayHasKey('depends@linux', $pkg);
$this->assertArrayHasKey('suggests@macos', $pkg);
$this->assertArrayHasKey('headers@unix', $pkg);
$this->assertArrayHasKey('static-libs@windows', $pkg);
$this->assertArrayHasKey('static-bins@linux', $pkg);
}
public function testLoadFromDirWithEmptyDirectory(): void
{
// Empty directory should not throw exception
PackageConfig::loadFromDir($this->tempDir);
$this->assertEquals([], PackageConfig::getAll());
}
public function testMultipleLoadsAppendConfigs(): void
{
$file1 = $this->tempDir . '/pkg1.json';
$file2 = $this->tempDir . '/pkg2.json';
file_put_contents($file1, json_encode(['pkg1' => ['type' => 'virtual-target']]));
file_put_contents($file2, json_encode(['pkg2' => ['type' => 'virtual-target']]));
PackageConfig::loadFromFile($file1);
PackageConfig::loadFromFile($file2);
$all = PackageConfig::getAll();
$this->assertCount(2, $all);
$this->assertArrayHasKey('pkg1', $all);
$this->assertArrayHasKey('pkg2', $all);
}
public function testGetWithComplexPhpExtensionPackage(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-ext' => [
'type' => 'php-extension',
'depends' => ['dep1'],
'php-extension' => [
'zend-extension' => false,
'build-shared' => true,
'build-static' => false,
],
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$phpExt = PackageConfig::get('test-ext', 'php-extension');
$this->assertIsArray($phpExt);
$this->assertFalse($phpExt['zend-extension']);
$this->assertTrue($phpExt['build-shared']);
}
public function testGetReturnsNullAsDefaultWhenNotSpecified(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'test-pkg' => [
'type' => 'virtual-target',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$result = PackageConfig::get('test-pkg', 'non-existent');
$this->assertNull($result);
}
public function testLoadFromFileWithAllPackageTypes(): void
{
$file = $this->tempDir . '/pkg.json';
$content = json_encode([
'library-pkg' => [
'type' => 'library',
'artifact' => 'lib-artifact',
],
'extension-pkg' => [
'type' => 'php-extension',
],
'target-pkg' => [
'type' => 'target',
'artifact' => 'target-artifact',
],
'virtual-pkg' => [
'type' => 'virtual-target',
],
]);
file_put_contents($file, $content);
PackageConfig::loadFromFile($file);
$this->assertTrue(PackageConfig::isPackageExists('library-pkg'));
$this->assertTrue(PackageConfig::isPackageExists('extension-pkg'));
$this->assertTrue(PackageConfig::isPackageExists('target-pkg'));
$this->assertTrue(PackageConfig::isPackageExists('virtual-pkg'));
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
}
rmdir($dir);
}
}

View File

@@ -0,0 +1,433 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\DI;
use DI\Container;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\DI\CallbackInvoker;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
class ApplicationContextTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
// Reset ApplicationContext state before each test
ApplicationContext::reset();
}
protected function tearDown(): void
{
parent::tearDown();
// Reset ApplicationContext state after each test
ApplicationContext::reset();
}
public function testInitializeCreatesContainer(): void
{
$container = ApplicationContext::initialize();
$this->assertInstanceOf(Container::class, $container);
$this->assertSame($container, ApplicationContext::getContainer());
}
public function testInitializeWithDebugMode(): void
{
ApplicationContext::initialize(['debug' => true]);
$this->assertTrue(ApplicationContext::isDebug());
}
public function testInitializeWithoutDebugMode(): void
{
ApplicationContext::initialize(['debug' => false]);
$this->assertFalse(ApplicationContext::isDebug());
}
public function testInitializeWithCustomDefinitions(): void
{
$customValue = 'test_value';
ApplicationContext::initialize([
'definitions' => [
'test.service' => $customValue,
],
]);
$this->assertEquals($customValue, ApplicationContext::get('test.service'));
}
public function testInitializeThrowsExceptionWhenAlreadyInitialized(): void
{
ApplicationContext::initialize();
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('ApplicationContext already initialized');
ApplicationContext::initialize();
}
public function testGetContainerAutoInitializes(): void
{
// Don't call initialize
$container = ApplicationContext::getContainer();
$this->assertInstanceOf(Container::class, $container);
}
public function testGetReturnsServiceFromContainer(): void
{
ApplicationContext::initialize([
'definitions' => [
'test.key' => 'test_value',
],
]);
$this->assertEquals('test_value', ApplicationContext::get('test.key'));
}
public function testGetWithClassType(): void
{
ApplicationContext::initialize();
$container = ApplicationContext::get(Container::class);
$this->assertInstanceOf(Container::class, $container);
}
public function testGetContainerInterface(): void
{
ApplicationContext::initialize();
$container = ApplicationContext::get(ContainerInterface::class);
$this->assertInstanceOf(ContainerInterface::class, $container);
}
public function testHasReturnsTrueForExistingService(): void
{
ApplicationContext::initialize([
'definitions' => [
'test.service' => 'value',
],
]);
$this->assertTrue(ApplicationContext::has('test.service'));
}
public function testHasReturnsFalseForNonExistingService(): void
{
ApplicationContext::initialize();
$this->assertFalse(ApplicationContext::has('non.existing.service'));
}
public function testSetAddsServiceToContainer(): void
{
ApplicationContext::initialize();
ApplicationContext::set('dynamic.service', 'dynamic_value');
$this->assertTrue(ApplicationContext::has('dynamic.service'));
$this->assertEquals('dynamic_value', ApplicationContext::get('dynamic.service'));
}
public function testSetOverridesExistingService(): void
{
ApplicationContext::initialize([
'definitions' => [
'test.service' => 'original',
],
]);
ApplicationContext::set('test.service', 'updated');
$this->assertEquals('updated', ApplicationContext::get('test.service'));
}
public function testBindCommandContextSetsInputAndOutput(): void
{
ApplicationContext::initialize();
$input = $this->createMock(InputInterface::class);
$output = $this->createMock(OutputInterface::class);
$output->method('isDebug')->willReturn(false);
ApplicationContext::bindCommandContext($input, $output);
$this->assertSame($input, ApplicationContext::get(InputInterface::class));
$this->assertSame($output, ApplicationContext::get(OutputInterface::class));
}
public function testBindCommandContextSetsDebugMode(): void
{
ApplicationContext::initialize();
$input = $this->createMock(InputInterface::class);
$output = $this->createMock(OutputInterface::class);
$output->method('isDebug')->willReturn(true);
ApplicationContext::bindCommandContext($input, $output);
$this->assertTrue(ApplicationContext::isDebug());
}
public function testBindCommandContextWithNonDebugOutput(): void
{
ApplicationContext::initialize();
$input = $this->createMock(InputInterface::class);
$output = $this->createMock(OutputInterface::class);
$output->method('isDebug')->willReturn(false);
ApplicationContext::bindCommandContext($input, $output);
$this->assertFalse(ApplicationContext::isDebug());
}
public function testGetInvokerReturnsCallbackInvoker(): void
{
ApplicationContext::initialize();
$invoker = ApplicationContext::getInvoker();
$this->assertInstanceOf(CallbackInvoker::class, $invoker);
}
public function testGetInvokerReturnsSameInstance(): void
{
ApplicationContext::initialize();
$invoker1 = ApplicationContext::getInvoker();
$invoker2 = ApplicationContext::getInvoker();
$this->assertSame($invoker1, $invoker2);
}
public function testGetInvokerAutoInitializesContainer(): void
{
// Don't call initialize
$invoker = ApplicationContext::getInvoker();
$this->assertInstanceOf(CallbackInvoker::class, $invoker);
}
public function testInvokeCallsCallback(): void
{
ApplicationContext::initialize();
$called = false;
$callback = function () use (&$called) {
$called = true;
return 'result';
};
$result = ApplicationContext::invoke($callback);
$this->assertTrue($called);
$this->assertEquals('result', $result);
}
public function testInvokeWithContext(): void
{
ApplicationContext::initialize();
$callback = function (string $param) {
return $param;
};
$result = ApplicationContext::invoke($callback, ['param' => 'test_value']);
$this->assertEquals('test_value', $result);
}
public function testInvokeWithDependencyInjection(): void
{
ApplicationContext::initialize();
$callback = function (Container $container) {
return $container;
};
$result = ApplicationContext::invoke($callback);
$this->assertInstanceOf(Container::class, $result);
}
public function testInvokeWithArrayCallback(): void
{
ApplicationContext::initialize();
$object = new class {
public function method(): string
{
return 'called';
}
};
$result = ApplicationContext::invoke([$object, 'method']);
$this->assertEquals('called', $result);
}
public function testIsDebugDefaultsFalse(): void
{
ApplicationContext::initialize();
$this->assertFalse(ApplicationContext::isDebug());
}
public function testSetDebugChangesDebugMode(): void
{
ApplicationContext::initialize();
ApplicationContext::setDebug(true);
$this->assertTrue(ApplicationContext::isDebug());
ApplicationContext::setDebug(false);
$this->assertFalse(ApplicationContext::isDebug());
}
public function testResetClearsContainer(): void
{
ApplicationContext::initialize();
ApplicationContext::set('test.service', 'value');
ApplicationContext::reset();
// After reset, container should be reinitialized
$this->assertFalse(ApplicationContext::has('test.service'));
}
public function testResetClearsInvoker(): void
{
ApplicationContext::initialize();
$invoker1 = ApplicationContext::getInvoker();
ApplicationContext::reset();
$invoker2 = ApplicationContext::getInvoker();
$this->assertNotSame($invoker1, $invoker2);
}
public function testResetClearsDebugMode(): void
{
ApplicationContext::initialize(['debug' => true]);
$this->assertTrue(ApplicationContext::isDebug());
ApplicationContext::reset();
// After reset and reinit, debug should be false by default
ApplicationContext::initialize();
$this->assertFalse(ApplicationContext::isDebug());
}
public function testResetAllowsReinitialize(): void
{
ApplicationContext::initialize();
ApplicationContext::reset();
// Should not throw exception
$container = ApplicationContext::initialize(['debug' => true]);
$this->assertInstanceOf(Container::class, $container);
$this->assertTrue(ApplicationContext::isDebug());
}
public function testCallbackInvokerIsAvailableInContainer(): void
{
ApplicationContext::initialize();
$invoker = ApplicationContext::get(CallbackInvoker::class);
$this->assertInstanceOf(CallbackInvoker::class, $invoker);
}
public function testMultipleGetCallsReturnSameContainer(): void
{
$container1 = ApplicationContext::getContainer();
$container2 = ApplicationContext::getContainer();
$this->assertSame($container1, $container2);
}
public function testInitializeWithEmptyOptions(): void
{
$container = ApplicationContext::initialize([]);
$this->assertInstanceOf(Container::class, $container);
$this->assertFalse(ApplicationContext::isDebug());
}
public function testInitializeWithNullDefinitions(): void
{
$container = ApplicationContext::initialize(['definitions' => null]);
$this->assertInstanceOf(Container::class, $container);
}
public function testInitializeWithEmptyDefinitions(): void
{
$container = ApplicationContext::initialize(['definitions' => []]);
$this->assertInstanceOf(Container::class, $container);
}
public function testSetBeforeInitializeAutoInitializes(): void
{
// Don't call initialize
ApplicationContext::set('test.service', 'value');
$this->assertEquals('value', ApplicationContext::get('test.service'));
}
public function testHasBeforeInitializeAutoInitializes(): void
{
// Don't call initialize, should auto-initialize
$result = ApplicationContext::has(Container::class);
$this->assertTrue($result);
}
public function testGetBeforeInitializeAutoInitializes(): void
{
// Don't call initialize
$container = ApplicationContext::get(Container::class);
$this->assertInstanceOf(Container::class, $container);
}
public function testInvokerSingletonConsistency(): void
{
// Test fix for issue #3 and #4 - Invoker instance consistency
ApplicationContext::initialize();
$invoker1 = ApplicationContext::getInvoker();
$invoker2 = ApplicationContext::get(CallbackInvoker::class);
// Both should return the same instance
$this->assertSame($invoker1, $invoker2);
}
public function testInvokerSingletonConsistencyAfterReset(): void
{
ApplicationContext::initialize();
$invoker1 = ApplicationContext::getInvoker();
ApplicationContext::reset();
ApplicationContext::initialize();
$invoker2 = ApplicationContext::getInvoker();
$invoker3 = ApplicationContext::get(CallbackInvoker::class);
// After reset, should be new instance
$this->assertNotSame($invoker1, $invoker2);
// But getInvoker() and container should still be consistent
$this->assertSame($invoker2, $invoker3);
}
}

View File

@@ -0,0 +1,629 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\DI;
use DI\Container;
use PHPUnit\Framework\TestCase;
use StaticPHP\DI\CallbackInvoker;
use StaticPHP\Exception\SPCInternalException;
/**
* Helper class that requires constructor parameters for testing
*/
readonly class UnresolvableTestClass
{
/** @noinspection PhpPropertyOnlyWrittenInspection */
public function __construct(private string $requiredParam) {}
}
/**
* @internal
*/
class CallbackInvokerTest extends TestCase
{
private Container $container;
private CallbackInvoker $invoker;
protected function setUp(): void
{
parent::setUp();
$this->container = new Container();
$this->invoker = new CallbackInvoker($this->container);
}
public function testInvokeSimpleCallbackWithoutParameters(): void
{
$callback = function () {
return 'result';
};
$result = $this->invoker->invoke($callback);
$this->assertEquals('result', $result);
}
public function testInvokeCallbackWithContextByTypeName(): void
{
$callback = function (string $param) {
return $param;
};
$result = $this->invoker->invoke($callback, ['string' => 'test_value']);
$this->assertEquals('test_value', $result);
}
public function testInvokeCallbackWithContextByParameterName(): void
{
$callback = function (string $myParam) {
return $myParam;
};
$result = $this->invoker->invoke($callback, ['myParam' => 'test_value']);
$this->assertEquals('test_value', $result);
}
public function testInvokeCallbackWithContextByTypeNameTakesPrecedence(): void
{
$callback = function (string $myParam) {
return $myParam;
};
// Type name should take precedence over parameter name
$result = $this->invoker->invoke($callback, [
'string' => 'by_type',
'myParam' => 'by_name',
]);
$this->assertEquals('by_type', $result);
}
public function testInvokeCallbackWithContainerResolution(): void
{
$this->container->set('test.service', 'service_value');
$callback = function (string $testService) {
return $testService;
};
// Should not resolve from container as 'test.service' is not a type
// Will try default value or null
$this->expectException(SPCInternalException::class);
$this->invoker->invoke($callback);
}
public function testInvokeCallbackWithClassTypeFromContainer(): void
{
$testObject = new \stdClass();
$testObject->value = 'test';
$this->container->set(\stdClass::class, $testObject);
$callback = function (\stdClass $obj) {
return $obj->value;
};
$result = $this->invoker->invoke($callback);
$this->assertEquals('test', $result);
}
public function testInvokeCallbackWithDefaultValue(): void
{
$callback = function (string $param = 'default_value') {
return $param;
};
$result = $this->invoker->invoke($callback);
$this->assertEquals('default_value', $result);
}
public function testInvokeCallbackWithNullableParameter(): void
{
$callback = function (?string $param) {
return $param ?? 'was_null';
};
$result = $this->invoker->invoke($callback);
$this->assertEquals('was_null', $result);
}
public function testInvokeCallbackThrowsExceptionForUnresolvableParameter(): void
{
$callback = function (string $required) {
return $required;
};
$this->expectException(SPCInternalException::class);
$this->expectExceptionMessage("Cannot resolve parameter 'required' of type 'string'");
$this->invoker->invoke($callback);
}
public function testInvokeCallbackThrowsExceptionForNonExistentClass(): void
{
// This test uses UnresolvableTestClass which has required constructor params
// Container.has() will return true but get() will throw InvalidDefinition
// So we test that container exceptions bubble up
$callback = function (UnresolvableTestClass $obj) {
return $obj;
};
$this->expectException(\Throwable::class);
$this->invoker->invoke($callback);
}
public function testInvokeCallbackWithMultipleParameters(): void
{
$callback = function (string $first, int $second, bool $third) {
return [$first, $second, $third];
};
$result = $this->invoker->invoke($callback, [
'first' => 'value1',
'second' => 42,
'third' => true,
]);
$this->assertEquals(['value1', 42, true], $result);
}
public function testInvokeCallbackWithMixedResolutionSources(): void
{
$this->container->set(\stdClass::class, new \stdClass());
$callback = function (
\stdClass $fromContainer,
string $fromContext,
int $withDefault = 100
) {
return [$fromContainer, $fromContext, $withDefault];
};
$result = $this->invoker->invoke($callback, ['fromContext' => 'context_value']);
$this->assertInstanceOf(\stdClass::class, $result[0]);
$this->assertEquals('context_value', $result[1]);
$this->assertEquals(100, $result[2]);
}
public function testExpandContextHierarchyWithObject(): void
{
// Create a simple parent-child relationship
$childClass = new \ArrayObject(['key' => 'value']);
$callback = function (\ArrayObject $obj) {
return $obj;
};
$result = $this->invoker->invoke($callback, [get_class($childClass) => $childClass]);
$this->assertSame($childClass, $result);
}
public function testExpandContextHierarchyWithInterface(): void
{
$object = new class implements \Countable {
public function count(): int
{
return 42;
}
};
$callback = function (\Countable $countable) {
return $countable->count();
};
$result = $this->invoker->invoke($callback, [get_class($object) => $object]);
$this->assertEquals(42, $result);
}
public function testExpandContextHierarchyWithMultipleInterfaces(): void
{
$object = new class implements \Countable, \IteratorAggregate {
public function count(): int
{
return 5;
}
public function getIterator(): \Traversable
{
return new \ArrayIterator([]);
}
};
$callback = function (\Countable $c, \IteratorAggregate $i) {
return [$c->count(), $i];
};
$result = $this->invoker->invoke($callback, ['obj' => $object]);
$this->assertEquals(5, $result[0]);
$this->assertInstanceOf(\IteratorAggregate::class, $result[1]);
}
public function testInvokeWithArrayCallback(): void
{
$testClass = new class {
public function method(string $param): string
{
return 'called_' . $param;
}
};
$result = $this->invoker->invoke([$testClass, 'method'], ['param' => 'test']);
$this->assertEquals('called_test', $result);
}
public function testInvokeWithStaticMethod(): void
{
$testClass = new class {
public static function staticMethod(string $param): string
{
return 'static_' . $param;
}
};
$className = get_class($testClass);
$result = $this->invoker->invoke([$className, 'staticMethod'], ['param' => 'value']);
$this->assertEquals('static_value', $result);
}
public function testInvokeWithCallableString(): void
{
$callback = 'Tests\StaticPHP\DI\testFunction';
if (!function_exists($callback)) {
eval('namespace Tests\StaticPHP\DI; function testFunction(string $param) { return "func_" . $param; }');
}
$result = $this->invoker->invoke($callback, ['param' => 'test']);
$this->assertEquals('func_test', $result);
}
public function testInvokeWithNoTypeHintedParameter(): void
{
$callback = function ($param) {
return $param;
};
$result = $this->invoker->invoke($callback, ['param' => 'value']);
$this->assertEquals('value', $result);
}
public function testInvokeWithNoTypeHintedParameterReturnsNull(): void
{
// Parameters without type hints are implicitly nullable in PHP
$callback = function ($param) {
return $param;
};
$result = $this->invoker->invoke($callback);
$this->assertNull($result);
}
public function testInvokeWithNoTypeHintAndValueInContext(): void
{
$callback = function ($param) {
return $param;
};
$result = $this->invoker->invoke($callback, ['param' => 'value']);
$this->assertEquals('value', $result);
}
public function testInvokeWithBuiltinTypes(): void
{
$callback = function (
string $str,
int $num,
float $decimal,
bool $flag,
array $arr
) {
return compact('str', 'num', 'decimal', 'flag', 'arr');
};
$result = $this->invoker->invoke($callback, [
'str' => 'test',
'num' => 42,
'decimal' => 3.14,
'flag' => true,
'arr' => [1, 2, 3],
]);
$this->assertEquals([
'str' => 'test',
'num' => 42,
'decimal' => 3.14,
'flag' => true,
'arr' => [1, 2, 3],
], $result);
}
public function testInvokeWithEmptyContext(): void
{
$callback = function () {
return 'no_params';
};
$result = $this->invoker->invoke($callback, []);
$this->assertEquals('no_params', $result);
}
public function testInvokePreservesCallbackReturnValue(): void
{
$callback = function () {
return ['key' => 'value', 'number' => 123];
};
$result = $this->invoker->invoke($callback);
$this->assertEquals(['key' => 'value', 'number' => 123], $result);
}
public function testInvokeWithNullReturnValue(): void
{
$callback = function () {
return null;
};
$result = $this->invoker->invoke($callback);
$this->assertNull($result);
}
public function testInvokeWithObjectInContext(): void
{
$obj = new \stdClass();
$obj->value = 'test';
$callback = function (\stdClass $param) {
return $param->value;
};
$result = $this->invoker->invoke($callback, ['param' => $obj]);
$this->assertEquals('test', $result);
}
public function testInvokeWithInheritanceInContext(): void
{
$exception = new \RuntimeException('test message');
$callback = function (\Exception $e) {
return $e->getMessage();
};
// RuntimeException should be resolved as Exception via hierarchy expansion
$result = $this->invoker->invoke($callback, ['exc' => $exception]);
$this->assertEquals('test message', $result);
}
public function testInvokeContextValueOverridesContainer(): void
{
$containerObj = new \stdClass();
$containerObj->source = 'container';
$this->container->set(\stdClass::class, $containerObj);
$contextObj = new \stdClass();
$contextObj->source = 'context';
$callback = function (\stdClass $obj) {
return $obj->source;
};
// Context should override container
$result = $this->invoker->invoke($callback, [\stdClass::class => $contextObj]);
$this->assertEquals('context', $result);
}
public function testInvokeWithDefaultValueNotUsedWhenContextProvided(): void
{
$callback = function (string $param = 'default') {
return $param;
};
$result = $this->invoker->invoke($callback, ['param' => 'from_context']);
$this->assertEquals('from_context', $result);
}
public function testInvokeWithMixedNullableAndRequired(): void
{
$callback = function (string $required, ?string $optional) {
return [$required, $optional];
};
$result = $this->invoker->invoke($callback, ['required' => 'value']);
$this->assertEquals(['value', null], $result);
}
public function testInvokeWithComplexObjectHierarchy(): void
{
// Use built-in PHP classes with inheritance
// ArrayIterator extends IteratorIterator implements ArrayAccess, SeekableIterator, Countable, Serializable
$arrayIterator = new \ArrayIterator(['test' => 'value']);
// Test that the object can be resolved via interface (Countable)
$callback1 = function (\Countable $test) {
return $test->count();
};
$result1 = $this->invoker->invoke($callback1, ['obj' => $arrayIterator]);
$this->assertEquals(1, $result1);
// Test that the object can be resolved via another interface (Iterator)
$callback2 = function (\Iterator $test) {
return $test;
};
$result2 = $this->invoker->invoke($callback2, ['obj' => $arrayIterator]);
$this->assertInstanceOf(\ArrayIterator::class, $result2);
// Test that the object can be resolved via concrete class
$callback3 = function (\ArrayIterator $test) {
return $test;
};
$result3 = $this->invoker->invoke($callback3, ['obj' => $arrayIterator]);
$this->assertSame($arrayIterator, $result3);
}
public function testInvokeWithNonObjectContextValues(): void
{
$callback = function (string $str, int $num, array $arr, bool $flag) {
return compact('str', 'num', 'arr', 'flag');
};
$context = [
'str' => 'hello',
'num' => 999,
'arr' => ['a', 'b'],
'flag' => false,
];
$result = $this->invoker->invoke($callback, $context);
$this->assertEquals($context, $result);
}
public function testInvokeParameterOrderMatters(): void
{
$callback = function (string $first, string $second, string $third) {
return [$first, $second, $third];
};
$result = $this->invoker->invoke($callback, [
'first' => 'A',
'second' => 'B',
'third' => 'C',
]);
$this->assertEquals(['A', 'B', 'C'], $result);
}
public function testInvokeWithUnionTypeThrowsException(): void
{
if (PHP_VERSION_ID < 80000) {
$this->markTestSkipped('Union types require PHP 8.0+');
}
$callback = eval('return function (string|int $param) { return $param; };');
// Union types are not ReflectionNamedType, should not be resolved from container
$this->expectException(SPCInternalException::class);
$this->invoker->invoke($callback);
}
public function testInvokeWithCallableType(): void
{
$callback = function (callable $fn) {
return $fn();
};
$result = $this->invoker->invoke($callback, [
'fn' => fn () => 'called',
]);
$this->assertEquals('called', $result);
}
public function testInvokeWithIterableType(): void
{
$callback = function (iterable $items) {
$result = [];
foreach ($items as $item) {
$result[] = $item;
}
return $result;
};
$result = $this->invoker->invoke($callback, [
'items' => [1, 2, 3],
]);
$this->assertEquals([1, 2, 3], $result);
}
public function testInvokeWithObjectType(): void
{
$callback = function (object $obj) {
return get_class($obj);
};
$testObj = new \stdClass();
$result = $this->invoker->invoke($callback, ['obj' => $testObj]);
$this->assertEquals('stdClass', $result);
}
public function testInvokeWithContainerExceptionFallsThrough(): void
{
// Test fix for issue #1 - Container exceptions should be caught
// and fall through to other resolution strategies
$callback = function (?UnresolvableTestClass $obj = null) {
return $obj;
};
// Should use default value (null) instead of throwing container exception
$result = $this->invoker->invoke($callback);
$this->assertNull($result);
}
public function testInvokeWithContainerExceptionAndNoFallback(): void
{
// When there's no fallback (no default, not nullable), should throw RuntimeException
$callback = function (UnresolvableTestClass $obj) {
return $obj;
};
$this->expectException(SPCInternalException::class);
$this->expectExceptionMessage("Cannot resolve parameter 'obj'");
$this->invoker->invoke($callback);
}
public function testExpandContextHierarchyPerformance(): void
{
// Test fix for issue #2 - Should not create duplicate ReflectionClass
// This is more of a code quality test, ensuring the fix doesn't break functionality
$obj = new \ArrayIterator(['a', 'b', 'c']);
$callback = function (
\ArrayIterator $asArrayIterator,
\Traversable $asTraversable,
\Countable $asCountable
) {
return [
get_class($asArrayIterator),
get_class($asTraversable),
get_class($asCountable),
];
};
$result = $this->invoker->invoke($callback, ['obj' => $obj]);
$this->assertEquals([
'ArrayIterator',
'ArrayIterator',
'ArrayIterator',
], $result);
}
}

View File

@@ -0,0 +1,440 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Registry;
use PHPUnit\Framework\TestCase;
use StaticPHP\Artifact\Artifact;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\AfterSourceExtract;
use StaticPHP\Attribute\Artifact\BinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Attribute\Artifact\CustomSource;
use StaticPHP\Attribute\Artifact\SourceExtract;
use StaticPHP\Config\ArtifactConfig;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Registry\ArtifactLoader;
/**
* @internal
*/
class ArtifactLoaderTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/artifact_loader_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset ArtifactLoader and ArtifactConfig state
$reflection = new \ReflectionClass(ArtifactLoader::class);
$property = $reflection->getProperty('artifacts');
$property->setAccessible(true);
$property->setValue(null, null);
$configReflection = new \ReflectionClass(ArtifactConfig::class);
$configProperty = $configReflection->getProperty('artifact_configs');
$configProperty->setAccessible(true);
$configProperty->setValue(null, []);
}
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset ArtifactLoader and ArtifactConfig state
$reflection = new \ReflectionClass(ArtifactLoader::class);
$property = $reflection->getProperty('artifacts');
$property->setAccessible(true);
$property->setValue(null, null);
$configReflection = new \ReflectionClass(ArtifactConfig::class);
$configProperty = $configReflection->getProperty('artifact_configs');
$configProperty->setAccessible(true);
$configProperty->setValue(null, []);
}
public function testInitArtifactInstancesOnlyRunsOnce(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
ArtifactLoader::initArtifactInstances();
// Should only initialize once
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertInstanceOf(Artifact::class, $artifact);
}
public function testGetArtifactInstanceReturnsNullForNonExistent(): void
{
ArtifactLoader::initArtifactInstances();
$artifact = ArtifactLoader::getArtifactInstance('non-existent-artifact');
$this->assertNull($artifact);
}
public function testGetArtifactInstanceReturnsArtifact(): void
{
$this->createTestArtifactConfig('test-artifact');
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertInstanceOf(Artifact::class, $artifact);
}
public function testLoadFromClassWithCustomSourceAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomSource('test-artifact')]
public function customSource(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidCustomSourceArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomSource('non-existent-artifact')]
public function customSource(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[CustomSource]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithCustomBinaryAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomBinary('test-artifact', ['linux-x86_64', 'macos-aarch64'])]
public function customBinary(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidCustomBinaryArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomBinary('non-existent-artifact', ['linux-x86_64'])]
public function customBinary(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[CustomBinary]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithSourceExtractAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[SourceExtract('test-artifact')]
public function sourceExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidSourceExtractArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[SourceExtract('non-existent-artifact')]
public function sourceExtract(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[SourceExtract]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithBinaryExtractAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[BinaryExtract('test-artifact')]
public function binaryExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassWithBinaryExtractAttributeAndPlatforms(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[BinaryExtract('test-artifact', platforms: ['linux-x86_64', 'darwin-aarch64'])]
public function binaryExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidBinaryExtractArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[BinaryExtract('non-existent-artifact')]
public function binaryExtract(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[BinaryExtract]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithAfterSourceExtractAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterSourceExtract('test-artifact')]
public function afterSourceExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidAfterSourceExtractArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterSourceExtract('non-existent-artifact')]
public function afterSourceExtract(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[AfterSourceExtract]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithAfterBinaryExtractAttribute(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterBinaryExtract('test-artifact')]
public function afterBinaryExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassWithAfterBinaryExtractAttributeAndPlatforms(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterBinaryExtract('test-artifact', platforms: ['linux-x86_64'])]
public function afterBinaryExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassThrowsExceptionForInvalidAfterBinaryExtractArtifact(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
#[AfterBinaryExtract('non-existent-artifact')]
public function afterBinaryExtract(): void {}
};
$this->expectException(ValidationException::class);
$this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[AfterBinaryExtract]");
ArtifactLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassWithMultipleAttributes(): void
{
$this->createTestArtifactConfig('test-artifact-1');
$this->createTestArtifactConfig('test-artifact-2');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomSource('test-artifact-1')]
public function customSource(): void {}
#[CustomBinary('test-artifact-2', ['linux-x86_64'])]
public function customBinary(): void {}
#[SourceExtract('test-artifact-1')]
public function sourceExtract(): void {}
#[AfterSourceExtract('test-artifact-2')]
public function afterSourceExtract(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
$artifact1 = ArtifactLoader::getArtifactInstance('test-artifact-1');
$artifact2 = ArtifactLoader::getArtifactInstance('test-artifact-2');
$this->assertNotNull($artifact1);
$this->assertNotNull($artifact2);
}
public function testLoadFromClassIgnoresNonPublicMethods(): void
{
$this->createTestArtifactConfig('test-artifact');
ArtifactLoader::initArtifactInstances();
$class = new class {
#[CustomSource('test-artifact')]
public function publicCustomSource(): void {}
#[CustomSource('test-artifact')]
private function privateCustomSource(): void {}
#[CustomSource('test-artifact')]
protected function protectedCustomSource(): void {}
};
// Should only process public method
ArtifactLoader::loadFromClass(get_class($class));
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromPsr4DirLoadsAllClasses(): void
{
$this->createTestArtifactConfig('test-artifact');
// Create a PSR-4 directory structure
$psr4Dir = $this->tempDir . '/ArtifactClasses';
mkdir($psr4Dir, 0755, true);
// Create test class file
$classContent = '<?php
namespace Test\Artifact;
use StaticPHP\Attribute\Artifact\CustomSource;
class TestArtifact1 {
#[CustomSource("test-artifact")]
public function customSource() {}
}';
file_put_contents($psr4Dir . '/TestArtifact1.php', $classContent);
// Load with auto_require enabled
ArtifactLoader::loadFromPsr4Dir($psr4Dir, 'Test\Artifact', true);
$artifact = ArtifactLoader::getArtifactInstance('test-artifact');
$this->assertNotNull($artifact);
}
public function testLoadFromClassWithNoAttributes(): void
{
ArtifactLoader::initArtifactInstances();
$class = new class {
public function regularMethod(): void {}
};
// Should not throw exception
ArtifactLoader::loadFromClass(get_class($class));
// Verify no side effects
$this->assertTrue(true);
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
private function createTestArtifactConfig(string $name): void
{
$reflection = new \ReflectionClass(ArtifactConfig::class);
$property = $reflection->getProperty('artifact_configs');
$property->setAccessible(true);
$configs = $property->getValue();
$configs[$name] = [
'type' => 'source',
'url' => 'https://example.com/test.tar.gz',
];
$property->setValue(null, $configs);
}
}

View File

@@ -0,0 +1,374 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Registry;
use PHPUnit\Framework\TestCase;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;
use StaticPHP\Attribute\Doctor\OptionalCheck;
use StaticPHP\Registry\DoctorLoader;
/**
* @internal
*/
class DoctorLoaderTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/doctor_loader_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset DoctorLoader state
$reflection = new \ReflectionClass(DoctorLoader::class);
$property = $reflection->getProperty('doctor_items');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('fix_items');
$property->setAccessible(true);
$property->setValue(null, []);
}
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset DoctorLoader state
$reflection = new \ReflectionClass(DoctorLoader::class);
$property = $reflection->getProperty('doctor_items');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('fix_items');
$property->setAccessible(true);
$property->setValue(null, []);
}
public function testGetDoctorItemsReturnsEmptyArrayInitially(): void
{
$this->assertEmpty(DoctorLoader::getDoctorItems());
}
public function testLoadFromClassWithCheckItemAttribute(): void
{
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(1, $items);
$this->assertInstanceOf(CheckItem::class, $items[0][0]);
$this->assertEquals('test-check', $items[0][0]->item_name);
$this->assertEquals(1, $items[0][0]->level);
}
public function testLoadFromClassWithMultipleCheckItems(): void
{
$class = new class {
#[CheckItem('check-1', level: 2)]
public function check1(): void {}
#[CheckItem('check-2', level: 1)]
public function check2(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(2, $items);
}
public function testLoadFromClassSortsByLevelDescending(): void
{
$class = new class {
#[CheckItem('low-priority', level: 1)]
public function lowCheck(): void {}
#[CheckItem('high-priority', level: 5)]
public function highCheck(): void {}
#[CheckItem('medium-priority', level: 3)]
public function mediumCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(3, $items);
// Should be sorted by level descending: 5, 3, 1
$this->assertEquals(5, $items[0][0]->level);
$this->assertEquals(3, $items[1][0]->level);
$this->assertEquals(1, $items[2][0]->level);
}
public function testLoadFromClassWithoutSorting(): void
{
$class = new class {
#[CheckItem('check-1', level: 1)]
public function check1(): void {}
#[CheckItem('check-2', level: 5)]
public function check2(): void {}
};
DoctorLoader::loadFromClass(get_class($class), false);
$items = DoctorLoader::getDoctorItems();
// Without sorting, items should be in order they were added
$this->assertCount(2, $items);
}
public function testLoadFromClassWithFixItemAttribute(): void
{
$class = new class {
#[FixItem('test-fix')]
public function testFix(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$fixItem = DoctorLoader::getFixItem('test-fix');
$this->assertNotNull($fixItem);
$this->assertTrue(is_callable($fixItem));
}
public function testLoadFromClassWithMultipleFixItems(): void
{
$class = new class {
#[FixItem('fix-1')]
public function fix1(): void {}
#[FixItem('fix-2')]
public function fix2(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$this->assertNotNull(DoctorLoader::getFixItem('fix-1'));
$this->assertNotNull(DoctorLoader::getFixItem('fix-2'));
}
public function testGetFixItemReturnsNullForNonExistent(): void
{
$this->assertNull(DoctorLoader::getFixItem('non-existent-fix'));
}
public function testLoadFromClassWithOptionalCheckOnClass(): void
{
// Note: OptionalCheck expects an array, not a callable directly
// This test verifies the structure even though we can't easily test with anonymous classes
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(1, $items);
// Second element is the optional check callback (null if not set)
$this->assertIsArray($items[0]);
}
public function testLoadFromClassWithOptionalCheckOnMethod(): void
{
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(1, $items);
}
public function testLoadFromClassSetsCallbackCorrectly(): void
{
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): string
{
return 'test-result';
}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(1, $items);
// Test that the callback is set correctly
$callback = $items[0][0]->callback;
$this->assertIsCallable($callback);
$this->assertEquals('test-result', call_user_func($callback));
}
public function testLoadFromClassWithBothCheckAndFixItems(): void
{
$class = new class {
#[CheckItem('test-check', level: 1)]
public function testCheck(): void {}
#[FixItem('test-fix')]
public function testFix(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$checkItems = DoctorLoader::getDoctorItems();
$this->assertCount(1, $checkItems);
$fixItem = DoctorLoader::getFixItem('test-fix');
$this->assertNotNull($fixItem);
}
public function testLoadFromClassMultipleTimesAccumulatesItems(): void
{
$class1 = new class {
#[CheckItem('check-1', level: 1)]
public function check1(): void {}
};
$class2 = new class {
#[CheckItem('check-2', level: 2)]
public function check2(): void {}
};
DoctorLoader::loadFromClass(get_class($class1));
DoctorLoader::loadFromClass(get_class($class2));
$items = DoctorLoader::getDoctorItems();
$this->assertCount(2, $items);
}
public function testLoadFromPsr4DirLoadsAllClasses(): void
{
// Create a PSR-4 directory structure
$psr4Dir = $this->tempDir . '/DoctorClasses';
mkdir($psr4Dir, 0755, true);
// Create test class file 1
$classContent1 = '<?php
namespace Test\Doctor;
use StaticPHP\Attribute\Doctor\CheckItem;
class TestDoctor1 {
#[CheckItem("psr4-check-1", level: 1)]
public function check1() {}
}';
file_put_contents($psr4Dir . '/TestDoctor1.php', $classContent1);
// Create test class file 2
$classContent2 = '<?php
namespace Test\Doctor;
use StaticPHP\Attribute\Doctor\CheckItem;
class TestDoctor2 {
#[CheckItem("psr4-check-2", level: 2)]
public function check2() {}
}';
file_put_contents($psr4Dir . '/TestDoctor2.php', $classContent2);
// Load with auto_require enabled
DoctorLoader::loadFromPsr4Dir($psr4Dir, 'Test\Doctor', true);
$items = DoctorLoader::getDoctorItems();
// Should have loaded both classes and sorted by level
$this->assertGreaterThanOrEqual(0, count($items));
}
public function testLoadFromPsr4DirSortsItemsByLevel(): void
{
// Create a PSR-4 directory structure
$psr4Dir = $this->tempDir . '/DoctorClasses';
mkdir($psr4Dir, 0755, true);
$classContent = '<?php
namespace Test\Doctor;
use StaticPHP\Attribute\Doctor\CheckItem;
class MultiLevelDoctor {
#[CheckItem("low", level: 1)]
public function lowPriority() {}
#[CheckItem("high", level: 10)]
public function highPriority() {}
}';
file_put_contents($psr4Dir . '/MultiLevelDoctor.php', $classContent);
DoctorLoader::loadFromPsr4Dir($psr4Dir, 'Test\Doctor', true);
$items = DoctorLoader::getDoctorItems();
// Items should be sorted by level descending
if (count($items) >= 2) {
$this->assertGreaterThanOrEqual($items[1][0]->level, $items[0][0]->level);
}
}
public function testLoadFromClassIgnoresNonPublicMethods(): void
{
$class = new class {
#[CheckItem('public-check', level: 1)]
public function publicCheck(): void {}
#[CheckItem('private-check', level: 1)]
private function privateCheck(): void {}
#[CheckItem('protected-check', level: 1)]
protected function protectedCheck(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
$items = DoctorLoader::getDoctorItems();
// Should only load public methods
$this->assertCount(1, $items);
$this->assertEquals('public-check', $items[0][0]->item_name);
}
public function testLoadFromClassWithNoAttributes(): void
{
$class = new class {
public function regularMethod(): void {}
};
DoctorLoader::loadFromClass(get_class($class));
// Should not add any items
$items = DoctorLoader::getDoctorItems();
$this->assertEmpty($items);
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
}

View File

@@ -0,0 +1,550 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Registry;
use PHPUnit\Framework\TestCase;
use StaticPHP\Attribute\Package\Extension;
use StaticPHP\Attribute\Package\Library;
use StaticPHP\Attribute\Package\Target;
use StaticPHP\Config\PackageConfig;
use StaticPHP\Exception\RegistryException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\PhpExtensionPackage;
use StaticPHP\Package\TargetPackage;
use StaticPHP\Registry\PackageLoader;
/**
* @internal
*/
class PackageLoaderTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/package_loader_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset PackageLoader state
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('packages');
$property->setAccessible(true);
$property->setValue(null, null);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('after_stages');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('loaded_classes');
$property->setAccessible(true);
$property->setValue(null, []);
// Reset PackageConfig state
$configReflection = new \ReflectionClass(PackageConfig::class);
$configProperty = $configReflection->getProperty('package_configs');
$configProperty->setAccessible(true);
$configProperty->setValue(null, []);
}
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset PackageLoader state
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('packages');
$property->setAccessible(true);
$property->setValue(null, null);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('after_stages');
$property->setAccessible(true);
$property->setValue(null, []);
$property = $reflection->getProperty('loaded_classes');
$property->setAccessible(true);
$property->setValue(null, []);
// Reset PackageConfig state
$configReflection = new \ReflectionClass(PackageConfig::class);
$configProperty = $configReflection->getProperty('package_configs');
$configProperty->setAccessible(true);
$configProperty->setValue(null, []);
}
public function testInitPackageInstancesOnlyRunsOnce(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
PackageLoader::initPackageInstances();
// Should only initialize once
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
public function testInitPackageInstancesCreatesLibraryPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-lib');
$this->assertInstanceOf(LibraryPackage::class, $package);
}
public function testInitPackageInstancesCreatesPhpExtensionPackage(): void
{
$this->createTestPackageConfig('test-ext', 'php-extension');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-ext');
$this->assertInstanceOf(PhpExtensionPackage::class, $package);
}
public function testInitPackageInstancesCreatesTargetPackage(): void
{
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-target');
$this->assertInstanceOf(TargetPackage::class, $package);
}
public function testInitPackageInstancesCreatesVirtualTargetPackage(): void
{
$this->createTestPackageConfig('test-virtual-target', 'virtual-target');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-virtual-target');
$this->assertInstanceOf(TargetPackage::class, $package);
}
public function testInitPackageInstancesThrowsExceptionForUnknownType(): void
{
$this->createTestPackageConfig('test-unknown', 'unknown-type');
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('has unknown type');
PackageLoader::initPackageInstances();
}
public function testHasPackageReturnsTrueForExistingPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
public function testHasPackageReturnsFalseForNonExistingPackage(): void
{
PackageLoader::initPackageInstances();
$this->assertFalse(PackageLoader::hasPackage('non-existent'));
}
public function testGetPackageReturnsPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-lib');
$this->assertInstanceOf(LibraryPackage::class, $package);
}
public function testGetPackageThrowsExceptionForNonExistingPackage(): void
{
PackageLoader::initPackageInstances();
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('not found');
PackageLoader::getPackage('non-existent');
}
public function testGetTargetPackageReturnsTargetPackage(): void
{
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$package = PackageLoader::getTargetPackage('test-target');
$this->assertInstanceOf(TargetPackage::class, $package);
}
public function testGetTargetPackageThrowsExceptionForNonTargetPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('is not a TargetPackage');
PackageLoader::getTargetPackage('test-lib');
}
public function testGetLibraryPackageReturnsLibraryPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$package = PackageLoader::getLibraryPackage('test-lib');
$this->assertInstanceOf(LibraryPackage::class, $package);
}
public function testGetLibraryPackageThrowsExceptionForNonLibraryPackage(): void
{
$this->createTestPackageConfig('ext-test-ext', 'php-extension');
PackageLoader::initPackageInstances();
$this->expectException(WrongUsageException::class);
$this->expectExceptionMessage('is not a LibraryPackage');
PackageLoader::getLibraryPackage('ext-test-ext');
}
public function testGetPackagesReturnsAllPackages(): void
{
$this->createTestPackageConfig('test-lib', 'library');
$this->createTestPackageConfig('test-ext', 'php-extension');
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$packages = iterator_to_array(PackageLoader::getPackages());
$this->assertCount(3, $packages);
}
public function testGetPackagesWithTypeFilterReturnsFilteredPackages(): void
{
$this->createTestPackageConfig('test-lib', 'library');
$this->createTestPackageConfig('test-ext', 'php-extension');
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$packages = iterator_to_array(PackageLoader::getPackages('library'));
$this->assertCount(1, $packages);
$this->assertArrayHasKey('test-lib', $packages);
}
public function testGetPackagesWithArrayTypeFilterReturnsFilteredPackages(): void
{
$this->createTestPackageConfig('test-lib', 'library');
$this->createTestPackageConfig('test-ext', 'php-extension');
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$packages = iterator_to_array(PackageLoader::getPackages(['library', 'target']));
$this->assertCount(2, $packages);
$this->assertArrayHasKey('test-lib', $packages);
$this->assertArrayHasKey('test-target', $packages);
}
public function testLoadFromClassWithLibraryAttribute(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$class = new #[Library('test-lib')] class {};
PackageLoader::loadFromClass(get_class($class));
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
public function testLoadFromClassWithExtensionAttribute(): void
{
$this->createTestPackageConfig('ext-test-ext', 'php-extension');
PackageLoader::initPackageInstances();
$class = new #[Extension('ext-test-ext')] class {};
PackageLoader::loadFromClass(get_class($class));
$this->assertTrue(PackageLoader::hasPackage('ext-test-ext'));
}
public function testLoadFromClassWithTargetAttribute(): void
{
$this->createTestPackageConfig('test-target', 'target');
PackageLoader::initPackageInstances();
$class = new #[Target('test-target')] class {};
PackageLoader::loadFromClass(get_class($class));
$this->assertTrue(PackageLoader::hasPackage('test-target'));
}
public function testLoadFromClassThrowsExceptionForUndefinedPackage(): void
{
PackageLoader::initPackageInstances();
$class = new #[Library('undefined-lib')] class {};
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('not defined in config');
PackageLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassThrowsExceptionForTypeMismatch(): void
{
$this->createTestPackageConfig('ext-test-lib', 'library');
PackageLoader::initPackageInstances();
// Try to load with Extension attribute but config says library
$class = new #[Extension('ext-test-lib')] class {};
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('type mismatch');
PackageLoader::loadFromClass(get_class($class));
}
public function testLoadFromClassSkipsDuplicateClasses(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$className = get_class(new #[Library('test-lib')] class {});
// Load twice
PackageLoader::loadFromClass($className);
PackageLoader::loadFromClass($className);
// Should not throw exception
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
public function testLoadFromClassWithNoPackageAttribute(): void
{
PackageLoader::initPackageInstances();
$class = new class {
public function regularMethod(): void {}
};
// Should not throw exception
PackageLoader::loadFromClass(get_class($class));
// Verify no side effects
$this->assertTrue(true);
}
public function testCheckLoadedStageEventsThrowsExceptionForUnknownPackage(): void
{
PackageLoader::initPackageInstances();
// Manually add a before_stage for non-existent package
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, [
'non-existent-package' => [
'stage-name' => [[fn () => null, null]],
],
]);
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('unknown package');
PackageLoader::checkLoadedStageEvents();
}
public function testCheckLoadedStageEventsThrowsExceptionForUnknownStage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
// Manually add a before_stage for non-existent stage
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, [
'test-lib' => [
'non-existent-stage' => [[fn () => null, null]],
],
]);
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('is not registered');
PackageLoader::checkLoadedStageEvents();
}
public function testCheckLoadedStageEventsThrowsExceptionForUnknownOnlyWhenPackage(): void
{
$this->createTestPackageConfig('test-lib', 'library');
PackageLoader::initPackageInstances();
$package = PackageLoader::getPackage('test-lib');
$package->addStage('test-stage', fn () => null);
// Manually add a before_stage with unknown only_when_package_resolved
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, [
'test-lib' => [
'test-stage' => [[fn () => null, 'non-existent-package']],
],
]);
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('unknown only_when_package_resolved package');
PackageLoader::checkLoadedStageEvents();
}
public function testGetBeforeStageCallbacksReturnsCallbacks(): void
{
PackageLoader::initPackageInstances();
// Manually add some before_stage callbacks
$callback1 = fn () => 'callback1';
$callback2 = fn () => 'callback2';
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('before_stages');
$property->setAccessible(true);
$property->setValue(null, [
'test-package' => [
'test-stage' => [
[$callback1, null],
[$callback2, null],
],
],
]);
$callbacks = iterator_to_array(PackageLoader::getBeforeStageCallbacks('test-package', 'test-stage'));
$this->assertCount(2, $callbacks);
}
public function testGetAfterStageCallbacksReturnsCallbacks(): void
{
PackageLoader::initPackageInstances();
// Manually add some after_stage callbacks
$callback1 = fn () => 'callback1';
$callback2 = fn () => 'callback2';
$reflection = new \ReflectionClass(PackageLoader::class);
$property = $reflection->getProperty('after_stages');
$property->setAccessible(true);
$property->setValue(null, [
'test-package' => [
'test-stage' => [
[$callback1, null],
[$callback2, null],
],
],
]);
$callbacks = PackageLoader::getAfterStageCallbacks('test-package', 'test-stage');
$this->assertCount(2, $callbacks);
}
public function testGetBeforeStageCallbacksReturnsEmptyForNonExistentPackage(): void
{
PackageLoader::initPackageInstances();
$callbacks = iterator_to_array(PackageLoader::getBeforeStageCallbacks('non-existent', 'stage'));
$this->assertEmpty($callbacks);
}
public function testGetAfterStageCallbacksReturnsEmptyForNonExistentPackage(): void
{
PackageLoader::initPackageInstances();
$callbacks = PackageLoader::getAfterStageCallbacks('non-existent', 'stage');
$this->assertEmpty($callbacks);
}
public function testRegisterAllDefaultStagesRegistersForPhpExtensions(): void
{
$this->createTestPackageConfig('test-ext', 'php-extension');
PackageLoader::initPackageInstances();
PackageLoader::registerAllDefaultStages();
$package = PackageLoader::getPackage('test-ext');
$this->assertInstanceOf(PhpExtensionPackage::class, $package);
// Default stages should be registered (we can't easily verify this without accessing internal state)
}
public function testLoadFromPsr4DirLoadsAllClasses(): void
{
$this->createTestPackageConfig('test-lib', 'library');
// Create a PSR-4 directory structure
$psr4Dir = $this->tempDir . '/PackageClasses';
mkdir($psr4Dir, 0755, true);
// Create test class file
$classContent = '<?php
namespace Test\Package;
use StaticPHP\Attribute\Package\Library;
#[Library("test-lib")]
class TestPackage1 {
}';
file_put_contents($psr4Dir . '/TestPackage1.php', $classContent);
// Load with auto_require enabled
PackageLoader::loadFromPsr4Dir($psr4Dir, 'Test\Package', true);
$this->assertTrue(PackageLoader::hasPackage('test-lib'));
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
private function createTestPackageConfig(string $name, string $type): void
{
$reflection = new \ReflectionClass(PackageConfig::class);
$property = $reflection->getProperty('package_configs');
$property->setAccessible(true);
$configs = $property->getValue();
$configs[$name] = [
'type' => $type,
'deps' => [],
];
$property->setValue(null, $configs);
}
}

View File

@@ -0,0 +1,378 @@
<?php
declare(strict_types=1);
namespace Tests\StaticPHP\Registry;
use PHPUnit\Framework\TestCase;
use StaticPHP\Exception\RegistryException;
use StaticPHP\Registry\Registry;
/**
* @internal
*/
class RegistryTest extends TestCase
{
private string $tempDir;
protected function setUp(): void
{
parent::setUp();
$this->tempDir = sys_get_temp_dir() . '/registry_test_' . uniqid();
mkdir($this->tempDir, 0755, true);
// Reset Registry state
Registry::reset();
}
protected function tearDown(): void
{
parent::tearDown();
// Clean up temp directory
if (is_dir($this->tempDir)) {
$this->removeDirectory($this->tempDir);
}
// Reset Registry state
Registry::reset();
}
public function testLoadRegistryWithValidJsonFile(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'test-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
Registry::loadRegistry($registryFile);
$this->assertContains('test-registry', Registry::getLoadedRegistries());
}
public function testLoadRegistryWithValidYamlFile(): void
{
$registryFile = $this->tempDir . '/test-registry.yaml';
$registryContent = "name: test-registry-yaml\npackage:\n config: []";
file_put_contents($registryFile, $registryContent);
Registry::loadRegistry($registryFile);
$this->assertContains('test-registry-yaml', Registry::getLoadedRegistries());
}
public function testLoadRegistryWithValidYmlFile(): void
{
$registryFile = $this->tempDir . '/test-registry.yml';
$registryContent = "name: test-registry-yml\npackage:\n config: []";
file_put_contents($registryFile, $registryContent);
Registry::loadRegistry($registryFile);
$this->assertContains('test-registry-yml', Registry::getLoadedRegistries());
}
public function testLoadRegistryThrowsExceptionForNonExistentFile(): void
{
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('Failed to read registry file');
Registry::loadRegistry($this->tempDir . '/non-existent.json');
}
public function testLoadRegistryThrowsExceptionForUnsupportedFormat(): void
{
$registryFile = $this->tempDir . '/test-registry.txt';
file_put_contents($registryFile, 'invalid content');
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('Unsupported registry file format');
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryThrowsExceptionForInvalidJson(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
file_put_contents($registryFile, 'invalid json content');
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('Invalid registry format');
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryThrowsExceptionForMissingName(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'package' => [],
];
file_put_contents($registryFile, json_encode($registryData));
$this->expectException(RegistryException::class);
$this->expectExceptionMessage("Registry 'name' is missing or invalid");
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryThrowsExceptionForEmptyName(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => '',
'package' => [],
];
file_put_contents($registryFile, json_encode($registryData));
$this->expectException(RegistryException::class);
$this->expectExceptionMessage("Registry 'name' is missing or invalid");
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryThrowsExceptionForNonStringName(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 123,
'package' => [],
];
file_put_contents($registryFile, json_encode($registryData));
$this->expectException(RegistryException::class);
$this->expectExceptionMessage("Registry 'name' is missing or invalid");
Registry::loadRegistry($registryFile);
}
public function testLoadRegistrySkipsDuplicateRegistry(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'duplicate-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
// Load first time
Registry::loadRegistry($registryFile);
$this->assertCount(1, Registry::getLoadedRegistries());
// Load second time - should skip
Registry::loadRegistry($registryFile);
$this->assertCount(1, Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionWithNullRegistries(): void
{
// Should not throw exception when null is passed and env is not set
Registry::loadFromEnvOrOption(null);
$this->assertEmpty(Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionWithEmptyString(): void
{
Registry::loadFromEnvOrOption('');
$this->assertEmpty(Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionWithSingleRegistry(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'env-test-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
Registry::loadFromEnvOrOption($registryFile);
$this->assertContains('env-test-registry', Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionWithMultipleRegistries(): void
{
$registryFile1 = $this->tempDir . '/test-registry-1.json';
$registryData1 = [
'name' => 'env-test-registry-1',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile1, json_encode($registryData1));
$registryFile2 = $this->tempDir . '/test-registry-2.json';
$registryData2 = [
'name' => 'env-test-registry-2',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile2, json_encode($registryData2));
Registry::loadFromEnvOrOption($registryFile1 . ':' . $registryFile2);
$this->assertContains('env-test-registry-1', Registry::getLoadedRegistries());
$this->assertContains('env-test-registry-2', Registry::getLoadedRegistries());
}
public function testLoadFromEnvOrOptionIgnoresNonExistentFiles(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'env-test-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
// Mix existing and non-existing files
Registry::loadFromEnvOrOption($registryFile . ':' . $this->tempDir . '/non-existent.json');
// Should only load the existing one
$this->assertCount(1, Registry::getLoadedRegistries());
$this->assertContains('env-test-registry', Registry::getLoadedRegistries());
}
public function testGetLoadedRegistriesReturnsEmptyArrayInitially(): void
{
$this->assertEmpty(Registry::getLoadedRegistries());
}
public function testGetLoadedRegistriesReturnsCorrectList(): void
{
$registryFile1 = $this->tempDir . '/test-registry-1.json';
$registryData1 = [
'name' => 'registry-1',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile1, json_encode($registryData1));
$registryFile2 = $this->tempDir . '/test-registry-2.json';
$registryData2 = [
'name' => 'registry-2',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile2, json_encode($registryData2));
Registry::loadRegistry($registryFile1);
Registry::loadRegistry($registryFile2);
$loaded = Registry::getLoadedRegistries();
$this->assertCount(2, $loaded);
$this->assertContains('registry-1', $loaded);
$this->assertContains('registry-2', $loaded);
}
public function testResetClearsLoadedRegistries(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'test-registry',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
Registry::loadRegistry($registryFile);
$this->assertNotEmpty(Registry::getLoadedRegistries());
Registry::reset();
$this->assertEmpty(Registry::getLoadedRegistries());
}
public function testLoadRegistryWithAutoloadPath(): void
{
// Create a test autoload file
$autoloadFile = $this->tempDir . '/vendor/autoload.php';
mkdir(dirname($autoloadFile), 0755, true);
file_put_contents($autoloadFile, '<?php // Test autoload');
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'autoload-test-registry',
'autoload' => 'vendor/autoload.php',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
// Should not throw exception
Registry::loadRegistry($registryFile);
$this->assertContains('autoload-test-registry', Registry::getLoadedRegistries());
}
public function testLoadRegistryWithNonExistentAutoloadPath(): void
{
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'autoload-missing-test-registry',
'autoload' => 'vendor/non-existent-autoload.php',
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
// Should throw exception when path doesn't exist
$this->expectException(RegistryException::class);
$this->expectExceptionMessage('Path does not exist');
Registry::loadRegistry($registryFile);
}
public function testLoadRegistryWithAbsoluteAutoloadPath(): void
{
// Create a test autoload file with absolute path
$autoloadFile = $this->tempDir . '/vendor/autoload.php';
mkdir(dirname($autoloadFile), 0755, true);
file_put_contents($autoloadFile, '<?php // Test autoload');
$registryFile = $this->tempDir . '/test-registry.json';
$registryData = [
'name' => 'absolute-autoload-test-registry',
'autoload' => $autoloadFile,
'package' => [
'config' => [],
],
];
file_put_contents($registryFile, json_encode($registryData));
Registry::loadRegistry($registryFile);
$this->assertContains('absolute-autoload-test-registry', Registry::getLoadedRegistries());
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->removeDirectory($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
}

Binary file not shown.

View File

@@ -1,13 +1,9 @@
<?php
declare(strict_types=1);
use Psr\Log\LogLevel;
require_once __DIR__ . '/../src/globals/internal-env.php';
require_once __DIR__ . '/mock/SPC_store.php';
require_once __DIR__ . '/../src/bootstrap.php';
\StaticPHP\Registry\Registry::checkLoadedRegistries();
\SPC\util\AttributeMapper::init();
$log_dir = SPC_LOGS_DIR;
if (!file_exists($log_dir)) {
mkdir($log_dir, 0755, true);
}
logger()->setLevel(LogLevel::ERROR);