mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-14 12:25:36 +08:00
Compare commits
54 Commits
14bfb4198a
...
v3-feat/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020a30315d | ||
|
|
bde1440617 | ||
|
|
78375632b4 | ||
|
|
f68adc3256 | ||
|
|
4a968757ba | ||
|
|
bcaef59a15 | ||
|
|
b0f630f95f | ||
|
|
ac01867e9c | ||
|
|
808aed2a66 | ||
|
|
e004d10861 | ||
|
|
0db26be826 | ||
|
|
a4bd2a79a9 | ||
|
|
7b16f683fc | ||
|
|
78234ef147 | ||
|
|
80128edd39 | ||
|
|
b384345723 | ||
|
|
f4bb0263f6 | ||
|
|
321f2e13e8 | ||
|
|
11e7a590c8 | ||
|
|
20e0711747 | ||
|
|
80d922ab3b | ||
|
|
a1cadecc54 | ||
|
|
127c935106 | ||
|
|
eab105965d | ||
|
|
abd6c2fa3a | ||
|
|
df6c27c98d | ||
|
|
3ff762c4c8 | ||
|
|
6775cb4674 | ||
|
|
88b86d3eaf | ||
|
|
dbc6dbee53 | ||
|
|
baddd60113 | ||
|
|
2f09ace82f | ||
|
|
d3b0f5de79 | ||
|
|
9ad7147155 | ||
|
|
106b55d4e7 | ||
|
|
93a697ebbf | ||
|
|
7fa6fd08d4 | ||
|
|
52553fb5ed | ||
|
|
c925914925 | ||
|
|
d16f5a972c | ||
|
|
ee46c1c387 | ||
|
|
64fde5fd8c | ||
|
|
dc5bf6dc98 | ||
|
|
20892ab194 | ||
|
|
e9d3f7e7eb | ||
|
|
2f8570b59e | ||
|
|
71d803d36f | ||
|
|
daa87e1350 | ||
|
|
c38f174a6b | ||
|
|
9903c2294c | ||
|
|
59a6e27532 | ||
|
|
6b3b841c0e | ||
|
|
e6591ffe9c | ||
|
|
e316971764 |
@@ -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());
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -e
|
||||
|
||||
# This file is using docker to run commands
|
||||
SPC_DOCKER_VERSION=v6
|
||||
SPC_DOCKER_VERSION=v7
|
||||
|
||||
# Detect docker can run
|
||||
if ! which docker >/dev/null; then
|
||||
@@ -123,6 +123,7 @@ COPY ./composer.* /app/
|
||||
ADD ./bin /app/bin
|
||||
RUN composer install --no-dev
|
||||
ADD ./config /app/config
|
||||
ADD ./spc.registry.json /app/spc.registry.json
|
||||
RUN bin/spc doctor --auto-fix
|
||||
RUN bin/spc install-pkg upx
|
||||
|
||||
|
||||
@@ -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
163
composer.lock
generated
@@ -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",
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"musl-wrapper": {
|
||||
"source": "https://musl.libc.org/releases/musl-1.2.5.tar.gz"
|
||||
},
|
||||
"php-src": {
|
||||
"source": {
|
||||
"type": "php-release"
|
||||
@@ -28,8 +31,16 @@
|
||||
},
|
||||
"musl-toolchain": {
|
||||
"binary": {
|
||||
"linux-x86_64": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz",
|
||||
"linux-aarch64": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz"
|
||||
"linux-x86_64": {
|
||||
"type": "url",
|
||||
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz",
|
||||
"extract": "{pkg_root_path}/musl-toolchain"
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"type": "url",
|
||||
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz",
|
||||
"extract": "{pkg_root_path}/musl-toolchain"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pkg-config": {
|
||||
|
||||
@@ -567,6 +567,13 @@
|
||||
"type": "builtin",
|
||||
"unix-only": true
|
||||
},
|
||||
"pcov": {
|
||||
"type": "external",
|
||||
"source": "pcov",
|
||||
"target": [
|
||||
"shared"
|
||||
]
|
||||
},
|
||||
"pdo": {
|
||||
"type": "builtin"
|
||||
},
|
||||
|
||||
@@ -711,6 +711,9 @@
|
||||
"ncurses": {
|
||||
"type": "library",
|
||||
"artifact": "ncurses",
|
||||
"static-libs@unix": [
|
||||
"libncurses.a"
|
||||
],
|
||||
"license": {
|
||||
"type": "file",
|
||||
"path": "COPYING"
|
||||
|
||||
@@ -963,6 +963,15 @@
|
||||
"path": "LICENSE"
|
||||
}
|
||||
},
|
||||
"pcov": {
|
||||
"type": "url",
|
||||
"url": "https://pecl.php.net/get/pcov",
|
||||
"filename": "pcov.tgz",
|
||||
"license": {
|
||||
"type": "file",
|
||||
"path": "LICENSE"
|
||||
}
|
||||
},
|
||||
"pdo_sqlsrv": {
|
||||
"type": "url",
|
||||
"url": "https://pecl.php.net/get/pdo_sqlsrv",
|
||||
|
||||
@@ -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\ValidationException;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
class go_xcaddy
|
||||
@@ -28,29 +28,41 @@ class go_xcaddy
|
||||
$arch = match (explode('-', $name)[1]) {
|
||||
'x86_64' => 'amd64',
|
||||
'aarch64' => 'arm64',
|
||||
default => throw new ValidationException('Unsupported architecture: ' . $name),
|
||||
default => throw new DownloaderException('Unsupported architecture: ' . $name),
|
||||
};
|
||||
$os = match (explode('-', $name)[0]) {
|
||||
'linux' => 'linux',
|
||||
'macos' => 'darwin',
|
||||
default => throw new ValidationException('Unsupported OS: ' . $name),
|
||||
default => throw new DownloaderException('Unsupported OS: ' . $name),
|
||||
};
|
||||
$hash = match ("{$os}-{$arch}") {
|
||||
'linux-amd64' => '2852af0cb20a13139b3448992e69b868e50ed0f8a1e5940ee1de9e19a123b613',
|
||||
'linux-arm64' => '05de75d6994a2783699815ee553bd5a9327d8b79991de36e38b66862782f54ae',
|
||||
'darwin-amd64' => '5bd60e823037062c2307c71e8111809865116714d6f6b410597cf5075dfd80ef',
|
||||
'darwin-arm64' => '544932844156d8172f7a28f77f2ac9c15a23046698b6243f633b0a0b00c0749c',
|
||||
};
|
||||
$go_version = '1.25.0';
|
||||
$url = "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz";
|
||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "go{$go_version}.{$os}-{$arch}.tar.gz";
|
||||
|
||||
// get version and hash
|
||||
[$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: '');
|
||||
if ($version === '') {
|
||||
throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text');
|
||||
}
|
||||
$page = default_shell()->executeCurl('https://go.dev/dl/');
|
||||
if ($page === '' || $page === false) {
|
||||
throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/');
|
||||
}
|
||||
|
||||
$version_regex = str_replace('.', '\.', $version);
|
||||
$pattern = "/href=\"\\/dl\\/{$version_regex}\\.{$os}-{$arch}\\.tar\\.gz\">.*?<tt>([a-f0-9]{64})<\\/tt>/s";
|
||||
if (preg_match($pattern, $page, $matches)) {
|
||||
$hash = $matches[1];
|
||||
} else {
|
||||
throw new DownloaderException("Failed to find download hash for Go {$version} {$os}-{$arch}");
|
||||
}
|
||||
|
||||
$url = "https://go.dev/dl/{$version}.{$os}-{$arch}.tar.gz";
|
||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "{$version}.{$os}-{$arch}.tar.gz";
|
||||
default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
|
||||
// verify hash
|
||||
$file_hash = hash_file('sha256', $path);
|
||||
if ($file_hash !== $hash) {
|
||||
throw new ValidationException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}");
|
||||
throw new DownloaderException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}");
|
||||
}
|
||||
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $go_version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $go_version);
|
||||
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version);
|
||||
}
|
||||
|
||||
#[AfterBinaryExtract('go-xcaddy', [
|
||||
|
||||
98
src/Package/Artifact/zig.php
Normal file
98
src/Package/Artifact/zig.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -67,13 +67,11 @@ class SwitchPhpVersionCommand extends BaseCommand
|
||||
InteractiveTerm::finish('Removed: ' . $source_dir);
|
||||
}
|
||||
|
||||
// Set the PHP version for download
|
||||
// This defines the version that will be used when resolving php-src artifact
|
||||
define('SPC_BUILD_PHP_VERSION', $php_ver);
|
||||
|
||||
// Download new PHP source
|
||||
$this->output->writeln("<info>Downloading PHP {$php_ver} source...</info>");
|
||||
|
||||
$this->input->setOption('with-php', $php_ver);
|
||||
|
||||
$downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->input->getOptions());
|
||||
$downloader = new ArtifactDownloader($downloaderOptions);
|
||||
|
||||
|
||||
37
src/Package/Extension/readline.php
Normal file
37
src/Package/Extension/readline.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
|
||||
#[Extension('readline')]
|
||||
class readline
|
||||
{
|
||||
#[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()) {
|
||||
$php_src = $installer->getBuildPackage('php')->getSourceDir();
|
||||
SourcePatcher::patchFile('musl_static_readline.patch', $php_src);
|
||||
}
|
||||
}
|
||||
|
||||
#[AfterStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')]
|
||||
public function afterMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
|
||||
{
|
||||
if ($toolchain->isStatic()) {
|
||||
$php_src = $installer->getBuildPackage('php')->getSourceDir();
|
||||
SourcePatcher::patchFile('musl_static_readline.patch', $php_src, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Package/Library/imap.php
Normal file
25
src/Package/Library/imap.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
#[Library('imap')]
|
||||
class imap
|
||||
{
|
||||
#[AfterStage('php', [php::class, 'patchEmbedScripts'], 'imap')]
|
||||
#[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')]
|
||||
public function afterPatchScripts(): void
|
||||
{
|
||||
if (SystemTarget::getLibc() === 'glibc') {
|
||||
FileSystem::replaceFileRegex(BUILD_BIN_PATH . '/php-config', '/^libs="(.*)"$/m', 'libs="$1 -lcrypt"');
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/Package/Library/libedit.php
Normal file
37
src/Package/Library/libedit.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
61
src/Package/Library/ncurses.php
Normal file
61
src/Package/Library/ncurses.php
Normal 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}');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
22
src/Package/Target/micro.php
Normal file
22
src/Package/Target/micro.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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,22 +13,26 @@ 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;
|
||||
use StaticPHP\Util\DirDiff;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\SourcePatcher;
|
||||
use StaticPHP\Util\SPCConfigUtil;
|
||||
use StaticPHP\Util\System\UnixUtil;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -42,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
|
||||
{
|
||||
@@ -109,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'));
|
||||
@@ -126,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.");
|
||||
}
|
||||
@@ -135,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];
|
||||
}
|
||||
|
||||
@@ -176,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 [
|
||||
@@ -225,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
|
||||
@@ -243,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'));
|
||||
@@ -251,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'));
|
||||
@@ -279,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));
|
||||
|
||||
@@ -301,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');
|
||||
@@ -309,18 +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-fpm')) {
|
||||
$package->runStage('unix-make-fpm');
|
||||
if ($installer->isPackageResolved('php-cgi')) {
|
||||
$package->runStage([self::class, 'makeCgiForUnix']);
|
||||
}
|
||||
if ($installer->isBuildPackage('php-cgi')) {
|
||||
$package->runStage('unix-make-cgi');
|
||||
if ($installer->isPackageResolved('php-fpm')) {
|
||||
$package->runStage([self::class, 'makeFpmForUnix']);
|
||||
}
|
||||
if ($installer->isPackageResolved('php-micro')) {
|
||||
$package->runStage([self::class, 'makeMicroForUnix']);
|
||||
}
|
||||
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'));
|
||||
@@ -328,6 +350,167 @@ 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]
|
||||
public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
|
||||
{
|
||||
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi'));
|
||||
$concurrency = $builder->concurrency;
|
||||
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]
|
||||
public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
|
||||
{
|
||||
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm'));
|
||||
$concurrency = $builder->concurrency;
|
||||
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]
|
||||
#[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')]
|
||||
public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
|
||||
{
|
||||
$phar_patched = false;
|
||||
try {
|
||||
if ($installer->isPackageResolved('ext-phar')) {
|
||||
$phar_patched = true;
|
||||
SourcePatcher::patchMicroPhar(self::getPHPVersionID());
|
||||
}
|
||||
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro'));
|
||||
// apply --with-micro-fake-cli option
|
||||
$vars = $this->makeVars($installer);
|
||||
$vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : '';
|
||||
// build
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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()
|
||||
);
|
||||
$install_modules = $shared_exts ? 'install-modules' : '';
|
||||
|
||||
// detect changes in module path
|
||||
$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_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 -------------
|
||||
|
||||
// process libphp.so for shared embed
|
||||
$suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so';
|
||||
$libphp_so = "{$package->getLibDir()}/libphp.{$suffix}";
|
||||
if (file_exists($libphp_so)) {
|
||||
// rename libphp.so if -release is set
|
||||
if (SystemTarget::getTargetOS() === 'Linux') {
|
||||
$this->processLibphpSoFile($libphp_so, $installer);
|
||||
}
|
||||
// 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, $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([$this, 'patchEmbedScripts']);
|
||||
}
|
||||
|
||||
#[Stage]
|
||||
public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void
|
||||
{
|
||||
// collect shared extensions
|
||||
/** @var PhpExtensionPackage[] $shared_extensions */
|
||||
$shared_extensions = array_filter(
|
||||
$installer->getResolvedPackages(PhpExtensionPackage::class),
|
||||
fn ($x) => $x->isBuildShared() && !$x->isBuildWithPhp()
|
||||
);
|
||||
if (!empty($shared_extensions)) {
|
||||
if ($toolchain->isStatic()) {
|
||||
throw new WrongUsageException(
|
||||
"You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions.\n" .
|
||||
'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . "\n" .
|
||||
'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.'
|
||||
);
|
||||
}
|
||||
FileSystem::createDir(BUILD_MODULES_PATH);
|
||||
|
||||
// backup
|
||||
FileSystem::backupFile(BUILD_BIN_PATH . '/php-config');
|
||||
FileSystem::backupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
|
||||
|
||||
FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', 'extension_dir="' . BUILD_MODULES_PATH . '"');
|
||||
FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes');
|
||||
}
|
||||
|
||||
try {
|
||||
logger()->debug('Building shared extensions...');
|
||||
foreach ($shared_extensions as $extension) {
|
||||
InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName()));
|
||||
$extension->buildShared();
|
||||
}
|
||||
} finally {
|
||||
// restore php-config
|
||||
if (!empty($shared_extensions)) {
|
||||
FileSystem::restoreBackupFile(BUILD_BIN_PATH . '/php-config');
|
||||
FileSystem::restoreBackupFile(BUILD_LIB_PATH . '/php/build/phpize.m4');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[BuildFor('Darwin')]
|
||||
@@ -339,9 +522,38 @@ class php
|
||||
return;
|
||||
}
|
||||
|
||||
$package->runStage('unix-buildconf');
|
||||
$package->runStage('unix-configure');
|
||||
$package->runStage('unix-make');
|
||||
$package->runStage([$this, 'buildconfForUnix']);
|
||||
$package->runStage([$this, 'configureForUnix']);
|
||||
$package->runStage([$this, 'makeForUnix']);
|
||||
|
||||
$package->runStage([$this, 'unixBuildSharedExt']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch phpize and php-config if needed
|
||||
*/
|
||||
#[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')) {
|
||||
logger()->debug('Patching php-config prefix and libs order');
|
||||
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
|
||||
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
|
||||
// move mimalloc to the beginning of libs
|
||||
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
|
||||
// move lstdc++ to the end of libs
|
||||
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
|
||||
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
|
||||
$this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -381,6 +593,10 @@ class php
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make environment variables for php make.
|
||||
* This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking.
|
||||
*/
|
||||
private function makeVars(PackageInstaller $installer): array
|
||||
{
|
||||
$config = (new SPCConfigUtil(['libs_only_deps' => true]))->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages()));
|
||||
@@ -394,4 +610,75 @@ class php
|
||||
'EXTRA_LIBS' => $config['libs'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename libphp.so to libphp-<release>.so if -release is set in LDFLAGS.
|
||||
*/
|
||||
private function processLibphpSoFile(string $libphpSo, PackageInstaller $installer): void
|
||||
{
|
||||
$ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: '';
|
||||
$libDir = BUILD_LIB_PATH;
|
||||
$modulesDir = BUILD_MODULES_PATH;
|
||||
$realLibName = 'libphp.so';
|
||||
$cwd = getcwd();
|
||||
|
||||
if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) {
|
||||
$release = $matches[1];
|
||||
$realLibName = "libphp-{$release}.so";
|
||||
$libphpRelease = "{$libDir}/{$realLibName}";
|
||||
if (!file_exists($libphpRelease) && file_exists($libphpSo)) {
|
||||
rename($libphpSo, $libphpRelease);
|
||||
}
|
||||
if (file_exists($libphpRelease)) {
|
||||
chdir($libDir);
|
||||
if (file_exists($libphpSo)) {
|
||||
unlink($libphpSo);
|
||||
}
|
||||
symlink($realLibName, 'libphp.so');
|
||||
shell()->exec(sprintf(
|
||||
'patchelf --set-soname %s %s',
|
||||
escapeshellarg($realLibName),
|
||||
escapeshellarg($libphpRelease)
|
||||
));
|
||||
}
|
||||
if (is_dir($modulesDir)) {
|
||||
chdir($modulesDir);
|
||||
foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) {
|
||||
if (!$ext->isBuildShared()) {
|
||||
continue;
|
||||
}
|
||||
$name = $ext->getName();
|
||||
$versioned = "{$name}-{$release}.so";
|
||||
$unversioned = "{$name}.so";
|
||||
$src = "{$modulesDir}/{$versioned}";
|
||||
$dst = "{$modulesDir}/{$unversioned}";
|
||||
if (is_file($src)) {
|
||||
rename($src, $dst);
|
||||
shell()->exec(sprintf(
|
||||
'patchelf --set-soname %s %s',
|
||||
escapeshellarg($unversioned),
|
||||
escapeshellarg($dst)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
chdir($cwd);
|
||||
}
|
||||
|
||||
$target = "{$libDir}/{$realLibName}";
|
||||
if (file_exists($target)) {
|
||||
[, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target));
|
||||
$output = implode("\n", $output);
|
||||
if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) {
|
||||
$currentSoname = $sonameMatch[1];
|
||||
if ($currentSoname !== basename($target)) {
|
||||
shell()->exec(sprintf(
|
||||
'patchelf --set-soname %s %s',
|
||||
escapeshellarg(basename($target)),
|
||||
escapeshellarg($target)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class SystemUtil
|
||||
{
|
||||
return [
|
||||
// debian-like
|
||||
'debian', 'ubuntu', 'Deepin',
|
||||
'debian', 'ubuntu', 'Deepin', 'neon',
|
||||
// rhel-like
|
||||
'redhat',
|
||||
// centos
|
||||
|
||||
@@ -112,7 +112,7 @@ class LinuxToolCheckList
|
||||
public function fixBuildTools(array $distro, array $missing): bool
|
||||
{
|
||||
$install_cmd = match ($distro['dist']) {
|
||||
'ubuntu', 'debian', 'Deepin' => 'apt-get install -y',
|
||||
'ubuntu', 'debian', 'Deepin', 'neon' => 'apt-get install -y',
|
||||
'alpine' => 'apk add',
|
||||
'redhat' => 'dnf install -y',
|
||||
'centos' => 'yum install -y',
|
||||
@@ -128,7 +128,7 @@ class LinuxToolCheckList
|
||||
logger()->warning('Current user (' . $user . ') is not root, using sudo for running command (may require password input)');
|
||||
}
|
||||
|
||||
$is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin']);
|
||||
$is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin', 'neon']);
|
||||
$to_install = $is_debian ? str_replace('xz', 'xz-utils', $missing) : $missing;
|
||||
// debian, alpine libtool -> libtoolize
|
||||
$to_install = str_replace('libtoolize', 'libtool', $to_install);
|
||||
|
||||
@@ -660,11 +660,19 @@ class FileSystem
|
||||
$source = self::convertPath($source);
|
||||
$dest = self::convertPath($dest);
|
||||
|
||||
// Try rename first (fast, atomic)
|
||||
if (@rename($source, $dest)) {
|
||||
return;
|
||||
// Check if source and dest are on the same device to avoid cross-device rename errors
|
||||
$source_stat = @stat($source);
|
||||
$dest_parent = dirname($dest);
|
||||
$dest_stat = @stat($dest_parent);
|
||||
|
||||
// Only use rename if on same device
|
||||
if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) {
|
||||
if (@rename($source, $dest)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to copy + delete for cross-device moves or if rename failed
|
||||
if (is_dir($source)) {
|
||||
self::copyDir($source, $dest);
|
||||
self::removeDir($source);
|
||||
|
||||
@@ -493,7 +493,7 @@ class Artifact
|
||||
* Emit all after binary extract callbacks for the specified platform.
|
||||
*
|
||||
* @param null|string $target_path The directory where binary was extracted
|
||||
* @param string $platform The platform string (e.g., 'linux-x86_64')
|
||||
* @param string $platform The platform string (e.g., 'linux-x86_64')
|
||||
*/
|
||||
public function emitAfterBinaryExtract(?string $target_path, string $platform): void
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ use StaticPHP\Artifact\Downloader\Type\FileList;
|
||||
use StaticPHP\Artifact\Downloader\Type\Git;
|
||||
use StaticPHP\Artifact\Downloader\Type\GitHubRelease;
|
||||
use StaticPHP\Artifact\Downloader\Type\GitHubTarball;
|
||||
use StaticPHP\Artifact\Downloader\Type\HostedPackageBin;
|
||||
use StaticPHP\Artifact\Downloader\Type\LocalDir;
|
||||
use StaticPHP\Artifact\Downloader\Type\PhpRelease;
|
||||
use StaticPHP\Artifact\Downloader\Type\PIE;
|
||||
@@ -23,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;
|
||||
@@ -35,6 +37,21 @@ use ZM\Logger\ConsoleColor;
|
||||
*/
|
||||
class ArtifactDownloader
|
||||
{
|
||||
/** @var array<string, class-string<DownloadTypeInterface>> */
|
||||
public const array DOWNLOADERS = [
|
||||
'bitbuckettag' => BitBucketTag::class,
|
||||
'filelist' => FileList::class,
|
||||
'git' => Git::class,
|
||||
'ghrel' => GitHubRelease::class,
|
||||
'ghtar' => GitHubTarball::class,
|
||||
'ghtagtar' => GitHubTarball::class,
|
||||
'local' => LocalDir::class,
|
||||
'pie' => PIE::class,
|
||||
'url' => Url::class,
|
||||
'php-release' => PhpRelease::class,
|
||||
'hosted' => HostedPackageBin::class,
|
||||
];
|
||||
|
||||
/** @var array<string, Artifact> Artifact objects */
|
||||
protected array $artifacts = [];
|
||||
|
||||
@@ -97,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;
|
||||
}
|
||||
}
|
||||
@@ -108,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;
|
||||
}
|
||||
}
|
||||
@@ -118,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;
|
||||
}
|
||||
}
|
||||
@@ -129,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;
|
||||
}
|
||||
}
|
||||
@@ -140,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;
|
||||
}
|
||||
}
|
||||
@@ -148,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;
|
||||
}
|
||||
}
|
||||
@@ -156,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;
|
||||
}
|
||||
}
|
||||
@@ -304,7 +321,7 @@ class ArtifactDownloader
|
||||
$skipped = [];
|
||||
foreach ($this->artifacts as $artifact) {
|
||||
++$current;
|
||||
if ($this->downloadWithType($artifact, $current, $count) === SPC_DOWNLOAD_STATUS_SKIPPED) {
|
||||
if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) {
|
||||
$skipped[] = $artifact->getName();
|
||||
continue;
|
||||
}
|
||||
@@ -342,7 +359,7 @@ class ArtifactDownloader
|
||||
return $this->options[$name] ?? $default;
|
||||
}
|
||||
|
||||
private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int
|
||||
private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int
|
||||
{
|
||||
$queue = $this->generateQueue($artifact);
|
||||
// already downloaded
|
||||
@@ -355,18 +372,7 @@ class ArtifactDownloader
|
||||
foreach ($queue as $item) {
|
||||
try {
|
||||
$instance = null;
|
||||
$call = match ($item['config']['type']) {
|
||||
'bitbuckettag' => BitBucketTag::class,
|
||||
'filelist' => FileList::class,
|
||||
'git' => Git::class,
|
||||
'ghrel' => GitHubRelease::class,
|
||||
'ghtar', 'ghtagtar' => GitHubTarball::class,
|
||||
'local' => LocalDir::class,
|
||||
'pie' => PIE::class,
|
||||
'url' => Url::class,
|
||||
'php-release' => PhpRelease::class,
|
||||
default => null,
|
||||
};
|
||||
$call = self::DOWNLOADERS[$item['config']['type']] ?? null;
|
||||
$type_display_name = match (true) {
|
||||
$item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null => 'user defined source downloader',
|
||||
$item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null => 'user defined binary downloader',
|
||||
@@ -374,7 +380,7 @@ class ArtifactDownloader
|
||||
};
|
||||
$try_h = $try ? 'Try downloading' : 'Downloading';
|
||||
logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ...");
|
||||
if ($parallel === false) {
|
||||
if ($parallel === false && $interactive) {
|
||||
InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ...");
|
||||
}
|
||||
// is valid download type
|
||||
@@ -392,7 +398,12 @@ class ArtifactDownloader
|
||||
$instance = new $call();
|
||||
$lock = $instance->download($artifact->getName(), $item['config'], $this);
|
||||
} else {
|
||||
throw new ValidationException("Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}.");
|
||||
if ($item['config']['type'] === 'custom') {
|
||||
$msg = "Artifact [{$artifact->getName()}] has no valid custom " . SystemTarget::getCurrentPlatformString() . ' download callback defined.';
|
||||
} else {
|
||||
$msg = "Artifact has invalid download type '{$item['config']['type']}' for {$item['display']}.";
|
||||
}
|
||||
throw new ValidationException($msg);
|
||||
}
|
||||
if (!$lock instanceof DownloadResult) {
|
||||
throw new ValidationException("Artifact {$artifact->getName()} has invalid custom return value. Must be instance of DownloadResult.");
|
||||
@@ -408,13 +419,13 @@ class ArtifactDownloader
|
||||
}
|
||||
// process lock
|
||||
ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString());
|
||||
if ($parallel === false) {
|
||||
if ($parallel === false && $interactive) {
|
||||
$ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : '';
|
||||
InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} .");
|
||||
}
|
||||
return SPC_DOWNLOAD_STATUS_SUCCESS;
|
||||
} catch (DownloaderException|ExecutionException $e) {
|
||||
if ($parallel === false) {
|
||||
if ($parallel === false && $interactive) {
|
||||
InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false);
|
||||
InteractiveTerm::error("Failed message: {$e->getMessage()}", true);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -78,12 +79,17 @@ class ArtifactExtractor
|
||||
/**
|
||||
* Extract a single artifact.
|
||||
*
|
||||
* @param Artifact $artifact The artifact to extract
|
||||
* @param bool $force_source If true, always extract source (ignore binary)
|
||||
* @param Artifact|string $artifact The artifact to extract
|
||||
* @param bool $force_source If true, always extract source (ignore binary)
|
||||
*/
|
||||
public function extract(Artifact $artifact, bool $force_source = false): int
|
||||
public function extract(Artifact|string $artifact, bool $force_source = false): int
|
||||
{
|
||||
$name = $artifact->getName();
|
||||
if (is_string($artifact)) {
|
||||
$name = $artifact;
|
||||
$artifact = ArtifactLoader::getArtifactInstance($name);
|
||||
} else {
|
||||
$name = $artifact->getName();
|
||||
}
|
||||
|
||||
// Already extracted in this session
|
||||
if (isset($this->extracted[$name])) {
|
||||
@@ -346,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
|
||||
{
|
||||
@@ -570,32 +574,6 @@ class ArtifactExtractor
|
||||
FileSystem::removeDir($temp_dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move file or directory to destination.
|
||||
*/
|
||||
protected function moveFileOrDir(string $source, string $dest): void
|
||||
{
|
||||
$source = FileSystem::convertPath($source);
|
||||
$dest = FileSystem::convertPath($dest);
|
||||
|
||||
// Try rename first (fast, atomic)
|
||||
if (@rename($source, $dest)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_dir($source)) {
|
||||
FileSystem::copyDir($source, $dest);
|
||||
FileSystem::removeDir($source);
|
||||
} else {
|
||||
if (!copy($source, $dest)) {
|
||||
throw new FileSystemException("Failed to copy file from {$source} to {$dest}");
|
||||
}
|
||||
if (!unlink($source)) {
|
||||
throw new FileSystemException("Failed to remove source file: {$source}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace path variables.
|
||||
*/
|
||||
@@ -611,6 +589,44 @@ class ArtifactExtractor
|
||||
return str_replace(array_keys($replacement), array_values($replacement), $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move file or directory, handling cross-device scenarios
|
||||
* Uses rename() if possible, falls back to copy+delete for cross-device moves
|
||||
*
|
||||
* @param string $source Source path
|
||||
* @param string $dest Destination path
|
||||
*/
|
||||
private static function moveFileOrDir(string $source, string $dest): void
|
||||
{
|
||||
$source = FileSystem::convertPath($source);
|
||||
$dest = FileSystem::convertPath($dest);
|
||||
|
||||
// Check if source and dest are on the same device to avoid cross-device rename errors
|
||||
$source_stat = @stat($source);
|
||||
$dest_parent = dirname($dest);
|
||||
$dest_stat = @stat($dest_parent);
|
||||
|
||||
// Only use rename if on same device
|
||||
if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) {
|
||||
if (@rename($source, $dest)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to copy + delete for cross-device moves or if rename failed
|
||||
if (is_dir($source)) {
|
||||
FileSystem::copyDir($source, $dest);
|
||||
FileSystem::removeDir($source);
|
||||
} else {
|
||||
if (!copy($source, $dest)) {
|
||||
throw new FileSystemException("Failed to copy file from {$source} to {$dest}");
|
||||
}
|
||||
if (!unlink($source)) {
|
||||
throw new FileSystemException("Failed to remove source file: {$source}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function copyFile(string $source_file, string $target_path): void
|
||||
{
|
||||
FileSystem::createDir(dirname($target_path));
|
||||
|
||||
@@ -21,6 +21,26 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface
|
||||
|
||||
private ?string $version = null;
|
||||
|
||||
public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true): array
|
||||
{
|
||||
logger()->debug("Fetching {$name} GitHub releases from {$repo}");
|
||||
$url = str_replace('{repo}', $repo, self::API_URL);
|
||||
$headers = $this->getGitHubTokenHeaders();
|
||||
$data2 = default_shell()->executeCurl($url, headers: $headers);
|
||||
$data = json_decode($data2 ?: '', true);
|
||||
if (!is_array($data)) {
|
||||
throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}");
|
||||
}
|
||||
$releases = [];
|
||||
foreach ($data as $release) {
|
||||
if ($prefer_stable && $release['prerelease'] === true) {
|
||||
continue;
|
||||
}
|
||||
$releases[] = $release;
|
||||
}
|
||||
return $releases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest GitHub release assets for a given repository.
|
||||
* match_asset is provided, only return the asset that matches the regex.
|
||||
|
||||
@@ -7,6 +7,11 @@ namespace StaticPHP\Artifact\Downloader\Type;
|
||||
trait GitHubTokenSetupTrait
|
||||
{
|
||||
public function getGitHubTokenHeaders(): array
|
||||
{
|
||||
return self::getGitHubTokenHeadersStatic();
|
||||
}
|
||||
|
||||
public static function getGitHubTokenHeadersStatic(): array
|
||||
{
|
||||
// GITHUB_TOKEN support
|
||||
if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) {
|
||||
|
||||
63
src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php
Normal file
63
src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
|
||||
class HostedPackageBin implements DownloadTypeInterface
|
||||
{
|
||||
use GitHubTokenSetupTrait;
|
||||
|
||||
public const string BASE_REPO = 'static-php/package-bin';
|
||||
|
||||
public const array ASSET_MATCHES = [
|
||||
'linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz',
|
||||
'darwin' => '{name}-{arch}-{os}.txz',
|
||||
'windows' => '{name}-{arch}-{os}.tgz',
|
||||
];
|
||||
|
||||
private static array $release_info = [];
|
||||
|
||||
public static function getReleaseInfo(): array
|
||||
{
|
||||
if (empty(self::$release_info)) {
|
||||
$rel = (new GitHubRelease())->getGitHubReleases('hosted', self::BASE_REPO);
|
||||
if (empty($rel)) {
|
||||
throw new DownloaderException('No releases found for hosted package-bin');
|
||||
}
|
||||
self::$release_info = $rel[0];
|
||||
}
|
||||
return self::$release_info;
|
||||
}
|
||||
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
$info = self::getReleaseInfo();
|
||||
$replace = [
|
||||
'{name}' => $name,
|
||||
'{arch}' => SystemTarget::getTargetArch(),
|
||||
'{os}' => strtolower(SystemTarget::getTargetOS()),
|
||||
'{libc}' => SystemTarget::getLibc() ?? 'default',
|
||||
'{libcver}' => SystemTarget::getLibcVersion() ?? 'default',
|
||||
];
|
||||
$find_str = str_replace(array_keys($replace), array_values($replace), self::ASSET_MATCHES[strtolower(SystemTarget::getTargetOS())]);
|
||||
foreach ($info['assets'] as $asset) {
|
||||
if ($asset['name'] === $find_str) {
|
||||
$download_url = $asset['browser_download_url'];
|
||||
$filename = $asset['name'];
|
||||
$version = ltrim($info['tag_name'], 'v');
|
||||
logger()->debug("Downloading hosted package-bin {$name} version {$version} from GitHub: {$download_url}");
|
||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
||||
$headers = $this->getGitHubTokenHeaders();
|
||||
default_shell()->executeCurlDownload($download_url, $path, headers: $headers, retries: $downloader->getRetry());
|
||||
return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version);
|
||||
}
|
||||
}
|
||||
throw new DownloaderException("No matching asset found for hosted package-bin {$name}: {$find_str}");
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,12 @@ namespace StaticPHP\Attribute\Package;
|
||||
* Indicates that the annotated class defines a PHP extension.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
|
||||
readonly class Extension
|
||||
class Extension
|
||||
{
|
||||
public function __construct(public string $name) {}
|
||||
public function __construct(public string $name)
|
||||
{
|
||||
if (!str_starts_with($name, 'ext-')) {
|
||||
$this->name = "ext-{$name}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
34
src/StaticPHP/Command/Dev/IsInstalledCommand.php
Normal file
34
src/StaticPHP/Command/Dev/IsInstalledCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
33
src/StaticPHP/Command/Dev/ShellCommand.php
Normal file
33
src/StaticPHP/Command/Dev/ShellCommand.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -27,6 +27,8 @@ class DownloadCommand extends BaseCommand
|
||||
$this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Fetch by libraries, e.g "libcares,openssl,onig"');
|
||||
$this->addOption('without-suggests', null, null, 'Do not fetch suggested sources when using --for-extensions');
|
||||
|
||||
$this->addOption('without-suggestions', null, null, '(deprecated) Do not fetch suggested sources when using --for-extensions');
|
||||
|
||||
// download command specific options
|
||||
$this->addOption('clean', null, null, 'Clean old download cache and source before fetch');
|
||||
$this->addOption('for-packages', null, InputOption::VALUE_REQUIRED, 'Fetch by packages, e.g "php,libssl,libcurl"');
|
||||
@@ -68,7 +70,8 @@ class DownloadCommand extends BaseCommand
|
||||
}
|
||||
|
||||
// resolve package dependencies and get artifacts directly
|
||||
$resolved = DependencyResolver::resolve($packages, [], !$this->getOption('without-suggests'));
|
||||
$suggests = !($this->getOption('without-suggests') || $this->getOption('without-suggestions'));
|
||||
$resolved = DependencyResolver::resolve($packages, [], $suggests);
|
||||
foreach ($resolved as $pkg_name) {
|
||||
$pkg = PackageLoader::getPackage($pkg_name);
|
||||
if ($artifact = $pkg->getArtifact()) {
|
||||
|
||||
@@ -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;
|
||||
@@ -29,14 +29,14 @@ class ExtractCommand extends BaseCommand
|
||||
$this->addOption('for-libs', 'l', InputOption::VALUE_REQUIRED, 'Extract artifacts for libraries, e.g "libcares,openssl"');
|
||||
$this->addOption('for-packages', null, InputOption::VALUE_REQUIRED, 'Extract artifacts for packages, e.g "php,libssl,libcurl"');
|
||||
$this->addOption('without-suggests', null, null, 'Do not include suggested packages when using --for-extensions');
|
||||
$this->addOption('force-source', null, null, 'Force extract source even if binary is available');
|
||||
$this->addOption('source-only', null, null, 'Force extract source even if binary is available');
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$cache = ApplicationContext::get(ArtifactCache::class);
|
||||
$extractor = new ArtifactExtractor($cache);
|
||||
$force_source = (bool) $this->getOption('force-source');
|
||||
$force_source = (bool) $this->getOption('source-only');
|
||||
|
||||
$artifacts = [];
|
||||
|
||||
@@ -59,6 +59,9 @@ class ExtractCommand extends BaseCommand
|
||||
$packages = array_map(fn ($x) => "ext-{$x}", parse_extension_list($exts));
|
||||
// Include php package when using for-extensions
|
||||
array_unshift($packages, 'php');
|
||||
array_unshift($packages, 'php-micro');
|
||||
array_unshift($packages, 'php-embed');
|
||||
array_unshift($packages, 'php-fpm');
|
||||
}
|
||||
if ($libs = $this->getOption('for-libs')) {
|
||||
$packages = array_merge($packages, parse_comma_list($libs));
|
||||
|
||||
@@ -8,7 +8,7 @@ use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand('install-pkg')]
|
||||
#[AsCommand('install-pkg', 'Install additional package', ['i', 'install-package'])]
|
||||
class InstallPackageCommand extends BaseCommand
|
||||
{
|
||||
public function configure()
|
||||
|
||||
@@ -18,6 +18,7 @@ class SPCConfigCommand extends BaseCommand
|
||||
{
|
||||
$this->addArgument('extensions', InputArgument::OPTIONAL, 'The extensions will be compiled, comma separated');
|
||||
$this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', '');
|
||||
$this->addOption('with-packages', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', '');
|
||||
$this->addOption('with-suggested-libs', 'L', null, 'Build with suggested libs for selected exts and libs');
|
||||
$this->addOption('with-suggests', null, null, 'Build with suggested packages for selected exts and libs');
|
||||
$this->addOption('with-suggested-exts', 'E', null, 'Build with suggested extensions for selected exts');
|
||||
@@ -31,15 +32,16 @@ class SPCConfigCommand extends BaseCommand
|
||||
public function handle(): int
|
||||
{
|
||||
// transform string to array
|
||||
$libraries = array_map('trim', array_filter(explode(',', $this->getOption('with-libs'))));
|
||||
$libraries = parse_comma_list($this->getOption('with-libs'));
|
||||
$libraries = array_merge($libraries, parse_comma_list($this->getOption('with-packages')));
|
||||
// transform string to array
|
||||
$extensions = $this->getArgument('extensions') ? parse_extension_list($this->getArgument('extensions')) : [];
|
||||
$include_suggests = $this->getOption('with-suggests') ?: $this->getOption('with-suggested-libs') || $this->getOption('with-suggested-exts');
|
||||
|
||||
$util = new SPCConfigUtil(options: [
|
||||
'no_php' => $this->getOption('no-php'),
|
||||
'libs_only_deps' => $this->getOption('libs-only-deps'),
|
||||
'absolute_libs' => $this->getOption('absolute-libs'),
|
||||
'no_php' => (bool) $this->getOption('no-php'),
|
||||
'libs_only_deps' => (bool) $this->getOption('libs-only-deps'),
|
||||
'absolute_libs' => (bool) $this->getOption('absolute-libs'),
|
||||
]);
|
||||
$packages = array_merge(array_map(fn ($x) => "ext-{$x}", $extensions), $libraries);
|
||||
$config = $util->config($packages, $include_suggests);
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -137,8 +137,14 @@ class ConfigValidator
|
||||
];
|
||||
continue;
|
||||
}
|
||||
// TODO: expand hosted to static-php hosted download urls
|
||||
if ($v === 'hosted') {
|
||||
$data[$name][$k] = [
|
||||
'linux-x86_64' => ['type' => 'hosted'],
|
||||
'linux-aarch64' => ['type' => 'hosted'],
|
||||
'windows-x86_64' => ['type' => 'hosted'],
|
||||
'macos-x86_64' => ['type' => 'hosted'],
|
||||
'macos-aarch64' => ['type' => 'hosted'],
|
||||
];
|
||||
continue;
|
||||
}
|
||||
if (is_assoc_array($v)) {
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -35,10 +41,11 @@ class ConsoleApplication extends Application
|
||||
// only add target that contains artifact.source
|
||||
if ($package->hasStage('build')) {
|
||||
logger()->debug("Registering build target command for package: {$name}");
|
||||
$this->add(new BuildTargetCommand($name));
|
||||
$this->addCommand(new BuildTargetCommand($name));
|
||||
}
|
||||
}
|
||||
|
||||
// add core commands
|
||||
$this->addCommands([
|
||||
new DownloadCommand(),
|
||||
new DoctorCommand(),
|
||||
@@ -46,6 +53,10 @@ class ConsoleApplication extends Application
|
||||
new BuildLibsCommand(),
|
||||
new ExtractCommand(),
|
||||
new SPCConfigCommand(),
|
||||
|
||||
// dev commands
|
||||
new ShellCommand(),
|
||||
new IsInstalledCommand(),
|
||||
]);
|
||||
|
||||
// add additional commands from registries
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -34,7 +35,7 @@ readonly class Doctor
|
||||
InteractiveTerm::notice('Starting doctor checks ...');
|
||||
}
|
||||
foreach ($this->getValidCheckList() as $check) {
|
||||
if (!$this->checkItem($check)) {
|
||||
if (!$this->checkItem($check, $interactive)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +48,7 @@ readonly class Doctor
|
||||
* @param CheckItem|string $check The check item to be checked
|
||||
* @return bool True if the check passed or was fixed, false otherwise
|
||||
*/
|
||||
public function checkItem(CheckItem|string $check): bool
|
||||
public function checkItem(CheckItem|string $check, bool $interactive = true): bool
|
||||
{
|
||||
if (is_string($check)) {
|
||||
$found = null;
|
||||
@@ -63,7 +64,8 @@ readonly class Doctor
|
||||
}
|
||||
$check = $found;
|
||||
}
|
||||
$this->output?->write("Checking <comment>{$check->item_name}</comment> ... ");
|
||||
$prepend = $interactive ? ' - ' : '';
|
||||
$this->output?->write("{$prepend}Checking <comment>{$check->item_name}</comment> ... ");
|
||||
|
||||
// call check
|
||||
$result = call_user_func($check->callback);
|
||||
|
||||
@@ -4,13 +4,21 @@ declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor\Item;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactCache;
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\ArtifactExtractor;
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
use StaticPHP\Attribute\Doctor\OptionalCheck;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Doctor\CheckResult;
|
||||
use StaticPHP\Runtime\Shell\Shell;
|
||||
use StaticPHP\Toolchain\Interface\ToolchainInterface;
|
||||
use StaticPHP\Toolchain\MuslToolchain;
|
||||
use StaticPHP\Toolchain\ZigToolchain;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\SourcePatcher;
|
||||
use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
#[OptionalCheck([self::class, 'optionalCheck'])]
|
||||
@@ -18,8 +26,8 @@ class LinuxMuslCheck
|
||||
{
|
||||
public static function optionalCheck(): bool
|
||||
{
|
||||
return getenv('SPC_TOOLCHAIN') === MuslToolchain::class ||
|
||||
(getenv('SPC_TOOLCHAIN') === ZigToolchain::class && !LinuxUtil::isMuslDist());
|
||||
$toolchain = ApplicationContext::get(ToolchainInterface::class);
|
||||
return $toolchain instanceof MuslToolchain || $toolchain instanceof ZigToolchain && !LinuxUtil::isMuslDist();
|
||||
}
|
||||
|
||||
/** @noinspection PhpUnused */
|
||||
@@ -51,23 +59,46 @@ class LinuxMuslCheck
|
||||
#[FixItem('fix-musl-wrapper')]
|
||||
public function fixMusl(): bool
|
||||
{
|
||||
// TODO: implement musl-wrapper installation
|
||||
// This should:
|
||||
// 1. Download musl source using Downloader::downloadSource()
|
||||
// 2. Extract the source using FileSystem::extractSource()
|
||||
// 3. Apply CVE patches using SourcePatcher::patchFile()
|
||||
// 4. Build and install musl wrapper
|
||||
// 5. Add path using putenv instead of editing /etc/profile
|
||||
return false;
|
||||
$downloader = new ArtifactDownloader();
|
||||
$downloader->add('musl-wrapper')->download(false);
|
||||
$extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class));
|
||||
$extractor->extract('musl-wrapper');
|
||||
|
||||
// Apply CVE-2025-26519 patch and install musl wrapper
|
||||
SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0001.patch', SOURCE_PATH . '/musl-wrapper');
|
||||
SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0002.patch', SOURCE_PATH . '/musl-wrapper');
|
||||
|
||||
$prefix = '';
|
||||
if (get_current_user() !== 'root') {
|
||||
$prefix = 'sudo ';
|
||||
logger()->warning('Current user is not root, using sudo for running command');
|
||||
}
|
||||
shell()->cd(SOURCE_PATH . '/musl-wrapper')
|
||||
->exec('CC=gcc CXX=g++ AR=ar LD=ld ./configure --disable-gcc-wrapper')
|
||||
->exec('CC=gcc CXX=g++ AR=ar LD=ld make -j')
|
||||
->exec("CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install");
|
||||
return true;
|
||||
}
|
||||
|
||||
#[FixItem('fix-musl-cross-make')]
|
||||
public function fixMuslCrossMake(): bool
|
||||
{
|
||||
// TODO: implement musl-cross-make installation
|
||||
// This should:
|
||||
// 1. Install musl-toolchain package using PackageManager::installPackage()
|
||||
// 2. Copy toolchain files to /usr/local/musl
|
||||
return false;
|
||||
// sudo
|
||||
$prefix = '';
|
||||
if (get_current_user() !== 'root') {
|
||||
$prefix = 'sudo ';
|
||||
logger()->warning('Current user is not root, using sudo for running command');
|
||||
}
|
||||
Shell::passthruCallback(function () {
|
||||
InteractiveTerm::advance();
|
||||
});
|
||||
$downloader = new ArtifactDownloader();
|
||||
$extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class));
|
||||
$downloader->add('musl-toolchain')->download(false);
|
||||
$extractor->extract('musl-toolchain');
|
||||
$pkg_root = PKG_ROOT_PATH . '/musl-toolchain';
|
||||
shell()->exec("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl");
|
||||
FileSystem::removeDir($pkg_root);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
147
src/StaticPHP/Doctor/Item/LinuxToolCheck.php
Normal file
147
src/StaticPHP/Doctor/Item/LinuxToolCheck.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
46
src/StaticPHP/Doctor/Item/ZigCheck.php
Normal file
46
src/StaticPHP/Doctor/Item/ZigCheck.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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) . '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
src/StaticPHP/Exception/RegistryException.php
Normal file
7
src/StaticPHP/Exception/RegistryException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Exception;
|
||||
|
||||
class RegistryException extends SPCException {}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,9 +9,11 @@ use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Runtime\Shell\Shell;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
class PackageBuilder
|
||||
{
|
||||
@@ -85,6 +87,92 @@ class PackageBuilder
|
||||
return $this->options[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deploy the binary file from src to dst.
|
||||
*/
|
||||
public function deployBinary(string $src, string $dst, bool $executable = true): string
|
||||
{
|
||||
logger()->debug("Deploying binary from {$src} to {$dst}");
|
||||
|
||||
// file must exists
|
||||
if (!file_exists($src)) {
|
||||
throw new SPCInternalException("Deploy failed. Cannot find file: {$src}");
|
||||
}
|
||||
// dst dir must exists
|
||||
FileSystem::createDir(dirname($dst));
|
||||
|
||||
// ignore copy to self
|
||||
if (realpath($src) !== realpath($dst)) {
|
||||
shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg($dst));
|
||||
}
|
||||
|
||||
// file exist
|
||||
if (!file_exists($dst)) {
|
||||
throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}");
|
||||
}
|
||||
|
||||
// extract debug info
|
||||
$this->extractDebugInfo($dst);
|
||||
|
||||
// strip
|
||||
if (!$this->getOption('no-strip')) {
|
||||
$this->stripBinary($dst);
|
||||
}
|
||||
|
||||
// UPX for linux
|
||||
$upx_option = $this->getOption('with-upx-pack');
|
||||
if ($upx_option && SystemTarget::getTargetOS() === 'Linux' && $executable) {
|
||||
if ($this->getOption('no-strip')) {
|
||||
logger()->warning('UPX compression is not recommended when --no-strip is enabled.');
|
||||
}
|
||||
logger()->info("Compressing {$dst} with UPX");
|
||||
shell()->exec(getenv('UPX_EXEC') . " --best {$dst}");
|
||||
}
|
||||
|
||||
return $dst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract debug information from binary file.
|
||||
*
|
||||
* @param string $binary_path the path to the binary file, including executables, shared libraries, etc
|
||||
*/
|
||||
public function extractDebugInfo(string $binary_path): string
|
||||
{
|
||||
$target_dir = BUILD_ROOT_PATH . '/debug';
|
||||
FileSystem::createDir($target_dir);
|
||||
$basename = basename($binary_path);
|
||||
$debug_file = "{$target_dir}/{$basename}" . (SystemTarget::getTargetOS() === 'Darwin' ? '.dwarf' : '.debug');
|
||||
if (SystemTarget::getTargetOS() === 'Darwin') {
|
||||
shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}");
|
||||
} elseif (SystemTarget::getTargetOS() === 'Linux') {
|
||||
if ($eu_strip = LinuxUtil::findCommand('eu-strip')) {
|
||||
shell()
|
||||
->exec("{$eu_strip} -f {$debug_file} {$binary_path}")
|
||||
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
|
||||
} else {
|
||||
shell()
|
||||
->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}")
|
||||
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
|
||||
}
|
||||
} else {
|
||||
throw new SPCInternalException('extractDebugInfo is only supported on Linux and macOS');
|
||||
}
|
||||
return $debug_file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip unneeded symbols from binary file.
|
||||
*/
|
||||
public function stripBinary(string $binary_path): void
|
||||
{
|
||||
shell()->exec(match (SystemTarget::getTargetOS()) {
|
||||
'Darwin' => "strip -S {$binary_path}",
|
||||
'Linux' => "strip --strip-unneeded {$binary_path}",
|
||||
default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'),
|
||||
});
|
||||
}
|
||||
|
||||
private function installLicense(Package $package, array $license): void
|
||||
{
|
||||
$dir = BUILD_ROOT_PATH . '/source-licenses/' . $package->getName();
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -195,19 +212,49 @@ class PackageInstaller
|
||||
|
||||
/**
|
||||
* Get all resolved packages.
|
||||
* You can filter by package type class if needed.
|
||||
*
|
||||
* @return array<string, Package>
|
||||
* @template T
|
||||
* @param class-string<T> $package_type Filter by package type
|
||||
* @return array<T>
|
||||
*/
|
||||
public function getResolvedPackages(): array
|
||||
public function getResolvedPackages(mixed $package_type = Package::class): array
|
||||
{
|
||||
return $this->packages;
|
||||
return array_filter($this->packages, function (Package $pkg) use ($package_type): bool {
|
||||
return $pkg instanceof $package_type;
|
||||
});
|
||||
}
|
||||
|
||||
public function isPackageBeingResolved(string $package_name): bool
|
||||
public function isPackageResolved(string $package_name): bool
|
||||
{
|
||||
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.
|
||||
*
|
||||
@@ -357,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
|
||||
{
|
||||
@@ -407,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;
|
||||
@@ -446,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()) {
|
||||
@@ -107,4 +127,152 @@ class PhpExtensionPackage extends Package
|
||||
{
|
||||
return $this->build_with_php;
|
||||
}
|
||||
|
||||
public function buildShared(): void
|
||||
{
|
||||
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)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor;
|
||||
namespace StaticPHP\Registry;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -232,7 +243,7 @@ class PackageLoader
|
||||
$installer = ApplicationContext::get(PackageInstaller::class);
|
||||
$stages = self::$before_stages[$package_name][$stage] ?? [];
|
||||
foreach ($stages as [$callback, $only_when_package_resolved]) {
|
||||
if ($only_when_package_resolved !== null && !$installer->isPackageBeingResolved($only_when_package_resolved)) {
|
||||
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
|
||||
continue;
|
||||
}
|
||||
yield $callback;
|
||||
@@ -243,10 +254,10 @@ 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->isPackageBeingResolved($only_when_package_resolved)) {
|
||||
if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) {
|
||||
continue;
|
||||
}
|
||||
$result[] = $callback;
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -87,7 +84,11 @@ class Registry
|
||||
if (isset($data['package']['config']) && is_array($data['package']['config'])) {
|
||||
foreach ($data['package']['config'] as $path) {
|
||||
$path = self::fullpath($path, dirname($registry_file));
|
||||
PackageConfig::loadFromFile($path);
|
||||
if (is_file($path)) {
|
||||
PackageConfig::loadFromFile($path);
|
||||
} elseif (is_dir($path)) {
|
||||
PackageConfig::loadFromDir($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +96,11 @@ class Registry
|
||||
if (isset($data['artifact']['config']) && is_array($data['artifact']['config'])) {
|
||||
foreach ($data['artifact']['config'] as $path) {
|
||||
$path = self::fullpath($path, dirname($registry_file));
|
||||
ArtifactConfig::loadFromFile($path);
|
||||
if (is_file($path)) {
|
||||
ArtifactConfig::loadFromFile($path);
|
||||
} elseif (is_dir($path)) {
|
||||
ArtifactConfig::loadFromDir($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,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.
|
||||
*
|
||||
@@ -232,7 +247,7 @@ class Registry
|
||||
*/
|
||||
private static function requireClassFile(string $class, ?string $file_path, string $base_path, bool $auto_require): void
|
||||
{
|
||||
if (!$auto_require || class_exists($class, true)) {
|
||||
if (!$auto_require || class_exists($class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -244,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" .
|
||||
@@ -252,13 +267,19 @@ class Registry
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return full path, resolving relative paths against a base path.
|
||||
*
|
||||
* @param string $path Input path (relative or absolute)
|
||||
* @param string $relative_path_base Base path for relative paths
|
||||
*/
|
||||
private static function fullpath(string $path, string $relative_path_base): string
|
||||
{
|
||||
if (FileSystem::isRelativePath($path)) {
|
||||
$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);
|
||||
}
|
||||
|
||||
95
src/StaticPHP/Util/DirDiff.php
Normal file
95
src/StaticPHP/Util/DirDiff.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Util;
|
||||
|
||||
/**
|
||||
* A util class to diff directory file increments.
|
||||
*/
|
||||
class DirDiff
|
||||
{
|
||||
protected array $before = [];
|
||||
|
||||
protected array $before_file_hashes = [];
|
||||
|
||||
public function __construct(protected string $dir, protected bool $track_content_changes = false)
|
||||
{
|
||||
$this->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the baseline to current state.
|
||||
*/
|
||||
public function reset(): void
|
||||
{
|
||||
$this->before = FileSystem::scanDirFiles($this->dir, relative: true) ?: [];
|
||||
|
||||
if ($this->track_content_changes) {
|
||||
$this->before_file_hashes = [];
|
||||
foreach ($this->before as $file) {
|
||||
$this->before_file_hashes[$file] = md5_file($this->dir . DIRECTORY_SEPARATOR . $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of incremented files.
|
||||
*
|
||||
* @param bool $relative Return relative paths or absolute paths
|
||||
* @return array<string> List of incremented files
|
||||
*/
|
||||
public function getIncrementFiles(bool $relative = false): array
|
||||
{
|
||||
$after = FileSystem::scanDirFiles($this->dir, relative: true) ?: [];
|
||||
$diff = array_diff($after, $this->before);
|
||||
if ($relative) {
|
||||
return $diff;
|
||||
}
|
||||
return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $diff);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of changed files (including new files).
|
||||
*
|
||||
* @param bool $relative Return relative paths or absolute paths
|
||||
* @param bool $include_new_files Include new files as changed files
|
||||
* @return array<string> List of changed files
|
||||
*/
|
||||
public function getChangedFiles(bool $relative = false, bool $include_new_files = true): array
|
||||
{
|
||||
$after = FileSystem::scanDirFiles($this->dir, relative: true) ?: [];
|
||||
$changed = [];
|
||||
foreach ($after as $file) {
|
||||
if (isset($this->before_file_hashes[$file])) {
|
||||
$after_hash = md5_file($this->dir . DIRECTORY_SEPARATOR . $file);
|
||||
if ($after_hash !== $this->before_file_hashes[$file]) {
|
||||
$changed[] = $file;
|
||||
}
|
||||
} elseif ($include_new_files) {
|
||||
// New file, consider as changed
|
||||
$changed[] = $file;
|
||||
}
|
||||
}
|
||||
if ($relative) {
|
||||
return $changed;
|
||||
}
|
||||
return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $changed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of removed files.
|
||||
*
|
||||
* @param bool $relative Return relative paths or absolute paths
|
||||
* @return array<string> List of removed files
|
||||
*/
|
||||
public function getRemovedFiles(bool $relative = false): array
|
||||
{
|
||||
$after = FileSystem::scanDirFiles($this->dir, relative: true) ?: [];
|
||||
$removed = array_diff($this->before, $after);
|
||||
if ($relative) {
|
||||
return $removed;
|
||||
}
|
||||
return array_map(fn ($f) => $this->dir . DIRECTORY_SEPARATOR . $f, $removed);
|
||||
}
|
||||
}
|
||||
@@ -370,17 +370,6 @@ class FileSystem
|
||||
self::createDir($dir_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add source extraction hook
|
||||
*
|
||||
* @param string $name Source name
|
||||
* @param callable $callback Callback function
|
||||
*/
|
||||
public static function addSourceExtractHook(string $name, callable $callback): void
|
||||
{
|
||||
self::$_extract_hook[$name][] = $callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path is relative
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace StaticPHP\Util;
|
||||
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Package\PhpExtensionPackage;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
|
||||
class SPCConfigUtil
|
||||
@@ -99,26 +101,21 @@ class SPCConfigUtil
|
||||
* [Helper function]
|
||||
* Get configuration for a specific extension(s) dependencies.
|
||||
*
|
||||
* @param Extension|Extension[] $extension Extension instance or list
|
||||
* @param bool $include_suggest_ext Whether to include suggested extensions
|
||||
* @param bool $include_suggest_lib Whether to include suggested libraries
|
||||
* @param array|PhpExtensionPackage $extension_packages Extension instance or list
|
||||
* @return array{
|
||||
* cflags: string,
|
||||
* ldflags: string,
|
||||
* libs: string
|
||||
* }
|
||||
*/
|
||||
public function getExtensionConfig(array|Extension $extension, bool $include_suggest_ext = false, bool $include_suggest_lib = false): array
|
||||
public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array
|
||||
{
|
||||
if (!is_array($extension)) {
|
||||
$extension = [$extension];
|
||||
if (!is_array($extension_packages)) {
|
||||
$extension_packages = [$extension_packages];
|
||||
}
|
||||
$libs = array_map(fn ($y) => $y->getName(), array_merge(...array_map(fn ($x) => $x->getLibraryDependencies(true), $extension)));
|
||||
return $this->config(
|
||||
extensions: array_map(fn ($x) => $x->getName(), $extension),
|
||||
libraries: $libs,
|
||||
include_suggest_ext: $include_suggest_ext ?: $this->builder?->getOption('with-suggested-exts') ?? false,
|
||||
include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false,
|
||||
packages: array_map(fn ($y) => $y->getName(), $extension_packages),
|
||||
include_suggests: $include_suggests,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -126,15 +123,15 @@ class SPCConfigUtil
|
||||
* [Helper function]
|
||||
* Get configuration for a specific library(s) dependencies.
|
||||
*
|
||||
* @param LibraryBase|LibraryBase[] $lib Library instance or list
|
||||
* @param bool $include_suggest_lib Whether to include suggested libraries
|
||||
* @param array|LibraryPackage $lib Library instance or list
|
||||
* @param bool $include_suggests Whether to include suggested libraries
|
||||
* @return array{
|
||||
* cflags: string,
|
||||
* ldflags: string,
|
||||
* libs: string
|
||||
* }
|
||||
*/
|
||||
public function getLibraryConfig(array|LibraryBase $lib, bool $include_suggest_lib = false): array
|
||||
public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array
|
||||
{
|
||||
if (!is_array($lib)) {
|
||||
$lib = [$lib];
|
||||
@@ -144,8 +141,8 @@ class SPCConfigUtil
|
||||
$save_libs_only_deps = $this->libs_only_deps;
|
||||
$this->libs_only_deps = true;
|
||||
$ret = $this->config(
|
||||
libraries: array_map(fn ($x) => $x->getName(), $lib),
|
||||
include_suggest_lib: $include_suggest_lib ?: $this->builder?->getOption('with-suggested-libs') ?? false,
|
||||
packages: array_map(fn ($y) => $y->getName(), $lib),
|
||||
include_suggests: $include_suggests,
|
||||
);
|
||||
$this->no_php = $save_no_php;
|
||||
$this->libs_only_deps = $save_libs_only_deps;
|
||||
@@ -228,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') {
|
||||
|
||||
@@ -159,4 +159,39 @@ class SourcePatcher
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Patch micro SAPI to support compressed phar loading from the current executable.
|
||||
*
|
||||
* @param int $version_id PHP version ID
|
||||
*/
|
||||
public static function patchMicroPhar(int $version_id): void
|
||||
{
|
||||
FileSystem::backupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c');
|
||||
FileSystem::replaceFileStr(
|
||||
SOURCE_PATH . '/php-src/ext/phar/phar.c',
|
||||
'static zend_op_array *phar_compile_file',
|
||||
"char *micro_get_filename(void);\n\nstatic zend_op_array *phar_compile_file"
|
||||
);
|
||||
if ($version_id < 80100) {
|
||||
// PHP 8.0.x
|
||||
FileSystem::replaceFileStr(
|
||||
SOURCE_PATH . '/php-src/ext/phar/phar.c',
|
||||
'if (strstr(file_handle->filename, ".phar") && !strstr(file_handle->filename, "://")) {',
|
||||
'if ((strstr(file_handle->filename, micro_get_filename()) || strstr(file_handle->filename, ".phar")) && !strstr(file_handle->filename, "://")) {'
|
||||
);
|
||||
} else {
|
||||
// PHP >= 8.1
|
||||
FileSystem::replaceFileStr(
|
||||
SOURCE_PATH . '/php-src/ext/phar/phar.c',
|
||||
'if (strstr(ZSTR_VAL(file_handle->filename), ".phar") && !strstr(ZSTR_VAL(file_handle->filename), "://")) {',
|
||||
'if ((strstr(ZSTR_VAL(file_handle->filename), micro_get_filename()) || strstr(ZSTR_VAL(file_handle->filename), ".phar")) && !strstr(ZSTR_VAL(file_handle->filename), "://")) {'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public static function unpatchMicroPhar(): void
|
||||
{
|
||||
FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,14 @@ class LinuxUtil extends UnixUtil
|
||||
* Get current linux distro name and version.
|
||||
*
|
||||
* @noinspection PhpMissingBreakStatementInspection
|
||||
* @return array{dist: string, ver: string} Linux distro info (unknown if not found)
|
||||
* @return array{dist: string, ver: string, family: string} Linux distro info (unknown if not found)
|
||||
*/
|
||||
public static function getOSRelease(): array
|
||||
{
|
||||
$ret = [
|
||||
'dist' => 'unknown',
|
||||
'ver' => 'unknown',
|
||||
'family' => 'unknown',
|
||||
];
|
||||
switch (true) {
|
||||
case file_exists('/etc/centos-release'):
|
||||
@@ -44,6 +45,9 @@ class LinuxUtil extends UnixUtil
|
||||
if (preg_match('/^ID=(.*)$/', $line, $matches)) {
|
||||
$ret['dist'] = $matches[1];
|
||||
}
|
||||
if (preg_match('/^ID_LIKE=(.*)$/', $line, $matches)) {
|
||||
$ret['family'] = $matches[1];
|
||||
}
|
||||
if (preg_match('/^VERSION_ID=(.*)$/', $line, $matches)) {
|
||||
$ret['ver'] = $matches[1];
|
||||
}
|
||||
@@ -91,7 +95,7 @@ class LinuxUtil extends UnixUtil
|
||||
{
|
||||
return [
|
||||
// debian-like
|
||||
'debian', 'ubuntu', 'Deepin',
|
||||
'debian', 'ubuntu', 'Deepin', 'neon',
|
||||
// rhel-like
|
||||
'redhat',
|
||||
// centos
|
||||
@@ -103,6 +107,16 @@ class LinuxUtil extends UnixUtil
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current linux distro is debian-based.
|
||||
*/
|
||||
public static function isDebianDist(): bool
|
||||
{
|
||||
$dist = static::getOSRelease()['dist'];
|
||||
$family = explode(' ', static::getOSRelease()['family']);
|
||||
return in_array($dist, ['debian', 'ubuntu', 'Deepin', 'neon']) || in_array('debian', $family);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get libc version string from ldd.
|
||||
*/
|
||||
|
||||
@@ -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
60
src/globals/scripts/zig-cc.sh
Executable 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[@]}"
|
||||
@@ -13,9 +13,9 @@ declare(strict_types=1);
|
||||
|
||||
// test php version (8.1 ~ 8.4 available, multiple for matrix)
|
||||
$test_php_version = [
|
||||
'8.1',
|
||||
'8.2',
|
||||
'8.3',
|
||||
// '8.1',
|
||||
// '8.2',
|
||||
// '8.3',
|
||||
'8.4',
|
||||
'8.5',
|
||||
// 'git',
|
||||
@@ -25,17 +25,17 @@ $test_php_version = [
|
||||
$test_os = [
|
||||
'macos-15-intel', // bin/spc for x86_64
|
||||
'macos-15', // bin/spc for arm64
|
||||
'ubuntu-latest', // bin/spc-alpine-docker for x86_64
|
||||
// 'ubuntu-latest', // bin/spc-alpine-docker for x86_64
|
||||
'ubuntu-22.04', // bin/spc-gnu-docker for x86_64
|
||||
'ubuntu-24.04', // bin/spc for x86_64
|
||||
'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64
|
||||
// 'ubuntu-24.04', // bin/spc for x86_64
|
||||
// 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64
|
||||
'ubuntu-24.04-arm', // bin/spc for arm64
|
||||
// 'windows-2022', // .\bin\spc.ps1
|
||||
// 'windows-2025',
|
||||
];
|
||||
|
||||
// whether enable thread safe
|
||||
$zts = false;
|
||||
$zts = true;
|
||||
|
||||
$no_strip = false;
|
||||
|
||||
@@ -50,14 +50,14 @@ $prefer_pre_built = false;
|
||||
|
||||
// If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`).
|
||||
$extensions = match (PHP_OS_FAMILY) {
|
||||
'Linux', 'Darwin' => 'maxminddb',
|
||||
'Linux', 'Darwin' => 'bcmath',
|
||||
'Windows' => 'bcmath',
|
||||
};
|
||||
|
||||
// If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`).
|
||||
$shared_extensions = match (PHP_OS_FAMILY) {
|
||||
'Linux' => '',
|
||||
'Darwin' => '',
|
||||
'Linux' => 'pcov',
|
||||
'Darwin' => 'pcov',
|
||||
'Windows' => '',
|
||||
};
|
||||
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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']));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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']));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
303
tests/StaticPHP/Config/ArtifactConfigTest.php
Normal file
303
tests/StaticPHP/Config/ArtifactConfigTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
196
tests/StaticPHP/Config/ConfigTypeTest.php
Normal file
196
tests/StaticPHP/Config/ConfigTypeTest.php
Normal 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));
|
||||
}
|
||||
}
|
||||
627
tests/StaticPHP/Config/ConfigValidatorTest.php
Normal file
627
tests/StaticPHP/Config/ConfigValidatorTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
434
tests/StaticPHP/Config/PackageConfigTest.php
Normal file
434
tests/StaticPHP/Config/PackageConfigTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user