diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 132b33f00..6c2730fdb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -2,7 +2,7 @@ name: Tests on: pull_request: - branches: [ "main" ] + branches: [ "main", "v3" ] types: [ opened, synchronize, reopened ] paths: - 'src/**' diff --git a/.gitignore b/.gitignore index 0d2bd5540..d33eae535 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ .idea -runtime/ -docker/libraries/ -docker/extensions/ -docker/source/ +/runtime/ +/docker/libraries/ +/docker/extensions/ +/docker/source/ # Vendor files /vendor/** diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index d1c30090e..b9eb063ef 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -69,6 +69,6 @@ '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()); diff --git a/bin/spc b/bin/spc index 18dd3a383..9bac188ae 100755 --- a/bin/spc +++ b/bin/spc @@ -1,13 +1,9 @@ #!/usr/bin/env php run(); -} catch (Exception $e) { - ExceptionHandler::getInstance()->handle($e); +} catch (SPCException $e) { + ExceptionHandler::handleSPCException($e); + exit(1); +} catch (\Throwable $e) { + ExceptionHandler::handleDefaultException($e); exit(1); } + diff --git a/bin/spc-alpine-docker b/bin/spc-alpine-docker index 2790a5c34..2640ffbad 100755 --- a/bin/spc-alpine-docker +++ b/bin/spc-alpine-docker @@ -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 diff --git a/bin/spc-debug b/bin/spc-debug new file mode 100755 index 000000000..d5a18c837 --- /dev/null +++ b/bin/spc-debug @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +# This script runs the 'spc' command with Xdebug enabled for debugging purposes. +php -d xdebug.mode=debug -d xdebug.client_host=127.0.0.1 -d xdebug.client_port=9003 -d xdebug.start_with_request=yes "$(dirname "$0")/../bin/spc" "$@" diff --git a/captainhook.json b/captainhook.json index 77be1d571..233e387eb 100644 --- a/captainhook.json +++ b/captainhook.json @@ -1,44 +1,44 @@ -{ - "pre-push": { - "enabled": true, - "actions": [ - { - "action": "composer analyse" - } - ] - }, - "pre-commit": { - "enabled": true, - "actions": [ - { - "action": "composer cs-fix -- --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}", - "conditions": [ - { - "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", - "args": ["php"] - } - ] - } - ] - }, - "post-change": { - "enabled": true, - "actions": [ - { - "action": "composer install", - "options": [], - "conditions": [ - { - "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any", - "args": [ - [ - "composer.json", - "composer.lock" - ] - ] - } - ] - } - ] - } -} +{ + "pre-push": { + "enabled": true, + "actions": [ + { + "action": "php vendor/bin/phpstan analyse --memory-limit 300M" + } + ] + }, + "pre-commit": { + "enabled": true, + "actions": [ + { + "action": "php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php} --sequential", + "conditions": [ + { + "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", + "args": ["php"] + } + ] + } + ] + }, + "post-change": { + "enabled": true, + "actions": [ + { + "action": "composer install", + "options": [], + "conditions": [ + { + "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any", + "args": [ + [ + "composer.json", + "composer.lock" + ] + ] + } + ] + } + ] + } +} diff --git a/composer.json b/composer.json index 1a1005c73..fb17f9a0d 100644 --- a/composer.json +++ b/composer.json @@ -9,14 +9,16 @@ } ], "require": { - "php": ">= 8.3", + "php": ">=8.4", "ext-mbstring": "*", "ext-zlib": "*", - "laravel/prompts": "^0.1.12", + "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", "symfony/yaml": "^7.2", - "zhamao/logger": "^1.1.3" + "zhamao/logger": "^1.1.4" }, "require-dev": { "captainhook/captainhook-phar": "^5.23", @@ -28,7 +30,9 @@ }, "autoload": { "psr-4": { - "SPC\\": "src/SPC" + "SPC\\": "src/SPC", + "StaticPHP\\": "src/StaticPHP", + "Package\\": "src/Package" }, "files": [ "src/globals/defines.php", @@ -37,7 +41,7 @@ }, "autoload-dev": { "psr-4": { - "SPC\\Tests\\": "tests/SPC" + "Tests\\StaticPHP\\": "tests/StaticPHP" } }, "bin": [ diff --git a/composer.lock b/composer.lock index e8f320c7c..a0538ce8e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,90 +4,100 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f81132977eb1310f5ccb27c8de76c8d2", + "content-hash": "edb3243ddaa8b05d8f6545266a146e93", "packages": [ { - "name": "illuminate/collections", - "version": "v11.46.1", + "name": "laravel/prompts", + "version": "v0.3.8", "source": { "type": "git", - "url": "https://github.com/illuminate/collections.git", - "reference": "856b1da953e46281ba61d7c82d337072d3ee1825" + "url": "https://github.com/laravel/prompts.git", + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/856b1da953e46281ba61d7c82d337072d3ee1825", - "reference": "856b1da953e46281ba61d7c82d337072d3ee1825", + "url": "https://api.github.com/repos/laravel/prompts/zipball/096748cdfb81988f60090bbb839ce3205ace0d35", + "reference": "096748cdfb81988f60090bbb839ce3205ace0d35", "shasum": "" }, "require": { - "illuminate/conditionable": "^11.0", - "illuminate/contracts": "^11.0", - "illuminate/macroable": "^11.0", - "php": "^8.2" + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" }, "suggest": { - "symfony/var-dumper": "Required to use the dump method (^7.0)." + "ext-pcntl": "Required for the spinner to be animated." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-main": "0.3.x-dev" } }, "autoload": { "files": [ - "functions.php", - "helpers.php" + "src/helpers.php" ], "psr-4": { - "Illuminate\\Support\\": "" + "Laravel\\Prompts\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Collections package.", - "homepage": "https://laravel.com", + "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.8" }, - "time": "2025-03-24T11:54:20+00:00" + "time": "2025-11-21T20:52:52+00:00" }, { - "name": "illuminate/conditionable", - "version": "v11.46.1", + "name": "laravel/serializable-closure", + "version": "v2.0.7", "source": { "type": "git", - "url": "https://github.com/illuminate/conditionable.git", - "reference": "319b717e0587bd7c8a3b44464f0e27867b4bcda9" + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/conditionable/zipball/319b717e0587bd7c8a3b44464f0e27867b4bcda9", - "reference": "319b717e0587bd7c8a3b44464f0e27867b4bcda9", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/cb291e4c998ac50637c7eeb58189c14f5de5b9dd", + "reference": "cb291e4c998ac50637c7eeb58189c14f5de5b9dd", "shasum": "" }, "require": { - "php": "^8.0.2" + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "11.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "Illuminate\\Support\\": "" + "Laravel\\SerializableClosure\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -98,167 +108,312 @@ { "name": "Taylor Otwell", "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" } ], - "description": "The Illuminate Conditionable package.", - "homepage": "https://laravel.com", + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" }, - "time": "2025-03-24T11:54:20+00:00" + "time": "2025-11-21T20:52:36+00:00" }, { - "name": "illuminate/contracts", - "version": "v11.46.1", + "name": "nette/php-generator", + "version": "v4.2.0", "source": { "type": "git", - "url": "https://github.com/illuminate/contracts.git", - "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8" + "url": "https://github.com/nette/php-generator.git", + "reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/4b2a67d1663f50085bc91e6371492697a5d2d4e8", - "reference": "4b2a67d1663f50085bc91e6371492697a5d2d4e8", + "url": "https://api.github.com/repos/nette/php-generator/zipball/4707546a1f11badd72f5d82af4f8a6bc64bd56ac", + "reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac", "shasum": "" }, "require": { - "php": "^8.2", - "psr/container": "^1.1.1|^2.0.1", - "psr/simple-cache": "^1.0|^2.0|^3.0" + "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": "11.x-dev" + "dev-master": "4.2-dev" } }, "autoload": { "psr-4": { - "Illuminate\\Contracts\\": "" - } + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" } ], - "description": "The Illuminate Contracts package.", - "homepage": "https://laravel.com", + "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/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/nette/php-generator/issues", + "source": "https://github.com/nette/php-generator/tree/v4.2.0" }, - "time": "2025-03-24T11:54:20+00:00" + "time": "2025-08-06T18:24:31+00:00" }, { - "name": "illuminate/macroable", - "version": "v11.46.1", + "name": "nette/utils", + "version": "v4.1.0", "source": { "type": "git", - "url": "https://github.com/illuminate/macroable.git", - "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed" + "url": "https://github.com/nette/utils.git", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/macroable/zipball/e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", - "reference": "e1cb9e51b9ed5d3c9bc1ab431d0a52fe42a990ed", + "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", "shasum": "" }, "require": { - "php": "^8.2" + "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": "11.x-dev" + "dev-master": "4.1-dev" } }, "autoload": { "psr-4": { - "Illuminate\\Support\\": "" - } + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" ], "authors": [ { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" } ], - "description": "The Illuminate Macroable package.", - "homepage": "https://laravel.com", + "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/laravel/framework/issues", - "source": "https://github.com/laravel/framework" + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.0" }, - "time": "2024-06-28T20:10:30+00:00" + "time": "2025-12-01T17:49:23+00:00" }, { - "name": "laravel/prompts", - "version": "v0.1.25", + "name": "php-di/invoker", + "version": "2.3.7", "source": { "type": "git", - "url": "https://github.com/laravel/prompts.git", - "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95" + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/7b4029a84c37cb2725fc7f011586e2997040bc95", - "reference": "7b4029a84c37cb2725fc7f011586e2997040bc95", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1", "shasum": "" }, "require": { - "ext-mbstring": "*", - "illuminate/collections": "^10.0|^11.0", - "php": "^8.1", - "symfony/console": "^6.2|^7.0" + "php": ">=7.3", + "psr/container": "^1.0|^2.0" }, - "conflict": { - "illuminate/console": ">=10.17.0 <10.25.0", - "laravel/framework": ">=10.17.0 <10.25.0" + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.7" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2025-08-30T10:22:22+00:00" + }, + { + "name": "php-di/php-di", + "version": "7.1.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352", + "shasum": "" + }, + "require": { + "laravel/serializable-closure": "^1.0 || ^2.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" }, "require-dev": { - "mockery/mockery": "^1.5", - "pestphp/pest": "^2.3", - "phpstan/phpstan": "^1.11", - "phpstan/phpstan-mockery": "^1.1" + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" }, "suggest": { - "ext-pcntl": "Required for the spinner to be animated." + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "0.1.x-dev" - } - }, "autoload": { "files": [ - "src/helpers.php" + "src/functions.php" ], "psr-4": { - "Laravel\\Prompts\\": "src/" + "DI\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Add beautiful and user-friendly forms to your command-line applications.", + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], "support": { - "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.1.25" + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1" }, - "time": "2024-08-12T22:06:33+00:00" + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2025-08-16T11:10:48+00:00" }, { "name": "psr/container", @@ -363,69 +518,18 @@ }, "time": "2024-09-11T13:17:53+00:00" }, - { - "name": "psr/simple-cache", - "version": "3.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/simple-cache.git", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", - "shasum": "" - }, - "require": { - "php": ">=8.0.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\SimpleCache\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interfaces for simple caching", - "keywords": [ - "cache", - "caching", - "psr", - "psr-16", - "simple-cache" - ], - "support": { - "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" - }, - "time": "2021-10-29T13:26:27+00:00" - }, { "name": "symfony/console", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a" + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", - "reference": "c28ad91448f86c5f6d9d2c70f0cf68bf135f252a", + "url": "https://api.github.com/repos/symfony/console/zipball/0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", + "reference": "0bc0f45254b99c58d45a8fbf9fb955d46cbd1bb8", "shasum": "" }, "require": { @@ -433,7 +537,7 @@ "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^7.2" + "symfony/string": "^7.2|^8.0" }, "conflict": { "symfony/dependency-injection": "<6.4", @@ -447,16 +551,16 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/event-dispatcher": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^6.4|^7.0", - "symfony/messenger": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/stopwatch": "^6.4|^7.0", - "symfony/var-dumper": "^6.4|^7.0" + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -490,7 +594,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.6" + "source": "https://github.com/symfony/console/tree/v7.4.0" }, "funding": [ { @@ -510,7 +614,7 @@ "type": "tidelift" } ], - "time": "2025-11-04T01:21:42+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/deprecation-contracts", @@ -916,16 +1020,16 @@ }, { "name": "symfony/process", - "version": "v7.3.4", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b" + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/f24f8f316367b30810810d4eb30c543d7003ff3b", - "reference": "f24f8f316367b30810810d4eb30c543d7003ff3b", + "url": "https://api.github.com/repos/symfony/process/zipball/7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", + "reference": "7ca8dc2d0dcf4882658313aba8be5d9fd01026c8", "shasum": "" }, "require": { @@ -957,7 +1061,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.3.4" + "source": "https://github.com/symfony/process/tree/v7.4.0" }, "funding": [ { @@ -977,7 +1081,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:12:26+00:00" + "time": "2025-10-16T11:21:06+00:00" }, { "name": "symfony/service-contracts", @@ -1068,34 +1172,34 @@ }, { "name": "symfony/string", - "version": "v7.3.4", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f96476035142921000338bad71e5247fbc138872" + "reference": "f929eccf09531078c243df72398560e32fa4cf4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", - "reference": "f96476035142921000338bad71e5247fbc138872", + "url": "https://api.github.com/repos/symfony/string/zipball/f929eccf09531078c243df72398560e32fa4cf4f", + "reference": "f929eccf09531078c243df72398560e32fa4cf4f", "shasum": "" }, "require": { - "php": ">=8.2", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0" + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "conflict": { "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/emoji": "^7.1", - "symfony/http-client": "^6.4|^7.0", - "symfony/intl": "^6.4|^7.0", + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/var-exporter": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -1134,7 +1238,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.4" + "source": "https://github.com/symfony/string/tree/v8.0.0" }, "funding": [ { @@ -1154,32 +1258,32 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:36:48+00:00" + "time": "2025-09-11T14:37:55+00:00" }, { "name": "symfony/yaml", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc" + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/90208e2fc6f68f613eae7ca25a2458a931b1bacc", - "reference": "90208e2fc6f68f613eae7ca25a2458a931b1bacc", + "url": "https://api.github.com/repos/symfony/yaml/zipball/6c84a4b55aee4cd02034d1c528e83f69ddf63810", + "reference": "6c84a4b55aee4cd02034d1c528e83f69ddf63810", "shasum": "" }, "require": { "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0" + "symfony/console": "^6.4|^7.0|^8.0" }, "bin": [ "Resources/bin/yaml-lint" @@ -1210,7 +1314,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v7.3.5" + "source": "https://github.com/symfony/yaml/tree/v7.4.0" }, "funding": [ { @@ -1230,7 +1334,7 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-11-16T10:14:42+00:00" }, { "name": "zhamao/logger", @@ -2113,7 +2217,7 @@ }, { "name": "captainhook/captainhook-phar", - "version": "5.25.11", + "version": "5.27.3", "source": { "type": "git", "url": "https://github.com/captainhook-git/captainhook-phar.git", @@ -2167,7 +2271,7 @@ ], "support": { "issues": "https://github.com/captainhook-git/captainhook/issues", - "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.25.11" + "source": "https://github.com/captainhook-git/captainhook-phar/tree/5.27.3" }, "funding": [ { @@ -2864,16 +2968,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.89.2", + "version": "v3.91.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf" + "reference": "c4a25f20390337789c26b693ae46faa125040352" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/7569658f91e475ec93b99bd5964b059ad1336dcf", - "reference": "7569658f91e475ec93b99bd5964b059ad1336dcf", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/c4a25f20390337789c26b693ae46faa125040352", + "reference": "c4a25f20390337789c26b693ae46faa125040352", "shasum": "" }, "require": { @@ -2891,17 +2995,17 @@ "react/socket": "^1.16", "react/stream": "^1.4", "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", - "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0", - "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0", - "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0", - "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0", - "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0", + "symfony/console": "^5.4.47 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/filesystem": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/finder": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", + "symfony/options-resolver": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0", "symfony/polyfill-mbstring": "^1.33", "symfony/polyfill-php80": "^1.33", "symfony/polyfill-php81": "^1.33", "symfony/polyfill-php84": "^1.33", - "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2", - "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0" + "symfony/process": "^5.4.47 || ^6.4.24 || ^7.2 || ^8.0", + "symfony/stopwatch": "^5.4.45 || ^6.4.24 || ^7.0 || ^8.0" }, "require-dev": { "facile-it/paraunit": "^1.3.1 || ^2.7", @@ -2913,8 +3017,8 @@ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", "phpunit/phpunit": "^9.6.25 || ^10.5.53 || ^11.5.34", - "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2", - "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2" + "symfony/var-dumper": "^5.4.48 || ^6.4.24 || ^7.3.2 || ^8.0", + "symfony/yaml": "^5.4.45 || ^6.4.24 || ^7.3.2 || ^8.0" }, "suggest": { "ext-dom": "For handling output formats in XML", @@ -2955,7 +3059,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.89.2" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.91.0" }, "funding": [ { @@ -2963,7 +3067,7 @@ "type": "github" } ], - "time": "2025-11-06T21:12:50+00:00" + "time": "2025-11-28T22:07:42+00:00" }, { "name": "humbug/box", @@ -3212,16 +3316,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.6.1", + "version": "6.6.2", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396" + "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", - "reference": "fd8e5c6b1badb998844ad34ce0abcd71a0aeb396", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/3c25fe750c1599716ef26aa997f7c026cee8c4b7", + "reference": "3c25fe750c1599716ef26aa997f7c026cee8c4b7", "shasum": "" }, "require": { @@ -3281,9 +3385,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.1" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.6.2" }, - "time": "2025-11-07T18:30:29+00:00" + "time": "2025-11-28T15:24:03+00:00" }, { "name": "kelunik/certificate", @@ -3345,33 +3449,38 @@ }, { "name": "league/uri", - "version": "7.5.1", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "81fb5145d2644324614cc532b28efd0215bda430" + "reference": "f625804987a0a9112d954f9209d91fec52182344" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", - "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/f625804987a0a9112d954f9209d91fec52182344", + "reference": "f625804987a0a9112d954f9209d91fec52182344", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.5", - "php": "^8.1" + "league/uri-interfaces": "^7.6", + "php": "^8.1", + "psr/http-factory": "^1" }, "conflict": { "league/uri-schemes": "^1.0" }, "suggest": { "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", "ext-fileinfo": "to create Data URI from file contennts", "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", "league/uri-components": "Needed to easily manipulate URI objects components", + "league/uri-polyfill": "Needed to backport the PHP URI extension for older versions of PHP", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3399,6 +3508,7 @@ "description": "URI manipulation library", "homepage": "https://uri.thephpleague.com", "keywords": [ + "URN", "data-uri", "file-uri", "ftp", @@ -3411,9 +3521,11 @@ "psr-7", "query-string", "querystring", + "rfc2141", "rfc3986", "rfc3987", "rfc6570", + "rfc8141", "uri", "uri-template", "url", @@ -3423,7 +3535,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.5.1" + "source": "https://github.com/thephpleague/uri/tree/7.6.0" }, "funding": [ { @@ -3431,26 +3543,25 @@ "type": "github" } ], - "time": "2024-12-08T08:40:02+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "league/uri-interfaces", - "version": "7.5.0", + "version": "7.6.0", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", - "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/ccbfb51c0445298e7e0b7f4481b942f589665368", + "reference": "ccbfb51c0445298e7e0b7f4481b942f589665368", "shasum": "" }, "require": { "ext-filter": "*", "php": "^8.1", - "psr/http-factory": "^1", "psr/http-message": "^1.1 || ^2.0" }, "suggest": { @@ -3458,6 +3569,7 @@ "ext-gmp": "to improve IPV4 host parsing", "ext-intl": "to handle IDN host with the best performance", "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle WHATWG URL", "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" }, "type": "library", @@ -3482,7 +3594,7 @@ "homepage": "https://nyamsprod.com" } ], - "description": "Common interfaces and classes for URI representation and interaction", + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", "homepage": "https://uri.thephpleague.com", "keywords": [ "data-uri", @@ -3507,7 +3619,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.6.0" }, "funding": [ { @@ -3515,7 +3627,7 @@ "type": "github" } ], - "time": "2024-12-08T08:18:47+00:00" + "time": "2025-11-18T12:17:23+00:00" }, { "name": "marc-mabe/php-enum", @@ -4147,16 +4259,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.3", + "version": "5.6.5", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", - "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/90614c73d3800e187615e2dd236ad0e2a01bf761", + "reference": "90614c73d3800e187615e2dd236ad0e2a01bf761", "shasum": "" }, "require": { @@ -4205,22 +4317,22 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.5" }, - "time": "2025-08-01T19:43:32+00:00" + "time": "2025-11-27T19:50:05+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.10.0", + "version": "1.12.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", - "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/92a98ada2b93d9b201a613cb5a33584dde25f195", + "reference": "92a98ada2b93d9b201a613cb5a33584dde25f195", "shasum": "" }, "require": { @@ -4263,9 +4375,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.12.0" }, - "time": "2024-11-09T15:12:26+00:00" + "time": "2025-11-21T15:09:14+00:00" }, { "name": "phpstan/phpdoc-parser", @@ -5104,16 +5216,16 @@ }, { "name": "react/dns", - "version": "v1.13.0", + "version": "v1.14.0", "source": { "type": "git", "url": "https://github.com/reactphp/dns.git", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "url": "https://api.github.com/repos/reactphp/dns/zipball/7562c05391f42701c1fccf189c8225fece1cd7c3", + "reference": "7562c05391f42701c1fccf189c8225fece1cd7c3", "shasum": "" }, "require": { @@ -5168,7 +5280,7 @@ ], "support": { "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.13.0" + "source": "https://github.com/reactphp/dns/tree/v1.14.0" }, "funding": [ { @@ -5176,20 +5288,20 @@ "type": "open_collective" } ], - "time": "2024-06-13T14:18:03+00:00" + "time": "2025-11-18T19:34:28+00:00" }, { "name": "react/event-loop", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/ba276bda6083df7e0050fd9b33f66ad7a4ac747a", + "reference": "ba276bda6083df7e0050fd9b33f66ad7a4ac747a", "shasum": "" }, "require": { @@ -5240,7 +5352,7 @@ ], "support": { "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + "source": "https://github.com/reactphp/event-loop/tree/v1.6.0" }, "funding": [ { @@ -5248,7 +5360,7 @@ "type": "open_collective" } ], - "time": "2023-11-13T13:48:05+00:00" + "time": "2025-11-17T20:46:25+00:00" }, { "name": "react/promise", @@ -5325,16 +5437,16 @@ }, { "name": "react/socket", - "version": "v1.16.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "url": "https://api.github.com/repos/reactphp/socket/zipball/ef5b17b81f6f60504c539313f94f2d826c5faa08", + "reference": "ef5b17b81f6f60504c539313f94f2d826c5faa08", "shasum": "" }, "require": { @@ -5393,7 +5505,7 @@ ], "support": { "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" + "source": "https://github.com/reactphp/socket/tree/v1.17.0" }, "funding": [ { @@ -5401,7 +5513,7 @@ "type": "open_collective" } ], - "time": "2024-07-26T10:38:09+00:00" + "time": "2025-11-19T20:47:34+00:00" }, { "name": "react/stream", @@ -5483,16 +5595,16 @@ }, { "name": "revolt/event-loop", - "version": "v1.0.7", + "version": "v1.0.8", "source": { "type": "git", "url": "https://github.com/revoltphp/event-loop.git", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c", + "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c", "shasum": "" }, "require": { @@ -5549,9 +5661,9 @@ ], "support": { "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.8" }, - "time": "2025-01-25T19:27:39+00:00" + "time": "2025-08-27T21:33:23+00:00" }, { "name": "sebastian/cli-parser", @@ -6620,24 +6732,24 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + "reference": "573f95783a2ec6e38752979db139f09fec033f03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", - "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/573f95783a2ec6e38752979db139f09fec033f03", + "reference": "573f95783a2ec6e38752979db139f09fec033f03", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/security-http": "<7.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -6646,13 +6758,14 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^7.4|^8.0" }, "type": "library", "autoload": { @@ -6680,7 +6793,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.0" }, "funding": [ { @@ -6700,7 +6813,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T11:49:31+00:00" + "time": "2025-10-30T14:17:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -6780,16 +6893,16 @@ }, { "name": "symfony/filesystem", - "version": "v7.3.6", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a" + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/e9bcfd7837928ab656276fe00464092cc9e1826a", - "reference": "e9bcfd7837928ab656276fe00464092cc9e1826a", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", "shasum": "" }, "require": { @@ -6798,7 +6911,7 @@ "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0" + "symfony/process": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6826,7 +6939,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.0" }, "funding": [ { @@ -6846,27 +6959,27 @@ "type": "tidelift" } ], - "time": "2025-11-05T09:52:27+00:00" + "time": "2025-11-27T13:27:24+00:00" }, { "name": "symfony/finder", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f" + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", - "reference": "9f696d2f1e340484b4683f7853b273abff94421f", + "url": "https://api.github.com/repos/symfony/finder/zipball/340b9ed7320570f319028a2cbec46d40535e94bd", + "reference": "340b9ed7320570f319028a2cbec46d40535e94bd", "shasum": "" }, "require": { "php": ">=8.2" }, "require-dev": { - "symfony/filesystem": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0" }, "type": "library", "autoload": { @@ -6894,7 +7007,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.5" + "source": "https://github.com/symfony/finder/tree/v7.4.0" }, "funding": [ { @@ -6914,24 +7027,24 @@ "type": "tidelift" } ], - "time": "2025-10-15T18:45:57+00:00" + "time": "2025-11-05T05:42:40+00:00" }, { "name": "symfony/options-resolver", - "version": "v7.3.3", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d" + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/0ff2f5c3df08a395232bbc3c2eb7e84912df911d", - "reference": "0ff2f5c3df08a395232bbc3c2eb7e84912df911d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7", + "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -6965,7 +7078,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.3" + "source": "https://github.com/symfony/options-resolver/tree/v8.0.0" }, "funding": [ { @@ -6985,7 +7098,7 @@ "type": "tidelift" } ], - "time": "2025-08-05T10:16:07+00:00" + "time": "2025-11-12T15:55:31+00:00" }, { "name": "symfony/polyfill-iconv", @@ -7153,20 +7266,20 @@ }, { "name": "symfony/stopwatch", - "version": "v7.3.0", + "version": "v8.0.0", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/67df1914c6ccd2d7b52f70d40cf2aea02159d942", + "reference": "67df1914c6ccd2d7b52f70d40cf2aea02159d942", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.4", "symfony/service-contracts": "^2.5|^3" }, "type": "library", @@ -7195,7 +7308,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" + "source": "https://github.com/symfony/stopwatch/tree/v8.0.0" }, "funding": [ { @@ -7206,25 +7319,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-02-24T10:49:57+00:00" + "time": "2025-08-04T07:36:47+00:00" }, { "name": "symfony/var-dumper", - "version": "v7.3.5", + "version": "v7.4.0", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d" + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/476c4ae17f43a9a36650c69879dcf5b1e6ae724d", - "reference": "476c4ae17f43a9a36650c69879dcf5b1e6ae724d", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/41fd6c4ae28c38b294b42af6db61446594a0dece", + "reference": "41fd6c4ae28c38b294b42af6db61446594a0dece", "shasum": "" }, "require": { @@ -7236,10 +7353,10 @@ "symfony/console": "<6.4" }, "require-dev": { - "symfony/console": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0", - "symfony/uid": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/uid": "^6.4|^7.0|^8.0", "twig/twig": "^3.12" }, "bin": [ @@ -7278,7 +7395,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v7.3.5" + "source": "https://github.com/symfony/var-dumper/tree/v7.4.0" }, "funding": [ { @@ -7298,7 +7415,7 @@ "type": "tidelift" } ], - "time": "2025-09-27T09:00:46+00:00" + "time": "2025-10-27T20:36:44+00:00" }, { "name": "thecodingmachine/safe", @@ -7441,16 +7558,16 @@ }, { "name": "theseer/tokenizer", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb" + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/d74205c497bfbca49f34d4bc4c19c17e22db4ebb", - "reference": "d74205c497bfbca49f34d4bc4c19c17e22db4ebb", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", "shasum": "" }, "require": { @@ -7479,7 +7596,7 @@ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", "support": { "issues": "https://github.com/theseer/tokenizer/issues", - "source": "https://github.com/theseer/tokenizer/tree/1.3.0" + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" }, "funding": [ { @@ -7487,7 +7604,7 @@ "type": "github" } ], - "time": "2025-11-13T13:44:09+00:00" + "time": "2025-11-17T20:03:58+00:00" }, { "name": "webmozart/assert", @@ -7554,10 +7671,10 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">= 8.3", + "php": ">=8.4", "ext-mbstring": "*", "ext-zlib": "*" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/config/artifact.json b/config/artifact.json new file mode 100644 index 000000000..c8fd66215 --- /dev/null +++ b/config/artifact.json @@ -0,0 +1,1061 @@ +{ + "vswhere": { + "binary": { + "windows-x86_64": { + "type": "url", + "url": "https://github.com/microsoft/vswhere/releases/download/3.1.7/vswhere.exe", + "extract": "{pkg_root_path}/bin/vswhere.exe" + } + } + }, + "musl-wrapper": { + "source": "https://musl.libc.org/releases/musl-1.2.5.tar.gz" + }, + "php-src": { + "source": { + "type": "php-release" + } + }, + "php-sdk-binary-tools": { + "binary": { + "windows-x86_64": { + "type": "git", + "rev": "master", + "url": "https://github.com/php/php-sdk-binary-tools.git", + "extract": "{php_sdk_path}" + } + } + }, + "go-xcaddy": { + "binary": "custom" + }, + "musl-toolchain": { + "binary": { + "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": { + "source": "https://dl.static-php.dev/static-php-cli/deps/pkg-config/pkg-config-0.29.2.tar.gz", + "binary": { + "linux-x86_64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-aarch64-linux-musl-1.2.5.txz", + "extract": { + "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" + } + }, + "linux-aarch64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-x86_64-linux-musl-1.2.5.txz", + "extract": { + "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" + } + }, + "macos-x86_64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-x86_64-darwin.txz", + "extract": { + "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" + } + }, + "macos-aarch64": { + "type": "ghrel", + "repo": "static-php/static-php-cli-hosted", + "match": "pkg-config-aarch64-darwin.txz", + "extract": "{pkg_root_path}" + } + } + }, + "strawberry-perl": { + "binary": { + "windows-x86_64": { + "type": "url", + "url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip", + "extract": "{pkg_root_path}/strawberry-perl" + } + } + }, + "upx": { + "binary": { + "linux-x86_64": { + "type": "ghrel", + "repo": "upx/upx", + "match": "upx.+-amd64_linux\\.tar\\.xz", + "extract": { + "upx": "{pkg_root_path}/bin/upx" + } + }, + "linux-aarch64": { + "type": "ghrel", + "repo": "upx/upx", + "match": "upx.+-arm64_linux\\.tar\\.xz", + "extract": { + "upx": "{pkg_root_path}/bin/upx" + } + }, + "windows-x86_64": { + "type": "ghrel", + "repo": "upx/upx", + "match": "upx.+-win64\\.zip", + "extract": { + "upx.exe": "{pkg_root_path}/bin/upx.exe" + } + } + } + }, + "zig": { + "binary": "custom" + }, + "nasm": { + "binary": { + "windows-x86_64": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", + "extract": { + "nasm.exe": "{php_sdk_path}/bin/nasm.exe", + "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" + } + } + } + }, + "amqp": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/amqp", + "filename": "amqp.tgz", + "extract": "php-src/ext/amqp" + } + }, + "apcu": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/APCu", + "filename": "apcu.tgz", + "extract": "php-src/ext/apcu" + } + }, + "ast": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/ast", + "filename": "ast.tgz", + "extract": "php-src/ext/ast" + } + }, + "attr": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://download.savannah.nongnu.org/releases/attr/attr-2.5.2.tar.gz" + }, + "source-mirror": { + "type": "url", + "url": "https://mirror.souseiseki.middlendian.com/nongnu/attr/attr-2.5.2.tar.gz" + } + }, + "brotli": { + "binary": "hosted", + "source": { + "type": "ghtagtar", + "repo": "google/brotli", + "match": "v1\\.\\d.*" + } + }, + "bzip2": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/bzip2/bzip2-1.0.8.tar.gz" + }, + "source-mirror": { + "type": "filelist", + "url": "https://sourceware.org/pub/bzip2/", + "regex": "/href=\"(?bzip2-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "curl": { + "source": { + "type": "ghrel", + "repo": "curl/curl", + "match": "curl.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "dio": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/dio", + "filename": "dio.tgz", + "extract": "php-src/ext/dio" + } + }, + "ev": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/ev", + "filename": "ev.tgz", + "extract": "php-src/ext/ev" + } + }, + "ext-brotli": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/kjdev/php-ext-brotli", + "extract": "php-src/ext/brotli" + } + }, + "ext-ds": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/ds", + "filename": "ds.tgz", + "extract": "php-src/ext/ds" + } + }, + "ext-event": { + "source": { + "type": "url", + "url": "https://bitbucket.org/osmanov/pecl-event/get/3.0.8.tar.gz", + "extract": "php-src/ext/event" + } + }, + "ext-glfw": { + "source": { + "type": "git", + "url": "https://github.com/mario-deluna/php-glfw", + "rev": "master" + } + }, + "ext-gmssl": { + "source": { + "type": "ghtar", + "repo": "gmssl/GmSSL-PHP", + "extract": "php-src/ext/gmssl" + } + }, + "ext-imagick": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/imagick", + "filename": "imagick.tgz", + "extract": "php-src/ext/imagick" + } + }, + "ext-imap": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/imap", + "filename": "imap.tgz", + "extract": "php-src/ext/imap" + } + }, + "ext-lz4": { + "source": { + "type": "ghtagtar", + "repo": "kjdev/php-ext-lz4", + "extract": "php-src/ext/lz4" + } + }, + "ext-memcache": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/memcache", + "filename": "memcache.tgz", + "extract": "php-src/ext/memcache" + } + }, + "ext-rdkafka": { + "source": { + "type": "ghtar", + "repo": "arnaud-lb/php-rdkafka", + "extract": "php-src/ext/rdkafka" + } + }, + "ext-simdjson": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/simdjson", + "filename": "simdjson.tgz", + "extract": "php-src/ext/simdjson" + } + }, + "ext-snappy": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/kjdev/php-ext-snappy", + "extract": "php-src/ext/snappy" + } + }, + "ext-ssh2": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/ssh2", + "filename": "ssh2.tgz", + "extract": "php-src/ext/ssh2" + } + }, + "ext-trader": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/trader", + "filename": "trader.tgz", + "extract": "php-src/ext/trader" + } + }, + "ext-uuid": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/uuid", + "filename": "uuid.tgz", + "extract": "php-src/ext/uuid" + } + }, + "ext-uv": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/uv", + "filename": "uv.tgz", + "extract": "php-src/ext/uv" + } + }, + "ext-xz": { + "source": { + "type": "git", + "rev": "main", + "url": "https://github.com/codemasher/php-ext-xz", + "extract": "php-src/ext/xz" + } + }, + "ext-zip": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/zip", + "filename": "ext-zip.tgz" + } + }, + "ext-zstd": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/kjdev/php-ext-zstd", + "extract": "php-src/ext/zstd" + } + }, + "fastlz": { + "source": { + "type": "git", + "url": "https://github.com/ariya/FastLZ.git", + "rev": "master" + } + }, + "freetype": { + "source": { + "type": "git", + "rev": "VER-2-13-2", + "url": "https://github.com/freetype/freetype" + } + }, + "gettext": { + "source": { + "type": "filelist", + "url": "https://ftp.gnu.org/pub/gnu/gettext/", + "regex": "/href=\"(?gettext-(?[^\"]+)\\.tar\\.xz)\"/" + } + }, + "gmp": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://gmplib.org/download/gmp/", + "regex": "/href=\"(?gmp-(?[^\"]+)\\.tar\\.xz)\"/" + }, + "source-mirror": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/gmp/gmp-6.3.0.tar.xz" + } + }, + "gmssl": { + "binary": "hosted", + "source": { + "type": "ghtar", + "repo": "guanzhi/GmSSL" + } + }, + "grpc": { + "binary": "hosted", + "source": { + "type": "git", + "regex": "v(?1.\\d+).x", + "url": "https://github.com/grpc/grpc.git" + } + }, + "icu": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "unicode-org/icu", + "match": "icu4c.+-src\\.tgz", + "prefer-stable": true + } + }, + "icu-static-win": { + "source": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/icu-static-windows-x64/icu-static-windows-x64.zip" + } + }, + "igbinary": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/igbinary", + "filename": "igbinary.tgz", + "extract": "php-src/ext/igbinary" + } + }, + "imagemagick": { + "source": { + "type": "ghtar", + "repo": "ImageMagick/ImageMagick" + } + }, + "imap": { + "source": { + "type": "git", + "url": "https://github.com/static-php/imap.git", + "rev": "master" + } + }, + "inotify": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/inotify", + "filename": "inotify.tgz", + "extract": "php-src/ext/inotify" + } + }, + "jbig": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/jbig/jbigkit-2.1.tar.gz" + }, + "source-mirror": { + "type": "url", + "url": "https://www.cl.cam.ac.uk/~mgk25/jbigkit/download/jbigkit-2.1.tar.gz" + } + }, + "ldap": { + "source": { + "type": "filelist", + "url": "https://www.openldap.org/software/download/OpenLDAP/openldap-release/", + "regex": "/href=\"(?openldap-(?[^\"]+)\\.tgz)\"/" + } + }, + "lerc": { + "binary": "hosted", + "source": { + "type": "ghtar", + "repo": "Esri/lerc", + "prefer-stable": true + } + }, + "libacl": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://download.savannah.nongnu.org/releases/acl/acl-2.3.2.tar.gz" + }, + "source-mirror": { + "type": "url", + "url": "https://mirror.souseiseki.middlendian.com/nongnu/acl/acl-2.3.2.tar.gz" + } + }, + "libaom": { + "binary": "hosted", + "source": { + "type": "git", + "rev": "main", + "url": "https://aomedia.googlesource.com/aom" + } + }, + "libargon2": { + "binary": "hosted", + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/static-php/phc-winner-argon2" + } + }, + "libavif": { + "binary": "hosted", + "source": { + "type": "ghtar", + "repo": "AOMediaCodec/libavif" + } + }, + "libcares": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "c-ares/c-ares", + "match": "c-ares-.+\\.tar\\.gz", + "prefer-stable": true + }, + "source-mirror": { + "type": "filelist", + "url": "https://c-ares.org/download/", + "regex": "/href=\"\\/download\\/(?c-ares-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "libde265": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "strukturag/libde265", + "match": "libde265-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libedit": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://thrysoee.dk/editline/", + "regex": "/href=\"(?libedit-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "libevent": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "libevent/libevent", + "match": "libevent.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libffi": { + "source": { + "type": "ghrel", + "repo": "libffi/libffi", + "match": "libffi.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libffi-win": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/static-php/libffi-win.git" + } + }, + "libheif": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "strukturag/libheif", + "match": "libheif-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libiconv": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://ftp.gnu.org/gnu/libiconv/", + "regex": "/href=\"(?libiconv-(?[^\"]+)\\.tar\\.gz)\"/" + }, + "source-mirror": "https://dl.static-php.dev/static-php-cli/deps/spc-download-mirror/libiconv/libiconv-spc-mirror.tar.gz" + }, + "libiconv-win": { + "source": { + "type": "git", + "rev": "master", + "url": "https://github.com/static-php/libiconv-win.git" + } + }, + "libjpeg": { + "source": { + "type": "ghtar", + "repo": "libjpeg-turbo/libjpeg-turbo" + } + }, + "libjxl": { + "source": { + "type": "git", + "url": "https://github.com/libjxl/libjxl", + "rev": "main", + "submodules": [ + "third_party/highway", + "third_party/libjpeg-turbo", + "third_party/sjpeg", + "third_party/skcms" + ] + } + }, + "liblz4": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "lz4/lz4", + "match": "lz4-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libmemcached": { + "source": { + "type": "ghtagtar", + "repo": "awesomized/libmemcached", + "match": "1.\\d.\\d" + } + }, + "libpng": { + "binary": "hosted", + "source": { + "type": "git", + "url": "https://github.com/glennrp/libpng.git", + "rev": "libpng16" + } + }, + "librabbitmq": { + "source": { + "type": "git", + "url": "https://github.com/alanxz/rabbitmq-c.git", + "rev": "master" + } + }, + "librdkafka": { + "source": { + "type": "ghtar", + "repo": "confluentinc/librdkafka" + } + }, + "libsodium": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "jedisct1/libsodium", + "match": "libsodium-\\d+(\\.\\d+)*\\.tar\\.gz", + "prefer-stable": true + } + }, + "libssh2": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "libssh2/libssh2", + "match": "libssh2.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libtiff": { + "source": { + "type": "filelist", + "url": "https://download.osgeo.org/libtiff/", + "regex": "/href=\"(?tiff-(?[^\"]+)\\.tar\\.xz)\"/" + } + }, + "liburing": { + "source": { + "type": "ghtar", + "repo": "axboe/liburing", + "prefer-stable": true + } + }, + "libuuid": { + "source": { + "type": "git", + "url": "https://github.com/static-php/libuuid.git", + "rev": "master" + } + }, + "libuv": { + "source": { + "type": "ghtar", + "repo": "libuv/libuv" + } + }, + "libwebp": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://github.com/webmproject/libwebp/archive/refs/tags/v1.3.2.tar.gz" + } + }, + "libxml2": { + "source": { + "type": "url", + "url": "https://github.com/GNOME/libxml2/archive/refs/tags/v2.12.5.tar.gz" + } + }, + "libxslt": { + "source": { + "type": "filelist", + "url": "https://download.gnome.org/sources/libxslt/1.1/", + "regex": "/href=\"(?libxslt-(?[^\"]+)\\.tar\\.xz)\"/" + } + }, + "libyaml": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "yaml/libyaml", + "match": "yaml-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "libzip": { + "source": { + "type": "ghrel", + "repo": "nih-at/libzip", + "match": "libzip.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "memcached": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/memcached", + "filename": "memcached.tgz", + "extract": "php-src/ext/memcached" + } + }, + "mimalloc": { + "source": { + "type": "ghtagtar", + "repo": "microsoft/mimalloc", + "match": "v2\\.\\d\\.[^3].*" + } + }, + "micro": { + "source": { + "type": "git", + "extract": "php-src/sapi/micro", + "rev": "master", + "url": "https://github.com/static-php/phpmicro" + } + }, + "mongodb": { + "source": { + "type": "ghrel", + "repo": "mongodb/mongo-php-driver", + "match": "mongodb.+\\.tgz", + "prefer-stable": true, + "extract": "php-src/ext/mongodb" + } + }, + "msgpack": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/msgpack", + "filename": "msgpack.tgz", + "extract": "php-src/ext/msgpack" + } + }, + "ncurses": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://ftp.gnu.org/pub/gnu/ncurses/", + "regex": "/href=\"(?ncurses-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "net-snmp": { + "source": { + "type": "ghtagtar", + "repo": "net-snmp/net-snmp" + } + }, + "nghttp2": { + "source": { + "type": "ghrel", + "repo": "nghttp2/nghttp2", + "match": "nghttp2.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "nghttp3": { + "source": { + "type": "ghrel", + "repo": "ngtcp2/nghttp3", + "match": "nghttp3.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "ngtcp2": { + "source": { + "type": "ghrel", + "repo": "ngtcp2/ngtcp2", + "match": "ngtcp2.+\\.tar\\.xz", + "prefer-stable": true + } + }, + "onig": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "kkos/oniguruma", + "match": "onig-.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "openssl": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "openssl/openssl", + "match": "openssl.+\\.tar\\.gz", + "prefer-stable": true + }, + "source-mirror": { + "type": "filelist", + "url": "https://www.openssl.org/source/", + "regex": "/href=\"(?openssl-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "opentelemetry": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/opentelemetry", + "filename": "opentelemetry.tgz", + "extract": "php-src/ext/opentelemetry" + } + }, + "parallel": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/parallel", + "filename": "parallel.tgz", + "extract": "php-src/ext/parallel" + } + }, + "pdo_sqlsrv": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/pdo_sqlsrv", + "filename": "pdo_sqlsrv.tgz", + "extract": "php-src/ext/pdo_sqlsrv" + } + }, + "postgresql": { + "source": { + "type": "ghtagtar", + "repo": "postgres/postgres", + "match": "REL_18_\\d+" + } + }, + "postgresql-win": { + "source": { + "type": "url", + "url": "https://get.enterprisedb.com/postgresql/postgresql-16.8-1-windows-x64-binaries.zip" + } + }, + "protobuf": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/protobuf", + "filename": "protobuf.tgz", + "extract": "php-src/ext/protobuf" + } + }, + "pthreads4w": { + "source": { + "type": "git", + "rev": "master", + "url": "https://git.code.sf.net/p/pthreads4w/code" + } + }, + "qdbm": { + "source": { + "type": "git", + "url": "https://github.com/static-php/qdbm.git", + "rev": "main" + } + }, + "rar": { + "source": { + "type": "git", + "url": "https://github.com/static-php/php-rar.git", + "rev": "issue-php82", + "extract": "php-src/ext/rar" + } + }, + "re2c": { + "source": { + "type": "ghrel", + "repo": "skvadrik/re2c", + "match": "re2c.+\\.tar\\.xz", + "prefer-stable": true + }, + "source-mirror": { + "type": "url", + "url": "https://dl.static-php.dev/static-php-cli/deps/re2c/re2c-4.3.tar.xz" + } + }, + "readline": { + "binary": "hosted", + "source": { + "type": "filelist", + "url": "https://ftp.gnu.org/pub/gnu/readline/", + "regex": "/href=\"(?readline-(?[^\"]+)\\.tar\\.gz)\"/" + } + }, + "redis": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/redis", + "filename": "redis.tgz", + "extract": "php-src/ext/redis" + } + }, + "snappy": { + "source": { + "type": "git", + "rev": "main", + "url": "https://github.com/google/snappy" + } + }, + "spx": { + "source": { + "type": "pie", + "repo": "noisebynorthwest/php-spx", + "extract": "php-src/ext/spx" + } + }, + "sqlite": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://www.sqlite.org/2024/sqlite-autoconf-3450200.tar.gz" + } + }, + "sqlsrv": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/sqlsrv", + "filename": "sqlsrv.tgz", + "extract": "php-src/ext/sqlsrv" + } + }, + "swoole": { + "source": { + "type": "ghtar", + "repo": "swoole/swoole-src", + "match": "v6\\.+", + "prefer-stable": true, + "extract": "php-src/ext/swoole" + } + }, + "swow": { + "source": { + "type": "ghtar", + "repo": "swow/swow", + "prefer-stable": true, + "extract": "php-src/ext/swow-src" + } + }, + "tidy": { + "source": { + "type": "ghtar", + "repo": "htacg/tidy-html5", + "prefer-stable": true + } + }, + "unixodbc": { + "binary": "hosted", + "source": { + "type": "url", + "url": "https://www.unixodbc.org/unixODBC-2.3.12.tar.gz", + "version": "2.3.12" + } + }, + "watcher": { + "source": { + "type": "ghtar", + "repo": "e-dant/watcher", + "prefer-stable": true + } + }, + "xdebug": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/xdebug", + "filename": "xdebug.tgz" + } + }, + "xhprof": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/xhprof", + "filename": "xhprof.tgz", + "extract": "php-src/ext/xhprof-src" + } + }, + "xlswriter": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/xlswriter", + "filename": "xlswriter.tgz", + "extract": "php-src/ext/xlswriter" + } + }, + "xz": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "tukaani-project/xz", + "match": "xz.+\\.tar\\.xz", + "prefer-stable": true + }, + "source-mirror": { + "type": "url", + "url": "https://github.com/tukaani-project/xz/releases/download/v5.8.1/xz-5.8.1.tar.gz" + } + }, + "yac": { + "source": { + "type": "url", + "url": "https://pecl.php.net/get/yac", + "filename": "yac.tgz", + "extract": "php-src/ext/yac" + } + }, + "yaml": { + "source": { + "type": "git", + "rev": "php7", + "url": "https://github.com/php/pecl-file_formats-yaml", + "extract": "php-src/ext/yaml" + } + }, + "zlib": { + "binary": "hosted", + "source": { + "type": "ghrel", + "repo": "madler/zlib", + "match": "zlib.+\\.tar\\.gz", + "prefer-stable": true + } + }, + "zstd": { + "source": { + "type": "ghrel", + "repo": "facebook/zstd", + "match": "zstd.+\\.tar\\.gz", + "prefer-stable": true + } + } +} diff --git a/config/env.ini b/config/env.ini index 7448cc373..8e25aa6e7 100644 --- a/config/env.ini +++ b/config/env.ini @@ -32,9 +32,10 @@ ; GNU_ARCH: the GNU arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `aarch64`) ; MAC_ARCH: the MAC arch of the current system. (default: `$(uname -m)`, e.g. `x86_64`, `arm64`) ; PKG_CONFIG: (*nix only) static-php-cli will set `$BUILD_BIN_PATH/pkg-config` to PKG_CONFIG. -; SPC_LINUX_DEFAULT_CC: (linux only) the default compiler for linux. (For alpine linux: `gcc`, default: `$GNU_ARCH-linux-musl-gcc`) -; SPC_LINUX_DEFAULT_CXX: (linux only) the default c++ compiler for linux. (For alpine linux: `g++`, default: `$GNU_ARCH-linux-musl-g++`) -; SPC_LINUX_DEFAULT_AR: (linux only) the default archiver for linux. (For alpine linux: `ar`, default: `$GNU_ARCH-linux-musl-ar`) +; SPC_DEFAULT_CC: (*nix only) the default compiler for selected toolchain. +; SPC_DEFAULT_CXX: (*nix only) the default c++ compiler selected toolchain. +; SPC_DEFAULT_AR: (*nix only) the default archiver for selected toolchain. +; SPC_DEFAULT_LD: (*nix only) the default linker for selected toolchain. ; SPC_EXTRA_PHP_VARS: (linux only) the extra vars for building php, used in `configure` and `make` command. [global] @@ -48,6 +49,12 @@ SPC_SKIP_DOCTOR_CHECK_ITEMS="" SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli" ; The display message for php version output (PHP >= 8.4 available) PHP_BUILD_PROVIDER="static-php-cli ${SPC_VERSION}" +; Whether to enable log file (if you are using vendor mode) +SPC_ENABLE_LOG_FILE="yes" +; The LOG DIR for spc logs +SPC_LOGS_DIR="${WORKING_DIR}/log" +; Preserve old logs when running new builds +SPC_PRESERVE_LOGS="no" ; EXTENSION_DIR where the built php will look for extension when a .ini instructs to load them ; only useful for builds targeting not pure-static linking @@ -120,11 +127,12 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" ; Currently we do not support universal and cross-compilation for macOS. SPC_TARGET=native-macos ; compiler environments -CC=clang -CXX=clang++ -AR=ar -LD=ld +CC=${SPC_LINUX_DEFAULT_CC} +CXX=${SPC_LINUX_DEFAULT_CXX} +AR=${SPC_LINUX_DEFAULT_AR} +LD=${SPC_LINUX_DEFAULT_LD} ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build +; this will be added to all CFLAGS and CXXFLAGS for the library builds SPC_DEFAULT_C_FLAGS="--target=${MAC_ARCH}-apple-darwin -Os" SPC_DEFAULT_CXX_FLAGS="--target=${MAC_ARCH}-apple-darwin -Os" SPC_DEFAULT_LD_FLAGS="" @@ -142,8 +150,3 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable- SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; EXTRA_CFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_C_FLAGS}" - -[freebsd] -; compiler environments -CC=clang -CXX=clang++ diff --git a/config/ext.json b/config/ext.json index e8f69339e..d3fd2aa27 100644 --- a/config/ext.json +++ b/config/ext.json @@ -567,6 +567,13 @@ "type": "builtin", "unix-only": true }, + "pcov": { + "type": "external", + "source": "pcov", + "target": [ + "shared" + ] + }, "pdo": { "type": "builtin" }, diff --git a/config/pkg.ext.json b/config/pkg.ext.json new file mode 100644 index 000000000..70fe34e63 --- /dev/null +++ b/config/pkg.ext.json @@ -0,0 +1,1541 @@ +{ + "ext-amqp": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "librabbitmq" + ], + "depends@windows": [ + "ext-openssl" + ], + "artifact": "amqp", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-apcu": { + "type": "php-extension", + "artifact": "apcu", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-ast": { + "type": "php-extension", + "artifact": "ast", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-bcmath": { + "type": "php-extension" + }, + "ext-brotli": { + "type": "php-extension", + "php-extension": { + "arg-type": "enable" + }, + "depends": [ + "brotli" + ], + "artifact": "ext-brotli", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-bz2": { + "type": "php-extension", + "php-extension": { + "arg-type@windows": "with", + "arg-type": "with-path" + }, + "depends": [ + "bzip2" + ] + }, + "ext-calendar": { + "type": "php-extension" + }, + "ext-ctype": { + "type": "php-extension" + }, + "ext-curl": { + "type": "php-extension", + "php-extension": { + "arg-type": "with", + "notes": true + }, + "depends": [ + "curl" + ], + "depends@windows": [ + "ext-zlib", + "ext-openssl" + ] + }, + "ext-dba": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom" + }, + "suggests": [ + "qdbm" + ] + }, + "ext-dio": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "artifact": "dio", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-dom": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "with" + }, + "depends": [ + "libxml2", + "zlib" + ], + "depends@windows": [ + "ext-xml" + ] + }, + "ext-ds": { + "type": "php-extension", + "artifact": "ext-ds", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-enchant": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip", + "Darwin": "wip", + "Linux": "wip" + } + } + }, + "ext-ev": { + "type": "php-extension", + "php-extension": { + "arg-type@windows": "with" + }, + "depends": [ + "ext-sockets" + ], + "artifact": "ev", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-event": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "libevent", + "ext-openssl" + ], + "suggests": [ + "ext-sockets" + ], + "artifact": "ext-event", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-exif": { + "type": "php-extension" + }, + "ext-ffi": { + "type": "php-extension", + "php-extension": { + "support": { + "Linux": "partial", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends@windows": [ + "libffi-win" + ], + "depends": [ + "libffi" + ] + }, + "ext-fileinfo": { + "type": "php-extension" + }, + "ext-filter": { + "type": "php-extension" + }, + "ext-ftp": { + "type": "php-extension", + "suggests": [ + "openssl" + ] + }, + "ext-gd": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "with", + "notes": true + }, + "depends": [ + "zlib", + "libpng", + "ext-zlib" + ], + "suggests": [ + "libavif", + "libwebp", + "libjpeg", + "freetype" + ] + }, + "ext-gettext": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "gettext" + ] + }, + "ext-glfw": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "no", + "Linux": "no" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "glfw" + ], + "depends@windows": [], + "artifact": "ext-glfw", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-gmp": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "gmp" + ] + }, + "ext-gmssl": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "depends": [ + "gmssl" + ], + "artifact": "ext-gmssl", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-grpc": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "enable-path" + }, + "depends": [ + "grpc" + ], + "lang": "cpp", + "artifact": "grpc", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-iconv": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with-path", + "arg-type@windows": "with" + }, + "depends@windows": [ + "libiconv-win" + ], + "depends": [ + "libiconv" + ] + }, + "ext-igbinary": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "suggests": [ + "ext-session", + "ext-apcu" + ], + "artifact": "igbinary", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "ext-imagick": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "imagemagick" + ], + "artifact": "ext-imagick", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-imap": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "imap" + ], + "suggests": [ + "ext-openssl" + ], + "artifact": "ext-imap", + "license": { + "type": "file", + "path": [ + "LICENSE" + ] + } + }, + "ext-inotify": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip", + "Darwin": "no" + } + }, + "artifact": "inotify", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-intl": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "depends@windows": [ + "icu-static-win" + ], + "depends": [ + "icu" + ] + }, + "ext-ldap": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "ldap" + ], + "suggests": [ + "gmp", + "libsodium", + "ext-openssl" + ] + }, + "ext-libxml": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "none", + "build-shared": false, + "build-static": true, + "build-with-php": true + }, + "depends": [ + "ext-xml" + ] + }, + "ext-lz4": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "liblz4" + ], + "artifact": "ext-lz4", + "license": { + "type": "file", + "path": [ + "LICENSE" + ] + } + }, + "ext-mbregex": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom", + "build-shared": false, + "build-static": true + }, + "depends": [ + "onig", + "ext-mbstring" + ] + }, + "ext-mbstring": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom" + } + }, + "ext-mcrypt": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "no", + "Darwin": "no", + "Linux": "no" + }, + "notes": true + } + }, + "ext-memcache": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "build-with-php": true + }, + "depends": [ + "ext-zlib", + "ext-session" + ], + "artifact": "ext-memcache", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-memcached": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "libmemcached", + "fastlz", + "ext-session", + "ext-zlib" + ], + "suggests": [ + "zstd", + "ext-igbinary", + "ext-msgpack", + "ext-session" + ], + "lang": "cpp", + "artifact": "memcached", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-mongodb": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "wip" + }, + "arg-type": "custom" + }, + "suggests": [ + "icu", + "openssl", + "zstd", + "zlib" + ], + "frameworks": [ + "CoreFoundation", + "Security" + ], + "artifact": "mongodb", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-msgpack": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type@windows": "enable", + "arg-type": "with" + }, + "depends": [ + "ext-session" + ], + "artifact": "msgpack", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-mysqli": { + "type": "php-extension", + "php-extension": { + "arg-type": "with", + "build-with-php": true + }, + "depends": [ + "ext-mysqlnd" + ] + }, + "ext-mysqlnd": { + "type": "php-extension", + "php-extension": { + "arg-type@windows": "with", + "build-with-php": true + }, + "depends": [ + "zlib" + ] + }, + "ext-oci8": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "no", + "Darwin": "no", + "Linux": "no" + }, + "notes": true + } + }, + "ext-odbc": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "unixodbc" + ] + }, + "ext-opcache": { + "type": "php-extension", + "php-extension": { + "arg-type@windows": "enable", + "arg-type": "custom", + "zend-extension": true + } + }, + "ext-openssl": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom", + "arg-type@windows": "with", + "build-with-php": true, + "notes": true + }, + "depends": [ + "openssl", + "zlib", + "ext-zlib" + ] + }, + "ext-opentelemetry": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "artifact": "opentelemetry", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-parallel": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type@windows": "with", + "notes": true + }, + "depends@windows": [ + "pthreads4w" + ], + "artifact": "parallel", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-password-argon2": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "libargon2", + "openssl" + ] + }, + "ext-pcntl": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no" + } + } + }, + "ext-pdo": { + "type": "php-extension" + }, + "ext-pdo_mysql": { + "type": "php-extension", + "php-extension": { + "arg-type": "with" + }, + "depends": [ + "ext-pdo", + "ext-mysqlnd" + ] + }, + "ext-pdo_odbc": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "unixodbc", + "ext-pdo", + "ext-odbc" + ] + }, + "ext-pdo_pgsql": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with-path", + "arg-type@windows": "custom" + }, + "depends@windows": [ + "postgresql-win" + ], + "depends": [ + "postgresql", + "ext-pdo", + "ext-pgsql" + ] + }, + "ext-pdo_sqlite": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with" + }, + "depends": [ + "sqlite", + "ext-pdo", + "ext-sqlite3" + ] + }, + "ext-pdo_sqlsrv": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with" + }, + "depends": [ + "ext-pdo", + "ext-sqlsrv" + ], + "artifact": "pdo_sqlsrv", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-pgsql": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends@windows": [ + "postgresql-win" + ], + "depends": [ + "postgresql" + ] + }, + "ext-phar": { + "type": "php-extension", + "depends": [ + "ext-zlib" + ] + }, + "ext-posix": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no" + } + } + }, + "ext-protobuf": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + } + }, + "artifact": "protobuf", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-rar": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Darwin": "partial" + }, + "notes": true + }, + "lang": "cpp", + "artifact": "rar", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-rdkafka": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "librdkafka" + ], + "lang": "cpp", + "artifact": "ext-rdkafka", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-readline": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path", + "build-shared": false, + "build-static": true + }, + "depends": [ + "libedit" + ] + }, + "ext-redis": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "suggests": [ + "zstd", + "liblz4", + "ext-session", + "ext-igbinary", + "ext-msgpack" + ], + "artifact": "redis", + "license": { + "type": "file", + "path": [ + "LICENSE", + "COPYING" + ] + } + }, + "ext-session": { + "type": "php-extension", + "php-extension": { + "build-with-php": true + } + }, + "ext-shmop": { + "type": "php-extension", + "php-extension": { + "build-with-php": true + } + }, + "ext-simdjson": { + "type": "php-extension", + "lang": "cpp", + "artifact": "ext-simdjson", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-simplexml": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "build-with-php": true + }, + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-xml" + ] + }, + "ext-snappy": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "snappy" + ], + "suggests": [ + "ext-apcu" + ], + "lang": "cpp", + "artifact": "ext-snappy", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-snmp": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type@windows": "with", + "arg-type": "with" + }, + "depends": [ + "net-snmp" + ] + }, + "ext-soap": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "ext-libxml", + "ext-session" + ] + }, + "ext-sockets": { + "type": "php-extension" + }, + "ext-sodium": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with" + }, + "depends": [ + "libsodium" + ] + }, + "ext-spx": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "no" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "zlib" + ], + "artifact": "spx", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-sqlite3": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with-path", + "arg-type@windows": "with", + "build-with-php": true + }, + "depends": [ + "sqlite" + ] + }, + "ext-sqlsrv": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + }, + "depends": [ + "unixodbc" + ], + "depends@linux": [ + "ext-pcntl" + ], + "lang": "cpp", + "artifact": "sqlsrv", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-ssh2": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "with-path", + "arg-type@windows": "with" + }, + "depends": [ + "libssh2", + "ext-openssl", + "ext-zlib" + ], + "artifact": "ext-ssh2", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-swoole": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "depends": [ + "libcares", + "brotli", + "nghttp2", + "zlib", + "ext-openssl", + "ext-curl" + ], + "suggests": [ + "zstd", + "ext-sockets", + "ext-swoole-hook-pgsql", + "ext-swoole-hook-mysql", + "ext-swoole-hook-sqlite", + "ext-swoole-hook-odbc" + ], + "suggests@linux": [ + "zstd", + "liburing" + ], + "lang": "cpp", + "artifact": "swoole", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-swoole-hook-mysql": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "none", + "notes": true + }, + "depends": [ + "ext-mysqlnd", + "ext-pdo", + "ext-pdo_mysql", + "ext-swoole" + ], + "suggests": [ + "ext-mysqli" + ] + }, + "ext-swoole-hook-odbc": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "none", + "notes": true + }, + "depends": [ + "unixodbc", + "ext-pdo", + "ext-swoole" + ] + }, + "ext-swoole-hook-pgsql": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip", + "Darwin": "partial" + }, + "arg-type": "none", + "notes": true + }, + "depends": [ + "ext-pgsql", + "ext-pdo", + "ext-swoole" + ] + }, + "ext-swoole-hook-sqlite": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + }, + "arg-type": "none", + "notes": true + }, + "depends": [ + "ext-sqlite3", + "ext-pdo", + "ext-swoole" + ] + }, + "ext-swow": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "notes": true + }, + "suggests": [ + "openssl", + "curl", + "ext-openssl", + "ext-curl" + ], + "artifact": "swow", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-sysvmsg": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + } + } + }, + "ext-sysvsem": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "no", + "BSD": "wip" + } + } + }, + "ext-sysvshm": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + } + } + }, + "ext-tidy": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "tidy" + ] + }, + "ext-tokenizer": { + "type": "php-extension", + "php-extension": { + "build-with-php": true + } + }, + "ext-trader": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip", + "Windows": "wip" + } + }, + "artifact": "ext-trader", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-uuid": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "libuuid" + ], + "artifact": "ext-uuid", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-uv": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "libuv", + "ext-sockets" + ], + "artifact": "ext-uv", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-xdebug": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "no", + "Darwin": "partial", + "Linux": "partial" + }, + "build-shared": true, + "build-static": false, + "notes": true, + "zend-extension": true + }, + "artifact": "xdebug", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-xhprof": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "build-with-php": true, + "notes": true + }, + "depends": [ + "ext-ctype" + ], + "artifact": "xhprof", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-xlswriter": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "suggests": [ + "openssl" + ], + "depends": [ + "ext-zlib", + "ext-zip" + ], + "artifact": "xlswriter", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-xml": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "with", + "build-with-php": true, + "notes": true + }, + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-iconv" + ] + }, + "ext-xmlreader": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "build-with-php": true + }, + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-xml", + "ext-dom" + ] + }, + "ext-xmlwriter": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "build-with-php": true + }, + "depends": [ + "libxml2" + ], + "depends@windows": [ + "ext-xml" + ] + }, + "ext-xsl": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "with-path" + }, + "depends": [ + "libxslt", + "ext-xml", + "ext-dom" + ] + }, + "ext-xz": { + "type": "php-extension", + "php-extension": { + "arg-type": "with" + }, + "depends": [ + "xz" + ], + "artifact": "ext-xz", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-yac": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "fastlz", + "ext-igbinary" + ], + "artifact": "yac", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-yaml": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type@windows": "with", + "arg-type": "with-path" + }, + "depends": [ + "libyaml" + ], + "artifact": "yaml", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-zip": { + "type": "php-extension", + "php-extension": { + "support": { + "BSD": "wip" + }, + "arg-type": "custom", + "arg-type@windows": "enable" + }, + "depends@windows": [ + "libzip", + "zlib", + "bzip2", + "xz", + "ext-zlib", + "ext-bz2" + ], + "depends": [ + "libzip" + ], + "artifact": "ext-zip", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ext-zlib": { + "type": "php-extension", + "php-extension": { + "arg-type": "custom", + "arg-type@windows": "enable", + "build-shared": false, + "build-static": true, + "build-with-php": true + }, + "depends": [ + "zlib" + ] + }, + "ext-zstd": { + "type": "php-extension", + "php-extension": { + "support": { + "Windows": "wip", + "BSD": "wip" + }, + "arg-type": "custom" + }, + "depends": [ + "zstd" + ], + "artifact": "ext-zstd", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} diff --git a/config/pkg.json b/config/pkg.json deleted file mode 100644 index d3b4fb909..000000000 --- a/config/pkg.json +++ /dev/null @@ -1,105 +0,0 @@ -{ - "go-xcaddy-aarch64-linux": { - "type": "custom" - }, - "go-xcaddy-aarch64-macos": { - "type": "custom" - }, - "go-xcaddy-x86_64-linux": { - "type": "custom" - }, - "go-xcaddy-x86_64-macos": { - "type": "custom" - }, - "musl-toolchain-aarch64-linux": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz" - }, - "musl-toolchain-x86_64-linux": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz" - }, - "nasm-x86_64-win": { - "type": "url", - "url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip", - "extract-files": { - "nasm.exe": "{php_sdk_path}/bin/nasm.exe", - "ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe" - } - }, - "pkg-config-aarch64-linux": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-aarch64-linux-musl-1.2.5.txz", - "extract-files": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "pkg-config-aarch64-macos": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-aarch64-darwin.txz", - "extract-files": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "pkg-config-x86_64-linux": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-x86_64-linux-musl-1.2.5.txz", - "extract-files": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "pkg-config-x86_64-macos": { - "type": "ghrel", - "repo": "static-php/static-php-cli-hosted", - "match": "pkg-config-x86_64-darwin.txz", - "extract-files": { - "bin/pkg-config": "{pkg_root_path}/bin/pkg-config" - } - }, - "strawberry-perl-x86_64-win": { - "type": "url", - "url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip" - }, - "upx-aarch64-linux": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-arm64_linux\\.tar\\.xz", - "extract-files": { - "upx": "{pkg_root_path}/bin/upx" - } - }, - "upx-x86_64-linux": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-amd64_linux\\.tar\\.xz", - "extract-files": { - "upx": "{pkg_root_path}/bin/upx" - } - }, - "upx-x86_64-win": { - "type": "ghrel", - "repo": "upx/upx", - "match": "upx.+-win64\\.zip", - "extract-files": { - "upx.exe": "{pkg_root_path}/bin/upx.exe" - } - }, - "zig-aarch64-linux": { - "type": "custom" - }, - "zig-aarch64-macos": { - "type": "custom" - }, - "zig-x86_64-linux": { - "type": "custom" - }, - "zig-x86_64-macos": { - "type": "custom" - }, - "zig-x86_64-win": { - "type": "custom" - } -} diff --git a/config/pkg.lib.json b/config/pkg.lib.json new file mode 100644 index 000000000..79e1a8534 --- /dev/null +++ b/config/pkg.lib.json @@ -0,0 +1,992 @@ +{ + "attr": { + "type": "library", + "artifact": "attr", + "license": { + "type": "file", + "path": "doc/COPYING.LGPL" + } + }, + "brotli": { + "type": "library", + "headers": [ + "brotli" + ], + "pkg-configs": [ + "libbrotlicommon", + "libbrotlidec", + "libbrotlienc" + ], + "artifact": "brotli", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "bzip2": { + "type": "library", + "headers": [ + "bzlib.h" + ], + "artifact": "bzip2", + "license": { + "type": "text", + "text": "This program, \"bzip2\", the associated library \"libbzip2\", and all documentation, are copyright (C) 1996-2010 Julian R Seward. All rights reserved. \n\nRedistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:\n\n 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.\n 2. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.\n 3. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.\n 4. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n\nJulian Seward, jseward@bzip.org bzip2/libbzip2 version 1.0.6 of 6 September 2010\n\nPATENTS: To the best of my knowledge, bzip2 and libbzip2 do not use any patented algorithms. However, I do not have the resources to carry out a patent search. Therefore I cannot give any guarantee of the above statement." + } + }, + "curl": { + "type": "library", + "depends@windows": [ + "zlib", + "libssh2", + "nghttp2" + ], + "depends": [ + "openssl", + "zlib" + ], + "suggests@windows": [ + "brotli", + "zstd" + ], + "suggests": [ + "libssh2", + "brotli", + "nghttp2", + "nghttp3", + "ngtcp2", + "zstd", + "libcares", + "ldap" + ], + "headers": [ + "curl" + ], + "frameworks": [ + "CoreFoundation", + "CoreServices", + "SystemConfiguration" + ], + "artifact": "curl", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "fastlz": { + "type": "library", + "headers": [ + "fastlz/fastlz.h" + ], + "artifact": "fastlz", + "license": { + "type": "file", + "path": "LICENSE.MIT" + } + }, + "freetype": { + "type": "library", + "depends": [ + "zlib" + ], + "suggests": [ + "libpng", + "bzip2", + "brotli" + ], + "headers": [ + "freetype2/freetype/freetype.h", + "freetype2/ft2build.h" + ], + "artifact": "freetype", + "license": { + "type": "file", + "path": "LICENSE.TXT" + } + }, + "gettext": { + "type": "library", + "depends": [ + "libiconv" + ], + "suggests": [ + "ncurses", + "libxml2" + ], + "frameworks": [ + "CoreFoundation" + ], + "artifact": "gettext", + "license": { + "type": "file", + "path": "gettext-runtime/intl/COPYING.LIB" + } + }, + "glfw": { + "type": "library", + "frameworks": [ + "CoreVideo", + "OpenGL", + "Cocoa", + "IOKit" + ], + "artifact": "ext-glfw", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "gmp": { + "type": "library", + "headers": [ + "gmp.h" + ], + "artifact": "gmp", + "license": { + "type": "text", + "text": "Since version 6, GMP is distributed under the dual licenses, GNU LGPL v3 and GNU GPL v2. These licenses make the library free to use, share, and improve, and allow you to pass on the result. The GNU licenses give freedoms, but also set firm restrictions on the use with non-free programs." + } + }, + "gmssl": { + "type": "library", + "frameworks": [ + "Security" + ], + "artifact": "gmssl", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "grpc": { + "type": "library", + "depends": [ + "zlib", + "openssl", + "libcares" + ], + "pkg-configs": [ + "grpc" + ], + "frameworks": [ + "CoreFoundation" + ], + "artifact": "grpc", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "icu": { + "type": "library", + "pkg-configs": [ + "icu-uc", + "icu-i18n", + "icu-io" + ], + "artifact": "icu", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "icu-static-win": { + "type": "library", + "headers@windows": [ + "unicode" + ], + "artifact": "icu-static-win", + "license": { + "type": "text", + "text": "none" + } + }, + "imagemagick": { + "type": "library", + "depends": [ + "zlib", + "libjpeg", + "libjxl", + "libpng", + "libwebp", + "freetype", + "libtiff", + "libheif", + "bzip2" + ], + "suggests": [ + "zstd", + "xz", + "libzip", + "libxml2" + ], + "pkg-configs": [ + "Magick++-7.Q16HDRI", + "MagickCore-7.Q16HDRI", + "MagickWand-7.Q16HDRI" + ], + "artifact": "imagemagick", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "imap": { + "type": "library", + "suggests": [ + "openssl" + ], + "artifact": "imap", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "jbig": { + "type": "library", + "headers": [ + "jbig.h", + "jbig85.h", + "jbig_ar.h" + ], + "artifact": "jbig", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "ldap": { + "type": "library", + "depends": [ + "openssl", + "zlib", + "gmp", + "libsodium" + ], + "pkg-configs": [ + "ldap", + "lber" + ], + "artifact": "ldap", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "lerc": { + "type": "library", + "artifact": "lerc", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libacl": { + "type": "library", + "depends": [ + "attr" + ], + "artifact": "libacl", + "license": { + "type": "file", + "path": "doc/COPYING.LGPL" + } + }, + "libaom": { + "type": "library", + "artifact": "libaom", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libargon2": { + "type": "library", + "artifact": "libargon2", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libavif": { + "type": "library", + "artifact": "libavif", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libcares": { + "type": "library", + "headers": [ + "ares.h", + "ares_dns.h", + "ares_nameser.h" + ], + "artifact": "libcares", + "license": { + "type": "file", + "path": "LICENSE.md" + } + }, + "libde265": { + "type": "library", + "artifact": "libde265", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libedit": { + "type": "library", + "depends": [ + "ncurses" + ], + "artifact": "libedit", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libevent": { + "type": "library", + "depends": [ + "openssl" + ], + "artifact": "libevent", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libffi": { + "type": "library", + "headers@windows": [ + "ffi.h", + "fficonfig.h", + "ffitarget.h" + ], + "headers": [ + "ffi.h", + "ffitarget.h" + ], + "artifact": "libffi", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libffi-win": { + "type": "library", + "headers@windows": [ + "ffi.h", + "ffitarget.h", + "fficonfig.h" + ], + "artifact": "libffi-win", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libheif": { + "type": "library", + "depends": [ + "libde265", + "libwebp", + "libaom", + "zlib", + "brotli" + ], + "artifact": "libheif", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libiconv": { + "type": "library", + "headers": [ + "iconv.h", + "libcharset.h", + "localcharset.h" + ], + "artifact": "libiconv", + "license": { + "type": "file", + "path": "COPYING.LIB" + } + }, + "libiconv-win": { + "type": "library", + "artifact": "libiconv-win", + "license": { + "type": "file", + "path": "source/COPYING" + } + }, + "libjpeg": { + "type": "library", + "suggests@windows": [ + "zlib" + ], + "artifact": "libjpeg", + "license": { + "type": "file", + "path": "LICENSE.md" + } + }, + "libjxl": { + "type": "library", + "depends": [ + "brotli", + "libjpeg", + "libpng", + "libwebp" + ], + "pkg-configs": [ + "libjxl", + "libjxl_cms", + "libjxl_threads", + "libhwy" + ], + "artifact": "libjxl", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "liblz4": { + "type": "library", + "artifact": "liblz4", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libmemcached": { + "type": "library", + "artifact": "libmemcached", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libpng": { + "type": "library", + "depends": [ + "zlib" + ], + "headers@windows": [ + "png.h", + "pngconf.h" + ], + "headers": [ + "png.h", + "pngconf.h", + "pnglibconf.h" + ], + "artifact": "libpng", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "librabbitmq": { + "type": "library", + "depends": [ + "openssl" + ], + "artifact": "librabbitmq", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "librdkafka": { + "type": "library", + "suggests": [ + "curl", + "liblz4", + "openssl", + "zlib", + "zstd" + ], + "pkg-configs": [ + "rdkafka++-static", + "rdkafka-static" + ], + "artifact": "librdkafka", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libsodium": { + "type": "library", + "artifact": "libsodium", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "libssh2": { + "type": "library", + "depends": [ + "openssl" + ], + "headers": [ + "libssh2.h", + "libssh2_publickey.h", + "libssh2_sftp.h" + ], + "artifact": "libssh2", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libtiff": { + "type": "library", + "depends": [ + "zlib", + "libjpeg" + ], + "suggests": [ + "lerc", + "libwebp", + "jbig", + "xz", + "zstd" + ], + "artifact": "libtiff", + "license": { + "type": "file", + "path": "LICENSE.md" + } + }, + "liburing": { + "type": "library", + "headers@linux": [ + "liburing/", + "liburing.h" + ], + "pkg-configs": [ + "liburing", + "liburing-ffi" + ], + "artifact": "liburing", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libuuid": { + "type": "library", + "headers": [ + "uuid/uuid.h" + ], + "artifact": "libuuid", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libuv": { + "type": "library", + "artifact": "libuv", + "license": [ + { + "type": "file", + "path": "LICENSE" + }, + { + "type": "file", + "path": "LICENSE-extra" + } + ] + }, + "libwebp": { + "type": "library", + "pkg-configs": [ + "libwebp", + "libwebpdecoder", + "libwebpdemux", + "libwebpmux", + "libsharpyuv" + ], + "artifact": "libwebp", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "libxml2": { + "type": "library", + "depends@windows": [ + "libiconv-win" + ], + "depends": [ + "libiconv" + ], + "suggests@windows": [ + "zlib" + ], + "suggests": [ + "xz", + "zlib" + ], + "headers": [ + "libxml2" + ], + "pkg-configs": [ + "libxml-2.0" + ], + "artifact": "libxml2", + "license": { + "type": "file", + "path": "Copyright" + } + }, + "libxslt": { + "type": "library", + "depends": [ + "libxml2" + ], + "artifact": "libxslt", + "license": { + "type": "file", + "path": "Copyright" + } + }, + "libyaml": { + "type": "library", + "headers": [ + "yaml.h" + ], + "artifact": "libyaml", + "license": { + "type": "file", + "path": "License" + } + }, + "libzip": { + "type": "library", + "depends@windows": [ + "zlib", + "bzip2", + "xz" + ], + "depends": [ + "zlib" + ], + "suggests@windows": [ + "zstd", + "openssl" + ], + "suggests": [ + "bzip2", + "xz", + "zstd", + "openssl" + ], + "headers": [ + "zip.h", + "zipconf.h" + ], + "artifact": "libzip", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "mimalloc": { + "type": "library", + "artifact": "mimalloc", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "ncurses": { + "type": "library", + "artifact": "ncurses", + "static-libs@unix": [ + "libncurses.a" + ], + "license": { + "type": "file", + "path": "COPYING" + } + }, + "net-snmp": { + "type": "library", + "depends": [ + "openssl", + "zlib" + ], + "pkg-configs": [ + "netsnmp", + "netsnmp-agent" + ], + "artifact": "net-snmp", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "nghttp2": { + "type": "library", + "depends": [ + "zlib", + "openssl" + ], + "suggests": [ + "libxml2", + "nghttp3", + "ngtcp2" + ], + "headers": [ + "nghttp2" + ], + "artifact": "nghttp2", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "nghttp3": { + "type": "library", + "depends": [ + "openssl" + ], + "headers": [ + "nghttp3" + ], + "artifact": "nghttp3", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "ngtcp2": { + "type": "library", + "depends": [ + "openssl" + ], + "suggests": [ + "nghttp3", + "brotli" + ], + "headers": [ + "ngtcp2" + ], + "artifact": "ngtcp2", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "onig": { + "type": "library", + "headers": [ + "oniggnu.h", + "oniguruma.h" + ], + "artifact": "onig", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "openssl": { + "type": "library", + "depends": [ + "zlib" + ], + "headers": [ + "openssl" + ], + "artifact": "openssl", + "license": { + "type": "file", + "path": "LICENSE.txt" + } + }, + "postgresql": { + "type": "library", + "depends": [ + "libiconv", + "libxml2", + "openssl", + "zlib", + "libedit" + ], + "suggests": [ + "icu", + "libxslt", + "ldap", + "zstd" + ], + "pkg-configs": [ + "libpq" + ], + "artifact": "postgresql", + "license": { + "type": "file", + "path": "COPYRIGHT" + } + }, + "postgresql-win": { + "type": "library", + "artifact": "postgresql-win", + "license": { + "type": "text", + "text": "PostgreSQL Database Management System\n(also known as Postgres, formerly as Postgres95)\n\nPortions Copyright (c) 1996-2025, The PostgreSQL Global Development Group\n\nPortions Copyright (c) 1994, The Regents of the University of California\n\nPermission to use, copy, modify, and distribute this software and its\ndocumentation for any purpose, without fee, and without a written\nagreement is hereby granted, provided that the above copyright notice\nand this paragraph and the following two paragraphs appear in all\ncopies.\n\nIN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY\nFOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,\nINCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS\nDOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF\nTHE POSSIBILITY OF SUCH DAMAGE.\n\nTHE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,\nINCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY\nAND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS\nON AN \"AS IS\" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS\nTO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS." + } + }, + "pthreads4w": { + "type": "library", + "artifact": "pthreads4w", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "qdbm": { + "type": "library", + "headers@windows": [ + "depot.h" + ], + "artifact": "qdbm", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "re2c": { + "type": "library", + "artifact": "re2c", + "license": { + "type": "file", + "path": "LICENSE" + } + }, + "readline": { + "type": "library", + "depends": [ + "ncurses" + ], + "artifact": "readline", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "snappy": { + "type": "library", + "depends": [ + "zlib" + ], + "headers": [ + "snappy.h", + "snappy-c.h", + "snappy-sinksource.h", + "snappy-stubs-public.h" + ], + "artifact": "snappy", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "sqlite": { + "type": "library", + "headers": [ + "sqlite3.h", + "sqlite3ext.h" + ], + "artifact": "sqlite", + "license": { + "type": "text", + "text": "The author disclaims copyright to this source code. In place of\na legal notice, here is a blessing:\n\n * May you do good and not evil.\n * May you find forgiveness for yourself and forgive others.\n * May you share freely, never taking more than you give." + } + }, + "tidy": { + "type": "library", + "artifact": "tidy", + "license": { + "type": "file", + "path": "README/LICENSE.md" + } + }, + "unixodbc": { + "type": "library", + "depends": [ + "libiconv" + ], + "artifact": "unixodbc", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "watcher": { + "type": "library", + "headers": [ + "wtr/watcher-c.h" + ], + "artifact": "watcher", + "license": { + "type": "file", + "path": "license" + } + }, + "xz": { + "type": "library", + "depends": [ + "libiconv" + ], + "headers@windows": [ + "lzma", + "lzma.h" + ], + "headers": [ + "lzma" + ], + "artifact": "xz", + "license": { + "type": "file", + "path": "COPYING" + } + }, + "zlib": { + "type": "library", + "headers": [ + "zlib.h", + "zconf.h" + ], + "artifact": "zlib", + "license": { + "type": "text", + "text": "(C) 1995-2022 Jean-loup Gailly and Mark Adler\n\nThis software is provided 'as-is', without any express or implied\nwarranty. In no event will the authors be held liable for any damages\narising from the use of this software.\n\nPermission is granted to anyone to use this software for any purpose,\nincluding commercial applications, and to alter it and redistribute it\nfreely, subject to the following restrictions:\n\n1. The origin of this software must not be misrepresented; you must not\n claim that you wrote the original software. If you use this software\n in a product, an acknowledgment in the product documentation would be\n appreciated but is not required.\n2. Altered source versions must be plainly marked as such, and must not be\n misrepresented as being the original software.\n3. This notice may not be removed or altered from any source distribution.\n\nJean-loup Gailly Mark Adler\njloup@gzip.org madler@alumni.caltech.edu" + } + }, + "zstd": { + "type": "library", + "headers@windows": [ + "zstd.h", + "zstd_errors.h" + ], + "headers": [ + "zdict.h", + "zstd.h", + "zstd_errors.h" + ], + "artifact": "zstd", + "license": { + "type": "file", + "path": "LICENSE" + } + } +} diff --git a/config/pkg.target.json b/config/pkg.target.json new file mode 100644 index 000000000..2ae49f400 --- /dev/null +++ b/config/pkg.target.json @@ -0,0 +1,98 @@ +{ + "vswhere": { + "type": "target", + "artifact": "vswhere", + "static-bins@windows": [ + "vswhere.exe" + ] + }, + "pkg-config": { + "type": "target", + "static-bins": [ + "pkg-config" + ], + "artifact": "pkg-config" + }, + "php": { + "type": "target", + "artifact": "php-src", + "depends@macos": [ + "libxml2" + ] + }, + "php-cli": { + "type": "virtual-target", + "depends": [ + "php" + ] + }, + "php-micro": { + "type": "virtual-target", + "artifact": "micro", + "depends": [ + "php" + ] + }, + "php-cgi": { + "type": "virtual-target", + "depends": [ + "php" + ] + }, + "php-fpm": { + "type": "virtual-target", + "depends": [ + "php" + ] + }, + "php-embed": { + "type": "virtual-target", + "depends": [ + "php" + ] + }, + "frankenphp": { + "type": "virtual-target", + "artifact": "frankenphp", + "depends": [ + "php-embed", + "go-xcaddy" + ], + "depends@macos": [ + "php-embed", + "go-xcaddy", + "libxml2" + ] + }, + "go-xcaddy": { + "type": "target", + "artifact": "go-xcaddy", + "static-bins": [ + "xcaddy" + ] + }, + "musl-toolchain": { + "type": "target", + "artifact": "musl-toolchain" + }, + "strawberry-perl": { + "type": "target", + "artifact": "strawberry-perl" + }, + "upx": { + "type": "target", + "artifact": "upx" + }, + "zig": { + "type": "target", + "artifact": "zig" + }, + "nasm": { + "type": "target", + "artifact": "nasm" + }, + "php-sdk-binary-tools": { + "type": "target", + "artifact": "php-sdk-binary-tools" + } +} diff --git a/config/source.json b/config/source.json index 06aa5c6f5..9a80cd059 100644 --- a/config/source.json +++ b/config/source.json @@ -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", diff --git a/phpstan.neon b/phpstan.neon index a8c1c72c5..cf6e49742 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -17,3 +17,4 @@ parameters: - ./src/globals/ext-tests/swoole.php - ./src/globals/ext-tests/swoole.phpt - ./src/globals/test-extensions.php + - ./src/SPC/ diff --git a/spc.registry.json b/spc.registry.json new file mode 100644 index 000000000..7c5a8ce7b --- /dev/null +++ b/spc.registry.json @@ -0,0 +1,32 @@ +{ + "name": "internal", + "autoload": "vendor/autoload.php", + "doctor": { + "psr-4": { + "StaticPHP\\Doctor\\Item": "src/StaticPHP/Doctor/Item" + } + }, + "package": { + "psr-4": { + "Package": "src/Package" + }, + "config": [ + "config/pkg.ext.json", + "config/pkg.lib.json", + "config/pkg.target.json" + ] + }, + "artifact": { + "config": [ + "config/artifact.json" + ], + "psr-4": { + "Package\\Artifact": "src/Package/Artifact" + } + }, + "command": { + "psr-4": { + "Package\\Command": "src/Package/Command" + } + } +} diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php new file mode 100644 index 000000000..293f7c62b --- /dev/null +++ b/src/Package/Artifact/go_xcaddy.php @@ -0,0 +1,94 @@ + 'amd64', + 'aarch64' => 'arm64', + default => throw new DownloaderException('Unsupported architecture: ' . $name), + }; + $os = match (explode('-', $name)[0]) { + 'linux' => 'linux', + 'macos' => 'darwin', + default => throw new DownloaderException('Unsupported OS: ' . $name), + }; + + // 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\">.*?([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 DownloaderException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}"); + } + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version); + } + + #[AfterBinaryExtract('go-xcaddy', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function afterExtract(string $target_path): void + { + if (file_exists("{$target_path}/bin/go") && file_exists("{$target_path}/bin/xcaddy")) { + return; + } + + $sanitizedPath = getenv('PATH'); + if (PHP_OS_FAMILY === 'Linux' && !LinuxUtil::isMuslDist()) { + $sanitizedPath = preg_replace('#(:?/?[^:]*musl[^:]*)#', '', $sanitizedPath); + $sanitizedPath = preg_replace('#^:|:$|::#', ':', $sanitizedPath); // clean up colons + } + + shell()->appendEnv([ + 'PATH' => "{$target_path}/bin:{$sanitizedPath}", + 'GOROOT' => "{$target_path}", + 'GOBIN' => "{$target_path}/bin", + 'GOPATH' => "{$target_path}/go", + ])->exec('CC=cc go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest'); + GlobalEnvManager::addPathIfNotExists("{$target_path}/bin"); + } +} diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php new file mode 100644 index 000000000..2ac7b454b --- /dev/null +++ b/src/Package/Artifact/zig.php @@ -0,0 +1,98 @@ +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); + } +} diff --git a/src/Package/Command/SwitchPhpVersionCommand.php b/src/Package/Command/SwitchPhpVersionCommand.php new file mode 100644 index 000000000..3782a6452 --- /dev/null +++ b/src/Package/Command/SwitchPhpVersionCommand.php @@ -0,0 +1,121 @@ +addArgument( + 'php-version', + InputArgument::REQUIRED, + 'PHP version (e.g., 8.4, 8.3, 8.2, 8.1, 8.0, 7.4, or specific like 8.4.5)', + ); + + // Downloader options + $this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions()); + + // Additional options + $this->addOption('keep-source', null, null, 'Keep extracted source directory (do not remove source/php-src)'); + } + + public function handle(): int + { + $php_ver = $this->getArgument('php-version'); + + // Validate version format + if (!$this->isValidPhpVersion($php_ver)) { + $this->output->writeln("Invalid PHP version '{$php_ver}'!"); + $this->output->writeln('Supported formats: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, or specific version like 8.4.5'); + return static::FAILURE; + } + + $cache = ApplicationContext::get(ArtifactCache::class); + + // Check if php-src is already locked + $source_info = $cache->getSourceInfo('php-src'); + if ($source_info !== null) { + $current_version = $source_info['version'] ?? 'unknown'; + $this->output->writeln("Current PHP version: {$current_version}, removing old PHP source cache..."); + + // Remove cache entry and optionally the downloaded file + $cache->removeSource('php-src', delete_file: true); + } + + // Remove extracted source directory if exists and --keep-source not set + $source_dir = SOURCE_PATH . '/php-src'; + if (!$this->getOption('keep-source') && is_dir($source_dir)) { + $this->output->writeln('Removing extracted PHP source directory...'); + InteractiveTerm::indicateProgress('Removing: ' . $source_dir); + FileSystem::removeDir($source_dir); + InteractiveTerm::finish('Removed: ' . $source_dir); + } + + // Download new PHP source + $this->output->writeln("Downloading PHP {$php_ver} source..."); + + $this->input->setOption('with-php', $php_ver); + + $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->input->getOptions()); + $downloader = new ArtifactDownloader($downloaderOptions); + + // Get php-src artifact from php package + $php_package = PackageLoader::getPackage('php'); + $artifact = $php_package->getArtifact(); + + if ($artifact === null) { + $this->output->writeln('Failed to get php-src artifact!'); + return static::FAILURE; + } + + $downloader->add($artifact); + $downloader->download(); + + // Get the new version info + $new_source_info = $cache->getSourceInfo('php-src'); + $new_version = $new_source_info['version'] ?? $php_ver; + + $this->output->writeln(''); + $this->output->writeln("Successfully switched to PHP {$new_version}!"); + + return static::SUCCESS; + } + + /** + * Validate PHP version format. + * + * Accepts: + * - Major.Minor format: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4 + * - Full version format: 8.4.5, 8.3.12, etc. + */ + private function isValidPhpVersion(string $version): bool + { + // Check major.minor format (e.g., 8.4) + if (in_array($version, ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'], true)) { + return true; + } + + // Check full version format (e.g., 8.4.5) + if (preg_match('/^\d+\.\d+\.\d+$/', $version)) { + return true; + } + + return false; + } +} diff --git a/src/Package/Extension/readline.php b/src/Package/Extension/readline.php new file mode 100644 index 000000000..80f3fa334 --- /dev/null +++ b/src/Package/Extension/readline.php @@ -0,0 +1,37 @@ +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); + } + } +} diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php new file mode 100644 index 000000000..69e6c8820 --- /dev/null +++ b/src/Package/Library/imap.php @@ -0,0 +1,25 @@ +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']); + } +} diff --git a/src/Package/Library/libiconv.php b/src/Package/Library/libiconv.php new file mode 100644 index 000000000..ac91d188a --- /dev/null +++ b/src/Package/Library/libiconv.php @@ -0,0 +1,27 @@ +configure( + '--enable-extra-encodings', + '--enable-year2038', + ) + ->make('install-lib', with_install: false) + ->make('install-lib', with_install: false, dir: "{$package->getSourceDir()}/libcharset"); + $package->patchLaDependencyPrefix(); + } +} diff --git a/src/Package/Library/libxml2.php b/src/Package/Library/libxml2.php new file mode 100644 index 000000000..4767efcb0 --- /dev/null +++ b/src/Package/Library/libxml2.php @@ -0,0 +1,54 @@ +optionalPackage( + 'zlib', + '-DLIBXML2_WITH_ZLIB=ON ' . + "-DZLIB_LIBRARY={$package->getLibDir()}/libz.a " . + "-DZLIB_INCLUDE_DIR={$package->getIncludeDir()}", + '-DLIBXML2_WITH_ZLIB=OFF', + ) + ->optionalPackage('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) + ->addConfigureArgs( + '-DLIBXML2_WITH_ICONV=ON', + '-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md + '-DLIBXML2_WITH_PYTHON=OFF', + '-DLIBXML2_WITH_PROGRAMS=OFF', + '-DLIBXML2_WITH_TESTS=OFF', + ); + + if (SystemTarget::getTargetOS() === 'Linux') { + $cmake->addConfigureArgs('-DIconv_IS_BUILT_IN=OFF'); + } + + $cmake->build(); + + FileSystem::replaceFileStr( + BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc', + '-lxml2 -liconv', + '-lxml2' + ); + FileSystem::replaceFileStr( + BUILD_LIB_PATH . '/pkgconfig/libxml-2.0.pc', + '-lxml2', + '-lxml2 -liconv' + ); + } +} diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php new file mode 100644 index 000000000..c7c39dc1f --- /dev/null +++ b/src/Package/Library/ncurses.php @@ -0,0 +1,61 @@ +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}'); + } + } +} diff --git a/src/Package/Library/onig.php b/src/Package/Library/onig.php new file mode 100644 index 000000000..2cea572bf --- /dev/null +++ b/src/Package/Library/onig.php @@ -0,0 +1,24 @@ +addConfigureArgs('-DMSVC_STATIC_RUNTIME=ON') + ->build(); + FileSystem::copy("{$package->getLibDir()}\\onig.lib", "{$package->getLibDir()}\\onig_a.lib"); + } +} diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php new file mode 100644 index 000000000..bd96da2c9 --- /dev/null +++ b/src/Package/Library/postgresql.php @@ -0,0 +1,23 @@ +cd($package->getSourceDir()) + ->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure'); + } +} diff --git a/src/Package/README.md b/src/Package/README.md new file mode 100644 index 000000000..81a45fa6f --- /dev/null +++ b/src/Package/README.md @@ -0,0 +1,3 @@ +# Package Implementation + +This directory contains the implementation of the `Package` module, which provides functionality for managing and manipulating packages within the system. diff --git a/src/Package/Target/go_xcaddy.php b/src/Package/Target/go_xcaddy.php new file mode 100644 index 000000000..01c4ade3d --- /dev/null +++ b/src/Package/Target/go_xcaddy.php @@ -0,0 +1,24 @@ +getSourceDir()}/Makefile", 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); + } +} diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php new file mode 100644 index 000000000..8cd6e9407 --- /dev/null +++ b/src/Package/Target/php.php @@ -0,0 +1,260 @@ +getSourceDir()}/main/php_version.h")) { + throw new WrongUsageException('PHP source files are not available, you need to download them first'); + } + + $file = file_get_contents("{$artifact->getSourceDir()}/main/php_version.h"); + if (preg_match('/PHP_VERSION_ID (\d+)/', $file, $match) !== 0) { + return intval($match[1]); + } + + throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); + } + + #[InitPackage] + public function init(TargetPackage $package): void + { + // universal build options (may move to base class later) + $package->addBuildOption('with-added-patch', 'P', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Inject patch script outside'); + + // basic build argument and options for PHP + $package->addBuildArgument('extensions', InputArgument::REQUIRED, 'Comma-separated list of static extensions to build'); + $package->addBuildOption('no-strip', null, null, 'build without strip, keep symbols to debug'); + $package->addBuildOption('with-upx-pack', null, null, 'Compress / pack binary using UPX tool (linux/windows only)'); + + // php configure and extra patch options + $package->addBuildOption('disable-opcache-jit', null, null, 'Disable opcache jit'); + $package->addBuildOption('with-config-file-path', null, InputOption::VALUE_REQUIRED, 'Set the path in which to look for php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php'); + $package->addBuildOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php/conf.d'); + $package->addBuildOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI'); + $package->addBuildOption('enable-zts', null, null, 'Enable thread safe support'); + + // phpmicro build options + if ($package->getName() === 'php' || $package->getName() === 'php-micro') { + $package->addBuildOption('with-micro-fake-cli', null, null, 'Let phpmicro\'s PHP_SAPI use "cli" instead of "micro"'); + $package->addBuildOption('without-micro-ext-test', null, null, 'Disable phpmicro with extension test code'); + $package->addBuildOption('with-micro-logo', null, InputOption::VALUE_REQUIRED, 'Use custom .ico for micro.sfx (windows only)'); + $package->addBuildOption('enable-micro-win32', null, null, 'Enable win32 mode for phpmicro (Windows only)'); + } + + // frankenphp build options + if ($package->getName() === 'php' || $package->getName() === 'frankenphp') { + $package->addBuildOption('with-frankenphp-app', null, InputOption::VALUE_REQUIRED, 'Path to a folder to be embedded in FrankenPHP'); + } + + // embed build options + if ($package->getName() === 'php' || $package->getName() === 'php-embed') { + $package->addBuildOption('build-shared', 'D', InputOption::VALUE_REQUIRED, 'Shared extensions to build, comma separated', ''); + } + + // legacy php target build options + V2CompatLayer::addLegacyBuildOptionsForPhp($package); + if ($package->getName() === 'php') { + $package->addBuildOption('build-micro', null, null, 'Build micro SAPI'); + $package->addBuildOption('build-cli', null, null, 'Build cli SAPI'); + $package->addBuildOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)'); + $package->addBuildOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)'); + $package->addBuildOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)'); + $package->addBuildOption('build-cgi', null, null, 'Build cgi SAPI'); + $package->addBuildOption('build-all', null, null, 'Build all SAPI'); + } + } + + #[ResolveBuild] + 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')); + $additional_libraries = parse_comma_list($package->getBuildOption('with-libs')); + $additional_packages = parse_comma_list($package->getBuildOption('with-packages')); + $additional_packages = array_merge($additional_libraries, $additional_packages); + $shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []); + + $extensions_pkg = array_map( + fn ($x) => "ext-{$x}", + array_values(array_unique([...$static_extensions, ...$shared_extensions])) + ); + + // 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."); + } + $instance = PackageLoader::getPackage($extension); + if (!$instance instanceof PhpExtensionPackage) { + throw new WrongUsageException("Package [{$extension}] is not a PHP extension package"); + } + // 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]; + } + + #[Validate] + public function validate(Package $package): void + { + // frankenphp + if ($package->getName() === 'frankenphp' && $package instanceof TargetPackage) { + if (!$package->getBuildOption('enable-zts')) { + throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!'); + } + // frankenphp doesn't support windows, BSD is currently not supported by static-php-cli + if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { + throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); + } + } + // linux does not support loading shared libraries when target is pure static + $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; + if (SystemTarget::getTargetOS() === 'Linux' && ApplicationContext::get(ToolchainInterface::class)->isStatic() && $embed_type === 'shared') { + throw new WrongUsageException( + 'Linux does not support loading shared libraries when linking libc statically. ' . + 'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.' + ); + } + } + + #[Info] + public function info(Package $package, PackageInstaller $installer): array + { + /** @var TargetPackage $package */ + if ($package->getName() !== 'php') { + return []; + } + $sapis = array_filter([ + $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 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 [ + 'Build OS' => SystemTarget::getTargetOS() . ' (' . SystemTarget::getTargetArch() . ')', + 'Build Target' => getenv('SPC_TARGET') ?: '', + 'Build Toolchain' => ToolchainManager::getToolchainClass(), + 'Build SAPI' => implode(', ', $sapis), + 'Static Extensions (' . count($static_extensions) . ')' => implode(',', array_map(fn ($x) => substr($x->getName(), 4), $static_extensions)), + 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), + 'Install Packages (' . count($install_packages) . ')' => implode(',', array_map(fn ($x) => $x->getName(), $install_packages)), + ]; + } + + #[BeforeStage('php', 'build')] + public function beforeBuild(PackageBuilder $builder, Package $package): void + { + // Process -I option + $custom_ini = []; + foreach ($builder->getOption('with-hardcoded-ini', []) as $value) { + [$source_name, $ini_value] = explode('=', $value, 2); + $custom_ini[$source_name] = $ini_value; + logger()->info("Adding hardcoded INI [{$source_name} = {$ini_value}]"); + } + if (!empty($custom_ini)) { + SourcePatcher::patchHardcodedINI($package->getSourceDir(), $custom_ini); + } + + // Patch StaticPHP version + // detect patch (remove this when 8.3 deprecated) + $file = FileSystem::readFile("{$package->getSourceDir()}/main/main.c"); + if (!str_contains($file, 'static-php-cli.version')) { + $version = SPC_VERSION; + logger()->debug('Inserting static-php-cli.version to php-src'); + $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"static-php-cli.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); + FileSystem::writeFile("{$package->getSourceDir()}/main/main.c", $file); + } + + // clean old modules that may conflict with the new php build + FileSystem::removeDir(BUILD_MODULES_PATH); + } + + private function makeStaticExtensionString(PackageInstaller $installer): string + { + $arg = []; + foreach ($installer->getResolvedPackages() as $package) { + /** @var PhpExtensionPackage $package */ + if ($package->getType() !== 'php-extension' || !$package instanceof PhpExtensionPackage) { + continue; + } + + // build-shared=true, build-static=false, build-with-php=true + if ($package->isBuildShared() && !$package->isBuildStatic() && $package->isBuildWithPhp()) { + $arg[] = $package->getPhpConfigureArg(SystemTarget::getTargetOS(), true); + } elseif ($package->isBuildStatic()) { + $arg[] = $package->getPhpConfigureArg(SystemTarget::getTargetOS(), false); + } + } + $str = implode(' ', $arg); + logger()->debug("Static extension configure args: {$str}"); + return $str; + } +} diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php new file mode 100644 index 000000000..23d465dfb --- /dev/null +++ b/src/Package/Target/php/unix.php @@ -0,0 +1,450 @@ +/dev/null && ldd --version 2>&1 | grep ^musl >/dev/null 2>&1', + 'if ' . ($musl ? 'true' : 'false') + ); + + // let php m4 tools use static pkg-config + FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); + } + + #[Stage] + public function buildconfForUnix(TargetPackage $package): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); + V2CompatLayer::emitPatchPoint('before-php-buildconf'); + shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); + } + + #[Stage] + public function configureForUnix(TargetPackage $package, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure')); + V2CompatLayer::emitPatchPoint('before-php-configure'); + $cmd = getenv('SPC_CMD_PREFIX_PHP_CONFIGURE'); + + $args = []; + $version_id = self::getPHPVersionID(); + // PHP JSON extension is built-in since PHP 8.0 + if ($version_id < 80000) { + $args[] = '--enable-json'; + } + // zts + if ($package->getBuildOption('enable-zts', false)) { + $args[] = '--enable-zts --disable-zend-signals'; + if ($version_id >= 80100 && SystemTarget::getTargetOS() === 'Linux') { + $args[] = '--enable-zend-max-execution-timers'; + } + } + // config-file-path and config-file-scan-dir + if ($option = $package->getBuildOption('with-config-file-path', false)) { + $args[] = "--with-config-file-path={$option}"; + } + if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) { + $args[] = "--with-config-file-scan-dir={$option}"; + } + // perform enable cli options + $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->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi'; + $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; + $args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; + $args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null; + $args = implode(' ', array_filter($args)); + + $static_extension_str = $this->makeStaticExtensionString($installer); + + // run ./configure with args + $this->seekPhpSrcLogFileOnException(fn () => shell()->cd($package->getSourceDir())->setEnv([ + 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), + 'CPPFLAGS' => "-I{$package->getIncludeDir()}", + 'LDFLAGS' => "-L{$package->getLibDir()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), + ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); + } + + #[Stage] + public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void + { + V2CompatLayer::emitPatchPoint('before-php-make'); + + logger()->info('cleaning up php-src build files'); + shell()->cd($package->getSourceDir())->exec('make clean'); + + if ($installer->isPackageResolved('php-cli')) { + $package->runStage([self::class, 'makeCliForUnix']); + } + if ($installer->isPackageResolved('php-cgi')) { + $package->runStage([self::class, 'makeCgiForUnix']); + } + 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] + public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli')); + $concurrency = $builder->concurrency; + 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')] + #[BuildFor('Linux')] + public function build(TargetPackage $package): void + { + // virtual target, do nothing + if ($package->getName() !== 'php') { + return; + } + + $package->runStage([$this, 'buildconfForUnix']); + $package->runStage([$this, 'configureForUnix']); + $package->runStage([$this, 'makeForUnix']); + + $package->runStage([$this, 'unixBuildSharedExt']); + } + + /** + * Patch phpize and php-config if needed + */ + #[Stage] + public function patchUnixEmbedScripts(): 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'); + } + } + + /** + * Seek php-src/config.log when building PHP, add it to exception. + */ + protected function seekPhpSrcLogFileOnException(callable $callback, string $source_dir): void + { + try { + $callback(); + } catch (SPCException $e) { + if (file_exists("{$source_dir}/config.log")) { + $e->addExtraLogFile('php-src config.log', 'php-src.config.log'); + copy("{$source_dir}/config.log", SPC_LOGS_DIR . '/php-src.config.log'); + } + throw $e; + } + } + + /** + * Rename libphp.so to libphp-.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) + )); + } + } + } + } + + /** + * 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())); + $static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : ''; + $pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : ''; + + return array_filter([ + 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), + 'EXTRA_LDFLAGS_PROGRAM' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . "{$config['ldflags']} {$static} {$pie}", + 'EXTRA_LDFLAGS' => $config['ldflags'], + 'EXTRA_LIBS' => $config['libs'], + ]); + } +} diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php new file mode 100644 index 000000000..e77b88cd1 --- /dev/null +++ b/src/Package/Target/php/windows.php @@ -0,0 +1,232 @@ +cd($package->getSourceDir())->exec('.\buildconf.bat'); + } + + #[Stage] + public function configureForWindows(TargetPackage $package, PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure.bat')); + V2CompatLayer::emitPatchPoint('before-php-configure'); + $args = [ + '--disable-all', + "--with-php-build={$package->getBuildRootPath()}", + "--with-extra-includes={$package->getIncludeDir()}", + "--with-extra-libs={$package->getLibDir()}", + ]; + // sapis + $cli = $installer->isPackageResolved('php-cli'); + $cgi = $installer->isPackageResolved('php-cgi'); + $micro = $installer->isPackageResolved('php-micro'); + $args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no'; + $args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no'; + $args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no'; + + // zts + $args[] = $package->getBuildOption('enable-zts', false) ? '--enable-zts=yes' : '--enable-zts=no'; + // opcache-jit + $args[] = !$package->getBuildOption('disable-opcache-jit', false) ? '--enable-opcache-jit=yes' : '--enable-opcache-jit=no'; + // micro win32 + if ($micro && $package->getBuildOption('enable-micro-win32', false)) { + $args[] = '--enable-micro-win32=yes'; + } + // config-file-scan-dir + if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) { + $args[] = "--with-config-file-scan-dir={$option}"; + } + // micro logo + if ($micro && ($logo = $this->getBuildOption('with-micro-logo')) !== null) { + $args[] = "--enable-micro-logo={$logo}"; + copy($logo, SOURCE_PATH . '\php-src\\' . $logo); + } + $args = implode(' ', $args); + $static_extension_str = $this->makeStaticExtensionString($installer); + cmd()->cd($package->getSourceDir())->exec(".\\configure.bat {$args} {$static_extension_str}"); + } + + #[BeforeStage('php', [self::class, 'makeCliForWindows'])] + #[PatchDescription('Patch Windows Makefile for CLI target')] + public function patchCLITarget(TargetPackage $package): void + { + // search Makefile code line contains "$(BUILD_DIR)\php.exe:" + $content = FileSystem::readFile("{$package->getSourceDir()}\\Makefile"); + $lines = explode("\r\n", $content); + $line_num = 0; + $found = false; + foreach ($lines as $v) { + if (str_contains($v, '$(BUILD_DIR)\php.exe:')) { + $found = $line_num; + break; + } + ++$line_num; + } + if ($found === false) { + throw new PatchException('Windows Makefile patching for php.exe target', 'Cannot patch windows CLI Makefile, Makefile does not contain "$(BUILD_DIR)\php.exe:" line'); + } + $lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(PHP_GLOBAL_OBJS) $(CLI_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest'; + $lines[$line_num + 1] = "\t" . '"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CLI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CLI) $(BUILD_DIR)\php.exe.res /out:$(BUILD_DIR)\php.exe $(LDFLAGS) $(LDFLAGS_CLI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286'; + FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines)); + } + + #[Stage] + public function makeCliForWindows(TargetPackage $package, PackageBuilder $builder): void + { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php.exe')); + + // extra lib + $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + + // Add debug symbols for release build if --no-strip is specified + // We need to modify CFLAGS to replace /Ox with /Zi and add /DEBUG to LDFLAGS + $debug_overrides = ''; + if ($package->getBuildOption('no-strip', false)) { + // Read current CFLAGS from Makefile and replace optimization flags + $makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile"); + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + // Replace /Ox (full optimization) with /Zi (debug info) and /Od (disable optimization) + // Keep optimization for speed: /O2 /Zi instead of /Od /Zi + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CLI=/DEBUG" '; + } + } + + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}LIBS_CLI=\"ws2_32.lib shell32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php.exe"); + + $this->deployWindowsBinary($builder, $package, 'php-cli'); + } + + #[Stage] + public function makeForWindows(TargetPackage $package, PackageInstaller $installer): void + { + V2CompatLayer::emitPatchPoint('before-php-make'); + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('nmake clean')); + cmd()->cd($package->getSourceDir())->exec('nmake clean'); + + if ($installer->isPackageResolved('php-cli')) { + $package->runStage([$this, 'makeCliForWindows']); + } + if ($installer->isPackageResolved('php-cgi')) { + $package->runStage([$this, 'makeCgiForWindows']); + } + if ($installer->isPackageResolved('php-micro')) { + $package->runStage([$this, 'makeMicroForWindows']); + } + } + + #[BuildFor('Windows')] + public function buildWin(TargetPackage $package): void + { + if ($package->getName() !== 'php') { + return; + } + + $package->runStage([$this, 'buildconfForWindows']); + $package->runStage([$this, 'configureForWindows']); + $package->runStage([$this, 'makeForWindows']); + } + + #[BeforeStage('php', [self::class, 'buildconfForWindows'])] + #[PatchDescription('Patch SPC_MICRO_PATCHES defined patches')] + #[PatchDescription('Fix PHP 8.1 static build bug on Windows')] + #[PatchDescription('Fix PHP Visual Studio version detection')] + public function patchBeforeBuildconfForWindows(TargetPackage $package): void + { + // php-src patches from micro + SourcePatcher::patchPhpSrc(); + + // php 8.1 bug + if ($this->getPHPVersionID() >= 80100 && $this->getPHPVersionID() < 80200) { + logger()->info('Patching PHP 8.1 windows Fiber bug'); + FileSystem::replaceFileStr( + "{$package->getSourceDir()}\\win32\\build\\config.w32", + "ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');", + "ADD_FLAG('ASM_OBJS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj $(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');" + ); + FileSystem::replaceFileStr( + "{$package->getSourceDir()}\\win32\\build\\config.w32", + "ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');", + '' + ); + } + + // Fix PHP VS version + // get vs version + $vc = WindowsUtil::findVisualStudio(); + if ($vc === false) { + $vc_matches = ['unknown', 'unknown']; + } else { + $vc_matches = match ($vc['major_version']) { + '17' => ['VS17', 'Visual C++ 2022'], + '16' => ['VS16', 'Visual C++ 2019'], + default => ['unknown', 'unknown'], + }; + } + // patch php-src/win32/build/confutils.js + FileSystem::replaceFileStr( + "{$package->getSourceDir()}\\win32\\build\\confutils.js", + 'var name = "unknown";', + "var name = short ? \"{$vc_matches[0]}\" : \"{$vc_matches[1]}\";return name;" + ); + + // patch micro win32 + if ($package->getBuildOption('enable-micro-win32') && !file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) { + copy("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", "{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak"); + FileSystem::replaceFileStr("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", '#include "php_variables.h"', '#include "php_variables.h"' . "\n#define PHP_MICRO_WIN32_NO_CONSOLE 1"); + } else { + if (file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) { + rename("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak", "{$package->getSourceDir()}\\sapi\\micro\\php_micro.c"); + } + } + } + + protected function deployWindowsBinary(PackageBuilder $builder, TargetPackage $package, string $sapi): void + { + $rel_type = 'Release'; // TODO: Debug build support + $ts = $builder->getOption('enable-zts') ? '_TS' : ''; + $debug_dir = BUILD_ROOT_PATH . '\debug'; + $src = match ($sapi) { + 'php-cli' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'php.exe', 'php.pdb'], + 'php-micro' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'micro.sfx', 'micro.pdb'], + 'php-cgi' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'php-cgi.exe', 'php-cgi.pdb'], + default => throw new SPCInternalException("Deployment does not accept type {$sapi}"), + }; + $src_file = "{$src[0]}\\{$src[1]}"; + $dst_file = BUILD_BIN_PATH . '\\' . basename($src_file); + + $builder->deployBinary($src_file, $dst_file); + + // make debug info file path + if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { + FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}"); + } + } +} diff --git a/src/Package/Target/pkgconfig.php b/src/Package/Target/pkgconfig.php new file mode 100644 index 000000000..e99e2d7c0 --- /dev/null +++ b/src/Package/Target/pkgconfig.php @@ -0,0 +1,45 @@ +appendEnv([ + 'CFLAGS' => '-Wimplicit-function-declaration -Wno-int-conversion', + 'LDFLAGS' => $toolchain->isStatic() ? '--static' : '', + ]) + ->configure( + '--with-internal-glib', + '--disable-host-tool', + '--without-sysroot', + '--without-system-include-path', + '--without-system-library-path', + '--without-pc-path', + ) + ->make(with_install: 'install-exec'); + + shell()->exec('strip ' . BUILD_ROOT_PATH . '/bin/pkg-config'); + } +} diff --git a/src/SPC/builder/linux/SystemUtil.php b/src/SPC/builder/linux/SystemUtil.php index 056af090d..30b36b888 100644 --- a/src/SPC/builder/linux/SystemUtil.php +++ b/src/SPC/builder/linux/SystemUtil.php @@ -141,7 +141,7 @@ public static function getSupportedDistros(): array { return [ // debian-like - 'debian', 'ubuntu', 'Deepin', + 'debian', 'ubuntu', 'Deepin', 'neon', // rhel-like 'redhat', // centos diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php index ab8144a21..08a2b4dc9 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -112,7 +112,7 @@ public function checkSystemOSPackages(): ?CheckResult 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 @@ public function fixBuildTools(array $distro, array $missing): bool 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); diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index 86daefc43..3b88a2bce 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -660,11 +660,19 @@ private static function moveFileOrDir(string $source, string $dest): void $source = self::convertPath($source); $dest = self::convertPath($dest); - // Try rename first (fast, atomic) - if (@rename($source, $dest)) { - return; + // 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); diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php new file mode 100644 index 000000000..b5cf74c00 --- /dev/null +++ b/src/StaticPHP/Artifact/Artifact.php @@ -0,0 +1,555 @@ + Bind custom binary fetcher callbacks */ + protected mixed $custom_binary_callbacks = []; + + /** @var null|callable Bind custom source extract callback (completely takes over extraction) */ + protected mixed $source_extract_callback = null; + + /** @var null|array{callback: callable, platforms: string[]} Bind custom binary extract callback (completely takes over extraction) */ + protected ?array $binary_extract_callback = null; + + /** @var array After source extract hooks */ + protected array $after_source_extract_callbacks = []; + + /** @var array After binary extract hooks */ + protected array $after_binary_extract_callbacks = []; + + public function __construct(protected readonly string $name, ?array $config = null) + { + $this->config = $config ?? ArtifactConfig::get($name); + if ($this->config === null) { + throw new WrongUsageException("Artifact '{$name}' not found."); + } + } + + public function getName(): string + { + return $this->name; + } + + /** + * Checks if the source of an artifact is already downloaded. + * + * @param bool $compare_hash Whether to compare hash of the downloaded source + */ + public function isSourceDownloaded(bool $compare_hash = false): bool + { + return ApplicationContext::get(ArtifactCache::class)->isSourceDownloaded($this->name, $compare_hash); + } + + /** + * Checks if the binary of an artifact is already downloaded for the specified target OS. + * + * @param null|string $target_os Target OS platform string, null for current platform + * @param bool $compare_hash Whether to compare hash of the downloaded binary + */ + public function isBinaryDownloaded(?string $target_os = null, bool $compare_hash = false): bool + { + $target_os = $target_os ?? SystemTarget::getCurrentPlatformString(); + return ApplicationContext::get(ArtifactCache::class)->isBinaryDownloaded($this->name, $target_os, $compare_hash); + } + + public function shouldUseBinary(): bool + { + $platform = SystemTarget::getCurrentPlatformString(); + return $this->isBinaryDownloaded($platform) && $this->hasPlatformBinary(); + } + + /** + * Checks if the source of an artifact is already extracted. + * + * @param bool $compare_hash Whether to compare hash of the extracted source + */ + public function isSourceExtracted(bool $compare_hash = false): bool + { + $target_path = $this->getSourceDir(); + + if (!is_dir($target_path)) { + return false; + } + + if (!$compare_hash) { + return true; + } + + // Get expected hash from cache + $cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name); + if ($cache_info === null) { + return false; + } + + $expected_hash = $cache_info['hash'] ?? null; + + // Local source: always consider extracted if directory exists + if ($expected_hash === null) { + return true; + } + + // Check hash marker file + $hash_file = "{$target_path}/.spc-hash"; + if (!file_exists($hash_file)) { + return false; + } + + return FileSystem::readFile($hash_file) === $expected_hash; + } + + /** + * Checks if the binary of an artifact is already extracted for the specified target OS. + * + * @param null|string $target_os Target OS platform string, null for current platform + * @param bool $compare_hash Whether to compare hash of the extracted binary + */ + public function isBinaryExtracted(?string $target_os = null, bool $compare_hash = false): bool + { + $target_os = $target_os ?? SystemTarget::getCurrentPlatformString(); + $extract_config = $this->getBinaryExtractConfig(); + $mode = $extract_config['mode']; + + // For merge mode, check marker file + if ($mode === 'merge') { + $target_path = $extract_config['path']; + $marker_file = "{$target_path}/.spc-{$this->name}-installed"; + + if (!file_exists($marker_file)) { + return false; + } + + if (!$compare_hash) { + return true; + } + + // Get expected hash from cache + $cache_info = ApplicationContext::get(ArtifactCache::class)->getBinaryInfo($this->name, $target_os); + if ($cache_info === null) { + return false; + } + + $expected_hash = $cache_info['hash'] ?? null; + if ($expected_hash === null) { + return true; // Local binary + } + + $installed_hash = FileSystem::readFile($marker_file); + return $installed_hash === $expected_hash; + } + + // For selective mode, cannot reliably check extraction status + if ($mode === 'selective') { + return false; + } + + // For standalone mode, check directory or file and hash + $target_path = $extract_config['path']; + + // Check if target is a file or directory + $is_file_target = !is_dir($target_path) && (pathinfo($target_path, PATHINFO_EXTENSION) !== ''); + + if ($is_file_target) { + // For single file extraction (e.g., vswhere.exe) + if (!file_exists($target_path)) { + return false; + } + } else { + // For directory extraction + if (!is_dir($target_path)) { + return false; + } + } + + if (!$compare_hash) { + return true; + } + + // Get expected hash from cache + $cache_info = ApplicationContext::get(ArtifactCache::class)->getBinaryInfo($this->name, $target_os); + if ($cache_info === null) { + return false; + } + + $expected_hash = $cache_info['hash'] ?? null; + + // Local binary: always consider extracted if directory exists + if ($expected_hash === null) { + return true; + } + + // Check hash marker file + $hash_file = "{$target_path}/.spc-hash"; + if (!file_exists($hash_file)) { + return false; + } + + return FileSystem::readFile($hash_file) === $expected_hash; + } + + /** + * Checks if the artifact has a source defined. + */ + public function hasSource(): bool + { + return isset($this->config['source']) || $this->custom_source_callback !== null; + } + + /** + * Checks if the artifact has a local binary defined for the current system target. + */ + public function hasPlatformBinary(): bool + { + $target = SystemTarget::getCurrentPlatformString(); + return isset($this->config['binary'][$target]) || isset($this->custom_binary_callbacks[$target]); + } + + public function getDownloadConfig(string $type): mixed + { + return $this->config[$type] ?? null; + } + + /** + * Get source extraction directory. + * + * Rules: + * 1. If extract is not specified: SOURCE_PATH/{artifact_name} + * 2. If extract is relative path: SOURCE_PATH/{value} + * 3. If extract is absolute path: {value} + * 4. If extract is array (dict): handled by extractor (selective extraction) + */ + public function getSourceDir(): string + { + // defined in config + $extract = $this->config['source']['extract'] ?? null; + + if ($extract === null) { + return FileSystem::convertPath(SOURCE_PATH . '/' . $this->name); + } + + // Array (dict) mode - return default path, actual handling is in extractor + if (is_array($extract)) { + return FileSystem::convertPath(SOURCE_PATH . '/' . $this->name); + } + + // String path + $path = $this->replaceExtractPathVariables($extract); + + // Absolute path + if (!FileSystem::isRelativePath($path)) { + return FileSystem::convertPath($path); + } + + // Relative path: based on SOURCE_PATH + return FileSystem::convertPath(SOURCE_PATH . '/' . $path); + } + + /** + * Get binary extraction directory and mode. + * + * Rules: + * 1. If extract is not specified: PKG_ROOT_PATH (standard mode) + * 2. If extract is "hosted": BUILD_ROOT_PATH (standard mode, for pre-built libraries) + * 3. If extract is relative path: PKG_ROOT_PATH/{value} (standard mode) + * 4. If extract is absolute path: {value} (standard mode) + * 5. If extract is array (dict): selective extraction mode + * + * @return array{path: ?string, mode: 'merge'|'selective'|'standard', files?: array} + */ + public function getBinaryExtractConfig(array $cache_info = []): array + { + if (is_string($cache_info['extract'] ?? null)) { + return ['path' => $this->replaceExtractPathVariables($cache_info['extract']), 'mode' => 'standard']; + } + + $platform = SystemTarget::getCurrentPlatformString(); + $binary_config = $this->config['binary'][$platform] ?? null; + + if ($binary_config === null) { + return ['path' => PKG_ROOT_PATH, 'mode' => 'standard']; + } + + $extract = $binary_config['extract'] ?? null; + + // Not specified: PKG_ROOT_PATH merge + if ($extract === null) { + return ['path' => PKG_ROOT_PATH, 'mode' => 'standard']; + } + + // "hosted" mode: BUILD_ROOT_PATH merge (for pre-built libraries) + if ($extract === 'hosted' || ($binary_config['type'] ?? '') === 'hosted') { + return ['path' => BUILD_ROOT_PATH, 'mode' => 'standard']; + } + + // Array (dict) mode: selective extraction + if (is_array($extract)) { + return [ + 'path' => null, + 'mode' => 'selective', + 'files' => $extract, + ]; + } + + // String path + $path = $this->replaceExtractPathVariables($extract); + + // Absolute path: standalone mode + if (!FileSystem::isRelativePath($path)) { + return ['path' => FileSystem::convertPath($path), 'mode' => 'standard']; + } + + // Relative path: PKG_ROOT_PATH/{value} standalone mode + return ['path' => FileSystem::convertPath(PKG_ROOT_PATH . '/' . $path), 'mode' => 'standard']; + } + + /** + * Get the binary extraction directory. + * For merge mode, returns the base path. + * For standalone mode, returns the specific directory. + */ + public function getBinaryDir(): ?string + { + $config = $this->getBinaryExtractConfig(); + return $config['path']; + } + + /** + * Set custom source fetcher callback. + */ + public function setCustomSourceCallback(callable $callback): void + { + $this->custom_source_callback = $callback; + } + + public function getCustomSourceCallback(): ?callable + { + return $this->custom_source_callback ?? null; + } + + public function getCustomBinaryCallback(): ?callable + { + $current_platform = SystemTarget::getCurrentPlatformString(); + return $this->custom_binary_callbacks[$current_platform] ?? null; + } + + public function emitCustomBinary(): void + { + $current_platform = SystemTarget::getCurrentPlatformString(); + if (!isset($this->custom_binary_callbacks[$current_platform])) { + throw new SPCInternalException("No custom binary callback defined for artifact '{$this->name}' on target OS '{$current_platform}'."); + } + $callback = $this->custom_binary_callbacks[$current_platform]; + ApplicationContext::invoke($callback, [Artifact::class => $this]); + } + + /** + * Set custom binary fetcher callback for a specific target OS. + * + * @param string $target_os Target OS platform string (e.g. linux-x86_64) + * @param callable $callback Custom binary fetcher callback + */ + public function setCustomBinaryCallback(string $target_os, callable $callback): void + { + ConfigValidator::validatePlatformString($target_os); + $this->custom_binary_callbacks[$target_os] = $callback; + } + + // ==================== Extraction Callbacks ==================== + + /** + * Set custom source extract callback. + * This callback completely takes over the source extraction process. + * + * Callback signature: function(Artifact $artifact, string $source_file, string $target_path): void + */ + public function setSourceExtractCallback(callable $callback): void + { + $this->source_extract_callback = $callback; + } + + /** + * Get the source extract callback. + */ + public function getSourceExtractCallback(): ?callable + { + return $this->source_extract_callback; + } + + /** + * Check if a custom source extract callback is set. + */ + public function hasSourceExtractCallback(): bool + { + return $this->source_extract_callback !== null; + } + + /** + * Set custom binary extract callback. + * This callback completely takes over the binary extraction process. + * + * Callback signature: function(Artifact $artifact, string $source_file, string $target_path, string $platform): void + * + * @param callable $callback The callback function + * @param string[] $platforms Platform filters (empty = all platforms) + */ + public function setBinaryExtractCallback(callable $callback, array $platforms = []): void + { + $this->binary_extract_callback = [ + 'callback' => $callback, + 'platforms' => $platforms, + ]; + } + + /** + * Get the binary extract callback for current platform. + * + * @return null|callable The callback if set and matches current platform, null otherwise + */ + public function getBinaryExtractCallback(): ?callable + { + if ($this->binary_extract_callback === null) { + return null; + } + + $platforms = $this->binary_extract_callback['platforms']; + $current_platform = SystemTarget::getCurrentPlatformString(); + + // Empty platforms array means all platforms + if (empty($platforms) || in_array($current_platform, $platforms, true)) { + return $this->binary_extract_callback['callback']; + } + + return null; + } + + /** + * Check if a custom binary extract callback is set for current platform. + */ + public function hasBinaryExtractCallback(): bool + { + return $this->getBinaryExtractCallback() !== null; + } + + /** + * Add a callback to run after source extraction completes. + * + * Callback signature: function(string $target_path): void + */ + public function addAfterSourceExtractCallback(callable $callback): void + { + $this->after_source_extract_callbacks[] = $callback; + } + + /** + * Add a callback to run after binary extraction completes. + * + * Callback signature: function(string $target_path, string $platform): void + * + * @param callable $callback The callback function + * @param string[] $platforms Platform filters (empty = all platforms) + */ + public function addAfterBinaryExtractCallback(callable $callback, array $platforms = []): void + { + $this->after_binary_extract_callbacks[] = [ + 'callback' => $callback, + 'platforms' => $platforms, + ]; + } + + /** + * Emit all after source extract callbacks. + * + * @param string $target_path The directory where source was extracted + */ + public function emitAfterSourceExtract(string $target_path): void + { + if (empty($this->after_source_extract_callbacks)) { + logger()->debug("No after-source-extract hooks registered for [{$this->name}]"); + return; + } + + logger()->debug('Executing ' . count($this->after_source_extract_callbacks) . " after-source-extract hook(s) for [{$this->name}]"); + foreach ($this->after_source_extract_callbacks as $callback) { + $callback_name = is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'); + logger()->debug(" 🪝 Running hook: {$callback_name}"); + ApplicationContext::invoke($callback, ['target_path' => $target_path, Artifact::class => $this]); + } + } + + /** + * 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') + */ + public function emitAfterBinaryExtract(?string $target_path, string $platform): void + { + if (empty($this->after_binary_extract_callbacks)) { + logger()->debug("No after-binary-extract hooks registered for [{$this->name}]"); + return; + } + + $executed = 0; + foreach ($this->after_binary_extract_callbacks as $item) { + $callback_platforms = $item['platforms']; + + // Empty platforms array means all platforms + if (empty($callback_platforms) || in_array($platform, $callback_platforms, true)) { + $callback = $item['callback']; + $callback_name = is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'); + logger()->debug(" 🪝 Running hook: {$callback_name} (platform: {$platform})"); + ApplicationContext::invoke($callback, [ + 'target_path' => $target_path, + 'platform' => $platform, + Artifact::class => $this, + ]); + ++$executed; + } + } + + logger()->debug("Executed {$executed} after-binary-extract hook(s) for [{$this->name}] on platform [{$platform}]"); + } + + /** + * Replaces variables in the extract path. + * + * @param string $extract the extract path with variables + */ + private function replaceExtractPathVariables(string $extract): string + { + $replacement = [ + '{artifact_name}' => $this->name, + '{pkg_root_path}' => PKG_ROOT_PATH, + '{build_root_path}' => BUILD_ROOT_PATH, + '{php_sdk_path}' => getenv('PHP_SDK_PATH') ?: WORKING_DIR . '/php-sdk-binary-tools', + '{working_dir}' => WORKING_DIR, + '{download_path}' => DOWNLOAD_PATH, + '{source_path}' => SOURCE_PATH, + ]; + return str_replace(array_keys($replacement), array_values($replacement), $extract); + } +} diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php new file mode 100644 index 000000000..3302a37bc --- /dev/null +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -0,0 +1,305 @@ + + */ + protected array $cache = []; + + /** + * @param string $cache_file Lock file position + */ + public function __construct(protected string $cache_file = DOWNLOAD_PATH . '/.cache.json') + { + if (!file_exists($this->cache_file)) { + logger()->debug("Cache file does not exist, creating new one at {$this->cache_file}"); + FileSystem::createDir(dirname($this->cache_file)); + file_put_contents($this->cache_file, json_encode([])); + } else { + $content = file_get_contents($this->cache_file); + $this->cache = json_decode($content ?: '{}', true) ?? []; + } + } + + /** + * Checks if the source of an artifact is already downloaded. + * + * @param string $artifact_name Artifact name + * @param bool $compare_hash Whether to compare hash of the downloaded source + */ + public function isSourceDownloaded(string $artifact_name, bool $compare_hash = false): bool + { + $item = $this->cache[$artifact_name] ?? null; + if ($item === null) { + return false; + } + return $this->isObjectDownloaded($this->cache[$artifact_name]['source'] ?? null, $compare_hash); + } + + /** + * Check if the binary of an artifact for target OS is already downloaded. + * + * @param string $artifact_name Artifact name + * @param string $target_os Target OS (accepts {windows|linux|macos}-{x86_64|aarch64}) + * @param bool $compare_hash Whether to compare hash of the downloaded binary + */ + public function isBinaryDownloaded(string $artifact_name, string $target_os, bool $compare_hash = false): bool + { + $item = $this->cache[$artifact_name] ?? null; + if ($item === null) { + return false; + } + return $this->isObjectDownloaded($this->cache[$artifact_name]['binary'][$target_os] ?? null, $compare_hash); + } + + /** + * Lock the downloaded artifact info into cache. + * + * @param Artifact|string $artifact Artifact instance + * @param 'binary'|'source' $lock_type Lock type ('source'|'binary') + * @param DownloadResult $download_result Download result object + * @param null|string $platform Target platform string for binary lock, null for source lock + */ + public function lock(Artifact|string $artifact, string $lock_type, DownloadResult $download_result, ?string $platform = null): void + { + $artifact_name = $artifact instanceof Artifact ? $artifact->getName() : $artifact; + if (!isset($this->cache[$artifact_name])) { + $this->cache[$artifact_name] = [ + 'source' => null, + 'binary' => [], + ]; + } + $obj = null; + if ($download_result->cache_type === 'archive') { + $obj = [ + 'lock_type' => $lock_type, + 'cache_type' => 'archive', + 'filename' => $download_result->filename, + 'extract' => $download_result->extract, + 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'version' => $download_result->version, + 'config' => $download_result->config, + ]; + } elseif ($download_result->cache_type === 'file') { + $obj = [ + 'lock_type' => $lock_type, + 'cache_type' => 'file', + 'filename' => $download_result->filename, + 'extract' => $download_result->extract, + 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'version' => $download_result->version, + 'config' => $download_result->config, + ]; + } elseif ($download_result->cache_type === 'git') { + $obj = [ + 'lock_type' => $lock_type, + 'cache_type' => 'git', + 'dirname' => $download_result->dirname, + 'extract' => $download_result->extract, + 'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')), + 'version' => $download_result->version, + 'config' => $download_result->config, + ]; + } elseif ($download_result->cache_type === 'local') { + $obj = [ + 'lock_type' => $lock_type, + 'cache_type' => 'local', + 'dirname' => $download_result->dirname, + 'extract' => $download_result->extract, + 'hash' => null, + 'version' => $download_result->version, + 'config' => $download_result->config, + ]; + } + if ($obj === null) { + throw new SPCInternalException("Invalid download result for locking artifact {$artifact_name}"); + } + if ($lock_type === 'binary') { + if ($platform === null) { + throw new SPCInternalException("Invalid download result for locking binary artifact {$artifact_name}: platform cannot be null"); + } + $obj['platform'] = $platform; + } + if ($lock_type === 'source') { + $this->cache[$artifact_name]['source'] = $obj; + } elseif ($lock_type === 'binary') { + $this->cache[$artifact_name]['binary'][$platform] = $obj; + } else { + throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}"); + } + // save cache to file + file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + } + + /** + * Get source cache info for an artifact. + * + * @param string $artifact_name Artifact name + * @return null|array Cache info array or null if not found + */ + public function getSourceInfo(string $artifact_name): ?array + { + return $this->cache[$artifact_name]['source'] ?? null; + } + + /** + * Get binary cache info for an artifact on specific platform. + * + * @param string $artifact_name Artifact name + * @param string $platform Platform string (e.g., 'linux-x86_64') + * @return null|array{ + * lock_type: 'binary'|'source', + * cache_type: 'archive'|'git'|'local', + * filename?: string, + * extract: null|'&custom'|string, + * hash: null|string, + * dirname?: string, + * version?: null|string + * } Cache info array or null if not found + */ + public function getBinaryInfo(string $artifact_name, string $platform): ?array + { + return $this->cache[$artifact_name]['binary'][$platform] ?? null; + } + + /** + * Get the full path to the cached file/directory. + * + * @param array $cache_info Cache info from getSourceInfo() or getBinaryInfo() + * @return string Full path to the cached file or directory + */ + public function getCacheFullPath(array $cache_info): string + { + return match ($cache_info['cache_type']) { + 'archive', 'file' => DOWNLOAD_PATH . '/' . $cache_info['filename'], + 'git' => DOWNLOAD_PATH . '/' . $cache_info['dirname'], + 'local' => $cache_info['dirname'], // local dirname is absolute path + default => throw new SPCInternalException("Unknown cache type: {$cache_info['cache_type']}"), + }; + } + + /** + * Remove source cache entry for an artifact. + * + * @param string $artifact_name Artifact name + * @param bool $delete_file Whether to also delete the cached file/directory + */ + public function removeSource(string $artifact_name, bool $delete_file = false): void + { + $source_info = $this->getSourceInfo($artifact_name); + if ($source_info === null) { + return; + } + + // Optionally delete the actual file/directory + if ($delete_file) { + $path = $this->getCacheFullPath($source_info); + if (in_array($source_info['cache_type'], ['archive', 'file']) && file_exists($path)) { + unlink($path); + logger()->debug("Deleted cached archive: {$path}"); + } elseif ($source_info['cache_type'] === 'git' && is_dir($path)) { + FileSystem::removeDir($path); + logger()->debug("Deleted cached git repository: {$path}"); + } + } + + // Remove from cache + $this->cache[$artifact_name]['source'] = null; + $this->save(); + logger()->debug("Removed source cache entry for [{$artifact_name}]"); + } + + /** + * Remove binary cache entry for an artifact on specific platform. + * + * @param string $artifact_name Artifact name + * @param string $platform Platform string (e.g., 'linux-x86_64') + * @param bool $delete_file Whether to also delete the cached file/directory + */ + public function removeBinary(string $artifact_name, string $platform, bool $delete_file = false): void + { + $binary_info = $this->getBinaryInfo($artifact_name, $platform); + if ($binary_info === null) { + return; + } + + // Optionally delete the actual file/directory + if ($delete_file) { + $path = $this->getCacheFullPath($binary_info); + if (in_array($binary_info['cache_type'], ['archive', 'file']) && file_exists($path)) { + unlink($path); + logger()->debug("Deleted cached binary archive: {$path}"); + } elseif ($binary_info['cache_type'] === 'git' && is_dir($path)) { + FileSystem::removeDir($path); + logger()->debug("Deleted cached binary git repository: {$path}"); + } + } + + // Remove from cache + unset($this->cache[$artifact_name]['binary'][$platform]); + $this->save(); + logger()->debug("Removed binary cache entry for [{$artifact_name}] on platform [{$platform}]"); + } + + /** + * Save cache to file. + */ + public function save(): void + { + file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + } + + private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool + { + if ($object === null) { + return false; + } + // check if source is cached and file/dir exists in downloads/ dir + return match ($object['cache_type'] ?? null) { + 'archive', 'file' => isset($object['filename']) && + file_exists(DOWNLOAD_PATH . '/' . $object['filename']) && + (!$compare_hash || ( + isset($object['hash']) && + sha1_file(DOWNLOAD_PATH . '/' . $object['filename']) === $object['hash'] + )), + 'git' => isset($object['dirname']) && + is_dir(DOWNLOAD_PATH . '/' . $object['dirname'] . '/.git') && + (!$compare_hash || ( + isset($object['hash']) && + trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $object['dirname']) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')) === $object['hash'] + )), + 'local' => isset($object['dirname']) && + is_dir($object['dirname']), // local dirname is absolute path + default => false, + }; + } +} diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php new file mode 100644 index 000000000..315cfb11d --- /dev/null +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -0,0 +1,676 @@ +> */ + 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 Artifact objects */ + protected array $artifacts = []; + + /** @var int Parallel process number (1 and 0 as single-threaded mode) */ + protected int $parallel = 1; + + protected int $retry = 0; + + /** @var array Override custom download urls from options */ + protected array $custom_urls = []; + + /** @var array Override custom git options from options ([branch, git url]) */ + protected array $custom_gits = []; + + /** @var array Override custom local paths from options */ + protected array $custom_locals = []; + + /** @var int Fetch type preference */ + protected int $default_fetch_pref = Artifact::FETCH_PREFER_SOURCE; + + /** @var array Specific fetch preference */ + protected array $fetch_prefs = []; + + /** @var array|bool Whether to ignore cache for specific artifacts or all */ + protected array|bool $ignore_cache = false; + + /** @var bool Whether to enable alternative mirror downloads */ + protected bool $alt = true; + + private array $_before_files; + + /** + * @param array{ + * parallel?: int, + * retry?: int, + * custom-url?: array, + * custom-git?: array, + * custom-local?: array, + * prefer-source?: null|bool|string, + * prefer-pre-built?: null|bool|string, + * prefer-binary?: null|bool|string, + * source-only?: null|bool|string, + * binary-only?: null|bool|string, + * ignore-cache?: null|bool|string, + * ignore-cache-sources?: null|bool|string, + * no-alt?: bool, + * no-shallow-clone?: bool + * } $options Downloader options + */ + public function __construct(protected array $options = []) + { + // Allow setting concurrency via options + $this->parallel = max(1, (int) ($options['parallel'] ?? 1)); + // Allow setting retry via options + $this->retry = max(0, (int) ($options['retry'] ?? 0)); + // Prefer source (default) + if (array_key_exists('prefer-source', $options)) { + if (is_string($options['prefer-source'])) { + $ls = parse_comma_list($options['prefer-source']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_SOURCE; + } + } elseif ($options['prefer-source'] === null || $options['prefer-source'] === true) { + $this->default_fetch_pref = Artifact::FETCH_PREFER_SOURCE; + } + } + // Prefer binary (originally prefer-pre-built) + if (array_key_exists('prefer-binary', $options)) { + if (is_string($options['prefer-binary'])) { + $ls = parse_comma_list($options['prefer-binary']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY; + } + } elseif ($options['prefer-binary'] === null || $options['prefer-binary'] === true) { + $this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY; + } + } + if (array_key_exists('prefer-pre-built', $options)) { + if (is_string($options['prefer-pre-built'])) { + $ls = parse_comma_list($options['prefer-pre-built']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_PREFER_BINARY; + } + } elseif ($options['prefer-pre-built'] === null || $options['prefer-pre-built'] === true) { + $this->default_fetch_pref = Artifact::FETCH_PREFER_BINARY; + } + } + // Source only + if (array_key_exists('source-only', $options)) { + if (is_string($options['source-only'])) { + $ls = parse_comma_list($options['source-only']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_ONLY_SOURCE; + } + } elseif ($options['source-only'] === null || $options['source-only'] === true) { + $this->default_fetch_pref = Artifact::FETCH_ONLY_SOURCE; + } + } + // Binary only + if (array_key_exists('binary-only', $options)) { + if (is_string($options['binary-only'])) { + $ls = parse_comma_list($options['binary-only']); + foreach ($ls as $name) { + $this->fetch_prefs[$name] = Artifact::FETCH_ONLY_BINARY; + } + } elseif ($options['binary-only'] === null || $options['binary-only'] === true) { + $this->default_fetch_pref = Artifact::FETCH_ONLY_BINARY; + } + } + // Ignore cache + 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 || $options['ignore-cache'] === true) { + $this->ignore_cache = true; + } + } + // backward compatibility for ignore-cache-sources + 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 || $options['ignore-cache-sources'] === true) { + $this->ignore_cache = true; + } + } + // Allow setting custom urls via options + foreach (($options['custom-url'] ?? []) as $value) { + [$artifact_name, $url] = explode(':', $value, 2); + $this->custom_urls[$artifact_name] = $url; + $this->ignore_cache = match ($this->ignore_cache) { + true => true, + false => [$artifact_name], + default => array_merge($this->ignore_cache, [$artifact_name]), + }; + } + // Allow setting custom git options via options + foreach (($options['custom-git'] ?? []) as $value) { + [$artifact_name, $branch, $git_url] = explode(':', $value, 3) + [null, null, null]; + $this->custom_gits[$artifact_name] = [$branch ?? 'main', $git_url]; + $this->ignore_cache = match ($this->ignore_cache) { + true => true, + false => [$artifact_name], + default => array_merge($this->ignore_cache, [$artifact_name]), + }; + } + // Allow setting custom local paths via options + foreach (($options['custom-local'] ?? []) as $value) { + [$artifact_name, $local_path] = explode(':', $value, 2); + $this->custom_locals[$artifact_name] = $local_path; + $this->ignore_cache = match ($this->ignore_cache) { + true => true, + false => [$artifact_name], + default => array_merge($this->ignore_cache, [$artifact_name]), + }; + } + // no alt + if (array_key_exists('no-alt', $options) && $options['no-alt'] === true) { + $this->alt = false; + } + + // read downloads dir + $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; + } + + /** + * Add an artifact to the download list. + * + * @param Artifact|string $artifact Artifact instance or artifact name + */ + public function add(Artifact|string $artifact): static + { + if (is_string($artifact)) { + $artifact_instance = ArtifactLoader::getArtifactInstance($artifact); + } else { + $artifact_instance = $artifact; + } + if ($artifact_instance === null) { + $name = $artifact; + throw new WrongUsageException("Artifact '{$name}' not found, please check the name."); + } + // only add if not already added + if (!isset($this->artifacts[$artifact_instance->getName()])) { + $this->artifacts[$artifact_instance->getName()] = $artifact_instance; + } + return $this; + } + + /** + * Add multiple artifacts to the download list. + * + * @param array $artifacts Multiple artifacts to add + */ + public function addArtifacts(array $artifacts): static + { + foreach ($artifacts as $artifact) { + $this->add($artifact); + } + return $this; + } + + /** + * Set the concurrency limit for parallel downloads. + * + * @param int $parallel Number of concurrent downloads (default: 3) + */ + public function setParallel(int $parallel): static + { + $this->parallel = max(1, $parallel); + return $this; + } + + /** + * Download all artifacts, with optional parallel processing. + * + * @param bool $interactive Enable interactive mode with Ctrl+C handling + */ + public function download(bool $interactive = true): void + { + if ($interactive) { + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + keyboard_interrupt_register(function () { + echo PHP_EOL; + InteractiveTerm::error('Download cancelled by user.'); + // scan changed files + $after_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; + $new_files = array_diff($after_files, $this->_before_files); + + // remove new files + foreach ($new_files as $file) { + if ($file === '.cache.json') { + continue; + } + logger()->debug("Removing corrupted artifact: {$file}"); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $file; + if (is_dir($path)) { + FileSystem::removeDir($path); + } elseif (is_file($path)) { + FileSystem::removeFileIfExists($path); + } + } + exit(2); + }); + } + + $this->applyCustomDownloads(); + + $count = count($this->artifacts); + $artifacts_str = implode(',', array_map(fn ($x) => '' . ConsoleColor::yellow($x->getName()), $this->artifacts)); + // mute the first line if not interactive + if ($interactive) { + InteractiveTerm::notice("Downloading {$count} artifacts: {$artifacts_str} ..."); + } + try { + // Create dir + if (!is_dir(DOWNLOAD_PATH)) { + FileSystem::createDir(DOWNLOAD_PATH); + } + logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ..."); + // Download artifacts parallely + if ($this->parallel > 1) { + $this->downloadWithConcurrency(); + } else { + // normal sequential download + $current = 0; + $skipped = []; + foreach ($this->artifacts as $artifact) { + ++$current; + if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) { + $skipped[] = $artifact->getName(); + continue; + } + $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; + } + if ($interactive) { + $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; + InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}", true); + echo PHP_EOL; + } + } + } catch (SPCException $e) { + array_map(fn ($x) => InteractiveTerm::error($x), explode("\n", $e->getMessage())); + throw new WrongUsageException(); + } finally { + if ($interactive) { + Shell::passthruCallback(null); + keyboard_interrupt_unregister(); + } + } + } + + public function getRetry(): int + { + return $this->retry; + } + + public function getArtifacts(): array + { + return $this->artifacts; + } + + public function getOption(string $name, mixed $default = null): mixed + { + return $this->options[$name] ?? $default; + } + + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int + { + $queue = $this->generateQueue($artifact); + // already downloaded + if ($queue === []) { + logger()->debug("Artifact '{$artifact->getName()}' is already downloaded, skipping."); + return SPC_DOWNLOAD_STATUS_SKIPPED; + } + + $try = false; + foreach ($queue as $item) { + try { + $instance = 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', + default => SPC_DOWNLOAD_TYPE_DISPLAY_NAME[$item['config']['type']] ?? $item['config']['type'], + }; + $try_h = $try ? 'Try downloading' : 'Downloading'; + logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ..."); + if ($parallel === false && $interactive) { + InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ..."); + } + // is valid download type + if ($item['lock'] === 'source' && ($callback = $artifact->getCustomSourceCallback()) !== null) { + $lock = ApplicationContext::invoke($callback, [ + Artifact::class => $artifact, + ArtifactDownloader::class => $this, + ]); + } elseif ($item['lock'] === 'binary' && ($callback = $artifact->getCustomBinaryCallback()) !== null) { + $lock = ApplicationContext::invoke($callback, [ + Artifact::class => $artifact, + ArtifactDownloader::class => $this, + ]); + } elseif (is_a($call, DownloadTypeInterface::class, true)) { + $instance = new $call(); + $lock = $instance->download($artifact->getName(), $item['config'], $this); + } else { + 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."); + } + // verifying hash if possible + $hash_validator = $instance ?? null; + $verified = $lock->verified; + if ($hash_validator instanceof ValidatorInterface) { + if (!$hash_validator->validate($artifact->getName(), $item['config'], $this, $lock)) { + throw new ValidationException("Hash validation failed for artifact '{$artifact->getName()}' {$item['display']}."); + } + $verified = true; + } + // process lock + ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString()); + 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 && $interactive) { + InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); + InteractiveTerm::error("Failed message: {$e->getMessage()}", true); + } + $try = true; + continue; + } catch (ValidationException $e) { + if ($parallel === false) { + InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); + InteractiveTerm::error("Validation failed: {$e->getMessage()}"); + } + break; + } + } + $vvv = ApplicationContext::isDebug() ? "\nIf the problem persists, consider using `-vvv` to enable verbose mode, and disable parallel downloading for more details." : ''; + throw new DownloaderException("Download artifact '{$artifact->getName()}' failed. Please check your internet connection and try again.{$vvv}"); + } + + private function downloadWithConcurrency(): void + { + $skipped = []; + $fiber_pool = []; + $old_verbosity = null; + $old_debug = null; + try { + $count = count($this->artifacts); + // must mute + $output = ApplicationContext::get(OutputInterface::class); + if ($output->isVerbose()) { + $old_verbosity = $output->getVerbosity(); + $old_debug = ApplicationContext::isDebug(); + logger()->warning('Parallel download is not supported in verbose mode, I will mute the output temporarily.'); + $output->setVerbosity(OutputInterface::VERBOSITY_NORMAL); + ApplicationContext::setDebug(false); + logger()->setLevel(LogLevel::ERROR); + } + $pool_count = $this->parallel; + $downloaded = 0; + $total = count($this->artifacts); + + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + \Fiber::suspend(); + }); + + InteractiveTerm::indicateProgress("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ..."); + $failed_downloads = []; + while (true) { + // fill pool + while (count($fiber_pool) < $pool_count && ($artifact = array_shift($this->artifacts)) !== null) { + $current = $count - count($this->artifacts); + $fiber = new \Fiber(function () use ($artifact, $current, $count) { + return [$artifact, $this->downloadWithType($artifact, $current, $count, true)]; + }); + $fiber->start(); + $fiber_pool[] = $fiber; + } + // check pool + foreach ($fiber_pool as $index => $fiber) { + if ($fiber->isTerminated()) { + try { + [$artifact, $int] = $fiber->getReturn(); + if ($int === SPC_DOWNLOAD_STATUS_SKIPPED) { + $skipped[] = $artifact->getName(); + } + } catch (\Throwable $e) { + $artifact_name = 'unknown'; + if (isset($artifact)) { + $artifact_name = $artifact->getName(); + } + $failed_downloads[] = ['artifact' => $artifact_name, 'error' => $e]; + InteractiveTerm::setMessage("[{$downloaded}/{$total}] Download failed: {$artifact_name}"); + InteractiveTerm::advance(); + } + // remove from pool + unset($fiber_pool[$index]); + ++$downloaded; + InteractiveTerm::setMessage("[{$downloaded}/{$total}] Downloading artifacts with concurrency {$this->parallel} ..."); + InteractiveTerm::advance(); + } else { + $fiber->resume(); + } + } + // all done + if (count($this->artifacts) === 0 && count($fiber_pool) === 0) { + if (!empty($failed_downloads)) { + InteractiveTerm::finish('Download completed with ' . count($failed_downloads) . ' failure(s).', false); + foreach ($failed_downloads as $failure) { + InteractiveTerm::error("Failed to download '{$failure['artifact']}': {$failure['error']->getMessage()}"); + } + throw new DownloaderException('Failed to download ' . count($failed_downloads) . ' artifact(s). Please check your internet connection and try again.'); + } + $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; + InteractiveTerm::finish("Downloaded all {$total} artifacts.{$skip_msg}"); + break; + } + } + } catch (\Throwable $e) { + // throw to all fibers to make them stop + foreach ($fiber_pool as $fiber) { + if (!$fiber->isTerminated()) { + try { + $fiber->throw($e); + } catch (\Throwable) { + // ignore errors when stopping fibers + } + } + } + InteractiveTerm::finish('Parallel download failed !', false); + throw $e; + } finally { + if ($old_verbosity !== null) { + ApplicationContext::get(OutputInterface::class)->setVerbosity($old_verbosity); + logger()->setLevel(match ($old_verbosity) { + OutputInterface::VERBOSITY_VERBOSE => LogLevel::INFO, + OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => LogLevel::DEBUG, + default => LogLevel::WARNING, + }); + } + if ($old_debug !== null) { + ApplicationContext::setDebug($old_debug); + } + Shell::passthruCallback(null); + } + } + + /** + * Generate download queue based on type preference. + */ + private function generateQueue(Artifact $artifact): array + { + /** @var array $queue */ + $queue = []; + $binary_downloaded = $artifact->isBinaryDownloaded(compare_hash: true); + $source_downloaded = $artifact->isSourceDownloaded(compare_hash: true); + + $item_source = ['display' => 'source', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source')]; + $item_source_mirror = ['display' => 'source (mirror)', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source-mirror')]; + + // For binary config, handle both array configs and custom callbacks + $binary_config = $artifact->getDownloadConfig('binary'); + $has_custom_binary = $artifact->getCustomBinaryCallback() !== null; + $item_binary_config = null; + if (is_array($binary_config)) { + $item_binary_config = $binary_config[SystemTarget::getCurrentPlatformString()] ?? null; + } elseif ($has_custom_binary) { + // For custom binaries, create a dummy config to allow queue generation + $item_binary_config = ['type' => 'custom']; + } + $item_binary = ['display' => 'binary', 'lock' => 'binary', 'config' => $item_binary_config]; + + $binary_mirror_config = $artifact->getDownloadConfig('binary-mirror'); + $item_binary_mirror_config = null; + if (is_array($binary_mirror_config)) { + $item_binary_mirror_config = $binary_mirror_config[SystemTarget::getCurrentPlatformString()] ?? null; + } + $item_binary_mirror = ['display' => 'binary (mirror)', 'lock' => 'binary', 'config' => $item_binary_mirror_config]; + + $pref = $this->fetch_prefs[$artifact->getName()] ?? $this->default_fetch_pref; + if ($pref === Artifact::FETCH_PREFER_SOURCE) { + $queue[] = $item_source['config'] !== null ? $item_source : null; + $queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null; + $queue[] = $item_binary['config'] !== null ? $item_binary : null; + $queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null; + } elseif ($pref === Artifact::FETCH_PREFER_BINARY) { + $queue[] = $item_binary['config'] !== null ? $item_binary : null; + $queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null; + $queue[] = $item_source['config'] !== null ? $item_source : null; + $queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null; + } elseif ($pref === Artifact::FETCH_ONLY_SOURCE) { + $queue[] = $item_source['config'] !== null ? $item_source : null; + $queue[] = $item_source_mirror['config'] !== null && $this->alt ? $item_source_mirror : null; + } elseif ($pref === Artifact::FETCH_ONLY_BINARY) { + $queue[] = $item_binary['config'] !== null ? $item_binary : null; + $queue[] = $item_binary_mirror['config'] !== null && $this->alt ? $item_binary_mirror : null; + } + // filter nulls + $queue = array_values(array_filter($queue)); + + // always download + if ($this->ignore_cache === true || is_array($this->ignore_cache) && in_array($artifact->getName(), $this->ignore_cache)) { + // validate: ensure at least one download source is available + if (empty($queue)) { + throw new ValidationException("Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').'); + } + return $queue; + } + + // check if already downloaded + $has_usable_download = false; + if ($pref === Artifact::FETCH_PREFER_SOURCE) { + // prefer source: check source first, if not available check binary + $has_usable_download = $source_downloaded || $binary_downloaded; + } elseif ($pref === Artifact::FETCH_PREFER_BINARY) { + // prefer binary: check binary first, if not available check source + $has_usable_download = $binary_downloaded || $source_downloaded; + } elseif ($pref === Artifact::FETCH_ONLY_SOURCE) { + // source-only: only check if source is downloaded + $has_usable_download = $source_downloaded; + } elseif ($pref === Artifact::FETCH_ONLY_BINARY) { + // binary-only: only check if binary for current platform is downloaded + $has_usable_download = $binary_downloaded; + } + + // if already downloaded, skip + if ($has_usable_download) { + return []; + } + + // validate: ensure at least one download source is available + if (empty($queue)) { + if ($pref === Artifact::FETCH_ONLY_SOURCE) { + throw new ValidationException("Artifact '{$artifact->getName()}' does not provide source download, cannot use --source-only mode."); + } + if ($pref === Artifact::FETCH_ONLY_BINARY) { + throw new ValidationException("Artifact '{$artifact->getName()}' does not provide binary download for current platform (" . SystemTarget::getCurrentPlatformString() . '), cannot use --binary-only mode.'); + } + // prefer modes should also throw error if no download source available + throw new ValidationException("Validation failed: Artifact '{$artifact->getName()}' does not provide any download source for current platform (" . SystemTarget::getCurrentPlatformString() . ').'); + } + + return $queue; + } + + private function applyCustomDownloads(): void + { + foreach ($this->custom_urls as $artifact_name => $custom_url) { + if (isset($this->artifacts[$artifact_name])) { + $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $custom_url) { + return (new Url())->download($artifact_name, ['url' => $custom_url], $downloader); + }); + } + } + foreach ($this->custom_gits as $artifact_name => [$branch, $git_url]) { + if (isset($this->artifacts[$artifact_name])) { + $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $branch, $git_url) { + return (new Git())->download($artifact_name, ['rev' => $branch, 'url' => $git_url], $downloader); + }); + } + } + foreach ($this->custom_locals as $artifact_name => $local_path) { + if (isset($this->artifacts[$artifact_name])) { + $this->artifacts[$artifact_name]->setCustomSourceCallback(function (ArtifactDownloader $downloader) use ($artifact_name, $local_path) { + return (new LocalDir())->download($artifact_name, ['dirname' => $local_path], $downloader); + }); + } + } + } +} diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php new file mode 100644 index 000000000..93b363823 --- /dev/null +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -0,0 +1,634 @@ + Track extracted artifacts to avoid duplicate extraction */ + protected array $extracted = []; + + public function __construct( + protected ArtifactCache $cache, + protected bool $interactive = true + ) {} + + /** + * Extract all artifacts for a list of packages. + * + * @param array $packages Packages to extract artifacts for + * @param bool $force_source If true, always extract source (ignore binary) + */ + public function extractForPackages(array $packages, bool $force_source = false): void + { + // Collect all unique artifacts + $artifacts = []; + foreach ($packages as $package) { + $artifact = $package->getArtifact(); + if ($artifact !== null && !isset($artifacts[$artifact->getName()])) { + $artifacts[$artifact->getName()] = $artifact; + } + } + + // Sort: php-src should be extracted first (extensions depend on it) + uksort($artifacts, function (string $a, string $b): int { + if ($a === 'php-src') { + return -1; + } + if ($b === 'php-src') { + return 1; + } + return 0; + }); + + // Extract each artifact + foreach ($artifacts as $artifact) { + $this->extract($artifact, $force_source); + } + } + + /** + * Extract a single artifact. + * + * @param Artifact|string $artifact The artifact to extract + * @param bool $force_source If true, always extract source (ignore binary) + */ + public function extract(Artifact|string $artifact, bool $force_source = false): int + { + if (is_string($artifact)) { + $name = $artifact; + $artifact = ArtifactLoader::getArtifactInstance($name); + } else { + $name = $artifact->getName(); + } + + // Already extracted in this session + if (isset($this->extracted[$name])) { + logger()->debug("Artifact [{$name}] already extracted in this session, skip."); + return SPC_STATUS_ALREADY_EXTRACTED; + } + + // Determine: use binary or source? + $use_binary = !$force_source && $artifact->shouldUseBinary(); + + if ($this->interactive) { + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + } + + try { + V2CompatLayer::beforeExtractHook($artifact); + if ($use_binary) { + $status = $this->extractBinary($artifact); + } else { + $status = $this->extractSource($artifact); + } + V2CompatLayer::afterExtractHook($artifact); + } finally { + if ($this->interactive) { + Shell::passthruCallback(null); + } + } + + $this->extracted[$name] = true; + return $status; + } + + /** + * Extract source artifact. + */ + protected function extractSource(Artifact $artifact): int + { + $name = $artifact->getName(); + $cache_info = $this->cache->getSourceInfo($name); + + if ($cache_info === null) { + throw new WrongUsageException("Artifact source [{$name}] not downloaded, please download it first!"); + } + + $source_file = $this->cache->getCacheFullPath($cache_info); + $target_path = $artifact->getSourceDir(); + + // Check for custom extract callback + if ($artifact->hasSourceExtractCallback()) { + logger()->info("Extracting source [{$name}] using custom callback..."); + $callback = $artifact->getSourceExtractCallback(); + ApplicationContext::invoke($callback, [ + Artifact::class => $artifact, + 'source_file' => $source_file, + 'target_path' => $target_path, + ]); + // Emit after hooks + $artifact->emitAfterSourceExtract($target_path); + logger()->debug("Emitted after-source-extract hooks for [{$name}]"); + return SPC_STATUS_EXTRACTED; + } + + // Check for selective extraction (dict mode) + $extract_config = $artifact->getDownloadConfig('source')['extract'] ?? null; + if (is_array($extract_config)) { + $this->doSelectiveExtract($name, $cache_info, $extract_config); + $artifact->emitAfterSourceExtract($target_path); + logger()->debug("Emitted after-source-extract hooks for [{$name}]"); + return SPC_STATUS_EXTRACTED; + } + + // Standard extraction + $hash = $cache_info['hash'] ?? null; + + if ($this->isAlreadyExtracted($target_path, $hash)) { + logger()->debug("Source [{$name}] already extracted at {$target_path}, skip."); + return SPC_STATUS_ALREADY_EXTRACTED; + } + + // Remove old directory if hash mismatch + if (is_dir($target_path)) { + logger()->notice("Source [{$name}] hash mismatch, re-extracting..."); + FileSystem::removeDir($target_path); + } + + logger()->info("Extracting source [{$name}] to {$target_path}..."); + $this->doStandardExtract($name, $cache_info, $target_path); + + // Emit after hooks + $artifact->emitAfterSourceExtract($target_path); + logger()->debug("Emitted after-source-extract hooks for [{$name}]"); + + // Write hash marker + if ($hash !== null) { + FileSystem::writeFile("{$target_path}/.spc-hash", $hash); + } + return SPC_STATUS_EXTRACTED; + } + + /** + * Extract binary artifact. + */ + protected function extractBinary(Artifact $artifact): int + { + $name = $artifact->getName(); + $platform = SystemTarget::getCurrentPlatformString(); + $cache_info = $this->cache->getBinaryInfo($name, $platform); + + if ($cache_info === null) { + throw new WrongUsageException("Artifact binary [{$name}] for platform [{$platform}] not downloaded!"); + } + + $source_file = $this->cache->getCacheFullPath($cache_info); + $extract_config = $artifact->getBinaryExtractConfig($cache_info); + $target_path = $extract_config['path']; + + // Check for custom extract callback + if ($artifact->hasBinaryExtractCallback()) { + logger()->info("Extracting binary [{$name}] using custom callback..."); + $callback = $artifact->getBinaryExtractCallback(); + ApplicationContext::invoke($callback, [ + Artifact::class => $artifact, + 'source_file' => $source_file, + 'target_path' => $target_path, + 'platform' => $platform, + ]); + // Emit after hooks + $artifact->emitAfterBinaryExtract($target_path, $platform); + logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); + return SPC_STATUS_EXTRACTED; + } + + // Handle different extraction modes + $mode = $extract_config['mode']; + + if ($mode === 'selective') { + $this->doSelectiveExtract($name, $cache_info, $extract_config['files']); + $artifact->emitAfterBinaryExtract($target_path, $platform); + logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); + return SPC_STATUS_EXTRACTED; + } + + $hash = $cache_info['hash'] ?? null; + + if ($this->isAlreadyExtracted($target_path, $hash)) { + logger()->debug("Binary [{$name}] already extracted at {$target_path}, skip."); + return SPC_STATUS_ALREADY_EXTRACTED; + } + + logger()->info("Extracting binary [{$name}] to {$target_path}..."); + $this->doStandardExtract($name, $cache_info, $target_path); + + $artifact->emitAfterBinaryExtract($target_path, $platform); + logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); + + if ($hash !== null && $cache_info['cache_type'] !== 'file') { + FileSystem::writeFile("{$target_path}/.spc-hash", $hash); + } + return SPC_STATUS_EXTRACTED; + } + + /** + * Standard extraction: extract entire archive to target directory. + */ + protected function doStandardExtract(string $name, array $cache_info, string $target_path): void + { + $source_file = $this->cache->getCacheFullPath($cache_info); + $cache_type = $cache_info['cache_type']; + + // Validate source file exists before extraction + $this->validateSourceFile($name, $source_file, $cache_type); + + $this->extractWithType($cache_type, $source_file, $target_path); + } + + /** + * Selective extraction: extract specific files to specific locations. + * + * @param string $name Artifact name + * @param array $cache_info Cache info + * @param array $file_map Map of source path => destination path + */ + protected function doSelectiveExtract(string $name, array $cache_info, array $file_map): void + { + // Extract to temp directory first + $temp_path = sys_get_temp_dir() . '/spc_extract_' . $name . '_' . bin2hex(random_bytes(8)); + + try { + logger()->info("Extracting [{$name}] with selective file mapping..."); + + $source_file = $this->cache->getCacheFullPath($cache_info); + $cache_type = $cache_info['cache_type']; + + // Validate source file exists before extraction + $this->validateSourceFile($name, $source_file, $cache_type); + + $this->extractWithType($cache_type, $source_file, $temp_path); + + // Process file mappings + foreach ($file_map as $src_pattern => $dst_path) { + $dst_path = $this->replacePathVariables($dst_path); + $src_full = FileSystem::convertPath("{$temp_path}/{$src_pattern}"); + + // Handle glob patterns + if (str_contains($src_pattern, '*')) { + $matches = glob($src_full); + if (empty($matches)) { + logger()->warning("No files matched pattern [{$src_pattern}] in [{$name}]"); + continue; + } + foreach ($matches as $match) { + $filename = basename($match); + $target = rtrim($dst_path, '/') . '/' . $filename; + $this->copyFileOrDir($match, $target); + } + } else { + // Direct file/directory copy + if (!file_exists($src_full) && !is_dir($src_full)) { + logger()->warning("Source [{$src_pattern}] not found in [{$name}]"); + continue; + } + $this->copyFileOrDir($src_full, $dst_path); + } + } + } finally { + // Cleanup temp directory + if (is_dir($temp_path)) { + FileSystem::removeDir($temp_path); + } + } + } + + /** + * Check if artifact is already extracted with correct hash. + */ + protected function isAlreadyExtracted(string $path, ?string $expected_hash): bool + { + if (!is_dir($path)) { + return false; + } + + // Local source: always re-extract + if ($expected_hash === null) { + return false; + } + + $hash_file = "{$path}/.spc-hash"; + if (!file_exists($hash_file)) { + return false; + } + + return FileSystem::readFile($hash_file) === $expected_hash; + } + + /** + * Validate that the source file/directory exists before extraction. + * + * @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 + */ + protected function validateSourceFile(string $name, string $source_file, string $cache_type): void + { + $converted_path = FileSystem::convertPath($source_file); + + switch ($cache_type) { + case 'archive': + if (!file_exists($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] source archive not found at: {$converted_path}\n" . + "The file may have been deleted or moved. Please run 'spc download {$name}' to re-download it." + ); + } + if (!is_file($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] source path exists but is not a file: {$converted_path}\n" . + 'Expected an archive file. Please check your downloads directory.' + ); + } + break; + case 'file': + if (!file_exists($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] source file not found at: {$converted_path}\n" . + "The file may have been deleted or moved. Please run 'spc download {$name}' to re-download it." + ); + } + if (!is_file($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] source path exists but is not a file: {$converted_path}\n" . + 'Expected a regular file. Please check your downloads directory.' + ); + } + break; + case 'git': + if (!is_dir($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] git repository not found at: {$converted_path}\n" . + "The directory may have been deleted. Please run 'spc download {$name}' to re-clone it." + ); + } + // Optionally check for .git directory to ensure it's a valid git repo + if (!is_dir("{$converted_path}/.git")) { + logger()->warning("Artifact [{$name}] directory exists but may not be a valid git repository (missing .git)"); + } + break; + case 'local': + if (!file_exists($converted_path) && !is_dir($converted_path)) { + throw new WrongUsageException( + "Artifact [{$name}] local source not found at: {$converted_path}\n" . + 'Please ensure the local path is correct and accessible.' + ); + } + break; + default: + throw new SPCInternalException("Unknown cache type: {$cache_type}"); + } + + logger()->debug("Validated source file for [{$name}]: {$converted_path} (type: {$cache_type})"); + } + + /** + * Copy file or directory to destination. + */ + protected function copyFileOrDir(string $src, string $dst): void + { + $dst_dir = dirname($dst); + if (!is_dir($dst_dir)) { + FileSystem::createDir($dst_dir); + } + + if (is_dir($src)) { + FileSystem::copyDir($src, $dst); + } else { + copy($src, $dst); + } + + logger()->debug("Copied {$src} -> {$dst}"); + } + + /** + * Extract source based on cache type. + * + * @param string $cache_type Cache type: archive, git, local + * @param string $source_file Path to source file or directory + * @param string $target_path Target extraction path + */ + protected function extractWithType(string $cache_type, string $source_file, string $target_path): void + { + match ($cache_type) { + 'archive' => $this->extractArchive($source_file, $target_path), + 'file' => $this->copyFile($source_file, $target_path), + 'git' => FileSystem::copyDir(FileSystem::convertPath($source_file), $target_path), + 'local' => symlink(FileSystem::convertPath($source_file), $target_path), + default => throw new SPCInternalException("Unknown cache type: {$cache_type}"), + }; + } + + /** + * Extract archive file to target directory. + * + * Supports: tar, tar.gz, tgz, tar.bz2, tar.xz, txz, zip, exe + */ + protected function extractArchive(string $filename, string $target): void + { + $target = FileSystem::convertPath($target); + $filename = FileSystem::convertPath($filename); + + $extname = FileSystem::extname($filename); + + if ($extname !== 'exe' && !is_dir($target)) { + FileSystem::createDir($target); + } + match (SystemTarget::getTargetOS()) { + 'Windows' => match ($extname) { + 'tar' => default_shell()->executeTarExtract($filename, $target, 'none'), + 'xz', 'txz', 'gz', 'tgz', 'bz2' => default_shell()->execute7zExtract($filename, $target), + 'zip' => $this->unzipWithStrip($filename, $target), + 'exe' => $this->copyFile($filename, $target), + default => throw new FileSystemException("Unknown archive format: {$filename}"), + }, + 'Linux', 'Darwin' => match ($extname) { + 'tar' => default_shell()->executeTarExtract($filename, $target, 'none'), + 'gz', 'tgz' => default_shell()->executeTarExtract($filename, $target, 'gz'), + 'bz2' => default_shell()->executeTarExtract($filename, $target, 'bz2'), + 'xz', 'txz' => default_shell()->executeTarExtract($filename, $target, 'xz'), + 'zip' => $this->unzipWithStrip($filename, $target), + 'exe' => $this->copyFile($filename, $target), + default => throw new FileSystemException("Unknown archive format: {$filename}"), + }, + default => throw new SPCInternalException('Unsupported OS for archive extraction') + }; + } + + /** + * Unzip file with stripping top-level directory. + */ + protected function unzipWithStrip(string $zip_file, string $extract_path): bool + { + $temp_dir = FileSystem::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16))); + $zip_file = FileSystem::convertPath($zip_file); + $extract_path = FileSystem::convertPath($extract_path); + + // Extract to temp dir + FileSystem::createDir($temp_dir); + + if (PHP_OS_FAMILY === 'Windows') { + default_shell()->execute7zExtract($zip_file, $temp_dir); + } else { + default_shell()->executeUnzip($zip_file, $temp_dir); + } + + // Scan first level dirs (relative, not recursive, include dirs) + $contents = FileSystem::scanDirFiles($temp_dir, false, true, true); + if ($contents === false) { + throw new FileSystemException('Cannot scan unzip temp dir: ' . $temp_dir); + } + + // If extract path already exists, remove it + if (is_dir($extract_path)) { + FileSystem::removeDir($extract_path); + } + + // If only one dir, move its contents to extract_path + $subdir = FileSystem::convertPath("{$temp_dir}/{$contents[0]}"); + if (count($contents) === 1 && is_dir($subdir)) { + $this->moveFileOrDir($subdir, $extract_path); + } else { + // Else, if it contains only one dir, strip dir and copy other files + $dircount = 0; + $dir = []; + $top_files = []; + foreach ($contents as $item) { + if (is_dir(FileSystem::convertPath("{$temp_dir}/{$item}"))) { + ++$dircount; + $dir[] = $item; + } else { + $top_files[] = $item; + } + } + + // Extract dir contents to extract_path + FileSystem::createDir($extract_path); + + // Extract move dir + if ($dircount === 1) { + $sub_contents = FileSystem::scanDirFiles("{$temp_dir}/{$dir[0]}", false, true, true); + if ($sub_contents === false) { + throw new FileSystemException("Cannot scan unzip temp sub-dir: {$dir[0]}"); + } + foreach ($sub_contents as $sub_item) { + $this->moveFileOrDir( + FileSystem::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"), + FileSystem::convertPath("{$extract_path}/{$sub_item}") + ); + } + } else { + foreach ($dir as $item) { + $this->moveFileOrDir( + FileSystem::convertPath("{$temp_dir}/{$item}"), + FileSystem::convertPath("{$extract_path}/{$item}") + ); + } + } + + // Move top-level files to extract_path + foreach ($top_files as $top_file) { + $this->moveFileOrDir( + FileSystem::convertPath("{$temp_dir}/{$top_file}"), + FileSystem::convertPath("{$extract_path}/{$top_file}") + ); + } + } + + // Clean up temp directory + FileSystem::removeDir($temp_dir); + + return true; + } + + /** + * Replace path variables. + */ + protected function replacePathVariables(string $path): string + { + $replacement = [ + '{pkg_root_path}' => PKG_ROOT_PATH, + '{build_root_path}' => BUILD_ROOT_PATH, + '{source_path}' => SOURCE_PATH, + '{download_path}' => DOWNLOAD_PATH, + '{working_dir}' => WORKING_DIR, + '{php_sdk_path}' => getenv('PHP_SDK_PATH') ?: '', + ]; + 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): bool + { + FileSystem::createDir(dirname($target_path)); + return FileSystem::copy(FileSystem::convertPath($source_file), $target_path); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/DownloadResult.php b/src/StaticPHP/Artifact/Downloader/DownloadResult.php new file mode 100644 index 000000000..6fa40bed7 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/DownloadResult.php @@ -0,0 +1,172 @@ +cache_type) { + case 'archive': + case 'file': + $this->filename !== null ?: throw new DownloaderException('Archive/file download result must have a filename.'); + $fn = FileSystem::isRelativePath($this->filename) ? (DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $this->filename) : $this->filename; + file_exists($fn) ?: throw new DownloaderException("Downloaded archive file does not exist: {$fn}"); + break; + case 'git': + case 'local': + $this->dirname !== null ?: throw new DownloaderException('Git/local download result must have a dirname.'); + $dn = FileSystem::isRelativePath($this->dirname) ? (DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $this->dirname) : $this->dirname; + file_exists($dn) ?: throw new DownloaderException("Downloaded directory does not exist: {$dn}"); + break; + } + } + + /** + * Create a download result for an archive file. + * + * @param string $filename Filename of the downloaded archive + * @param mixed $extract Extraction path or configuration + * @param bool $verified Whether the archive has been hash-verified + * @param null|string $version Version string of the downloaded artifact + * @param array $metadata Additional metadata + */ + public static function archive( + string $filename, + array $config, + mixed $extract = null, + bool $verified = false, + ?string $version = null, + array $metadata = [] + ): DownloadResult { + // judge if it is archive or just a pure file + $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; + return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata); + } + + public static function file( + string $filename, + array $config, + bool $verified = false, + ?string $version = null, + array $metadata = [] + ): DownloadResult { + $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; + return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata); + } + + /** + * Create a download result for a git clone. + * + * @param string $dirname Directory name of the cloned repository + * @param mixed $extract Extraction path or configuration + * @param null|string $version Version string (tag, branch, or commit) + * @param array $metadata Additional metadata (e.g., commit hash) + */ + public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + { + return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + } + + /** + * Create a download result for a local directory. + * + * @param string $dirname Directory name + * @param mixed $extract Extraction path or configuration + * @param null|string $version Version string if known + * @param array $metadata Additional metadata + */ + public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + { + return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + } + + /** + * Check if version information is available. + */ + public function hasVersion(): bool + { + return $this->version !== null; + } + + /** + * Get a metadata value by key. + * + * @param string $key Metadata key + * @param mixed $default Default value if key doesn't exist + */ + public function getMeta(string $key, mixed $default = null): mixed + { + return $this->metadata[$key] ?? $default; + } + + /** + * Create a new DownloadResult with updated version. + * (Immutable pattern - returns a new instance) + */ + public function withVersion(string $version): self + { + return new self( + $this->cache_type, + $this->config, + $this->filename, + $this->dirname, + $this->extract, + $this->verified, + $version, + $this->metadata + ); + } + + /** + * Create a new DownloadResult with additional metadata. + * (Immutable pattern - returns a new instance) + */ + public function withMeta(string $key, mixed $value): self + { + return new self( + $this->cache_type, + $this->config, + $this->filename, + $this->dirname, + $this->extract, + $this->verified, + $this->version, + array_merge($this->metadata, [$key => $value]) + ); + } + + /** + * Check + */ + private static function isArchiveFile(string $filename): bool + { + $archive_extensions = [ + 'zip', 'tar', 'tar.gz', 'tgz', 'tar.bz2', 'tbz2', 'tar.xz', 'txz', 'rar', '7z', + ]; + $lower_filename = strtolower($filename); + return array_any($archive_extensions, fn ($ext) => str_ends_with($lower_filename, '.' . $ext)); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php new file mode 100644 index 000000000..30942fe17 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php @@ -0,0 +1,41 @@ +debug("Fetching {$name} API info from bitbucket tag"); + $data = default_shell()->executeCurl(str_replace('{repo}', $config['repo'], self::BITBUCKET_API_URL), retries: $downloader->getRetry()); + $data = json_decode($data ?: '', true); + $ver = $data['values'][0]['name'] ?? null; + if (!$ver) { + throw new DownloaderException("Failed to get {$name} version from BitBucket API"); + } + $download_url = str_replace(['{repo}', '{version}'], [$config['repo'], $ver], self::BITBUCKET_DOWNLOAD_URL); + + $headers = default_shell()->executeCurl($download_url, method: 'HEAD', retries: $downloader->getRetry()); + preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?.+\.tar\.gz)\1/im', $headers, $matches); + if ($matches) { + $filename = $matches['filename']; + } else { + $filename = "{$name}-{$data['tag_name']}.tar.gz"; + } + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} version {$ver} from BitBucket: {$download_url}"); + default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/DownloadTypeInterface.php b/src/StaticPHP/Artifact/Downloader/Type/DownloadTypeInterface.php new file mode 100644 index 000000000..bc1dc8210 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/DownloadTypeInterface.php @@ -0,0 +1,18 @@ +debug("Fetching file list from {$config['url']}"); + $page = default_shell()->executeCurl($config['url'], retries: $downloader->getRetry()); + preg_match_all($config['regex'], $page ?: '', $matches); + if (!$matches) { + throw new DownloaderException("Failed to get {$name} file list from {$config['url']}"); + } + $versions = []; + foreach ($matches['version'] as $i => $version) { + $lower = strtolower($version); + foreach (['alpha', 'beta', 'rc', 'pre', 'nightly', 'snapshot', 'dev'] as $beta) { + if (str_contains($lower, $beta)) { + continue 2; + } + } + $versions[$version] = $matches['file'][$i]; + } + uksort($versions, 'version_compare'); + $filename = end($versions); + $version = array_key_last($versions); + if (isset($config['download-url'])) { + $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); + } else { + $url = $config['url'] . $filename; + } + $filename = end($versions); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php new file mode 100644 index 000000000..83c236eb4 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -0,0 +1,71 @@ +debug("Cloning git repository for {$name} from {$config['url']}"); + $shallow = !$downloader->getOption('no-shallow-clone', false); + + // direct branch clone + if (isset($config['rev'])) { + default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); + $version = "dev-{$config['rev']}"; + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + } + if (!isset($config['regex'])) { + throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); + } + + // regex matches branch first, we need to fetch all refs in emptyfirst + $gitdir = sys_get_temp_dir() . '/' . $name; + FileSystem::resetDir($gitdir); + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->cd($gitdir) + ->exec(SPC_GIT_EXEC . ' init') + ->exec(SPC_GIT_EXEC . ' remote add origin ' . escapeshellarg($config['url'])) + ->execWithResult(SPC_GIT_EXEC . ' ls-remote origin'); + if ($result[0] !== 0) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $refs = $result[1]; + $matched_version_branch = []; + $matched_count = 0; + + $regex = '/^' . $config['regex'] . '$/'; + foreach ($refs as $ref) { + $matches = null; + if (preg_match('/^[0-9a-f]{40}\s+refs\/heads\/(.+)$/', $ref, $matches)) { + ++$matched_count; + $branch = $matches[1]; + if (preg_match($regex, $branch, $vermatch) && isset($vermatch['version'])) { + $matched_version_branch[$vermatch['version']] = $vermatch[0]; + } + } + } + // sort versions + uksort($matched_version_branch, function ($a, $b) { + return version_compare($b, $a); + }); + if (!empty($matched_version_branch)) { + // use the highest version + $version = array_key_first($matched_version_branch); + $branch = $matched_version_branch[$version]; + logger()->info("Matched version {$version} from branch {$branch} for {$name}"); + default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null); + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + } + throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches)."); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php new file mode 100644 index 000000000..731e8297e --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -0,0 +1,118 @@ +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. + */ + public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset): array + { + $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}"); + } + foreach ($data as $release) { + if ($prefer_stable && $release['prerelease'] === true) { + continue; + } + foreach ($release['assets'] as $asset) { + if (preg_match("|{$match_asset}|", $asset['name'])) { + if (isset($asset['id'], $asset['name'])) { + // store ghrel asset array (id: ghrel.{$repo}.{stable|unstable}.{$match_asset}) + if ($asset['digest'] !== null && str_starts_with($asset['digest'], 'sha256:')) { + $this->sha256 = substr($asset['digest'], 7); + } + $this->version = $release['tag_name'] ?? null; + return $asset; + } + throw new DownloaderException("Failed to get asset name and id for {$repo}"); + } + } + } + throw new DownloaderException("No suitable GitHub release found for {$repo}"); + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + logger()->debug("Fetching GitHub release for {$name} from {$config['repo']}"); + if (!isset($config['match'])) { + throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); + } + $rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match']); + + // download file using curl + $asset_url = str_replace(['{repo}', '{id}'], [$config['repo'], $rel['id']], self::ASSET_URL); + $headers = array_merge( + $this->getGitHubTokenHeaders(), + ['Accept: application/octet-stream'] + ); + $filename = $rel['name']; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} asset from URL: {$asset_url}"); + default_shell()->executeCurlDownload($asset_url, $path, headers: $headers, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version); + } + + public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool + { + if ($result->cache_type != 'archive') { + logger()->warning("GitHub release validator only supports archive download type for {$name} ."); + return false; + } + + if ($this->sha256 !== '') { + $calculated_hash = hash_file('sha256', DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $result->filename); + if ($this->sha256 !== $calculated_hash) { + logger()->error("Hash mismatch for downloaded GitHub release asset of {$name}: expected {$this->sha256}, got {$calculated_hash}"); + return false; + } + logger()->debug("Hash verified for downloaded GitHub release asset of {$name}"); + return true; + } + logger()->debug("No sha256 digest found for GitHub release asset of {$name}, skipping hash validation"); + return true; + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php new file mode 100644 index 000000000..7917e4c01 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -0,0 +1,78 @@ +executeCurl($url, headers: $this->getGitHubTokenHeaders()); + $data = json_decode($data ?: '', true); + if (!is_array($data)) { + throw new DownloaderException("Failed to get GitHub tarball URL for {$repo} from {$url}"); + } + $url = null; + foreach ($data as $rel) { + if (($rel['prerelease'] ?? false) === true && $prefer_stable) { + continue; + } + if ($match_url === null) { + $url = $rel['tarball_url'] ?? null; + $version = $rel['tag_name'] ?? null; + break; + } + if (preg_match("|{$match_url}|", $rel['tarball_url'] ?? '')) { + $url = $rel['tarball_url']; + $version = $rel['tag_name'] ?? null; + break; + } + } + if (!$url) { + throw new DownloaderException("No suitable GitHub tarball found for {$repo}"); + } + $this->version = $version ?? null; + $head = default_shell()->executeCurl($url, 'HEAD', headers: $this->getGitHubTokenHeaders()) ?: ''; + preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?.+\.tar\.gz)\1/im', $head, $matches); + if ($matches) { + $filename = $matches['filename']; + } else { + $basename = $basename ?? basename($repo); + $filename = "{$basename}-" . ($rel_type === 'releases' ? $data['tag_name'] : $data['name']) . '.tar.gz'; + } + return [$url, $filename]; + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + logger()->debug("Downloading GitHub tarball for {$name} from {$config['repo']}"); + $rel_type = match ($config['type']) { + 'ghtar' => 'releases', + 'ghtagtar' => 'tags', + default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), + }; + [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name); + $path = DOWNLOAD_PATH . "/{$filename}"; + default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php new file mode 100644 index 000000000..90c425075 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php @@ -0,0 +1,27 @@ +debug("Using 'GITHUB_TOKEN' with user {$user} for authentication"); + return ['Authorization: Basic ' . base64_encode("{$user}:{$token}")]; + } + if (($token = getenv('GITHUB_TOKEN')) !== false) { + logger()->debug("Using 'GITHUB_TOKEN' for authentication"); + return ["Authorization: Bearer {$token}"]; + } + return []; + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php new file mode 100644 index 000000000..c5cbb3b50 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php @@ -0,0 +1,63 @@ + '{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}"); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php new file mode 100644 index 000000000..93315ce3a --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php @@ -0,0 +1,18 @@ +debug("Using local source directory for {$name} from {$config['dirname']}"); + return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php new file mode 100644 index 000000000..e4f1a1173 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -0,0 +1,47 @@ +debug("Fetching {$name} source from packagist index: {$packagist_url}"); + $data = default_shell()->executeCurl($packagist_url, retries: $downloader->getRetry()); + if ($data === false) { + throw new DownloaderException("Failed to fetch packagist index for {$name} from {$packagist_url}"); + } + $data = json_decode($data, true); + if (!isset($data['packages'][$config['repo']]) || !is_array($data['packages'][$config['repo']])) { + throw new DownloaderException("failed to find {$name} repo info from packagist"); + } + // get the first version + $first = $data['packages'][$config['repo']][0] ?? []; + // check 'type' => 'php-ext' or contains 'php-ext' key + if (!isset($first['php-ext'])) { + throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); + } + // get download link from dist + $dist_url = $first['dist']['url'] ?? null; + $dist_type = $first['dist']['type'] ?? null; + if (!$dist_url || !$dist_type) { + throw new DownloaderException("failed to find {$name} dist info from packagist"); + } + $name = str_replace('/', '_', $config['repo']); + $version = $first['version'] ?? 'unknown'; + $filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz'); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php new file mode 100644 index 000000000..ec6c33fa4 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -0,0 +1,76 @@ +getOption('with-php', '8.4'); + // Handle 'git' version to clone from php-src repository + if ($phpver === 'git') { + $this->sha256 = null; + return (new Git())->download($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $downloader); + } + + // Fetch PHP release info first + $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); + if ($info === false) { + throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); + } + $info = json_decode($info, true); + if (!is_array($info) || !isset($info['version'])) { + throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); + } + $version = $info['version']; + foreach ($info['source'] as $source) { + if (str_ends_with($source['filename'], '.tar.xz')) { + $this->sha256 = $source['sha256']; + $filename = $source['filename']; + break; + } + } + if (!isset($filename)) { + throw new DownloaderException("No suitable source tarball found for PHP version {$version}"); + } + $url = str_replace('{version}', $version, self::DOWNLOAD_URL); + logger()->debug("Downloading PHP release {$version} from {$url}"); + $path = DOWNLOAD_PATH . "/{$filename}"; + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + } + + public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool + { + if ($this->sha256 === null) { + logger()->debug('Php-src is downloaded from non-release source, skipping validation.'); + return true; + } + + if ($this->sha256 === '') { + logger()->error("No SHA256 checksum available for validation of {$name}."); + return false; + } + + $path = DOWNLOAD_PATH . "/{$result->filename}"; + $hash = hash_file('sha256', $path); + if ($hash !== $this->sha256) { + logger()->error("SHA256 checksum mismatch for {$name}: expected {$this->sha256}, got {$hash}"); + return false; + } + logger()->debug("SHA256 checksum validated successfully for {$name}."); + return true; + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/Url.php b/src/StaticPHP/Artifact/Downloader/Type/Url.php new file mode 100644 index 000000000..a56f4dc71 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/Url.php @@ -0,0 +1,23 @@ +debug("Downloading {$name} from URL: {$url}"); + $version = $config['version'] ?? null; + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + } +} diff --git a/src/StaticPHP/Artifact/Downloader/Type/ValidatorInterface.php b/src/StaticPHP/Artifact/Downloader/Type/ValidatorInterface.php new file mode 100644 index 000000000..1180ff78e --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/ValidatorInterface.php @@ -0,0 +1,13 @@ +stage = $stage; + } +} diff --git a/src/StaticPHP/Attribute/Package/BuildFor.php b/src/StaticPHP/Attribute/Package/BuildFor.php new file mode 100644 index 000000000..271784e7d --- /dev/null +++ b/src/StaticPHP/Attribute/Package/BuildFor.php @@ -0,0 +1,17 @@ +name = "ext-{$name}"; + } + } +} diff --git a/src/StaticPHP/Attribute/Package/Info.php b/src/StaticPHP/Attribute/Package/Info.php new file mode 100644 index 000000000..decf6a35b --- /dev/null +++ b/src/StaticPHP/Attribute/Package/Info.php @@ -0,0 +1,8 @@ +addOption('debug', null, null, '(deprecated) Enable debug mode'); + $this->addOption('no-motd', null, null, 'Disable motd'); + } + + public function initialize(InputInterface $input, OutputInterface $output): void + { + $this->input = $input; + $this->output = $output; + + // Bind command context to ApplicationContext + ApplicationContext::bindCommandContext($input, $output); + + if ($input->getOption('no-motd')) { + $this->no_motd = true; + } + + set_error_handler(static function ($error_no, $error_msg, $error_file, $error_line) { + $tips = [ + E_WARNING => ['PHP Warning: ', 'warning'], + E_NOTICE => ['PHP Notice: ', 'notice'], + E_USER_ERROR => ['PHP Error: ', 'error'], + E_USER_WARNING => ['PHP Warning: ', 'warning'], + E_USER_NOTICE => ['PHP Notice: ', 'notice'], + E_RECOVERABLE_ERROR => ['PHP Recoverable Error: ', 'error'], + E_DEPRECATED => ['PHP Deprecated: ', 'notice'], + E_USER_DEPRECATED => ['PHP User Deprecated: ', 'notice'], + ]; + $level_tip = $tips[$error_no] ?? ['PHP Unknown: ', 'error']; + $error = $level_tip[0] . $error_msg . ' in ' . $error_file . ' on ' . $error_line; + logger()->{$level_tip[1]}($error); + // 如果 return false 则错误会继续递交给 PHP 标准错误处理 + return true; + }); + $version = $this->getVersionWithCommit(); + if (!$this->no_motd) { + $str = str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd)); + echo $this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str; + } + } + + abstract public function handle(): int; + + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + // handle verbose option + $level = match ($this->output->getVerbosity()) { + OutputInterface::VERBOSITY_VERBOSE => 'info', + OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => 'debug', + default => 'warning', + }; + logger()->setLevel($level); + + // ansi + if ($this->input->getOption('no-ansi')) { + logger()->setDecorated(false); + } + + // Set debug mode in ApplicationContext + $isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; + ApplicationContext::setDebug($isDebug); + + // show raw argv list for logger()->debug + logger()->debug('argv: ' . implode(' ', $_SERVER['argv'])); + return $this->handle(); + } /* @noinspection PhpRedundantCatchClauseInspection */ catch (SPCException $e) { + // Handle SPCException and log it + ExceptionHandler::handleSPCException($e); + return static::FAILURE; + } catch (\Throwable $e) { + // Handle any other exceptions + ExceptionHandler::handleDefaultException($e); + return static::FAILURE; + } + } + + protected function getOption(string $name): mixed + { + return $this->input->getOption($name); + } + + protected function getArgument(string $name): mixed + { + return $this->input->getArgument($name); + } + + /** + * Get version string with git commit short ID if available. + */ + private function getVersionWithCommit(): string + { + $version = $this->getApplication()->getVersion(); + + // Don't show commit ID when running in phar + if (\Phar::running()) { + return $version; + } + + $commitId = $this->getGitCommitShortId(); + if ($commitId) { + return "{$version} ({$commitId})"; + } + + return $version; + } + + /** + * Get git commit short ID without executing git command. + */ + private function getGitCommitShortId(): ?string + { + try { + $gitDir = ROOT_DIR . '/.git'; + + if (!is_dir($gitDir)) { + return null; + } + + $headFile = $gitDir . '/HEAD'; + if (!file_exists($headFile)) { + return null; + } + + $head = trim(file_get_contents($headFile)); + + // If HEAD contains 'ref:', it's a branch reference + if (str_starts_with($head, 'ref: ')) { + $ref = substr($head, 5); + $refFile = $gitDir . '/' . $ref; + + if (file_exists($refFile)) { + $commit = trim(file_get_contents($refFile)); + return substr($commit, 0, 7); + } + } else { + // HEAD contains the commit hash directly (detached HEAD) + return substr($head, 0, 7); + } + } catch (\Throwable) { + // Silently fail if we can't read git info + } + + return null; + } +} diff --git a/src/StaticPHP/Command/BuildLibsCommand.php b/src/StaticPHP/Command/BuildLibsCommand.php new file mode 100644 index 000000000..2ff36b49a --- /dev/null +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -0,0 +1,29 @@ +addArgument('libraries', InputArgument::REQUIRED, 'The library packages will be compiled, comma separated'); + } + + public function handle(): int + { + $libs = parse_comma_list($this->input->getArgument('libraries')); + + $installer = new \StaticPHP\Package\PackageInstaller($this->input->getOptions()); + foreach ($libs as $lib) { + $installer->addBuildPackage($lib); + } + $installer->run(); + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php new file mode 100644 index 000000000..2756070bf --- /dev/null +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -0,0 +1,58 @@ +setAliases(['build']); + } + $this->setDescription($description ?? "Build {$target} target from source"); + $pkg = PackageLoader::getTargetPackage($target); + $this->getDefinition()->addOptions($pkg->_exportBuildOptions()); + $this->getDefinition()->addArguments($pkg->_exportBuildArguments()); + + // Builder options + $this->getDefinition()->addOptions([ + new InputOption('with-suggests', ['L', 'E'], null, 'Resolve and install suggested packages as well'), + new InputOption('with-packages', null, InputOption::VALUE_REQUIRED, 'add additional packages to install/build, comma separated', ''), + new InputOption('no-download', null, null, 'Skip downloading artifacts (use existing cached files)'), + ...V2CompatLayer::getLegacyBuildOptions(), + ]); + + // Downloader options (with 'dl-' prefix to avoid conflicts) + $this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions('dl')); + } + + public function handle(): int + { + // resolve legacy options to new options + V2CompatLayer::convertOptions($this->input); + + $starttime = microtime(true); + // run installer + $installer = new PackageInstaller($this->input->getOptions()); + $installer->addBuildPackage($this->target); + $installer->run(); + + $usedtime = round(microtime(true) - $starttime, 1); + $this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); + $this->output->writeln("✔ BUILD SUCCESSFUL ({$usedtime} s)"); + $this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + + $installer->printBuildPackageOutputs(); + + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/Dev/EnvCommand.php b/src/StaticPHP/Command/Dev/EnvCommand.php new file mode 100644 index 000000000..160504ed9 --- /dev/null +++ b/src/StaticPHP/Command/Dev/EnvCommand.php @@ -0,0 +1,43 @@ +addArgument('env', InputArgument::OPTIONAL, 'The environment variable to show, if not set, all will be shown'); + } + + public function initialize(InputInterface $input, OutputInterface $output): void + { + $this->no_motd = true; + parent::initialize($input, $output); + } + + public function handle(): int + { + $env = $this->getArgument('env'); + if (($val = getenv($env)) === false) { + $this->output->writeln("Environment variable '{$env}' is not set."); + return static::FAILURE; + } + if (is_array($val)) { + foreach ($val as $k => $v) { + $this->output->writeln("{$k}={$v}"); + } + return static::SUCCESS; + } + $this->output->writeln("{$val}"); + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/Dev/IsInstalledCommand.php b/src/StaticPHP/Command/Dev/IsInstalledCommand.php new file mode 100644 index 000000000..a3f693217 --- /dev/null +++ b/src/StaticPHP/Command/Dev/IsInstalledCommand.php @@ -0,0 +1,34 @@ +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("Package [{$package}] is installed correctly."); + return static::SUCCESS; + } + $this->output->writeln("Package [{$package}] is not installed."); + return static::FAILURE; + } +} diff --git a/src/StaticPHP/Command/Dev/ShellCommand.php b/src/StaticPHP/Command/Dev/ShellCommand.php new file mode 100644 index 000000000..560cc7fed --- /dev/null +++ b/src/StaticPHP/Command/Dev/ShellCommand.php @@ -0,0 +1,33 @@ +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('Unsupported OS for shell command.'); + return static::FAILURE; + } +} diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php new file mode 100644 index 000000000..cd90cd94c --- /dev/null +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -0,0 +1,35 @@ +addOption('auto-fix', null, InputOption::VALUE_OPTIONAL, 'Automatically fix failed items (if possible)', false); + } + + public function handle(): int + { + f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes'); + $fix_policy = match ($this->input->getOption('auto-fix')) { + 'never' => FIX_POLICY_DIE, + true, null => FIX_POLICY_AUTOFIX, + default => FIX_POLICY_PROMPT, + }; + $doctor = new Doctor($this->output, $fix_policy); + if ($doctor->checkAll()) { + $this->output->writeln('Doctor check complete !'); + return static::SUCCESS; + } + + return static::FAILURE; + } +} diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php new file mode 100644 index 000000000..277585e51 --- /dev/null +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -0,0 +1,119 @@ +addArgument('artifacts', InputArgument::OPTIONAL, 'Specific artifacts to download, comma separated, e.g "php-src,openssl,curl"'); + + // 2.x compatible options + $this->addOption('shallow-clone', null, null, '(deprecated) Clone shallowly repositories when downloading sources'); + $this->addOption('for-extensions', 'e', InputOption::VALUE_REQUIRED, 'Fetch by extensions, e.g "openssl,mbstring"'); + $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"'); + + // shared downloader options (no prefix for download command) + $this->getDefinition()->addOptions(DownloaderOptions::getConsoleOptions()); + } + + public function handle(): int + { + // handle --clean option + if ($this->getOption('clean')) { + return $this->handleClean(); + } + + $downloader = new ArtifactDownloader(DownloaderOptions::extractFromConsoleOptions($this->input->getOptions())); + + // arguments + if ($artifacts = $this->getArgument('artifacts')) { + $artifacts = parse_comma_list($artifacts); + $downloader->addArtifacts($artifacts); + } + // for-extensions + $packages = []; + if ($exts = $this->getOption('for-extensions')) { + $packages = array_map(fn ($x) => "ext-{$x}", parse_extension_list($exts)); + // when using for-extensions, also include php package + array_unshift($packages, 'php'); + array_unshift($packages, 'php-micro'); + array_unshift($packages, 'php-embed'); + array_unshift($packages, 'php-fpm'); + } + // for-libs / for-packages + if ($libs = $this->getOption('for-libs')) { + $packages = array_merge($packages, parse_comma_list($libs)); + } + if ($libs = $this->getOption('for-packages')) { + $packages = array_merge($packages, parse_comma_list($libs)); + } + + // resolve package dependencies and get artifacts directly + $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()) { + $downloader->add($artifact); + } + } + $starttime = microtime(true); + $downloader->download(); + + $endtime = microtime(true); + $elapsed = round($endtime - $starttime); + $this->output->writeln(''); + $this->output->writeln('Download completed in ' . $elapsed . ' s.'); + return static::SUCCESS; + } + + private function handleClean(): int + { + logger()->warning('You are doing some operations that are not recoverable:'); + logger()->warning('- Removing directory: ' . SOURCE_PATH); + logger()->warning('- Removing directory: ' . DOWNLOAD_PATH); + logger()->warning('- Removing directory: ' . BUILD_ROOT_PATH); + logger()->alert('I will remove these directories after 5 seconds!'); + sleep(5); + + if (is_dir(SOURCE_PATH)) { + InteractiveTerm::indicateProgress('Removing: ' . SOURCE_PATH); + FileSystem::removeDir(SOURCE_PATH); + InteractiveTerm::finish('Removed: ' . SOURCE_PATH); + } + if (is_dir(DOWNLOAD_PATH)) { + InteractiveTerm::indicateProgress('Removing: ' . DOWNLOAD_PATH); + FileSystem::removeDir(DOWNLOAD_PATH); + InteractiveTerm::finish('Removed: ' . DOWNLOAD_PATH); + } + if (is_dir(BUILD_ROOT_PATH)) { + InteractiveTerm::indicateProgress('Removing: ' . BUILD_ROOT_PATH); + FileSystem::removeDir(BUILD_ROOT_PATH); + InteractiveTerm::finish('Removed: ' . BUILD_ROOT_PATH); + } + + InteractiveTerm::notice('Clean completed.'); + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/ExtractCommand.php b/src/StaticPHP/Command/ExtractCommand.php new file mode 100644 index 000000000..14951a341 --- /dev/null +++ b/src/StaticPHP/Command/ExtractCommand.php @@ -0,0 +1,111 @@ +setDescription('Extract downloaded artifacts to their target locations'); + + $this->addArgument('artifacts', InputArgument::OPTIONAL, 'Specific artifacts to extract, comma separated, e.g "php-src,openssl,curl"'); + + $this->addOption('for-extensions', 'e', InputOption::VALUE_REQUIRED, 'Extract artifacts for extensions, e.g "openssl,mbstring"'); + $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('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('source-only'); + + $artifacts = []; + + // Direct artifact names + if ($artifact_arg = $this->getArgument('artifacts')) { + $artifact_names = parse_comma_list($artifact_arg); + foreach ($artifact_names as $name) { + $artifact = ArtifactLoader::getArtifactInstance($name); + if ($artifact === null) { + $this->output->writeln("Artifact '{$name}' not found."); + return static::FAILURE; + } + $artifacts[$name] = $artifact; + } + } + + // Resolve packages and get their artifacts + $packages = []; + if ($exts = $this->getOption('for-extensions')) { + $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)); + } + if ($pkgs = $this->getOption('for-packages')) { + $packages = array_merge($packages, parse_comma_list($pkgs)); + } + + if (!empty($packages)) { + $resolved = DependencyResolver::resolve($packages, [], !$this->getOption('without-suggests')); + foreach ($resolved as $pkg_name) { + $pkg = PackageLoader::getPackage($pkg_name); + if ($artifact = $pkg->getArtifact()) { + $artifacts[$artifact->getName()] = $artifact; + } + } + } + + if (empty($artifacts)) { + $this->output->writeln('No artifacts specified. Use artifact names or --for-extensions/--for-libs/--for-packages options.'); + $this->output->writeln(''); + $this->output->writeln('Examples:'); + $this->output->writeln(' spc extract php-src,openssl'); + $this->output->writeln(' spc extract --for-extensions=openssl,mbstring'); + $this->output->writeln(' spc extract --for-libs=libcurl,libssl'); + return static::SUCCESS; + } + + // make php-src always extracted first + uksort($artifacts, fn ($a, $b) => $a === 'php-src' ? -1 : ($b === 'php-src' ? 1 : 0)); + + try { + InteractiveTerm::notice('Extracting ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . '...'); + InteractiveTerm::indicateProgress('Extracting artifacts'); + foreach ($artifacts as $artifact) { + InteractiveTerm::setMessage('Extracting artifact: ' . ConsoleColor::green($artifact->getName())); + $extractor->extract($artifact, $force_source); + } + InteractiveTerm::finish('Extracted all artifacts successfully.'); + } catch (\Exception $e) { + InteractiveTerm::finish('Extraction failed!', false); + throw $e; + } + + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php new file mode 100644 index 000000000..89814f013 --- /dev/null +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -0,0 +1,27 @@ +addArgument('package', null, 'The package to install (name or path)'); + } + + public function handle(): int + { + ApplicationContext::set('elephant', true); + $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true]); + $installer->addInstallPackage($this->input->getArgument('package')); + $installer->run(true, true); + return static::SUCCESS; + } +} diff --git a/src/StaticPHP/Command/SPCConfigCommand.php b/src/StaticPHP/Command/SPCConfigCommand.php new file mode 100644 index 000000000..f8afd0e44 --- /dev/null +++ b/src/StaticPHP/Command/SPCConfigCommand.php @@ -0,0 +1,58 @@ +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'); + $this->addOption('includes', null, null, 'Add additional include path'); + $this->addOption('libs', null, null, 'Add additional libs path'); + $this->addOption('libs-only-deps', null, null, 'Output dependent libraries with -l prefix'); + $this->addOption('absolute-libs', null, null, 'Output absolute paths for libraries'); + $this->addOption('no-php', null, null, 'Link to PHP library'); + } + + public function handle(): int + { + // transform string to array + $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' => (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); + + $this->output->writeln(match (true) { + $this->getOption('includes') => $config['cflags'], + $this->getOption('libs-only-deps') => $config['libs'], + $this->getOption('libs') => "{$config['ldflags']} {$config['libs']}", + default => "{$config['cflags']} {$config['ldflags']} {$config['libs']}", + }); + + return 0; + } +} diff --git a/src/StaticPHP/Config/ArtifactConfig.php b/src/StaticPHP/Config/ArtifactConfig.php new file mode 100644 index 000000000..d25c6dd1a --- /dev/null +++ b/src/StaticPHP/Config/ArtifactConfig.php @@ -0,0 +1,68 @@ + $config) { + self::$artifact_configs[$artifact_name] = $config; + } + } + + /** + * Get all loaded artifact configurations. + * + * @return array an associative array of artifact configurations + */ + public static function getAll(): array + { + return self::$artifact_configs; + } + + /** + * Get the configuration for a specific artifact by name. + * + * @param string $artifact_name the name of the artifact + * @return null|array the configuration array for the specified artifact, or null if not found + */ + public static function get(string $artifact_name): ?array + { + return self::$artifact_configs[$artifact_name] ?? null; + } +} diff --git a/src/StaticPHP/Config/ConfigType.php b/src/StaticPHP/Config/ConfigType.php new file mode 100644 index 000000000..31f96ee41 --- /dev/null +++ b/src/StaticPHP/Config/ConfigType.php @@ -0,0 +1,52 @@ + match (true) { + !isset($value['path']), !is_string($value['path']) && !is_array($value['path']) => false, + default => true, + }, + 'text' => match (true) { + !isset($value['text']), !is_string($value['text']) => false, + default => true, + }, + default => false, + }; + } +} diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php new file mode 100644 index 000000000..b32f41063 --- /dev/null +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -0,0 +1,376 @@ + ConfigType::STRING, + 'depends' => ConfigType::LIST_ARRAY, // @ + 'suggests' => ConfigType::LIST_ARRAY, // @ + 'artifact' => ConfigType::STRING, + 'license' => [ConfigType::class, 'validateLicenseField'], + 'lang' => ConfigType::STRING, + 'frameworks' => ConfigType::LIST_ARRAY, // @ + + // php-extension type fields + 'php-extension' => ConfigType::ASSOC_ARRAY, + 'zend-extension' => ConfigType::BOOL, + 'support' => ConfigType::ASSOC_ARRAY, + 'arg-type' => ConfigType::STRING, + 'build-shared' => ConfigType::BOOL, + 'build-static' => ConfigType::BOOL, + 'build-with-php' => ConfigType::BOOL, + 'notes' => ConfigType::BOOL, + + // library and target fields + 'headers' => ConfigType::LIST_ARRAY, // @ + 'static-libs' => ConfigType::LIST_ARRAY, // @ + 'pkg-configs' => ConfigType::LIST_ARRAY, + 'static-bins' => ConfigType::LIST_ARRAY, // @ + ]; + + public const array PACKAGE_FIELDS = [ + 'type' => true, + 'depends' => false, // @ + 'suggests' => false, // @ + 'artifact' => false, + 'license' => false, + 'lang' => false, + 'frameworks' => false, // @ + + // php-extension type fields + 'php-extension' => false, + + // library and target fields + 'headers' => false, // @ + 'static-libs' => false, // @ + 'pkg-configs' => false, + 'static-bins' => false, // @ + ]; + + public const array SUFFIX_ALLOWED_FIELDS = [ + 'depends', + 'suggests', + 'headers', + 'static-libs', + 'static-bins', + ]; + + public const array PHP_EXTENSION_FIELDS = [ + 'zend-extension' => false, + 'support' => false, + 'arg-type' => false, // @ + 'build-shared' => false, + 'build-static' => false, + 'build-with-php' => false, + 'notes' => false, + ]; + + public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields] + 'filelist' => [['url', 'regex'], ['extract']], + 'git' => [['url'], ['extract', 'submodules', 'rev', 'regex']], + 'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match']], + 'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match']], + 'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable']], + 'url' => [['url'], ['filename', 'extract', 'version']], + 'bitbuckettag' => [['repo'], ['extract']], + 'local' => [['dirname'], ['extract']], + 'pie' => [['repo'], ['extract']], + 'php-release' => [[], ['extract']], + 'custom' => [[], ['func']], + ]; + + /** + * Validate and standardize artifacts configuration data. + * + * @param string $config_file_name Name of the configuration file (for error messages) + * @param mixed $data The configuration data to validate + */ + public static function validateAndLintArtifacts(string $config_file_name, mixed &$data): void + { + if (!is_array($data)) { + throw new ValidationException("{$config_file_name} is broken"); + } + foreach ($data as $name => $artifact) { + foreach ($artifact as $k => $v) { + // check source field + if ($k === 'source' || $k === 'source-mirror') { + // source === custom is allowed + if ($v === 'custom') { + continue; + } + // expand string to url type (start with http:// or https://) + if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) { + $data[$name][$k] = [ + 'type' => 'url', + 'url' => $v, + ]; + continue; + } + // source: object with type field + if (is_assoc_array($v)) { + self::validateArtifactObjectField($name, $v); + } + continue; + } + // check binary field + if ($k === 'binary') { + // binary === custom is allowed + if ($v === 'custom') { + $data[$name][$k] = [ + 'linux-x86_64' => ['type' => 'custom'], + 'linux-aarch64' => ['type' => 'custom'], + 'windows-x86_64' => ['type' => 'custom'], + 'macos-x86_64' => ['type' => 'custom'], + 'macos-aarch64' => ['type' => 'custom'], + ]; + continue; + } + 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)) { + foreach ($v as $platform => $v_obj) { + self::validatePlatformString($platform); + // expand string to url type (start with http:// or https://) + if (is_string($v_obj) && (str_starts_with($v_obj, 'http://') || str_starts_with($v_obj, 'https://'))) { + $data[$name][$k][$platform] = [ + 'type' => 'url', + 'url' => $v_obj, + ]; + continue; + } + // binary: object with type field + if (is_assoc_array($v_obj)) { + self::validateArtifactObjectField("{$name}::{$platform}", $v_obj); + } + } + } + } + } + } + } + + /** + * Validate packages configuration data. + * + * @param string $config_file_name Name of the configuration file (for error messages) + * @param mixed $data The configuration data to validate + */ + public static function validateAndLintPackages(string $config_file_name, mixed &$data): void + { + if (!is_array($data)) { + throw new ValidationException("{$config_file_name} is broken"); + } + foreach ($data as $name => $pkg) { + if (!is_assoc_array($pkg)) { + throw new ValidationException("Package [{$name}] in {$config_file_name} is not a valid associative array"); + } + // check if package has valid type + if (!isset($pkg['type']) || !in_array($pkg['type'], ConfigType::PACKAGE_TYPES)) { + throw new ValidationException("Package [{$name}] in {$config_file_name} has invalid or missing 'type' field"); + } + + // validate basic fields using unified method + self::validatePackageFields($name, $pkg); + + // validate list of suffix-allowed fields + $suffixes = ['', '@windows', '@unix', '@macos', '@linux']; + $fields = self::SUFFIX_ALLOWED_FIELDS; + self::validateSuffixAllowedFields($name, $pkg, $fields, $suffixes); + + // check if "library|target" package has artifact field for target and library types + if (in_array($pkg['type'], ['target', 'library']) && !isset($pkg['artifact'])) { + throw new ValidationException("Package [{$name}] in {$config_file_name} of type '{$pkg['type']}' must have an 'artifact' field"); + } + + // check if "php-extension" package has php-extension specific fields and validate inside + if ($pkg['type'] === 'php-extension') { + self::validatePhpExtensionFields($name, $pkg); + } + + // check for unknown fields + self::validateNoInvalidFields('package', $name, $pkg, array_keys(self::PACKAGE_FIELD_TYPES)); + } + } + + /** + * Validate platform string format. + * + * @param string $platform Platform string, like windows-x86_64 + */ + public static function validatePlatformString(string $platform): void + { + $valid_platforms = ['windows', 'linux', 'macos']; + $valid_arch = ['x86_64', 'aarch64']; + $parts = explode('-', $platform); + if (count($parts) !== 2) { + throw new ValidationException("Invalid platform format '{$platform}', expected format 'os-arch'"); + } + [$os, $arch] = $parts; + if (!in_array($os, $valid_platforms)) { + throw new ValidationException("Invalid platform OS '{$os}' in platform '{$platform}'"); + } + if (!in_array($arch, $valid_arch)) { + throw new ValidationException("Invalid platform architecture '{$arch}' in platform '{$platform}'"); + } + } + + /** + * Validate an artifact download object field. + * + * @param string $item_name Artifact name (for error messages) + * @param array $data Artifact source object data + */ + private static function validateArtifactObjectField(string $item_name, array $data): void + { + if (!isset($data['type']) || !is_string($data['type'])) { + throw new ValidationException("Artifact source object must have a valid 'type' field"); + } + $type = $data['type']; + if (!isset(self::ARTIFACT_TYPE_FIELDS[$type])) { + throw new ValidationException("Artifact source object has unknown type '{$type}'"); + } + [$required_fields, $optional_fields] = self::ARTIFACT_TYPE_FIELDS[$type]; + // check required fields + foreach ($required_fields as $field) { + if (!isset($data[$field])) { + throw new ValidationException("Artifact source object of type '{$type}' must have required field '{$field}'"); + } + } + // check for unknown fields + $allowed_fields = array_merge(['type'], $required_fields, $optional_fields); + self::validateNoInvalidFields('artifact object', $item_name, $data, $allowed_fields); + } + + /** + * Unified method to validate config fields based on field definitions + * + * @param string $package_name Package name + * @param mixed $pkg The package configuration array + */ + private static function validatePackageFields(string $package_name, mixed $pkg): void + { + foreach (self::PACKAGE_FIELDS as $field => $required) { + if ($required && !isset($pkg[$field])) { + throw new ValidationException("Package {$package_name} must have [{$field}] field"); + } + + if (isset($pkg[$field])) { + self::validatePackageFieldType($field, $pkg[$field], $package_name); + } + } + } + + /** + * Validate a field based on its global type definition + * + * @param string $field Field name + * @param mixed $value Field value + * @param string $package_name Package name (for error messages) + */ + private static function validatePackageFieldType(string $field, mixed $value, string $package_name): void + { + // Check if field exists in FIELD_TYPES + if (!isset(self::PACKAGE_FIELD_TYPES[$field])) { + // Try to strip suffix and check base field name + $suffixes = ['@windows', '@unix', '@macos', '@linux']; + $base_field = $field; + foreach ($suffixes as $suffix) { + if (str_ends_with($field, $suffix)) { + $base_field = substr($field, 0, -strlen($suffix)); + break; + } + } + + if (!isset(self::PACKAGE_FIELD_TYPES[$base_field])) { + // Unknown field is not allowed - strict validation + throw new ValidationException("Package {$package_name} has unknown field [{$field}]"); + } + + // Use base field type for validation + $expected_type = self::PACKAGE_FIELD_TYPES[$base_field]; + } else { + $expected_type = self::PACKAGE_FIELD_TYPES[$field]; + } + + match ($expected_type) { + ConfigType::STRING => is_string($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be string"), + ConfigType::BOOL => is_bool($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be boolean"), + ConfigType::LIST_ARRAY => is_list_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be a list"), + ConfigType::ASSOC_ARRAY => is_assoc_array($value) ?: throw new ValidationException("Package {$package_name} [{$field}] must be an object"), + default => $expected_type($value) ?: throw new ValidationException("Package {$package_name} [{$field}] has invalid type specification"), + }; + } + + /** + * Validate that fields with suffixes are list arrays + */ + private static function validateSuffixAllowedFields(int|string $name, mixed $item, array $fields, array $suffixes): void + { + foreach ($fields as $field) { + foreach ($suffixes as $suffix) { + $key = $field . $suffix; + if (isset($item[$key])) { + self::validatePackageFieldType($key, $item[$key], $name); + } + } + } + } + + /** + * Validate php-extension specific fields for php-extension package + */ + private static function validatePhpExtensionFields(int|string $name, mixed $pkg): void + { + if (!isset($pkg['php-extension'])) { + return; + } + if (!is_assoc_array($pkg['php-extension'])) { + throw new ValidationException("Package {$name} [php-extension] must be an object"); + } + foreach (self::PHP_EXTENSION_FIELDS as $field => $required) { + if (isset($pkg['php-extension'][$field])) { + self::validatePackageFieldType($field, $pkg['php-extension'][$field], $name); + } + } + // check for unknown fields in php-extension + self::validateNoInvalidFields('php-extension', $name, $pkg['php-extension'], array_keys(self::PHP_EXTENSION_FIELDS)); + } + + private static function validateNoInvalidFields(string $config_type, int|string $item_name, mixed $item_content, array $allowed_fields): void + { + foreach ($item_content as $k => $v) { + // remove suffixes for checking + $base_k = $k; + $suffixes = ['@windows', '@unix', '@macos', '@linux']; + foreach ($suffixes as $suffix) { + if (str_ends_with($k, $suffix)) { + $base_k = substr($k, 0, -strlen($suffix)); + break; + } + } + if (!in_array($base_k, $allowed_fields)) { + throw new ValidationException("{$config_type} [{$item_name}] has invalid field [{$base_k}]"); + } + } + } +} diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php new file mode 100644 index 000000000..dc0b3d546 --- /dev/null +++ b/src/StaticPHP/Config/PackageConfig.php @@ -0,0 +1,102 @@ + $config) { + self::$package_configs[$pkg_name] = $config; + } + } + + /** + * Check if a package configuration exists. + */ + public static function isPackageExists(string $pkg_name): bool + { + return isset(self::$package_configs[$pkg_name]); + } + + public static function getAll(): array + { + return self::$package_configs; + } + + /** + * Get a specific field from a package configuration. + * + * @param string $pkg_name Package name + * @param null|string $field_name Package config field name + * @param null|mixed $default Default value if field not found + * @return mixed The value of the specified field or the default value + */ + public static function get(string $pkg_name, ?string $field_name = null, mixed $default = null): mixed + { + if (!self::isPackageExists($pkg_name)) { + return $default; + } + // use suffixes to find field + $suffixes = match (SystemTarget::getTargetOS()) { + 'Windows' => ['@windows', ''], + 'Darwin' => ['@macos', '@unix', ''], + 'Linux' => ['@linux', '@unix', ''], + 'BSD' => ['@freebsd', '@bsd', '@unix', ''], + }; + if ($field_name === null) { + return self::$package_configs[$pkg_name]; + } + if (in_array($field_name, ConfigValidator::SUFFIX_ALLOWED_FIELDS)) { + foreach ($suffixes as $suffix) { + $suffixed_field = $field_name . $suffix; + if (isset(self::$package_configs[$pkg_name][$suffixed_field])) { + return self::$package_configs[$pkg_name][$suffixed_field]; + } + } + return $default; + } + return self::$package_configs[$pkg_name][$field_name] ?? $default; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php new file mode 100644 index 000000000..0e5371ac4 --- /dev/null +++ b/src/StaticPHP/ConsoleApplication.php @@ -0,0 +1,77 @@ + $package) { + // only add target that contains artifact.source + if ($package->hasStage('build')) { + logger()->debug("Registering build target command for package: {$name}"); + $this->addCommand(new BuildTargetCommand($name)); + } + } + + // add core commands + $this->addCommands([ + new DownloadCommand(), + new DoctorCommand(), + new InstallPackageCommand(), + new BuildLibsCommand(), + new ExtractCommand(), + new SPCConfigCommand(), + + // dev commands + new ShellCommand(), + new IsInstalledCommand(), + new EnvCommand(), + ]); + + // add additional commands from registries + if (!empty(self::$additional_commands)) { + $this->addCommands(self::$additional_commands); + } + } + + /** + * @internal + */ + public static function _addAdditionalCommands(array $additional_commands): void + { + self::$additional_commands = array_merge(self::$additional_commands, $additional_commands); + } +} diff --git a/src/StaticPHP/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php new file mode 100644 index 000000000..73ff9f2d4 --- /dev/null +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -0,0 +1,209 @@ +set() calls throughout the codebase. + */ +class ApplicationContext +{ + private static ?Container $container = null; + + private static ?CallbackInvoker $invoker = null; + + private static bool $debug = false; + + /** + * Initialize the container with configuration. + * Should only be called once at application startup. + * + * @param array $options Initialization options + * - 'debug': Enable debug mode (disables compilation) + * - 'definitions': Additional container definitions + */ + public static function initialize(array $options = []): Container + { + if (self::$container !== null) { + throw new \RuntimeException('ApplicationContext already initialized. Use reset() first if you need to reinitialize.'); + } + + $builder = new ContainerBuilder(); + $builder->useAutowiring(true); + $builder->useAttributes(true); + + // Load default definitions + self::configureDefaults($builder); + + // Add custom definitions if provided + if (isset($options['definitions']) && is_array($options['definitions'])) { + $builder->addDefinitions($options['definitions']); + } + + // Set debug mode + self::$debug = $options['debug'] ?? false; + + self::$container = $builder->build(); + // Get invoker from container to ensure singleton consistency + self::$invoker = self::$container->get(CallbackInvoker::class); + + return self::$container; + } + + /** + * Get the container instance. + * If not initialized, initializes with default configuration. + */ + public static function getContainer(): Container + { + if (self::$container === null) { + self::initialize(); + } + return self::$container; + } + + /** + * Get a service from the container. + * + * @template T + * + * @param class-string $id Service identifier + * + * @return null|T + */ + public static function get(string $id): mixed + { + return self::getContainer()->get($id); + } + + /** + * Check if a service exists in the container. + */ + public static function has(string $id): bool + { + return self::getContainer()->has($id); + } + + /** + * Set a service in the container. + * Use sparingly - prefer configuration-based definitions. + */ + public static function set(string $id, mixed $value): void + { + self::getContainer()->set($id, $value); + } + + /** + * Bind command-line context to the container. + * Called at the start of each command execution. + */ + public static function bindCommandContext(InputInterface $input, OutputInterface $output): void + { + $container = self::getContainer(); + $container->set(InputInterface::class, $input); + $container->set(OutputInterface::class, $output); + self::$debug = $output->isDebug(); + } + + /** + * Get the callback invoker instance. + */ + public static function getInvoker(): CallbackInvoker + { + if (self::$invoker === null) { + // Get from container to ensure singleton consistency + self::$invoker = self::getContainer()->get(CallbackInvoker::class); + } + return self::$invoker; + } + + /** + * Invoke a callback with automatic dependency injection and context. + * + * @param callable $callback The callback to invoke + * @param array $context Context parameters for injection + */ + public static function invoke(callable $callback, array $context = []): mixed + { + 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); + } + + /** + * Check if debug mode is enabled. + */ + public static function isDebug(): bool + { + return self::$debug; + } + + /** + * Set debug mode. + */ + public static function setDebug(bool $debug): void + { + self::$debug = $debug; + } + + /** + * Reset the container. + * Primarily used for testing to ensure isolation between tests. + */ + public static function reset(): void + { + self::$container = null; + self::$invoker = null; + self::$debug = false; + } + + /** + * Configure default container definitions. + */ + private static function configureDefaults(ContainerBuilder $builder): void + { + $builder->addDefinitions([ + // Self-reference for container + ContainerInterface::class => factory(function (Container $c) { + return $c; + }), + Container::class => factory(function (Container $c) { + return $c; + }), + + // CallbackInvoker is created separately to avoid circular dependency + CallbackInvoker::class => factory(function (Container $c) { + return new CallbackInvoker($c); + }), + + // Command context (set at runtime via bindCommandContext) + InputInterface::class => \DI\value(null), + OutputInterface::class => \DI\value(null), + ]); + } +} diff --git a/src/StaticPHP/DI/CallbackInvoker.php b/src/StaticPHP/DI/CallbackInvoker.php new file mode 100644 index 000000000..0d77f7aab --- /dev/null +++ b/src/StaticPHP/DI/CallbackInvoker.php @@ -0,0 +1,149 @@ + value or name => value) + * + * @return mixed The return value of the callback + */ + 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 = []; + + foreach ($reflection->getParameters() as $param) { + $type = $param->getType(); + $typeName = $type instanceof \ReflectionNamedType ? $type->getName() : null; + $paramName = $param->getName(); + + // 1. Look up by type name in context + if ($typeName !== null && array_key_exists($typeName, $context)) { + $args[] = $context[$typeName]; + continue; + } + + // 2. Look up by parameter name in context + if (array_key_exists($paramName, $context)) { + $args[] = $context[$paramName]; + continue; + } + + // 3. Look up in container by type + if ($typeName !== null && !$this->isBuiltinType($typeName) && $this->container->has($typeName)) { + 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 + if ($param->isDefaultValueAvailable()) { + $args[] = $param->getDefaultValue(); + continue; + } + + // 5. Allow null if nullable + if ($param->allowsNull()) { + $args[] = null; + continue; + } + + // Cannot resolve parameter + throw new SPCInternalException( + "Cannot resolve parameter '{$paramName}'" . + ($typeName ? " of type '{$typeName}'" : '') . + ' for callback invocation' + ); + } + + return $callback(...$args); + } + + /** + * Check if a type name is a PHP builtin type. + */ + private function isBuiltinType(string $typeName): bool + { + return in_array($typeName, [ + 'string', 'int', 'float', 'bool', 'array', + 'object', 'callable', 'iterable', 'mixed', + '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; + } +} diff --git a/src/StaticPHP/Doctor/CheckResult.php b/src/StaticPHP/Doctor/CheckResult.php new file mode 100644 index 000000000..327b1e8b7 --- /dev/null +++ b/src/StaticPHP/Doctor/CheckResult.php @@ -0,0 +1,46 @@ +message; + } + + public function getFixItem(): string + { + return $this->fix_item; + } + + public function getFixParams(): array + { + return $this->fix_params; + } + + public function isOK(): bool + { + return $this->ok; + } + + public function setFixItem(string $fix_item = '', array $fix_params = []): void + { + $this->fix_item = $fix_item; + $this->fix_params = $fix_params; + } +} diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php new file mode 100644 index 000000000..d86e42ac2 --- /dev/null +++ b/src/StaticPHP/Doctor/Doctor.php @@ -0,0 +1,164 @@ + $i->item_name, array_map(fn ($x) => $x[0], $items)); + logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names)); + } + + /** + * Check all valid check items. + * @return bool true if all checks passed, false otherwise + */ + public function checkAll(bool $interactive = true): bool + { + if ($interactive) { + InteractiveTerm::notice('Starting doctor checks ...'); + } + foreach ($this->getValidCheckList() as $check) { + if (!$this->checkItem($check, $interactive)) { + return false; + } + } + return true; + } + + /** + * Check a single check item. + * + * @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 $interactive = true): bool + { + if (is_string($check)) { + $found = null; + foreach (DoctorLoader::getDoctorItems() as $item) { + if ($item[0]->item_name === $check) { + $found = $item[0]; + break; + } + } + if ($found === null) { + $this->output?->writeln("Check item '{$check}' not found."); + return false; + } + $check = $found; + } + $prepend = $interactive ? ' - ' : ''; + $this->output?->write("{$prepend}Checking {$check->item_name} ... "); + + // call check + $result = call_user_func($check->callback); + + if ($result === null) { + $this->output?->writeln('skipped'); + return true; + } + if (!$result instanceof CheckResult) { + $this->output?->writeln('Skipped due to invalid return value'); + return true; + } + if ($result->isOK()) { + /* @phpstan-ignore-next-line */ + $this->output?->writeln($result->getMessage() ?? (string) ConsoleColor::green('✓')); + return true; + } + $this->output?->writeln('' . $result->getMessage() . ''); + + // if the check item is not fixable, fail immediately + if ($result->getFixItem() === '') { + $this->output?->writeln('This check item can not be fixed automatically !'); + return false; + } + // unknown fix item + if (!DoctorLoader::getFixItem($result->getFixItem())) { + $this->output?->writeln("Internal error: Unknown fix item: {$result->getFixItem()}"); + return false; + } + // skip fix + if ($this->auto_fix === FIX_POLICY_DIE) { + $this->output?->writeln('Auto-fix is disabled. Please fix this issue manually.'); + return false; + } + // prompt for fix + if ($this->auto_fix === FIX_POLICY_PROMPT && !confirm('Do you want to try to fix this issue now?')) { + $this->output?->writeln('You canceled fix.'); + return false; + } + // perform fix + InteractiveTerm::indicateProgress("Fixing {$result->getFixItem()} ... "); + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + // $this->output?->writeln("Fixing {$check->item_name} ... "); + if ($this->emitFix($result->getFixItem(), $result->getFixParams())) { + InteractiveTerm::finish('Fix applied successfully!'); + return true; + } + InteractiveTerm::finish('Failed to apply fix!', false); + return false; + } + + private function emitFix(string $fix_item, array $fix_item_params = []): bool + { + keyboard_interrupt_register(function () { + $this->output?->writeln('You cancelled fix'); + }); + try { + return ApplicationContext::invoke(DoctorLoader::getFixItem($fix_item), $fix_item_params); + } catch (SPCException $e) { + $this->output?->writeln('Fix failed: ' . $e->getMessage() . ''); + return false; + } catch (\Throwable $e) { + logger()->debug('Error: ' . $e->getMessage() . " at {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString()); + $this->output?->writeln('Fix failed with an unexpected error: ' . $e->getMessage() . ''); + return false; + } finally { + keyboard_interrupt_unregister(); + } + } + + /** + * Get a list of valid check items for current environment. + */ + private function getValidCheckList(): iterable + { + foreach (DoctorLoader::getDoctorItems() as [$item, $optional]) { + /* @var CheckItem $item */ + // optional check + if ($optional !== null && !call_user_func($optional)) { + continue; // skip this when the optional check is false + } + // limit_os check + if ($item->limit_os !== null && $item->limit_os !== PHP_OS_FAMILY) { + continue; + } + // skipped items by env + $skip_items = array_filter(explode(',', getenv('SPC_SKIP_DOCTOR_CHECK_ITEMS') ?: '')); + if (in_array($item->item_name, $skip_items)) { + continue; // skip this item + } + yield $item; + } + } +} diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php new file mode 100644 index 000000000..4d7a86bee --- /dev/null +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -0,0 +1,104 @@ +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 + { + // 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; + } +} diff --git a/src/StaticPHP/Doctor/Item/LinuxToolCheck.php b/src/StaticPHP/Doctor/Item/LinuxToolCheck.php new file mode 100644 index 000000000..09161c74e --- /dev/null +++ b/src/StaticPHP/Doctor/Item/LinuxToolCheck.php @@ -0,0 +1,147 @@ + '/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; + } +} diff --git a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php new file mode 100644 index 000000000..d9c869ea7 --- /dev/null +++ b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php @@ -0,0 +1,106 @@ +execWithResult("{$bison} --version", false); + if (preg_match('/bison \(GNU Bison\) (\d+)\.(\d+)(?:\.(\d+))?/', $version[1][0], $matches)) { + $major = (int) $matches[1]; + // major should be 3 or later + if ($major < 3) { + // find homebrew keg-only bison + if ($command_path !== []) { + return CheckResult::fail("Current {$bison} version is too old: " . $matches[0]); + } + return $this->checkBisonVersion(['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin']); + } + return CheckResult::ok($matches[0]); + } + return CheckResult::fail('bison version cannot be determined'); + } + + #[FixItem('brew')] + public function fixBrew(): bool + { + shell(true)->exec('/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"'); + return true; + } + + #[FixItem('build-tools')] + public function fixBuildTools(array $missing): bool + { + $replacement = [ + 'glibtoolize' => 'libtool', + ]; + foreach ($missing as $cmd) { + if (isset($replacement[$cmd])) { + $cmd = $replacement[$cmd]; + } + shell()->exec('brew install --formula ' . escapeshellarg($cmd)); + } + return true; + } +} diff --git a/src/StaticPHP/Doctor/Item/OSCheck.php b/src/StaticPHP/Doctor/Item/OSCheck.php new file mode 100644 index 000000000..7bd19df81 --- /dev/null +++ b/src/StaticPHP/Doctor/Item/OSCheck.php @@ -0,0 +1,23 @@ +execWithResult("{$pkgconf} --version", false); + if ($ret !== 0) { + return CheckResult::fail('pkg-config is not functional', 'install-pkg-config'); + } + return CheckResult::ok(trim($output[0])); + } + + #[FixItem('install-pkg-config')] + public function fix(): bool + { + ApplicationContext::set('elephant', true); + $installer = new PackageInstaller(['dl-binary-only' => true]); + $installer->addInstallPackage('pkg-config'); + $installer->run(false, true); + return true; + } +} diff --git a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php new file mode 100644 index 000000000..fce3350be --- /dev/null +++ b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php @@ -0,0 +1,38 @@ += 1.0.3', limit_os: 'Linux', level: 20)] + #[CheckItem('if re2c version >= 1.0.3', limit_os: 'Darwin', level: 20)] + public function checkRe2cVersion(): ?CheckResult + { + $ver = shell(false)->execWithResult('re2c --version', false); + // match version: re2c X.X(.X) + if ($ver[0] !== 0 || !preg_match('/re2c\s+(\d+\.\d+(\.\d+)?)/', $ver[1][0], $matches)) { + return CheckResult::fail('Failed to get re2c version', 'build-re2c'); + } + $version_string = $matches[1]; + if (version_compare($version_string, '1.0.3') < 0) { + return CheckResult::fail('re2c version is too low (' . $version_string . ')', 'build-re2c'); + } + return CheckResult::ok($version_string); + } + + #[FixItem('build-re2c')] + public function buildRe2c(): bool + { + $installer = new PackageInstaller(); + $installer->addInstallPackage('re2c'); + $installer->run(false); + return true; + } +} diff --git a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php new file mode 100644 index 000000000..e6a042d3b --- /dev/null +++ b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php @@ -0,0 +1,142 @@ +addInstallPackage('vswhere'); + $is_installed = $installer->isPackageInstalled('vswhere'); + if ($is_installed) { + return CheckResult::ok(); + } + return CheckResult::fail('vswhere is not installed', 'install-vswhere'); + } + + #[CheckItem('if Visual Studio is installed', level: 998)] + public function findVS(): ?CheckResult + { + $a = WindowsUtil::findVisualStudio(); + if ($a !== false) { + return CheckResult::ok("{$a['version']} at {$a['dir']}"); + } + return CheckResult::fail('Visual Studio with C++ tools is not installed. Please install Visual Studio with C++ tools.'); + } + + #[CheckItem('if git associated command exists', level: 997)] + public function checkGitPatch(): ?CheckResult + { + if (WindowsUtil::findCommand('patch.exe') === null) { + return CheckResult::fail('Git patch (minGW command) not found in path. You need to add "C:\Program Files\Git\usr\bin" in Path.'); + } + return CheckResult::ok(); + } + + #[CheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 996)] + public function checkSDK(): ?CheckResult + { + if (!file_exists(getenv('PHP_SDK_PATH') . DIRECTORY_SEPARATOR . 'phpsdk-starter.bat')) { + return CheckResult::fail('php-sdk-binary-tools not downloaded', 'install-php-sdk'); + } + return CheckResult::ok(getenv('PHP_SDK_PATH')); + } + + #[CheckItem('if nasm installed', level: 995)] + public function checkNasm(): ?CheckResult + { + if (($a = WindowsUtil::findCommand('nasm.exe')) === null) { + return CheckResult::fail('nasm.exe not found in path.', 'install-nasm'); + } + return CheckResult::ok($a); + } + + #[CheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)] + public function checkPerl(): ?CheckResult + { + if (($path = WindowsUtil::findCommand('perl.exe')) === null) { + return CheckResult::fail('perl not found in path.', 'install-perl'); + } + if (!str_contains(implode('', cmd()->execWithResult(quote($path) . ' -v', false)[1]), 'MSWin32')) { + return CheckResult::fail($path . ' is not built for msvc.', 'install-perl'); + } + return CheckResult::ok($path); + } + + #[CheckItem('if environment is properly set up', level: 1)] + public function checkenv(): ?CheckResult + { + // manually trigger after init + try { + ToolchainManager::afterInitToolchain(); + } catch (\Exception $e) { + return CheckResult::fail('Environment setup failed: ' . $e->getMessage()); + } + $required_cmd = ['cl.exe', 'link.exe', 'lib.exe', 'dumpbin.exe', 'msbuild.exe', 'nmake.exe']; + foreach ($required_cmd as $cmd) { + if (WindowsUtil::findCommand($cmd) === null) { + return CheckResult::fail("{$cmd} not found in path. Please make sure Visual Studio with C++ tools is properly installed."); + } + } + return CheckResult::ok(); + } + + #[FixItem('install-perl')] + public function installPerl(): bool + { + $installer = new PackageInstaller(); + $installer->addInstallPackage('strawberry-perl'); + $installer->run(false); + GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl'); + return true; + } + + #[FixItem('install-php-sdk')] + public function installSDK(): bool + { + FileSystem::removeDir(getenv('PHP_SDK_PATH')); + $installer = new PackageInstaller(); + $installer->addInstallPackage('php-sdk-binary-tools'); + $installer->run(false); + return true; + } + + #[FixItem('install-nasm')] + public function installNasm(): bool + { + $installer = new PackageInstaller(); + $installer->addInstallPackage('nasm'); + $installer->run(false); + return true; + } + + #[FixItem('install-vswhere')] + public function installVSWhere(): bool + { + $installer = new PackageInstaller(); + $installer->addInstallPackage('vswhere'); + $installer->run(false); + return true; + } +} diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php new file mode 100644 index 000000000..4157e9d60 --- /dev/null +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -0,0 +1,46 @@ +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'); + } +} diff --git a/src/StaticPHP/Exception/BuildFailureException.php b/src/StaticPHP/Exception/BuildFailureException.php new file mode 100644 index 000000000..11ccf26b4 --- /dev/null +++ b/src/StaticPHP/Exception/BuildFailureException.php @@ -0,0 +1,13 @@ +solution; + } +} diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php new file mode 100644 index 000000000..53dc15a85 --- /dev/null +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -0,0 +1,232 @@ + Build PHP extra info binding */ + private static array $build_php_extra_info = []; + + public static function handleSPCException(SPCException $e): void + { + // XXX error: yyy + $head_msg = match ($class = get_class($e)) { + BuildFailureException::class => "✗ Build failed: {$e->getMessage()}", + DownloaderException::class => "✗ Download failed: {$e->getMessage()}", + EnvironmentException::class => "⚠ Environment check failed: {$e->getMessage()}", + ExecutionException::class => "✗ Command execution failed: {$e->getMessage()}", + FileSystemException::class => "✗ File system error: {$e->getMessage()}", + InterruptException::class => "⚠ Build interrupted by user: {$e->getMessage()}", + PatchException::class => "✗ Patch apply failed: {$e->getMessage()}", + 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); + + // ---------------------------------------- + $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); + + if ($minor_logs) { + return; + } + + self::logError("----------------------------------------\n"); + + // get the SPCException module + if ($lib_info = $e->getLibraryInfo()) { + self::logError('Failed module: ' . ConsoleColor::yellow("library {$lib_info['library_name']} builder for {$lib_info['os']}")); + } elseif ($ext_info = $e->getExtensionInfo()) { + self::logError('Failed module: ' . ConsoleColor::yellow("shared extension {$ext_info['extension_name']} builder")); + } elseif (self::$builder) { + $os = match (get_class(self::$builder)) { + WindowsBuilder::class => 'Windows', + MacOSBuilder::class => 'macOS', + LinuxBuilder::class => 'Linux', + BSDBuilder::class => 'FreeBSD', + default => 'Unknown OS', + }; + self::logError('Failed module: ' . ConsoleColor::yellow("Builder for {$os}")); + } elseif (!in_array($class, self::KNOWN_EXCEPTIONS)) { + self::logError('Failed From: ' . ConsoleColor::yellow('Unknown SPC module ' . $class)); + } + + // get command execution info + if ($e instanceof ExecutionException) { + self::logError(''); + self::logError('Failed command: ' . ConsoleColor::yellow($e->getExecutionCommand())); + if ($cd = $e->getCd()) { + self::logError('Command executed in: ' . ConsoleColor::yellow($cd)); + } + if ($env = $e->getEnv()) { + self::logError('Command inline env variables:'); + foreach ($env as $k => $v) { + self::logError(ConsoleColor::yellow("{$k}={$v}"), 4); + } + } + } + + // validation error + if ($e instanceof ValidationException) { + self::logError('Failed validation module: ' . ConsoleColor::yellow($e->getValidationModuleString())); + } + + // environment error + if ($e instanceof EnvironmentException) { + self::logError('Failed environment check: ' . ConsoleColor::yellow($e->getMessage())); + if (($solution = $e->getSolution()) !== null) { + self::logError('Solution: ' . ConsoleColor::yellow($solution)); + } + } + + // get patch info + if ($e instanceof PatchException) { + self::logError("Failed patch module: {$e->getPatchModule()}"); + } + + // get internal trace + if ($e instanceof SPCInternalException) { + self::logError('Internal trace:'); + self::logError(ConsoleColor::gray("{$e->getTraceAsString()}\n"), 4); + } + + // get the full build info if possible + if ($info = ExceptionHandler::$build_php_extra_info) { + self::logError('', output_log: ApplicationContext::isDebug()); + self::logError('Build PHP extra info:', output_log: ApplicationContext::isDebug()); + self::printArrayInfo($info); + } + + // get the full builder options if possible + if ($e->getBuildPHPInfo()) { + $info = $e->getBuildPHPInfo(); + self::logError('', output_log: ApplicationContext::isDebug()); + self::logError('Builder function: ' . ConsoleColor::yellow($info['builder_function']), output_log: ApplicationContext::isDebug()); + } + + self::logError("\n----------------------------------------\n"); + + // convert log file path if in docker + $spc_log_convert = get_display_path(SPC_OUTPUT_LOG); + $shell_log_convert = get_display_path(SPC_SHELL_LOG); + $spc_logs_dir_convert = get_display_path(SPC_LOGS_DIR); + + self::logError('⚠ The ' . ConsoleColor::cyan('console output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::cyan($spc_log_convert)); + if (file_exists(SPC_SHELL_LOG)) { + self::logError('⚠ The ' . ConsoleColor::cyan('shell output log') . ConsoleColor::red(' is saved in ') . ConsoleColor::cyan($shell_log_convert)); + } + if ($e->getExtraLogFiles() !== []) { + foreach ($e->getExtraLogFiles() as $key => $file) { + self::logError("⚠ Log file [{$key}] is saved in: " . ConsoleColor::cyan("{$spc_logs_dir_convert}/{$file}")); + } + } + if (!ApplicationContext::isDebug()) { + self::logError('⚠ If you want to see more details in console, use `--debug` option.'); + } + } + + public static function handleDefaultException(\Throwable $e): void + { + $class = get_class($e); + $file = $e->getFile(); + $line = $e->getLine(); + self::logError("✗ Unhandled exception {$class} on {$file} line {$line}:\n\t{$e->getMessage()}\n"); + self::logError('Stack trace:'); + self::logError(ConsoleColor::gray($e->getTraceAsString()) . PHP_EOL, 4); + self::logError('⚠ Please report this exception to: https://github.com/crazywhalecc/static-php-cli/issues'); + } + + public static function bindBuilder(?BuilderBase $bind_builder): void + { + self::$builder = $bind_builder; + } + + public static function bindBuildPhpExtraInfo(array $build_php_extra_info): void + { + self::$build_php_extra_info = $build_php_extra_info; + } + + private static function logError($message, int $indent_space = 0, bool $output_log = true): void + { + $spc_log = fopen(SPC_OUTPUT_LOG, 'a'); + $msg = explode("\n", (string) $message); + foreach ($msg as $v) { + $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); + fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); + if ($output_log) { + InteractiveTerm::plain(ConsoleColor::red($line) . '', 'error'); + } + } + } + + /** + * Print array info to console and log. + */ + private static function printArrayInfo(array $info): void + { + $log_output = ApplicationContext::isDebug(); + $maxlen = 0; + foreach ($info as $k => $v) { + $maxlen = max(strlen($k), $maxlen); + } + foreach ($info as $k => $v) { + if (is_string($v)) { + if ($v === '') { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow('""'), 4, $log_output); + } else { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v), 4, $log_output); + } + } elseif (is_array($v) && !is_assoc_array($v)) { + if ($v === []) { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow('[]'), 4, $log_output); + continue; + } + $first = array_shift($v); + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first), 4, $log_output); + foreach ($v as $vs) { + self::logError(str_pad('', $maxlen + 2) . ConsoleColor::yellow($vs), 4, $log_output); + } + } elseif (is_bool($v) || is_null($v)) { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::cyan($v === true ? 'true' : ($v === false ? 'false' : 'null')), 4, $log_output); + } else { + self::logError($k . ': ' . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow(json_encode($v, JSON_PRETTY_PRINT)), 4, $log_output); + } + } + } +} diff --git a/src/StaticPHP/Exception/ExecutionException.php b/src/StaticPHP/Exception/ExecutionException.php new file mode 100644 index 000000000..3fc4c67f4 --- /dev/null +++ b/src/StaticPHP/Exception/ExecutionException.php @@ -0,0 +1,58 @@ +cmd instanceof UnixShell || $this->cmd instanceof WindowsCmd) { + return $this->cmd->getLastCommand(); + } + return $this->cmd; + } + + /** + * Returns the directory in which the command was executed. + */ + public function getCd(): ?string + { + return $this->cd; + } + + /** + * Returns the environment variables that were set during the command execution. + */ + public function getEnv(): array + { + return $this->env; + } +} diff --git a/src/StaticPHP/Exception/FileSystemException.php b/src/StaticPHP/Exception/FileSystemException.php new file mode 100644 index 000000000..b4de97f94 --- /dev/null +++ b/src/StaticPHP/Exception/FileSystemException.php @@ -0,0 +1,7 @@ +patch_module; + } +} diff --git a/src/StaticPHP/Exception/RegistryException.php b/src/StaticPHP/Exception/RegistryException.php new file mode 100644 index 000000000..347a132ad --- /dev/null +++ b/src/StaticPHP/Exception/RegistryException.php @@ -0,0 +1,7 @@ +loadStackTraceInfo(); + } + + public function bindExtensionInfo(array $extension_info): void + { + $this->extension_info = $extension_info; + } + + public function addExtraLogFile(string $key, string $filename): void + { + $this->extra_log_files[$key] = $filename; + } + + /** + * Returns an array containing information about the SPC module. + * + * This method can be overridden by subclasses to provide specific module information. + * + * @return null|array{ + * library_name: string, + * library_class: string, + * os: string, + * file: null|string, + * line: null|int, + * } an array containing module information + */ + public function getLibraryInfo(): ?array + { + return $this->library_info; + } + + /** + * Returns an array containing information about the PHP build process. + * + * @return null|array{ + * builder_function: string, + * file: null|string, + * line: null|int, + * } an array containing PHP build information + */ + public function getBuildPHPInfo(): ?array + { + return $this->build_php_info; + } + + /** + * Returns an array containing information about the SPC extension. + * + * This method can be overridden by subclasses to provide specific extension information. + * + * @return null|array{ + * extension_name: string, + * extension_class: string, + * file: null|string, + * line: null|int, + * } an array containing extension information + */ + public function getExtensionInfo(): ?array + { + return $this->extension_info; + } + + public function getExtraLogFiles(): array + { + return $this->extra_log_files; + } + + private function loadStackTraceInfo(): void + { + $trace = $this->getTrace(); + foreach ($trace as $frame) { + if (!isset($frame['class'])) { + continue; + } + + // Check if the class is a subclass of LibraryBase + if (!$this->library_info && is_a($frame['class'], LibraryBase::class, true)) { + try { + $reflection = new \ReflectionClass($frame['class']); + if ($reflection->hasConstant('NAME')) { + $name = $reflection->getConstant('NAME'); + if ($name !== 'unknown') { + $this->library_info = [ + 'library_name' => $name, + 'library_class' => $frame['class'], + 'os' => match (true) { + is_a($frame['class'], BSDLibraryBase::class, true) => 'BSD', + is_a($frame['class'], LinuxLibraryBase::class, true) => 'Linux', + is_a($frame['class'], MacOSLibraryBase::class, true) => 'macOS', + is_a($frame['class'], WindowsLibraryBase::class, true) => 'Windows', + default => 'Unknown', + }, + 'file' => $frame['file'] ?? null, + 'line' => $frame['line'] ?? null, + ]; + continue; + } + } + } catch (\ReflectionException) { + continue; + } + } + + // Check if the class is a subclass of BuilderBase and the method is buildPHP + if (!$this->build_php_info && is_a($frame['class'], BuilderBase::class, true)) { + $this->build_php_info = [ + 'builder_function' => $frame['function'], + 'file' => $frame['file'] ?? null, + 'line' => $frame['line'] ?? null, + ]; + } + } + } +} diff --git a/src/StaticPHP/Exception/SPCInternalException.php b/src/StaticPHP/Exception/SPCInternalException.php new file mode 100644 index 000000000..bb468920a --- /dev/null +++ b/src/StaticPHP/Exception/SPCInternalException.php @@ -0,0 +1,12 @@ +getTrace() as $trace) { + // Extension validate() => "Extension validator" + if (is_a($trace['class'] ?? null, Extension::class, true) && $trace['function'] === 'validate') { + $this->validation_module = 'Extension validator'; + break; + } + + // Other => "ClassName::functionName" + $this->validation_module = [ + 'class' => $trace['class'] ?? null, + 'function' => $trace['function'], + ]; + break; + } + } else { + $this->validation_module = $validation_module; + } + } + + /** + * Returns the validation module string. + */ + public function getValidationModuleString(): string + { + if ($this->validation_module === null) { + return 'Unknown'; + } + if (is_string($this->validation_module)) { + return $this->validation_module; + } + $str = $this->validation_module['class'] ?? null; + if ($str !== null) { + $str .= '::'; + } + return ($str ?? '') . $this->validation_module['function']; + } +} diff --git a/src/StaticPHP/Exception/WrongUsageException.php b/src/StaticPHP/Exception/WrongUsageException.php new file mode 100644 index 000000000..2044a82c0 --- /dev/null +++ b/src/StaticPHP/Exception/WrongUsageException.php @@ -0,0 +1,13 @@ +getName(), 'static-libs', []) as $lib) { + $path = FileSystem::isRelativePath($lib) ? "{$this->getLibDir()}/{$lib}" : $lib; + if (!file_exists($path)) { + return false; + } + } + foreach (PackageConfig::get($this->getName(), 'headers', []) as $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 (!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) { + $path = FileSystem::isRelativePath($bin) ? "{$this->getBinDir()}/{$bin}" : $bin; + if (!file_exists($path)) { + return false; + } + } + return true; + } + + public function patchLaDependencyPrefix(?array $files = null): void + { + logger()->info("Patching library {$this->name} la files"); + $throwOnMissing = true; + if ($files === null) { + $files = PackageConfig::get($this->getName(), 'static-libs', []); + $files = array_map(fn ($name) => str_replace('.a', '.la', $name), $files); + $throwOnMissing = false; + } + foreach ($files as $name) { + $realpath = realpath(BUILD_LIB_PATH . '/' . $name); + if ($realpath === false) { + if ($throwOnMissing) { + throw new PatchException('la dependency patcher', "Cannot find library [{$this->name}] la file [{$name}] !"); + } + logger()->warning(message: 'Cannot find library [' . $this->name . '] la file [' . $name . '] !'); + continue; + } + logger()->debug('Patching ' . $realpath); + // replace prefix + $file = FileSystem::readFile($realpath); + $file = str_replace( + ' /lib/', + ' ' . BUILD_LIB_PATH . '/', + $file + ); + $file = preg_replace('/^libdir=.*$/m', "libdir='" . BUILD_LIB_PATH . "'", $file); + FileSystem::writeFile($realpath, $file); + } + } + + /** + * Get extra CFLAGS for current package. + * You need to define the environment variable in the format of {LIBRARY_NAME}_CFLAGS + * where {LIBRARY_NAME} is the snake_case name of the library. + * For example, for libjpeg, the environment variable should be libjpeg_CFLAGS. + */ + public function getLibExtraCFlags(): string + { + // get environment variable + $env = getenv($this->getSnakeCaseName() . '_CFLAGS') ?: ''; + // get default c flags + $arch_c_flags = getenv('SPC_DEFAULT_C_FLAGS') ?: ''; + if (!empty(getenv('SPC_DEFAULT_C_FLAGS')) && !str_contains($env, $arch_c_flags)) { + $env .= ' ' . $arch_c_flags; + } + return trim($env); + } + + /** + * Get extra CXXFLAGS for current package. + * You need to define the environment variable in the format of {LIBRARY_NAME}_CXXFLAGS + * where {LIBRARY_NAME} is the snake_case name of the library. + * For example, for libjpeg, the environment variable should be libjpeg_CXXFLAGS. + */ + public function getLibExtraCxxFlags(): string + { + // get environment variable + $env = getenv($this->getSnakeCaseName() . '_CXXFLAGS') ?: ''; + // get default cxx flags + $arch_cxx_flags = getenv('SPC_DEFAULT_CXX_FLAGS') ?: ''; + if (!empty(getenv('SPC_DEFAULT_CXX_FLAGS')) && !str_contains($env, $arch_cxx_flags)) { + $env .= ' ' . $arch_cxx_flags; + } + return trim($env); + } + + /** + * Get extra LDFLAGS for current package. + * You need to define the environment variable in the format of {LIBRARY_NAME}_LDFLAGS + * where {LIBRARY_NAME} is the snake_case name of the library. + * For example, for libjpeg, the environment variable should be libjpeg_LDFLAGS. + */ + public function getLibExtraLdFlags(): string + { + // get environment variable + $env = getenv($this->getSnakeCaseName() . '_LDFLAGS') ?: ''; + // get default ld flags + $arch_ld_flags = getenv('SPC_DEFAULT_LD_FLAGS') ?: ''; + if (!empty(getenv('SPC_DEFAULT_LD_FLAGS')) && !str_contains($env, $arch_ld_flags)) { + $env .= ' ' . $arch_ld_flags; + } + 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 + * where {LIBRARY_NAME} is the snake_case name of the library. + * For example, for libjpeg, the environment variable should be libjpeg_LIBS. + */ + public function getLibExtraLibs(): string + { + return getenv($this->getSnakeCaseName() . '_LIBS') ?: ''; + } + + /** + * Get the build root path for the package. + * + * TODO: Can be changed to support per-package build root path in the future. + */ + public function getBuildRootPath(): string + { + return BUILD_ROOT_PATH; + } + + /** + * Get the include directory for the package. + * + * TODO: Can be changed to support per-package include directory in the future. + */ + public function getIncludeDir(): string + { + return BUILD_INCLUDE_PATH; + } + + /** + * Get the library directory for the package. + * + * TODO: Can be changed to support per-package library directory in the future. + */ + public function getLibDir(): string + { + return BUILD_LIB_PATH; + } + + public function getBinDir(): string + { + return BUILD_BIN_PATH; + } +} diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php new file mode 100644 index 000000000..aa8ab6f00 --- /dev/null +++ b/src/StaticPHP/Package/Package.php @@ -0,0 +1,215 @@ + $stages Defined stages for the package + */ + protected array $stages = []; + + /** @var array $build_functions Build functions for different OS binding */ + protected array $build_functions = []; + + /** @var array */ + protected array $outputs = []; + + /** + * @param string $name Name of the package + * @param string $type Type of the package + */ + public function __construct(public readonly string $name, public readonly string $type) {} + + /** + * Run a defined stage of the package. + * If the stage is not defined, an exception should be thrown. + * + * @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(mixed $name, array $context = []): mixed + { + 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 */ + $stageContext = array_merge([ + Package::class => $this, + static::class => $this, + ], $context); + + // emit BeforeStage + $this->emitBeforeStage($name, $stageContext); + + $ret = ApplicationContext::invoke($this->stages[$name], $stageContext); + // emit AfterStage + $this->emitAfterStage($name, $stageContext, $ret); + 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. + return false; + } + + /** + * Add a stage to the package. + */ + public function addStage(string $name, callable $stage): void + { + $this->stages[$name] = $stage; + } + + /** + * Check if the package has a specific stage defined. + * + * @param mixed $name Stage name + */ + public function hasStage(mixed $name): bool + { + 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; + } + + /** + * Check if the package has a build function for the current OS. + */ + public function hasBuildFunctionForCurrentOS(): bool + { + return isset($this->build_functions[PHP_OS_FAMILY]); + } + + /** + * Get the name of the package. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get the type of the package. + */ + public function getType(): string + { + return $this->type; + } + + /** + * Get the artifact associated with the package, or null if none is defined. + * + * @return null|Artifact Artifact instance or null + */ + public function getArtifact(): ?Artifact + { + // find config + $artifact_name = PackageConfig::get($this->name, 'artifact'); + return $artifact_name !== null ? ArtifactLoader::getArtifactInstance($artifact_name) : null; + } + + /** + * Check if the artifact has source available. + */ + public function hasSource(): bool + { + return $this->getArtifact()?->hasSource() ?? false; + } + + /** + * Get source directory of the package. + * If the source artifact is not available, an exception will be thrown. + */ + public function getSourceDir(): string + { + if (($artifact = $this->getArtifact()) && $artifact->hasSource()) { + return $artifact->getSourceDir(); + } + throw new SPCInternalException("Source directory for package {$this->name} is not available because the source artifact is missing."); + } + + /** + * Check if the package has a binary available for current OS and architecture. + */ + public function hasLocalBinary(): bool + { + return $this->getArtifact()?->hasPlatformBinary() ?? false; + } + + /** + * Get the snake_case name of the package. + */ + protected function getSnakeCaseName(): string + { + return str_replace('-', '_', $this->name); + } + + private function emitBeforeStage(string $stage, array $stageContext): void + { + foreach (PackageLoader::getBeforeStageCallbacks($this->getName(), $stage) as $callback) { + ApplicationContext::invoke($callback, $stageContext); + } + } + + private function emitAfterStage(string $stage, array $stageContext, mixed $return_value): void + { + foreach (PackageLoader::getAfterStageCallbacks($this->getName(), $stage) as $callback) { + ApplicationContext::invoke($callback, array_merge($stageContext, ['return_value' => $return_value])); + } + } +} diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php new file mode 100644 index 000000000..94c55a66e --- /dev/null +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -0,0 +1,193 @@ +concurrency = (int) getenv('SPC_CONCURRENCY') ?: 1; + } + + public function buildPackage(Package $package, bool $force = false): int + { + // init build dirs + if (!$package instanceof LibraryPackage) { + throw new SPCInternalException('Please, never try to build non-library packages directly.'); + } + FileSystem::createDir($package->getBuildRootPath()); + FileSystem::createDir($package->getIncludeDir()); + FileSystem::createDir($package->getBinDir()); + FileSystem::createDir($package->getLibDir()); + + if (!$package->hasStage('build')) { + throw new WrongUsageException("Package '{$package->name}' does not have a current platform 'build' stage defined."); + } + + // validate package should be built + if (!$force) { + return $package->isInstalled() ? SPC_STATUS_ALREADY_BUILT : $this->buildPackage($package, true); + } + // check source is ready + if ($package->getType() !== 'virtual-target' && !is_dir($package->getSourceDir())) { + throw new WrongUsageException("Source directory for package '{$package->name}' does not exist. Please fetch the source before building."); + } + Shell::passthruCallback(function () { + InteractiveTerm::advance(); + }); + + if ($package->getType() !== 'virtual-target') { + // patch before build + $package->patchBeforeBuild(); + } + + // build + $package->runStage('build'); + + if ($package->getType() !== 'virtual-target') { + // install license + if (($license = PackageConfig::get($package->getName(), 'license')) !== null) { + $this->installLicense($package, $license); + } + } + return SPC_STATUS_BUILT; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getOption(string $key, mixed $default = null): mixed + { + 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)) { + FileSystem::copy($src, $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') && SystemTarget::isUnix()) { + $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}"); + } elseif ($upx_option && SystemTarget::getTargetOS() === 'Windows' && $executable) { + logger()->info("Compressing {$dst} with UPX"); + shell()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($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'; + $basename = basename($binary_path); + $debug_file = "{$target_dir}/{$basename}" . (SystemTarget::getTargetOS() === 'Darwin' ? '.dwarf' : '.debug'); + if (SystemTarget::getTargetOS() === 'Darwin') { + FileSystem::createDir($target_dir); + shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}"); + } elseif (SystemTarget::getTargetOS() === 'Linux') { + FileSystem::createDir($target_dir); + 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 { + logger()->debug('extractDebugInfo is only supported on Linux and macOS'); + return ''; + } + 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(); + FileSystem::createDir($dir); + if (is_assoc_array($license)) { + $license = [$license]; + } + + foreach ($license as $index => $item) { + if ($item['type'] === 'text') { + FileSystem::writeFile("{$dir}/{$index}.txt", $item['text']); + } elseif ($item['type'] === 'file') { + FileSystem::copy("{$package->getSourceDir()}/{$item['path']}", "{$dir}/{$index}.txt"); + } + } + } +} diff --git a/src/StaticPHP/Package/PackageCallbacksTrait.php b/src/StaticPHP/Package/PackageCallbacksTrait.php new file mode 100644 index 000000000..96ad039ee --- /dev/null +++ b/src/StaticPHP/Package/PackageCallbacksTrait.php @@ -0,0 +1,89 @@ +info_callback = $callback; + } + + /** + * Get package info by invoking the info callback. + * + * @return array Package information + */ + public function getPackageInfo(): array + { + if ($this->info_callback === null) { + return []; + } + + // Use CallbackInvoker with current package as context + $result = ApplicationContext::invoke($this->info_callback, [ + Package::class => $this, + static::class => $this, + ]); + + return is_array($result) ? $result : []; + } + + public function setValidateCallback(callable $callback): void + { + $this->validate_callback = $callback; + } + + public function setPatchBeforeBuildCallback(callable $callback): void + { + $this->patch_before_build_callback = $callback; + } + + public function patchBeforeBuild(): void + { + if (file_exists("{$this->getSourceDir()}/.spc-patched")) { + return; + } + if ($this->patch_before_build_callback === null) { + return; + } + // Use CallbackInvoker with current package as context + $ret = ApplicationContext::invoke($this->patch_before_build_callback, [ + Package::class => $this, + static::class => $this, + ]); + if ($ret === true) { + FileSystem::writeFile("{$this->getSourceDir()}/.spc-patched", 'PATCHED!!!'); + } + } + + /** + * Validate the package by invoking the validate callback. + */ + public function validatePackage(): void + { + if ($this->validate_callback === null) { + return; + } + + // Use CallbackInvoker with current package as context + ApplicationContext::invoke($this->validate_callback, [ + Package::class => $this, + static::class => $this, + ]); + } +} diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php new file mode 100644 index 000000000..4c44ce920 --- /dev/null +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -0,0 +1,602 @@ + Resolved package list */ + protected array $packages = []; + + /** @var array Packages to be built from source */ + protected array $build_packages = []; + + /** @var array Packages to be installed */ + protected array $install_packages = []; + + /** @var array> Unresolved target additional dependencies defined in #[ResolveBuild] */ + protected array $target_additional_dependencies = []; + + /** @var bool Whether to download missing sources automatically */ + protected bool $download = true; + + public function __construct(protected array $options = []) + { + ApplicationContext::set(PackageInstaller::class, $this); + $builder = new PackageBuilder($options); + ApplicationContext::set(PackageBuilder::class, $builder); + ApplicationContext::set('patch_point', ''); + + // Check for no-download option + if (!empty($options['no-download'])) { + $this->download = false; + } + } + + /** + * Add a package to the build list. + * This means the package will be built from source. + */ + public function addBuildPackage(LibraryPackage|string|TargetPackage $package): static + { + if (is_string($package)) { + $package = PackageLoader::getPackage($package); + } + // special check for php target packages + if (in_array($package->getName(), ['php', 'php-cli', 'php-fpm', 'php-micro', 'php-cgi', 'php-embed', 'frankenphp'], true)) { + $this->handlePhpTargetPackage($package); + return $this; + } + if (!$package->hasStage('build')) { + throw new WrongUsageException("Target package '{$package->getName()}' does not define build process for current OS: " . PHP_OS_FAMILY . '.'); + } + $this->build_packages[$package->getName()] = $package; + return $this; + } + + /** + * @param string $name Package name + * @return null|Package The build package instance or null if not found + */ + public function getBuildPackage(string $name): ?Package + { + return $this->build_packages[$name] ?? null; + } + + /** + * Add a package to the installation list. + * This means the package will try to install binary artifacts first. + * If no artifacts found, it will fallback to build from source. + */ + public function addInstallPackage(LibraryPackage|string $package): static + { + if (is_string($package)) { + $package = PackageLoader::getPackage($package); + } + $this->install_packages[$package->getName()] = $package; + return $this; + } + + /** + * Set whether to download packages before installation. + */ + public function setDownload(bool $download = true): static + { + $this->download = $download; + 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 + { + // apply build toolchain envs + GlobalEnvManager::afterInit(); + + 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 + $this->printInstallerInfo(); + + InteractiveTerm::notice('Build process will start after 2s ...'); + sleep(2); + echo PHP_EOL; + } + + // Early validation: check if packages can be built or installed before downloading + $this->validatePackagesBeforeBuild(); + + // check download + if ($this->download) { + $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl'); + $downloader = new ArtifactDownloader([...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))]); + $downloader->addArtifacts($this->getArtifacts())->download($interactive); + } else { + logger()->notice('Skipping download (--no-download option enabled)'); + } + + // extract sources + $this->extractSourceArtifacts(interactive: $interactive); + + // validate packages + foreach ($this->packages as $package) { + // 1. call validate package + $package->validatePackage(); + } + + // build/install packages + if ($interactive) { + InteractiveTerm::notice('Building/Installing packages ...'); + keyboard_interrupt_register(function () { + InteractiveTerm::finish('Build/Install process interrupted by user!', false); + exit(130); + }); + } + $builder = ApplicationContext::get(PackageBuilder::class); + foreach ($this->packages as $package) { + $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())); + } + try { + $status = $this->installBinary($package); + } catch (\Throwable $e) { + if ($interactive) { + InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); + echo PHP_EOL; + } + throw $e; + } + if ($interactive) { + InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); + } + } 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)' : '')); + } + } + } + } + + public function isBuildPackage(Package|string $package): bool + { + return isset($this->build_packages[is_string($package) ? $package : $package->getName()]); + } + + /** + * Get all resolved packages. + * You can filter by package type class if needed. + * + * @template T + * @param class-string $package_type Filter by package type + * @return array + */ + public function getResolvedPackages(mixed $package_type = Package::class): array + { + return array_filter($this->packages, function (Package $pkg) use ($package_type): bool { + return $pkg instanceof $package_type; + }); + } + + 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. + * + * @return array artifact name => [source downloaded, binary downloaded] + */ + public function getArtifactDownloadStatus(): array + { + $download_status = []; + foreach ($this->getResolvedPackages() as $package) { + if (($artifact = $package->getArtifact()) !== null && !isset($download_status[$artifact->getName()])) { + // [0: source, 1: binary for current OS] + $download_status[$artifact->getName()] = [ + 'source-downloaded' => $artifact->isSourceDownloaded(), + 'binary-downloaded' => $artifact->isBinaryDownloaded(), + 'has-source' => $artifact->hasSource(), + 'has-binary' => $artifact->hasPlatformBinary(), + ]; + $download_status[$artifact->getName()] = [$artifact->isSourceDownloaded(), $artifact->isBinaryDownloaded()]; + } + } + return $download_status; + } + + /** + * Get all artifacts from resolved and build packages. + * + * @return Artifact[] + */ + public function getArtifacts(): array + { + $artifacts = []; + foreach ($this->getResolvedPackages() as $package) { + // Validate package artifacts + $this->validatePackageArtifact($package); + if (($artifact = $package->getArtifact()) !== null && !in_array($artifact, $artifacts, true)) { + $artifacts[] = $artifact; + } + } + // add target artifacts + foreach ($this->build_packages as $package) { + // Validate package artifacts + $this->validatePackageArtifact($package); + if (($artifact = $package->getArtifact()) !== null && !in_array($artifact, $artifacts, true)) { + $artifacts[] = $artifact; + } + } + return $artifacts; + } + + /** + * Extract all artifacts for resolved packages. + */ + public function extractSourceArtifacts(bool $interactive = true): void + { + $packages = array_values($this->packages); + + $cache = ApplicationContext::get(ArtifactCache::class); + $extractor = new ArtifactExtractor($cache); + + // Collect all unique artifacts + $artifacts = []; + $pkg_artifact_map = []; + foreach ($packages as $package) { + $artifact = $package->getArtifact(); + if ($artifact !== null && !isset($artifacts[$artifact->getName()]) && (!$artifact->shouldUseBinary() || $this->isBuildPackage($package))) { + $pkg_artifact_map[$package->getName()] = $artifact->getName(); + $artifacts[$artifact->getName()] = $artifact; + } + } + + // Sort: php-src should be extracted first (extensions depend on it) + uksort($artifacts, function (string $a, string $b): int { + if ($a === 'php-src') { + return -1; + } + if ($b === 'php-src') { + return 1; + } + return 0; + }); + + if (count($artifacts) === 0) { + return; + } + + // Extract each artifact + if ($interactive) { + InteractiveTerm::notice('Extracting source for ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . ' ...'); + InteractiveTerm::indicateProgress('Extracting artifacts'); + } + + try { + V2CompatLayer::beforeExtsExtractHook(); + foreach ($artifacts as $artifact) { + if ($interactive) { + InteractiveTerm::setMessage('Extracting source: ' . ConsoleColor::green($artifact->getName())); + } + if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) { + V2CompatLayer::beforeLibExtractHook($pkg); + } + $extractor->extract($artifact, true); + if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) { + V2CompatLayer::afterLibExtractHook($pkg); + } + } + V2CompatLayer::afterExtsExtractHook(); + if ($interactive) { + InteractiveTerm::finish('Extracted all sources successfully.'); + echo PHP_EOL; + } + } catch (\Throwable $e) { + if ($interactive) { + InteractiveTerm::finish('Artifact extraction failed!', false); + echo PHP_EOL; + } + throw $e; + } + } + + public function installBinary(Package $package): int + { + $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); + $artifact = $package->getArtifact(); + if ($artifact === null || !$artifact->shouldUseBinary()) { + throw new WrongUsageException("Package '{$package->getName()}' does not have a binary artifact to install."); + } + + $status = $extractor->extract($artifact); + if ($status === SPC_STATUS_ALREADY_EXTRACTED) { + return SPC_STATUS_ALREADY_INSTALLED; + } + + // perform package after-install actions + $this->performAfterInstallActions($package); + return SPC_STATUS_INSTALLED; + } + + public function getPackage(string $package_name): ?Package + { + return $this->packages[$package_name] ?? null; + } + + /** + * Validate that a package has required artifacts. + */ + private function validatePackageArtifact(Package $package): void + { + // target and library must have at least source or platform binary + if (in_array($package->getType(), ['library', 'target']) && !$package->getArtifact()?->hasSource() && !$package->getArtifact()?->hasPlatformBinary()) { + throw new WrongUsageException("Validation failed: Target package '{$package->getName()}' has no source or platform binary defined."); + } + } + + private function resolvePackages(): void + { + $pkgs = []; + + foreach ($this->build_packages as $package) { + // call #[ResolveBuild] annotation methods if defined + if ($package instanceof TargetPackage && is_array($deps = $package->_emitResolveBuild($this))) { + $this->target_additional_dependencies[$package->getName()] = $deps; + } + $pkgs[] = $package->getName(); + } + + // gather install packages + foreach ($this->install_packages as $package) { + $pkgs[] = $package->getName(); + } + + // resolve dependencies string + $resolved_packages = DependencyResolver::resolve( + $pkgs, + $this->target_additional_dependencies, + $this->options['with-suggests'] ?? false + ); + + foreach ($resolved_packages as $pkg_name) { + $this->packages[$pkg_name] = PackageLoader::getPackage($pkg_name); + } + } + + private function handlePhpTargetPackage(TargetPackage $package): void + { + // process 'php' target + if ($package->getName() === 'php') { + logger()->warning("Building 'php' target is deprecated, please use specific targets like 'build:php-cli' instead."); + + $added = false; + + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cli')) { + $cli = PackageLoader::getPackage('php-cli'); + $this->install_packages[$cli->getName()] = $cli; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-fpm')) { + $fpm = PackageLoader::getPackage('php-fpm'); + $this->install_packages[$fpm->getName()] = $fpm; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-micro')) { + $micro = PackageLoader::getPackage('php-micro'); + $this->install_packages[$micro->getName()] = $micro; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cgi')) { + $cgi = PackageLoader::getPackage('php-cgi'); + $this->install_packages[$cgi->getName()] = $cgi; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-embed')) { + $embed = PackageLoader::getPackage('php-embed'); + $this->install_packages[$embed->getName()] = $embed; + $added = true; + } + if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) { + $frankenphp = PackageLoader::getPackage('frankenphp'); + $this->install_packages[$frankenphp->getName()] = $frankenphp; + $added = true; + } + $this->build_packages[$package->getName()] = $package; + + if (!$added) { + throw new WrongUsageException( + "No SAPI target specified to build. Please use '--build-cli', '--build-fpm', '--build-micro', " . + "'--build-cgi', '--build-embed', '--build-frankenphp' or '--build-all' options." + ); + } + } else { + // process specific php sapi targets + $this->build_packages['php'] = PackageLoader::getPackage('php'); + $this->install_packages[$package->getName()] = $package; + } + } + + private function printInstallerInfo(): void + { + InteractiveTerm::notice('Installation summary:'); + $summary['Packages to be built'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->build_packages))); + $summary['Packages to be installed'] = implode(',', array_map(fn ($x) => $x->getName(), array_values($this->packages))); + $summary['Artifacts to be downloaded'] = implode(',', array_map(fn ($x) => $x->getName(), $this->getArtifacts())); + $this->printArrayInfo(array_filter($summary)); + echo PHP_EOL; + + foreach ($this->build_packages as $package) { + $info = $package->getPackageInfo(); + if ($info === []) { + continue; + } + InteractiveTerm::notice("{$package->getName()} build options:"); + // calculate space count for every line + $this->printArrayInfo($info); + echo PHP_EOL; + } + } + + private function printArrayInfo(array $info): void + { + $maxlen = 0; + foreach ($info as $k => $v) { + $maxlen = max(strlen($k), $maxlen); + } + foreach ($info as $k => $v) { + if (is_string($v)) { + InteractiveTerm::plain(" {$k}: " . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($v)); + } elseif (is_array($v) && !is_assoc_array($v)) { + $first = array_shift($v); + InteractiveTerm::plain(" {$k}: " . str_pad('', $maxlen - strlen($k)) . ConsoleColor::yellow($first)); + foreach ($v as $vs) { + InteractiveTerm::plain(str_pad('', $maxlen + 4) . ConsoleColor::yellow($vs)); + } + } + } + } + + private function validatePackagesBeforeBuild(): void + { + foreach ($this->packages as $package) { + if ($package->getType() !== 'library') { + continue; + } + $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); + + // Check if package can neither be built nor installed + if (!$is_to_build && !$should_use_binary && !$has_build_stage) { + throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS: " . SystemTarget::getCurrentPlatformString()); + } + } + } + + private function performAfterInstallActions(Package $package): void + { + // ----------- perform post-install actions from extracted .package.{pkg_name}.postinstall.json ----------- + $root_dir = ($package->getArtifact()?->getBinaryDir() ?? '') !== '' ? $package->getArtifact()?->getBinaryDir() : null; + if ($root_dir !== null) { + $action_json = "{$root_dir}/.package.{$package->getName()}.postinstall.json"; + if (is_file($action_json)) { + $action_json = json_decode(file_get_contents($action_json), true); + if (!is_array($action_json)) { + throw new WrongUsageException("Invalid post-install action JSON format for package '{$package->getName()}'."); + } + $placeholders = get_pack_replace(); + foreach ($action_json as $action) { + $action_name = $action['action'] ?? ''; + switch ($action_name) { + // replace-path: => files: [relative_path1, relative_path2] + case 'replace-path': + $files = $action['files'] ?? []; + foreach ($files as $file) { + $filepath = $root_dir . "/{$file}"; + FileSystem::replaceFileStr($filepath, array_values($placeholders), array_keys($placeholders)); + } + break; + // replace-to-env: => file: "relative_path", search: "SEARCH_STR", replace-env: "ENV_VAR_NAME" + case 'replace-to-env': + $file = $action['file'] ?? ''; + $search = $action['search'] ?? ''; + $env_var = $action['replace-env'] ?? ''; + $replace = getenv($env_var) ?: ''; + $filepath = $root_dir . "/{$file}"; + FileSystem::replaceFileStr($filepath, $search, $replace); + break; + default: + throw new WrongUsageException("Unknown post-install action '{$action_name}' for package '{$package->getName()}'."); + } + } + // remove the action file after processing + unlink($root_dir . "/.package.{$package->getName()}.postinstall.json"); + } + } + } +} diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php new file mode 100644 index 000000000..84aa3020d --- /dev/null +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -0,0 +1,278 @@ + Callbacks for custom PHP configure arguments per OS + */ + protected array $custom_php_configure_arg_callbacks = []; + + protected bool $build_shared = false; + + protected bool $build_static = false; + + protected bool $build_with_php = false; + + /** + * @param string $name Name of the php extension + * @param string $type Type of the package, defaults to 'php-extension' + */ + public function __construct(string $name, string $type = 'php-extension', protected array $extension_config = []) + { + // Ensure the package name starts with 'ext-' + if (!str_starts_with($name, 'ext-')) { + $name = "ext-{$name}"; + } + if ($this->extension_config === []) { + $this->extension_config = PackageConfig::get($name, 'php-extension', []); + } + 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 === '') { + foreach (['Linux', 'Windows', 'Darwin'] as $supported_os) { + $this->custom_php_configure_arg_callbacks[$supported_os] = $fn; + } + } else { + $this->custom_php_configure_arg_callbacks[$os] = $fn; + } + } + + public function getPhpConfigureArg(string $os, bool $shared): string + { + if (isset($this->custom_php_configure_arg_callbacks[$os])) { + $callback = $this->custom_php_configure_arg_callbacks[$os]; + 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('_', '-', $this->getExtensionName()); + $ext_config = PackageConfig::get($name, 'php-extension', []); + + $arg_type = match (SystemTarget::getTargetOS()) { + 'Windows' => $ext_config['arg-type@windows'] ?? $ext_config['arg-type'] ?? 'enable', + 'Darwin' => $ext_config['arg-type@macos'] ?? $ext_config['arg-type@unix'] ?? $ext_config['arg-type'] ?? 'enable', + 'Linux' => $ext_config['arg-type@linux'] ?? $ext_config['arg-type@unix'] ?? $ext_config['arg-type'] ?? 'enable', + default => $ext_config['arg-type'] ?? 'enable', + }; + + return match ($arg_type) { + 'enable' => $shared ? "--enable-{$name}=shared" : "--enable-{$name}", + 'enable-path' => $shared ? "--enable-{$name}=shared,{$escapedPath}" : "--enable-{$name}={$escapedPath}", + 'with' => $shared ? "--with-{$name}=shared" : "--with-{$name}", + 'with-path' => $shared ? "--with-{$name}=shared,{$escapedPath}" : "--with-{$name}={$escapedPath}", + default => throw new WrongUsageException("Unknown argument type '{$arg_type}' for PHP extension '{$name}'"), + }; + } + + public function setBuildShared(bool $build_shared = true): void + { + $this->build_shared = $build_shared; + } + + public function setBuildStatic(bool $build_static = true): void + { + $this->build_static = $build_static; + } + + public function setBuildWithPhp(bool $build_with_php = true): void + { + $this->build_with_php = $build_with_php; + } + + public function isBuildShared(): bool + { + return $this->build_shared; + } + + public function isBuildStatic(): bool + { + return $this->build_static; + } + + public function isBuildWithPhp(): bool + { + 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)]; + } +} diff --git a/src/StaticPHP/Package/TargetPackage.php b/src/StaticPHP/Package/TargetPackage.php new file mode 100644 index 000000000..d29cf9238 --- /dev/null +++ b/src/StaticPHP/Package/TargetPackage.php @@ -0,0 +1,142 @@ + $build_options Build options for the target package + */ + protected array $build_options = []; + + protected array $build_arguments = []; + + protected mixed $resolve_build_callback = null; + + /** + * Checks if the target is virtual. + */ + public function isVirtualTarget(): bool + { + return $this->type === 'virtual-target'; + } + + /** + * Adds a build option to the target package. + * + * @param string $name The name of the build option + * @param null|string $shortcut The shortcut for the build option + * @param null|int $mode The mode of the build option + * @param string $description The description of the build option + * @param null|mixed $default The default value of the build option + */ + public function addBuildOption(string $name, ?string $shortcut = null, ?int $mode = InputOption::VALUE_NONE, string $description = '', mixed $default = null): void + { + $this->build_options[$name] = new InputOption($name, $shortcut, $mode, $description, $default); + } + + /** + * Adds a build argument to the target package. + * + * @param string $name The name of the build argument + * @param null|int $mode The mode of the build argument + * @param string $description The description of the build argument + * @param null|mixed $default The default value of the build argument + */ + public function addBuildArgument(string $name, ?int $mode = null, string $description = '', mixed $default = null): void + { + $this->build_arguments[$name] = new InputArgument($name, $mode, $description, $default); + } + + public function setResolveBuildCallback(callable $callback): static + { + $this->resolve_build_callback = $callback; + return $this; + } + + /** + * Get a build option value for the target package. + * + * @param string $key The build option key + * @param null|mixed $default The default value if the option is not set + * @return mixed The value of the build option + */ + public function getBuildOption(string $key, mixed $default = null): mixed + { + $input = ApplicationContext::has(InputInterface::class) + ? ApplicationContext::get(InputInterface::class) + : null; + + if ($input !== null && $input->hasOption($key)) { + return $input->getOption($key); + } + return $default; + } + + /** + * Get a build argument value for the target package. + * + * @param string $key The build argument key + * @return mixed The value of the build argument + */ + public function getBuildArgument(string $key): mixed + { + $input = ApplicationContext::has(InputInterface::class) + ? ApplicationContext::get(InputInterface::class) + : null; + + if ($input !== null && $input->hasArgument($key)) { + return $input->getArgument($key); + } + return null; + } + + /** + * Gets all build options for the target package. + * + * @internal + * @return InputOption[] Get all build options for the target package + */ + public function _exportBuildOptions(): array + { + return $this->build_options; + } + + /** + * Gets all build arguments for the target package. + * + * @internal + * @return InputArgument[] Get all build arguments for the target package + */ + public function _exportBuildArguments(): array + { + return $this->build_arguments; + } + + /** + * Run the init build callback to prepare its dependencies. + * + * @internal + */ + public function _emitResolveBuild(PackageInstaller $installer): mixed + { + if (!is_callable($this->resolve_build_callback)) { + return null; + } + + return ApplicationContext::invoke($this->resolve_build_callback, [ + TargetPackage::class => $this, + PackageInstaller::class => $installer, + ]); + } +} diff --git a/src/StaticPHP/Registry/ArtifactLoader.php b/src/StaticPHP/Registry/ArtifactLoader.php new file mode 100644 index 000000000..22942452f --- /dev/null +++ b/src/StaticPHP/Registry/ArtifactLoader.php @@ -0,0 +1,191 @@ + Artifact instances */ + private static ?array $artifacts = null; + + public static function initArtifactInstances(): void + { + if (self::$artifacts !== null) { + return; + } + foreach (ArtifactConfig::getAll() as $name => $item) { + $artifact = new Artifact($name, $item); + self::$artifacts[$name] = $artifact; + } + } + + public static function getArtifactInstance(string $artifact_name): ?Artifact + { + self::initArtifactInstances(); + return self::$artifacts[$artifact_name] ?? null; + } + + /** + * Load artifact definitions from PSR-4 directory. + * + * @param string $dir Directory path + * @param string $base_namespace Base namespace for dir's PSR-4 mapping + * @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload) + */ + public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void + { + self::initArtifactInstances(); + $classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require); + foreach ($classes as $class) { + self::loadFromClass($class); + } + } + + public static function loadFromClass(string $class): void + { + $ref = new \ReflectionClass($class); + + $class_instance = $ref->newInstance(); + + foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + self::processCustomSourceAttribute($ref, $method, $class_instance); + self::processCustomBinaryAttribute($ref, $method, $class_instance); + self::processSourceExtractAttribute($ref, $method, $class_instance); + self::processBinaryExtractAttribute($ref, $method, $class_instance); + self::processAfterSourceExtractAttribute($ref, $method, $class_instance); + self::processAfterBinaryExtractAttribute($ref, $method, $class_instance); + } + } + + /** + * Process #[CustomSource] attribute. + */ + private static function processCustomSourceAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomSource::class); + foreach ($attributes as $attribute) { + /** @var CustomSource $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setCustomSourceCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomSource] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[CustomBinary] attribute. + */ + private static function processCustomBinaryAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomBinary::class); + foreach ($attributes as $attribute) { + /** @var CustomBinary $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + foreach ($instance->support_os as $os) { + self::$artifacts[$artifact_name]->setCustomBinaryCallback($os, [$class_instance, $method->getName()]); + } + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomBinary] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[SourceExtract] attribute. + * This attribute allows completely taking over the source extraction process. + */ + private static function processSourceExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(SourceExtract::class); + foreach ($attributes as $attribute) { + /** @var SourceExtract $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setSourceExtractCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[SourceExtract] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[BinaryExtract] attribute. + * This attribute allows completely taking over the binary extraction process. + */ + private static function processBinaryExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(BinaryExtract::class); + foreach ($attributes as $attribute) { + /** @var BinaryExtract $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setBinaryExtractCallback( + [$class_instance, $method->getName()], + $instance->platforms + ); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[BinaryExtract] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[AfterSourceExtract] attribute. + * This attribute registers a hook that runs after source extraction completes. + */ + private static function processAfterSourceExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(AfterSourceExtract::class); + foreach ($attributes as $attribute) { + /** @var AfterSourceExtract $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->addAfterSourceExtractCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[AfterSourceExtract] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + + /** + * Process #[AfterBinaryExtract] attribute. + * This attribute registers a hook that runs after binary extraction completes. + */ + private static function processAfterBinaryExtractAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(AfterBinaryExtract::class); + foreach ($attributes as $attribute) { + /** @var AfterBinaryExtract $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->addAfterBinaryExtractCallback( + [$class_instance, $method->getName()], + $instance->platforms + ); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[AfterBinaryExtract] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } +} diff --git a/src/StaticPHP/Registry/DoctorLoader.php b/src/StaticPHP/Registry/DoctorLoader.php new file mode 100644 index 000000000..e992d5562 --- /dev/null +++ b/src/StaticPHP/Registry/DoctorLoader.php @@ -0,0 +1,123 @@ + $doctor_items Loaded doctor check item instances + */ + private static array $doctor_items = []; + + /** + * @var array $fix_items loaded doctor fix item instances + */ + private static array $fix_items = []; + + /** + * Load doctor check items from PSR-4 directory. + * + * @param string $dir Directory path + * @param string $base_namespace Base namespace for dir's PSR-4 mapping + * @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload) + */ + public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void + { + $classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require); + foreach ($classes as $class) { + self::loadFromClass($class, false); + } + + // sort check items by level + usort(self::$doctor_items, function ($a, $b) { + return $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1); + }); + } + + /** + * Load doctor check items from a class. + * + * @param string $class Class name to load doctor check items from + * @param bool $sort Whether to re-sort Doctor items (default: true) + */ + public static function loadFromClass(string $class, bool $sort = true): void + { + // passthough to all the functions if #[OptionalCheck] is set on class level + $optional_passthrough = null; + $reflection = new \ReflectionClass($class); + $class_instance = $reflection->newInstance(); + // parse #[OptionalCheck] + $optional = $reflection->getAttributes(OptionalCheck::class)[0] ?? null; + if ($optional !== null) { + /** @var OptionalCheck $instance */ + $instance = $optional->newInstance(); + if (is_callable($instance->check)) { + $optional_passthrough = $instance->check; + } + } + + $doctor_items = []; + $fix_item_map = []; + + // finx check items and fix items from methods in class + foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { + // passthrough for this method if #[OptionalCheck] is set on method level + $optional = $optional_passthrough ?? null; + foreach ($method->getAttributes(OptionalCheck::class) as $method_attr) { + $optional_check = $method_attr->newInstance(); + if (is_callable($optional_check->check)) { + $optional = $optional_check->check; + } + } + + // parse #[CheckItem] + foreach ($method->getAttributes(CheckItem::class) as $attr) { + /** @var CheckItem $instance */ + $instance = $attr->newInstance(); + $instance->callback = [$class_instance, $method->getName()]; + // put CheckItem instance and optional check callback (or null) to $doctor_items + $doctor_items[] = [$instance, $optional]; + } + + // parse #[FixItem] + $fix_item = $method->getAttributes(FixItem::class)[0] ?? null; + if ($fix_item !== null) { + $instance = $fix_item->newInstance(); + $fix_item_map[$instance->name] = [$class_instance, $method->getName()]; + } + } + + // add to array + self::$doctor_items = array_merge(self::$doctor_items, $doctor_items); + self::$fix_items = array_merge(self::$fix_items, $fix_item_map); + + if ($sort) { + // sort check items by level + usort(self::$doctor_items, function ($a, $b) { + return $a[0]->level > $b[0]->level ? -1 : ($a[0]->level == $b[0]->level ? 0 : 1); + }); + } + } + + /** + * Returns loaded doctor check items. + * + * @return array + */ + public static function getDoctorItems(): array + { + return self::$doctor_items; + } + + public static function getFixItem(string $name): ?callable + { + return self::$fix_items[$name] ?? null; + } +} diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php new file mode 100644 index 000000000..29ce0a96b --- /dev/null +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -0,0 +1,374 @@ + */ + private static ?array $packages = null; + + private static array $before_stages = []; + + private static array $after_stages = []; + + /** @var array Track loaded classes to prevent duplicates */ + private static array $loaded_classes = []; + + public static function initPackageInstances(): void + { + if (self::$packages !== null) { + return; + } + // init packages instance from config + foreach (PackageConfig::getAll() as $name => $item) { + $pkg = match ($item['type']) { + 'target', 'virtual-target' => new TargetPackage($name, $item['type']), + 'library' => new LibraryPackage($name, $item['type']), + 'php-extension' => new PhpExtensionPackage($name, $item['type']), + default => null, + }; + if ($pkg !== null) { + self::$packages[$name] = $pkg; + } else { + throw new RegistryException("Package [{$name}] has unknown type [{$item['type']}]"); + } + } + } + + /** + * Load package definitions from PSR-4 directory. + * + * @param string $dir Directory path + * @param string $base_namespace Base namespace for dir's PSR-4 mapping + * @param bool $auto_require Whether to auto-require PHP files (for external plugins not in autoload) + */ + public static function loadFromPsr4Dir(string $dir, string $base_namespace, bool $auto_require = false): void + { + self::initPackageInstances(); + $classes = FileSystem::getClassesPsr4($dir, $base_namespace, auto_require: $auto_require); + foreach ($classes as $class) { + self::loadFromClass($class); + } + } + + public static function hasPackage(string $name): bool + { + return isset(self::$packages[$name]); + } + + /** + * Get a Package instance by its name. + * + * @param string $name The name of the package + * @return Package Returns the Package instance if found, otherwise null + */ + public static function getPackage(string $name): Package + { + if (!isset(self::$packages[$name])) { + throw new WrongUsageException("Package [{$name}] not found."); + } + return self::$packages[$name]; + } + + public static function getTargetPackage(string $name): TargetPackage + { + $pkg = self::getPackage($name); + if ($pkg instanceof TargetPackage) { + return $pkg; + } + throw new WrongUsageException("Package [{$name}] is not a TargetPackage."); + } + + public static function getLibraryPackage(string $name): LibraryPackage + { + $pkg = self::getPackage($name); + if ($pkg instanceof LibraryPackage) { + return $pkg; + } + throw new WrongUsageException("Package [{$name}] is not a LibraryPackage."); + } + + /** + * Get all loaded Package instances. + */ + public static function getPackages(array|string|null $type_filter = null): iterable + { + foreach (self::$packages as $name => $package) { + if ($type_filter === null) { + yield $name => $package; + } elseif ($package->getType() === $type_filter) { + yield $name => $package; + } elseif (is_array($type_filter) && in_array($package->getType(), $type_filter, true)) { + yield $name => $package; + } + } + } + + /** + * Init package instance from defined classes and attributes. + * + * @internal + */ + public static function loadFromClass(mixed $class): void + { + $refClass = new \ReflectionClass($class); + $class_name = $refClass->getName(); + + // Skip if already loaded to prevent duplicate registrations + if (isset(self::$loaded_classes[$class_name])) { + return; + } + self::$loaded_classes[$class_name] = true; + + $attributes = $refClass->getAttributes(); + foreach ($attributes as $attribute) { + $pkg = null; + + $attribute_instance = $attribute->newInstance(); + if ($attribute_instance instanceof Target === false && + $attribute_instance instanceof Library === false && + $attribute_instance instanceof Extension === false) { + // not a package attribute + continue; + } + $package_type = PackageConfig::get($attribute_instance->name, 'type'); + if ($package_type === null) { + 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'], + Library::class => ['library'], + Extension::class => ['php-extension'], + default => null, + }; + if (!in_array($package_type, $pkg_type_attr, true)) { + 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 RegistryException("Package [{$pkg->getName()}] config not found for class {$class}"); + } + + // init method attributes + $methods = $refClass->getMethods(\ReflectionMethod::IS_PUBLIC); + foreach ($methods as $method) { + $method_attributes = $method->getAttributes(); + foreach ($method_attributes as $method_attribute) { + $method_instance = $method_attribute->newInstance(); + match ($method_attribute->getName()) { + // #[BuildFor(PHP_OS_FAMILY)] + BuildFor::class => self::addBuildFunction($pkg, $method_instance, [$instance_class, $method->getName()]), + // #[CustomPhpConfigureArg(PHP_OS_FAMILY)] + CustomPhpConfigureArg::class => self::bindCustomPhpConfigureArg($pkg, $method_attribute->newInstance(), [$instance_class, $method->getName()]), + // #[Stage('stage_name')] + 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' => $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()]), + default => null, + }; + } + } + // register package + 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(); + foreach ($method_attributes as $method_attribute) { + $method_instance = $method_attribute->newInstance(); + match ($method_attribute->getName()) { + // #[BeforeStage('package_name', 'stage')] and #[AfterStage('package_name', 'stage')] + BeforeStage::class => self::addBeforeStage($method, $pkg ?? null, $instance_class, $method_instance), + AfterStage::class => self::addAfterStage($method, $pkg ?? null, $instance_class, $method_instance), + + default => null, + }; + } + } + } + + public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable + { + // match condition + $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->isPackageResolved($only_when_package_resolved)) { + continue; + } + yield $callback; + } + } + + public static function getAfterStageCallbacks(string $package_name, string $stage): array + { + // match condition + $installer = ApplicationContext::get(PackageInstaller::class); + $stages = self::$after_stages[$package_name][$stage] ?? []; + $result = []; + foreach ($stages as [$callback, $only_when_package_resolved]) { + if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { + continue; + } + $result[] = $callback; + } + return $result; + } + + /** + * Register default stages for all PhpExtensionPackage instances. + * Should be called after all registries have been loaded. + */ + public static function registerAllDefaultStages(): void + { + 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 + // Skip validation if the package has no build function for current OS + // (e.g., libedit has BeforeStage for 'build' but only BuildFor('Darwin'/'Linux')) + if (!$pkg->hasStage($stage_name) && $pkg->hasBuildFunctionForCurrentOS()) { + throw new RegistryException("Package stage [{$stage_name}] is not registered in package [{$package_name}]."); + } + } + } + } + } + + /** + * Bind a custom PHP configure argument callback to a php-extension package. + */ + private static function bindCustomPhpConfigureArg(Package $pkg, object $attr, callable $fn): void + { + if (!$pkg instanceof PhpExtensionPackage) { + 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 + { + $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]; + } +} diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php new file mode 100644 index 000000000..4ae5df4f4 --- /dev/null +++ b/src/StaticPHP/Registry/Registry.php @@ -0,0 +1,286 @@ + json_decode($yaml, true), + 'yaml', 'yml' => Yaml::parse($yaml), + default => throw new RegistryException("Unsupported registry file format: {$registry_file}"), + }; + if (!is_array($data)) { + 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 RegistryException("Registry 'name' is missing or invalid in file: {$registry_file}"); + } + + // Prevent loading the same registry twice + if (in_array($registry_name, self::$loaded_registries, true)) { + logger()->debug("Registry '{$registry_name}' already loaded, skipping."); + return; + } + self::$loaded_registries[] = $registry_name; + + logger()->debug("Loading registry '{$registry_name}' from file: {$registry_file}"); + + // Load composer autoload if specified (for external registries with their own dependencies) + if (isset($data['autoload']) && is_string($data['autoload'])) { + $autoload_path = self::fullpath($data['autoload'], dirname($registry_file)); + if (file_exists($autoload_path)) { + logger()->debug("Loading external autoload from: {$autoload_path}"); + require_once $autoload_path; + } else { + logger()->warning("Autoload file not found: {$autoload_path}"); + } + } + + // load doctor items from PSR-4 directories + if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) { + foreach ($data['doctor']['psr-4'] as $namespace => $path) { + $path = self::fullpath($path, dirname($registry_file)); + DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } + } + + // load doctor items from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['doctor']['classes']) && is_array($data['doctor']['classes'])) { + foreach ($data['doctor']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + DoctorLoader::loadFromClass($class); + } + } + + // load package configs + if (isset($data['package']['config']) && is_array($data['package']['config'])) { + foreach ($data['package']['config'] as $path) { + $path = self::fullpath($path, dirname($registry_file)); + if (is_file($path)) { + PackageConfig::loadFromFile($path); + } elseif (is_dir($path)) { + PackageConfig::loadFromDir($path); + } + } + } + + // load artifact configs + if (isset($data['artifact']['config']) && is_array($data['artifact']['config'])) { + foreach ($data['artifact']['config'] as $path) { + $path = self::fullpath($path, dirname($registry_file)); + if (is_file($path)) { + ArtifactConfig::loadFromFile($path); + } elseif (is_dir($path)) { + ArtifactConfig::loadFromDir($path); + } + } + } + + // load packages from PSR-4 directories + if (isset($data['package']['psr-4']) && is_assoc_array($data['package']['psr-4'])) { + foreach ($data['package']['psr-4'] as $namespace => $path) { + $path = self::fullpath($path, dirname($registry_file)); + PackageLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } + } + + // load packages from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['package']['classes']) && is_array($data['package']['classes'])) { + foreach ($data['package']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + PackageLoader::loadFromClass($class); + } + } + + // load artifacts from PSR-4 directories + if (isset($data['artifact']['psr-4']) && is_assoc_array($data['artifact']['psr-4'])) { + foreach ($data['artifact']['psr-4'] as $namespace => $path) { + $path = self::fullpath($path, dirname($registry_file)); + ArtifactLoader::loadFromPsr4Dir($path, $namespace, $auto_require); + } + } + + // load artifacts from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['artifact']['classes']) && is_array($data['artifact']['classes'])) { + foreach ($data['artifact']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + ArtifactLoader::loadFromClass($class); + } + } + + // load additional commands from PSR-4 directories + if (isset($data['command']['psr-4']) && is_assoc_array($data['command']['psr-4'])) { + foreach ($data['command']['psr-4'] as $namespace => $path) { + $path = self::fullpath($path, dirname($registry_file)); + $classes = FileSystem::getClassesPsr4($path, $namespace, auto_require: $auto_require); + $instances = array_map(fn ($x) => new $x(), $classes); + ConsoleApplication::_addAdditionalCommands($instances); + } + } + + // load additional commands from specific classes + // Supports both array format ["ClassName"] and map format {"ClassName": "path/to/file.php"} + if (isset($data['command']['classes']) && is_array($data['command']['classes'])) { + $instances = []; + foreach ($data['command']['classes'] as $key => $value) { + [$class, $file] = self::parseClassEntry($key, $value); + self::requireClassFile($class, $file, dirname($registry_file), $auto_require); + $instances[] = new $class(); + } + ConsoleApplication::_addAdditionalCommands($instances); + } + } + + /** + * Load registries from environment variable or CLI option. + * Supports comma-separated list of registry file paths. + * + * @param null|string $registries Comma-separated registry paths, or null to read from SPC_REGISTRIES env + */ + public static function loadFromEnvOrOption(?string $registries = null): void + { + $registries ??= getenv('SPC_REGISTRIES') ?: null; + + if ($registries === null) { + return; + } + + $paths = array_filter(array_map('trim', explode(':', $registries))); + foreach ($paths as $path) { + if (!file_exists($path)) { + logger()->warning("Registry file not found: {$path}"); + continue; + } + self::loadRegistry($path); + } + } + + 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. + * + * @return string[] + */ + public static function getLoadedRegistries(): array + { + return self::$loaded_registries; + } + + /** + * Reset loaded registries (for testing). + * + * @internal + */ + public static function reset(): void + { + self::$loaded_registries = []; + } + + /** + * Parse a class entry from the classes array. + * Supports two formats: + * - Array format: ["ClassName"] where key is numeric and value is class name + * - Map format: {"ClassName": "path/to/file.php"} where key is class name and value is file path + * + * @param int|string $key Array key (numeric for array format, class name for map format) + * @param string $value Array value (class name for array format, file path for map format) + * @return array{string, ?string} [class_name, file_path or null] + */ + private static function parseClassEntry(int|string $key, string $value): array + { + if (is_int($key)) { + // Array format: ["ClassName"] - value is the class name, no file path + return [$value, null]; + } + // Map format: {"ClassName": "path/to/file.php"} - key is class name, value is file path + return [$key, $value]; + } + + /** + * Require a class file if the class doesn't exist and auto_require is enabled. + * + * @param string $class Full class name + * @param null|string $file_path File path (relative or absolute), null if not provided + * @param string $base_path Base path for relative paths + * @param bool $auto_require Whether to auto-require + */ + private static function requireClassFile(string $class, ?string $file_path, string $base_path, bool $auto_require): void + { + if (!$auto_require || class_exists($class)) { + return; + } + + // If file path is provided, require it + if ($file_path !== null) { + $full_path = self::fullpath($file_path, $base_path); + require_once $full_path; + return; + } + + // Class not found and no file path provided + 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" . + " 3. Provide file path in classes map: \"{$class}\": \"path/to/file.php\"" + ); + } + + /** + * 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 RegistryException("Path does not exist: {$path}"); + } + return FileSystem::convertPath($path); + } +} diff --git a/src/StaticPHP/Runtime/Executor/Executor.php b/src/StaticPHP/Runtime/Executor/Executor.php new file mode 100644 index 000000000..a57e11e0d --- /dev/null +++ b/src/StaticPHP/Runtime/Executor/Executor.php @@ -0,0 +1,17 @@ +installer = $installer; + } elseif (ApplicationContext::has(PackageInstaller::class)) { + $this->installer = ApplicationContext::get(PackageInstaller::class); + } else { + throw new SPCInternalException('PackageInstaller not found in container'); + } + $this->initShell(); + + // judge that this package has artifact.source and defined build stage + if (!$this->package->hasStage('build')) { + throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined."); + } + } + + /** + * Run ./configure + */ + public function configure(...$args): static + { + // remove all the ignored args + $args = array_merge($args, $this->getDefaultConfigureArgs(), $this->configure_args); + $args = array_diff($args, $this->ignore_args); + $configure_args = implode(' ', $args); + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (./configure)'); + return $this->seekLogFileOnException(fn () => $this->shell->exec("./configure {$configure_args}")); + } + + public function getConfigureArgsString(): string + { + return implode(' ', array_merge($this->getDefaultConfigureArgs(), $this->configure_args)); + } + + /** + * Run make + * + * @param string $target Build target + * @param false|string $with_install Run `make install` after building, or false to skip + * @param bool $with_clean Whether to clean before building + * @param array $after_env_vars Environment variables postfix + */ + public function make(string $target = '', false|string $with_install = 'install', bool $with_clean = true, array $after_env_vars = [], ?string $dir = null): static + { + return $this->seekLogFileOnException(function () use ($target, $with_install, $with_clean, $after_env_vars, $dir) { + $shell = $this->shell; + if ($dir) { + $shell = $shell->cd($dir); + } + if ($with_clean) { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (make clean)'); + $shell->exec('make clean'); + } + $after_env_vars_str = $after_env_vars !== [] ? shell()->setEnv($after_env_vars)->getEnvString() : ''; + $concurrency = ApplicationContext::get(PackageBuilder::class)->concurrency; + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (make)'); + $shell->exec("make -j{$concurrency} {$target} {$after_env_vars_str}"); + if ($with_install !== false) { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (make ' . $with_install . ')'); + $shell->exec("make {$with_install}"); + } + return $shell; + }); + } + + public function exec(string $cmd): static + { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName())); + $this->shell->exec($cmd); + return $this; + } + + /** + * Add optional library configuration. + * This method checks if a library is available and adds the corresponding arguments to the CMake configuration. + * + * @param string $name library name to check + * @param \Closure|string $true_args arguments to use if the library is available (allow closure, returns string) + * @param string $false_args arguments to use if the library is not available + * @return $this + */ + public function optionalPackage(string $name, \Closure|string $true_args, string $false_args = ''): static + { + if ($get = $this->installer->getResolvedPackages()[$name] ?? null) { + logger()->info("Building package [{$this->package->getName()}] with {$name} support"); + $args = $true_args instanceof \Closure ? $true_args($get) : $true_args; + } else { + logger()->info("Building package [{$this->package->getName()}] without {$name} support"); + $args = $false_args; + } + $this->addConfigureArgs($args); + return $this; + } + + /** + * Add configure args. + */ + public function addConfigureArgs(...$args): static + { + $this->configure_args = [...$this->configure_args, ...$args]; + return $this; + } + + /** + * Remove some configure args, to bypass the configure option checking for some libs. + */ + public function removeConfigureArgs(...$args): static + { + $this->ignore_args = [...$this->ignore_args, ...$args]; + return $this; + } + + public function setEnv(array $env): static + { + $this->shell->setEnv($env); + return $this; + } + + public function appendEnv(array $env): static + { + $this->shell->appendEnv($env); + return $this; + } + + /** + * Returns the default autoconf ./configure arguments + */ + private function getDefaultConfigureArgs(): array + { + return [ + '--disable-shared', + '--enable-static', + "--prefix={$this->package->getBuildRootPath()}", + '--with-pic', + '--enable-pic', + ]; + } + + /** + * Initialize UnixShell class. + */ + private function initShell(): void + { + $this->shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([ + 'CFLAGS' => "-I{$this->package->getIncludeDir()}", + 'CXXFLAGS' => "-I{$this->package->getIncludeDir()}", + 'LDFLAGS' => "-L{$this->package->getLibDir()}", + ]); + } + + /** + * When an exception occurs, this method will check if the config log file exists. + */ + private function seekLogFileOnException(mixed $callable): static + { + try { + $callable(); + return $this; + } catch (SPCException $e) { + if (file_exists("{$this->package->getSourceDir()}/config.log")) { + logger()->debug("Config log file found: {$this->package->getSourceDir()}/config.log"); + $log_file = "lib.{$this->package->getName()}.console.log"; + logger()->debug('Saved config log file to: ' . SPC_LOGS_DIR . "/{$log_file}"); + $e->addExtraLogFile("{$this->package->getName()} library config.log", $log_file); + copy("{$this->package->getSourceDir()}/config.log", SPC_LOGS_DIR . "/{$log_file}"); + } + throw $e; + } + } +} diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php new file mode 100644 index 000000000..107aac116 --- /dev/null +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -0,0 +1,333 @@ +installer = $installer; + } elseif (ApplicationContext::has(PackageInstaller::class)) { + $this->installer = ApplicationContext::get(PackageInstaller::class); + } else { + throw new SPCInternalException('PackageInstaller not found in container'); + } + $this->initShell(); + + // judge that this package has artifact.source and defined build stage + if (!$this->package->hasStage('build')) { + throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined."); + } + } + + /** + * Run cmake configure, build and install. + * + * @param string $build_pos Build position relative to build directory + */ + public function build(string $build_pos = '..'): static + { + return $this->seekLogFileOnException(function () use ($build_pos) { + // set cmake dir + $this->initBuildDir(); + + if ($this->reset) { + FileSystem::resetDir($this->build_dir); + } + + $this->shell = $this->shell->cd($this->build_dir); + + // config + if ($this->steps >= 1) { + $args = array_merge($this->configure_args, $this->getDefaultCMakeArgs()); + $args = array_diff($args, $this->ignore_args); + $configure_args = implode(' ', $args); + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (cmake configure)'); + $this->shell->exec("cmake {$configure_args} {$build_pos}"); + } + + // make + if ($this->steps >= 2) { + $concurrency = ApplicationContext::get(PackageBuilder::class)->concurrency; + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (cmake build)'); + $this->shell->exec("cmake --build . -j {$concurrency}"); + } + + // install + if ($this->steps >= 3) { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (cmake install)'); + $this->shell->exec('make install'); + } + + return $this; + }); + } + + /** + * Execute a custom command. + */ + public function exec(string $cmd): static + { + $this->shell->exec($cmd); + return $this; + } + + /** + * Add optional package configuration. + * This method checks if a package is available and adds the corresponding arguments to the CMake configuration. + * + * @param string $name package name to check + * @param \Closure|string $true_args arguments to use if the package is available (allow closure, returns string) + * @param string $false_args arguments to use if the package is not available + * @return $this + */ + public function optionalPackage(string $name, \Closure|string $true_args, string $false_args = ''): static + { + if ($get = $this->installer->getResolvedPackages()[$name] ?? null) { + logger()->info("Building package [{$this->package->getName()}] with {$name} support"); + $args = $true_args instanceof \Closure ? $true_args($get) : $true_args; + } else { + logger()->info("Building package [{$this->package->getName()}] without {$name} support"); + $args = $false_args; + } + $this->addConfigureArgs($args); + return $this; + } + + /** + * Add configure args. + */ + public function addConfigureArgs(...$args): static + { + $this->configure_args = [...$this->configure_args, ...$args]; + return $this; + } + + /** + * Remove some configure args, to bypass the configure option checking for some libs. + */ + public function removeConfigureArgs(...$args): static + { + $this->ignore_args = [...$this->ignore_args, ...$args]; + return $this; + } + + public function setEnv(array $env): static + { + $this->shell->setEnv($env); + return $this; + } + + public function appendEnv(array $env): static + { + $this->shell->appendEnv($env); + return $this; + } + + /** + * To build steps. + * + * @param int $step Step number, accept 1-3 + * @return $this + */ + public function toStep(int $step): static + { + $this->steps = $step; + return $this; + } + + /** + * Set custom CMake build directory. + * + * @param string $dir custom CMake build directory + */ + public function setBuildDir(string $dir): static + { + $this->build_dir = $dir; + return $this; + } + + /** + * Set the custom default args. + */ + public function setCustomDefaultArgs(...$args): static + { + $this->custom_default_args = $args; + return $this; + } + + /** + * Set the reset status. + * If we set it to false, it will not clean and create the specified cmake working directory. + */ + public function setReset(bool $reset): static + { + $this->reset = $reset; + return $this; + } + + /** + * Get configure argument string. + */ + public function getConfigureArgsString(): string + { + return implode(' ', array_merge($this->configure_args, $this->getDefaultCMakeArgs())); + } + + /** + * Returns the default CMake args. + */ + private function getDefaultCMakeArgs(): array + { + return $this->custom_default_args ?? [ + '-DCMAKE_BUILD_TYPE=Release', + "-DCMAKE_INSTALL_PREFIX={$this->package->getBuildRootPath()}", + '-DCMAKE_INSTALL_BINDIR=bin', + '-DCMAKE_INSTALL_LIBDIR=lib', + '-DCMAKE_INSTALL_INCLUDEDIR=include', + '-DPOSITION_INDEPENDENT_CODE=ON', + '-DBUILD_SHARED_LIBS=OFF', + "-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}", + ]; + } + + /** + * Initialize the CMake build directory. + * If the directory is not set, it defaults to the package's source directory with '/build' appended. + */ + private function initBuildDir(): void + { + if ($this->build_dir === null) { + $this->build_dir = "{$this->package->getSourceDir()}/build"; + } + } + + /** + * Generate cmake toolchain file for current spc instance, and return the file path. + * + * @return string CMake toolchain file path + */ + private function makeCmakeToolchainFile(): string + { + static $created; + if (isset($created)) { + return $created; + } + $os = PHP_OS_FAMILY; + $target_arch = arch2gnu(php_uname('m')); + $cflags = getenv('SPC_DEFAULT_C_FLAGS'); + $cc = getenv('CC'); + $cxx = getenv('CXX'); + logger()->debug("making cmake tool chain file for {$os} {$target_arch} with CFLAGS='{$cflags}'"); + $root = BUILD_ROOT_PATH; + $pkgConfigExecutable = PkgConfigUtil::findPkgConfig(); + $ccLine = ''; + if ($cc) { + $ccLine = 'SET(CMAKE_C_COMPILER ' . $cc . ')'; + } + $cxxLine = ''; + if ($cxx) { + $cxxLine = 'SET(CMAKE_CXX_COMPILER ' . $cxx . ')'; + } + $toolchain = <<shell = shell()->cd($this->package->getSourceDir())->initializeEnv($this->package)->appendEnv([ + 'CFLAGS' => "-I{$this->package->getIncludeDir()}", + 'CXXFLAGS' => "-I{$this->package->getIncludeDir()}", + 'LDFLAGS' => "-L{$this->package->getLibDir()}", + ]); + } + + /** + * When an exception occurs, this method will check if the cmake log file exists. + */ + private function seekLogFileOnException(mixed $callable): static + { + try { + $callable(); + return $this; + } catch (SPCException $e) { + $cmake_log = "{$this->build_dir}/CMakeFiles/CMakeError.log"; + if (file_exists($cmake_log)) { + logger()->debug("CMake error log file found: {$cmake_log}"); + $log_file = "lib.{$this->package->getName()}.cmake-error.log"; + logger()->debug('Saved CMake error log file to: ' . SPC_LOGS_DIR . "/{$log_file}"); + $e->addExtraLogFile("{$this->package->getName()} library CMakeError.log", $log_file); + copy($cmake_log, SPC_LOGS_DIR . "/{$log_file}"); + } + $cmake_output = "{$this->build_dir}/CMakeFiles/CMakeOutput.log"; + if (file_exists($cmake_output)) { + logger()->debug("CMake output log file found: {$cmake_output}"); + $log_file = "lib.{$this->package->getName()}.cmake-output.log"; + logger()->debug('Saved CMake output log file to: ' . SPC_LOGS_DIR . "/{$log_file}"); + $e->addExtraLogFile("{$this->package->getName()} library CMakeOutput.log", $log_file); + copy($cmake_output, SPC_LOGS_DIR . "/{$log_file}"); + } + throw $e; + } + } +} diff --git a/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php new file mode 100644 index 000000000..9e0978196 --- /dev/null +++ b/src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php @@ -0,0 +1,218 @@ +package); + $this->builder = ApplicationContext::get(PackageBuilder::class); + $this->installer = ApplicationContext::get(PackageInstaller::class); + $this->initCmd(); + + // judge that this package has artifact.source and defined build stage + if (!$this->package->hasStage('build')) { + throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined."); + } + } + + public function build(): static + { + $this->initBuildDir(); + + if ($this->reset) { + FileSystem::resetDir($this->build_dir); + } + + // configure + if ($this->steps >= 1) { + $args = array_merge($this->configure_args, $this->getDefaultCMakeArgs()); + $args = array_diff($args, $this->ignore_args); + $configure_args = implode(' ', $args); + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName() . ' (cmake configure)')); + $this->cmd->exec("cmake {$configure_args}"); + } + + // make + if ($this->steps >= 2) { + InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName() . ' (cmake build)')); + $this->cmd->cd($this->build_dir)->exec("cmake --build {$this->build_dir} --config Release --target install -j{$this->builder->concurrency}"); + } + + return $this; + } + + /** + * Add optional package configuration. + * This method checks if a package is available and adds the corresponding arguments to the CMake configuration. + * + * @param string $name package name to check + * @param \Closure|string $true_args arguments to use if the package is available (allow closure, returns string) + * @param string $false_args arguments to use if the package is not available + * @return $this + */ + public function optionalPackage(string $name, \Closure|string $true_args, string $false_args = ''): static + { + if ($get = $this->installer->getResolvedPackages()[$name] ?? null) { + logger()->info("Building package [{$this->package->getName()}] with {$name} support"); + $args = $true_args instanceof \Closure ? $true_args($get) : $true_args; + } else { + logger()->info("Building package [{$this->package->getName()}] without {$name} support"); + $args = $false_args; + } + $this->addConfigureArgs($args); + return $this; + } + + /** + * Add configure args. + */ + public function addConfigureArgs(...$args): static + { + $this->configure_args = [...$this->configure_args, ...$args]; + return $this; + } + + /** + * Remove some configure args, to bypass the configure option checking for some libs. + */ + public function removeConfigureArgs(...$args): static + { + $this->ignore_args = [...$this->ignore_args, ...$args]; + return $this; + } + + public function setEnv(array $env): static + { + $this->cmd->setEnv($env); + return $this; + } + + public function appendEnv(array $env): static + { + $this->cmd->appendEnv($env); + return $this; + } + + /** + * To build steps. + * + * @param int $step Step number, accept 1-3 + * @return $this + */ + public function toStep(int $step): static + { + $this->steps = $step; + return $this; + } + + /** + * Set custom CMake build directory. + * + * @param string $dir custom CMake build directory + */ + public function setBuildDir(string $dir): static + { + $this->build_dir = $dir; + return $this; + } + + /** + * Set the custom default args. + */ + public function setCustomDefaultArgs(...$args): static + { + $this->custom_default_args = $args; + return $this; + } + + /** + * Set the reset status. + * If we set it to false, it will not clean and create the specified cmake working directory. + */ + public function setReset(bool $reset): static + { + $this->reset = $reset; + return $this; + } + + /** + * Get configure argument string. + */ + public function getConfigureArgsString(): string + { + return implode(' ', array_merge($this->configure_args, $this->getDefaultCMakeArgs())); + } + + /** + * Returns the default CMake args. + */ + private function getDefaultCMakeArgs(): array + { + return $this->custom_default_args ?? [ + '-A x64', + '-DCMAKE_BUILD_TYPE=Release', + '-DBUILD_SHARED_LIBS=OFF', + '-DBUILD_STATIC_LIBS=ON', + "-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}", + '-DCMAKE_INSTALL_PREFIX=' . escapeshellarg($this->package->getBuildRootPath()), + '-B ' . escapeshellarg(FileSystem::convertPath($this->build_dir)), + ]; + } + + private function makeCmakeToolchainFile(): string + { + if (file_exists(SOURCE_PATH . '\toolchain.cmake')) { + return SOURCE_PATH . '\toolchain.cmake'; + } + return WindowsUtil::makeCmakeToolchainFile(); + } + + /** + * Initialize the CMake build directory. + * If the directory is not set, it defaults to the package's source directory with '/build' appended. + */ + private function initBuildDir(): void + { + if ($this->build_dir === null) { + $this->build_dir = "{$this->package->getSourceDir()}\\build"; + } + } + + private function initCmd(): void + { + $this->cmd = cmd()->cd($this->package->getSourceDir()); + } +} diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php new file mode 100644 index 000000000..a6421bdb9 --- /dev/null +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -0,0 +1,195 @@ + '', + 'HEAD' => '-I', + default => "-X {$method}", + }; + $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); + $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; + $cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}"; + + $this->logCommandInfo($cmd); + $result = $this->passthru($cmd, capture_output: true, throw_on_error: false); + $ret = $result['code']; + $output = $result['output']; + if ($ret !== 0) { + logger()->debug("[CURL ERROR] Command exited with code: {$ret}"); + } + if ($ret === 2 || $ret === -1073741510) { + throw new InterruptException(sprintf('Canceled fetching "%s"', $url)); + } + if ($ret !== 0) { + return false; + } + + return trim($output); + } + + /** + * Execute a cURL command to download a file from a URL. + */ + public function executeCurlDownload(string $url, string $path, array $headers = [], array $hooks = [], int $retries = 0): void + { + foreach ($hooks as $hook) { + $hook('GET', $url, $headers); + } + $url_arg = escapeshellarg($url); + $path_arg = escapeshellarg($path); + + $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); + $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; + $check = $this->console_putput ? '#' : 's'; + $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); + $this->logCommandInfo($cmd); + logger()->debug('[CURL DOWNLOAD] ' . $cmd); + $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); + } + + /** + * Execute a Git clone command to clone a repository. + */ + public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null): void + { + $path = FileSystem::convertPath($path); + if (file_exists($path)) { + FileSystem::removeDir($path); + } + $git = SPC_GIT_EXEC; + $url_arg = escapeshellarg($url); + $branch_arg = escapeshellarg($branch); + $path_arg = escapeshellarg($path); + $shallow_arg = $shallow ? '--depth 1 --single-branch' : ''; + $submodules_arg = ($submodules === null && $shallow) ? '--recursive --shallow-submodules' : ($submodules === null ? '--recursive' : ''); + $cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); + $this->logCommandInfo($cmd); + logger()->debug("[GIT CLONE] {$cmd}"); + $this->passthru($cmd, $this->console_putput); + if ($submodules !== null) { + $depth_flag = $shallow ? '--depth 1' : ''; + foreach ($submodules as $submodule) { + $submodule = escapeshellarg($submodule); + $submodule_cmd = clean_spaces("{$git} submodule update --init {$depth_flag} {$submodule}"); + $this->logCommandInfo($submodule_cmd); + logger()->debug("[GIT SUBMODULE] {$submodule_cmd}"); + $this->passthru($submodule_cmd, $this->console_putput, cwd: $path_arg); + } + } + } + + /** + * Execute a tar command to extract an archive. + * + * @param string $archive_path Path to the archive file + * @param string $target_path Path to extract to + * @param string $compression Compression type: 'gz', 'bz2', 'xz', or 'none' + * @param int $strip Number of leading components to strip (default: 1) + */ + public function executeTarExtract(string $archive_path, string $target_path, string $compression, int $strip = 1): bool + { + $archive_arg = escapeshellarg(FileSystem::convertPath($archive_path)); + $target_arg = escapeshellarg(FileSystem::convertPath($target_path)); + + $compression_flag = match ($compression) { + 'gz' => '-z', + 'bz2' => '-j', + 'xz' => '-J', + 'none' => '', + default => throw new SPCInternalException("Unknown compression type: {$compression}"), + }; + + $mute = $this->console_putput ? '' : ' 2>/dev/null'; + $cmd = "tar {$compression_flag}xf {$archive_arg} --strip-components {$strip} -C {$target_arg}{$mute}"; + + $this->logCommandInfo($cmd); + logger()->debug("[TAR EXTRACT] {$cmd}"); + $this->passthru($cmd, $this->console_putput); + return true; + } + + /** + * Execute an unzip command to extract a zip archive. + * + * @param string $zip_path Path to the zip file + * @param string $target_path Path to extract to + */ + public function executeUnzip(string $zip_path, string $target_path): void + { + $zip_arg = escapeshellarg(FileSystem::convertPath($zip_path)); + $target_arg = escapeshellarg(FileSystem::convertPath($target_path)); + + $mute = $this->console_putput ? '' : ' > /dev/null'; + $cmd = "unzip {$zip_arg} -d {$target_arg}{$mute}"; + + $this->logCommandInfo($cmd); + logger()->debug("[UNZIP] {$cmd}"); + $this->passthru($cmd, $this->console_putput); + } + + /** + * Execute a 7za command to extract an archive (Windows). + * + * @param string $archive_path Path to the archive file + * @param string $target_path Path to extract to + */ + public function execute7zExtract(string $archive_path, string $target_path): bool + { + $sdk_path = getenv('PHP_SDK_PATH'); + if ($sdk_path === false) { + throw new SPCInternalException('PHP_SDK_PATH environment variable is not set'); + } + + $_7z = escapeshellarg(FileSystem::convertPath($sdk_path . '/bin/7za.exe')); + $archive_arg = escapeshellarg(FileSystem::convertPath($archive_path)); + $target_arg = escapeshellarg(FileSystem::convertPath($target_path)); + + $mute = $this->console_putput ? '' : ' > NUL'; + + $run = function ($cmd) { + $this->logCommandInfo($cmd); + logger()->debug("[7Z EXTRACT] {$cmd}"); + $this->passthru($cmd, $this->console_putput); + }; + + $extname = FileSystem::extname($archive_path); + match ($extname) { + 'tar' => $this->executeTarExtract($archive_path, $target_path, 'none'), + 'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | tar -f - -x -C {$target_arg} --strip-components 1"), + default => $run("{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}"), + }; + + return true; + } +} diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php new file mode 100644 index 000000000..2d0d90b8c --- /dev/null +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -0,0 +1,267 @@ +console_putput = $console_output ?? ApplicationContext::isDebug(); + $this->enable_log_file = $enable_log_file; + } + + public static function passthruCallback(?callable $callback): void + { + static::$passthru_callback = $callback; + } + + /** + * Equivalent to `cd` command in shell. + * + * @param string $dir Directory to change to + */ + public function cd(string $dir): static + { + logger()->debug('Entering dir: ' . $dir); + $c = clone $this; + $c->cd = $dir; + return $c; + } + + /** + * Set temporarily defined environment variables for current shell commands. + * + * @param array $env Environment variables sets + */ + public function setEnv(array $env): static + { + foreach ($env as $k => $v) { + if (trim($v) === '') { + continue; + } + $this->env[$k] = trim($v); + } + return $this; + } + + /** + * Append temporarily defined environment variables for current shell commands. + * + * @param array $env Environment variables sets + */ + public function appendEnv(array $env): static + { + foreach ($env as $k => $v) { + if ($v === '') { + continue; + } + if (!isset($this->env[$k])) { + $this->env[$k] = $v; + } else { + $this->env[$k] = "{$v} {$this->env[$k]}"; + } + } + return $this; + } + + /** + * Executes a command in the shell. + */ + abstract public function exec(string $cmd): static; + + /** + * Returns the last executed command. + */ + public function getLastCommand(): string + { + return $this->last_cmd; + } + + /** + * Returns unix-style environment variable string. + */ + public function getEnvString(): string + { + $str = ''; + foreach ($this->env as $k => $v) { + $str .= ' ' . $k . '="' . $v . '"'; + } + return trim($str); + } + + /** + * Logs the command information to a log file. + */ + protected function logCommandInfo(string $cmd): void + { + if (!$this->enable_log_file) { + return; + } + // write executed command to log file using fwrite + $log_file = fopen(SPC_SHELL_LOG, 'a'); + fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); + fwrite($log_file, "> Executing command: {$cmd}\n"); + // get the backtrace to find the file and line number + $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); + if (isset($backtrace[1]['file'], $backtrace[1]['line'])) { + $file = $backtrace[1]['file']; + $line = $backtrace[1]['line']; + fwrite($log_file, "> Called from: {$file} at line {$line}\n"); + } + fwrite($log_file, "> Environment variables: {$this->getEnvString()}\n"); + if ($this->cd !== null) { + fwrite($log_file, "> Working dir: {$this->cd}\n"); + } + fwrite($log_file, "\n"); + } + + /** + * Executes a command with console and log file output. + * + * @param string $cmd Full command to execute (including cd and env vars) + * @param bool $console_output If true, output will be printed to console + * @param null|string $original_command Original command string for logging + * @param bool $capture_output If true, capture and return output + * @param bool $throw_on_error If true, throw exception on non-zero exit code + * + * @return array{code: int, output: string} Returns exit code and captured output + */ + protected function passthru( + string $cmd, + bool $console_output = false, + ?string $original_command = null, + bool $capture_output = false, + bool $throw_on_error = true, + ?string $cwd = null, + ?array $env = null, + ): array { + $file_res = null; + if ($this->enable_log_file) { + // write executed command to the log file using fwrite + $file_res = fopen(SPC_SHELL_LOG, 'a'); + } + if ($console_output) { + $console_res = STDOUT; + } + $descriptors = [ + 0 => ['file', 'php://stdin', 'r'], // stdin + 1 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stdout + 2 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stderr + ]; + if ($env !== null && $env !== []) { + // merge current PHP envs + $env = array_merge(getenv(), $env); + } else { + $env = null; + } + $process = proc_open($cmd, $descriptors, $pipes, $cwd, env_vars: $env, options: PHP_OS_FAMILY === 'Windows' ? ['create_process_group' => true] : null); + + $output_value = ''; + try { + if (!is_resource($process)) { + throw new ExecutionException( + cmd: $original_command ?? $cmd, + message: 'Failed to open process for command, proc_open() failed.', + code: -1, + cd: $this->cd, + env: $this->env + ); + } + // fclose($pipes[0]); + stream_set_blocking($pipes[1], false); + stream_set_blocking($pipes[2], false); + + while (true) { + $status = proc_get_status($process); + if (!$status['running']) { + foreach ([$pipes[1], $pipes[2]] as $pipe) { + while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { + if ($console_output) { + fwrite($console_res, $chunk); + } + if ($file_res !== null) { + fwrite($file_res, $chunk); + } + if ($capture_output) { + $output_value .= $chunk; + } + } + } + // check exit code + if ($throw_on_error && $status['exitcode'] !== 0) { + if ($file_res !== null) { + fwrite($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); + } + throw new ExecutionException( + cmd: $original_command ?? $cmd, + message: "Command exited with non-zero code: {$status['exitcode']}", + code: $status['exitcode'], + cd: $this->cd, + env: $this->env, + ); + } + break; + } + + if (static::$passthru_callback !== null) { + $callback = static::$passthru_callback; + $callback(); + } + $read = [$pipes[1], $pipes[2]]; + $write = null; + $except = null; + + $ready = stream_select($read, $write, $except, 0, 100000); + + if ($ready === false) { + continue; + } + + if ($ready > 0) { + foreach ($read as $pipe) { + while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { + if ($console_output) { + fwrite($console_res, $chunk); + } + if ($file_res !== null) { + fwrite($file_res, $chunk); + } + if ($capture_output) { + $output_value .= $chunk; + } + } + } + } + } + + return [ + 'code' => $status['exitcode'], + 'output' => $output_value, + ]; + } finally { + fclose($pipes[1]); + fclose($pipes[2]); + if ($file_res !== null) { + fclose($file_res); + } + proc_close($process); + } + } +} diff --git a/src/StaticPHP/Runtime/Shell/UnixShell.php b/src/StaticPHP/Runtime/Shell/UnixShell.php new file mode 100644 index 000000000..7d74f65f3 --- /dev/null +++ b/src/StaticPHP/Runtime/Shell/UnixShell.php @@ -0,0 +1,88 @@ +info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); + $original_command = $cmd; + $this->logCommandInfo($original_command); + $this->last_cmd = $cmd = $this->getExecString($cmd); + $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd); + return $this; + } + + /** + * Init the environment variable that common build will be used. + * + * @param LibraryPackage $library Library package + */ + public function initializeEnv(LibraryPackage $library): UnixShell + { + $this->setEnv([ + 'CFLAGS' => $library->getLibExtraCFlags(), + 'CXXFLAGS' => $library->getLibExtraCXXFlags(), + 'LDFLAGS' => $library->getLibExtraLdFlags(), + 'LIBS' => $library->getLibExtraLibs() . SystemTarget::getRuntimeLibs(), + ]); + return $this; + } + + /** + * Execute a command and return the result. + * + * @param string $cmd Command to execute + * @param bool $with_log Whether to log the command + * @return array{0: int, 1: string[]} Return code and output lines + */ + public function execWithResult(string $cmd, bool $with_log = true): array + { + if ($with_log) { + /* @phpstan-ignore-next-line */ + logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); + } else { + /* @phpstan-ignore-next-line */ + logger()->debug(ConsoleColor::blue('[EXEC] ') . ConsoleColor::gray($cmd)); + } + $cmd = $this->getExecString($cmd); + $this->logCommandInfo($cmd); + $result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd); + $out = explode("\n", $result['output']); + return [$result['code'], $out]; + } + + private function getExecString(string $cmd): string + { + // logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']); + $env_str = $this->getEnvString(); + if (!empty($env_str)) { + $cmd = "{$env_str} {$cmd}"; + } + return $cmd; + } +} diff --git a/src/StaticPHP/Runtime/Shell/WindowsCmd.php b/src/StaticPHP/Runtime/Shell/WindowsCmd.php new file mode 100644 index 000000000..e9d7a6c0d --- /dev/null +++ b/src/StaticPHP/Runtime/Shell/WindowsCmd.php @@ -0,0 +1,62 @@ +info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd)); + + $original_command = $cmd; + $this->logCommandInfo($original_command); + $this->last_cmd = $cmd = $this->getExecString($cmd); + // echo $cmd . PHP_EOL; + + $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd); + return $this; + } + + public function execWithWrapper(string $wrapper, string $args): WindowsCmd + { + return $this->exec($wrapper . ' "' . str_replace('"', '^"', $args) . '"'); + } + + public function execWithResult(string $cmd, bool $with_log = true): array + { + if ($with_log) { + /* @phpstan-ignore-next-line */ + logger()->info(ConsoleColor::blue('[EXEC] ') . ConsoleColor::green($cmd)); + } else { + logger()->debug('Running command with result: ' . $cmd); + } + $cmd = $this->getExecString($cmd); + $result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd, env: $this->env); + $out = explode("\n", $result['output']); + return [$result['code'], $out]; + } + + public function getLastCommand(): string + { + return $this->last_cmd; + } + + private function getExecString(string $cmd): string + { + return $cmd; + } +} diff --git a/src/StaticPHP/Runtime/SystemTarget.php b/src/StaticPHP/Runtime/SystemTarget.php new file mode 100644 index 000000000..584fa49a6 --- /dev/null +++ b/src/StaticPHP/Runtime/SystemTarget.php @@ -0,0 +1,132 @@ + 'Linux', + str_contains($target, '-macos') => 'Darwin', + str_contains($target, '-windows') => 'Windows', + str_contains($target, '-native') => PHP_OS_FAMILY, + default => PHP_OS_FAMILY, + }; + } + + /** + * Returns the target architecture, e.g. x86_64, aarch64. + * Currently, we only support 'x86_64' and 'aarch64' and both can only be built natively. + * + * @return 'aarch64'|'x86_64' + */ + public static function getTargetArch(): string + { + $target = (string) getenv('SPC_TARGET'); + return match (true) { + str_contains($target, 'x86_64') || str_contains($target, 'amd64') => 'x86_64', + str_contains($target, 'aarch64') || str_contains($target, 'arm64') => 'aarch64', + // str_contains($target, 'armv7') || str_contains($target, 'armhf') => 'armv7', + // str_contains($target, 'armv6') || str_contains($target, 'armel') => 'armv6', + // str_contains($target, 'i386') || str_contains($target, 'i686') => 'i386', + default => GNU_ARCH, + }; + } + + /** + * Get the current platform string in the format of {os}-{arch}, e.g. linux-x86_64. + */ + public static function getCurrentPlatformString(): string + { + $os = match (self::getTargetOS()) { + 'Darwin' => 'macos', + 'Linux' => 'linux', + 'Windows' => 'windows', + default => 'unknown', + }; + $arch = self::getTargetArch(); + if (getenv('EMULATE_PLATFORM') !== false) { + return getenv('EMULATE_PLATFORM'); + } + return "{$os}-{$arch}"; + } + + /** + * Check if the target OS is a Unix-like system. + */ + public static function isUnix(): bool + { + return in_array(self::getTargetOS(), ['Linux', 'Darwin', 'BSD']); + } +} diff --git a/src/StaticPHP/Toolchain/ClangNativeToolchain.php b/src/StaticPHP/Toolchain/ClangNativeToolchain.php new file mode 100644 index 000000000..c34e619cd --- /dev/null +++ b/src/StaticPHP/Toolchain/ClangNativeToolchain.php @@ -0,0 +1,57 @@ + LinuxUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), + 'Darwin' => MacOSUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), + default => throw new EnvironmentException(__CLASS__ . ' is not supported on ' . PHP_OS_FAMILY . '.'), + }; + } + } + + public function getCompilerInfo(): ?string + { + $compiler = getenv('CC') ?: 'clang'; + $version = shell(false)->execWithResult("{$compiler} --version", false); + $head = pathinfo($compiler, PATHINFO_BASENAME); + if ($version[0] === 0 && preg_match('/clang version (\d+\.\d+\.\d+)/', $version[1][0], $match)) { + return "{$head} {$match[1]}"; + } + return $head; + } + + public function isStatic(): bool + { + return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist(); + } +} diff --git a/src/StaticPHP/Toolchain/GccNativeToolchain.php b/src/StaticPHP/Toolchain/GccNativeToolchain.php new file mode 100644 index 000000000..92b82892e --- /dev/null +++ b/src/StaticPHP/Toolchain/GccNativeToolchain.php @@ -0,0 +1,54 @@ + LinuxUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), + 'Darwin' => MacOSUtil::findCommand($command) ?? throw new WrongUsageException("{$command} not found, please install it or set {$env} to a valid path."), + default => throw new EnvironmentException(__CLASS__ . ' is not supported on ' . PHP_OS_FAMILY . '.'), + }; + } + } + + public function getCompilerInfo(): ?string + { + $compiler = getenv('CC') ?: 'gcc'; + $version = shell(false)->execWithResult("{$compiler} --version", false); + $head = pathinfo($compiler, PATHINFO_BASENAME); + if ($version[0] === 0 && preg_match('/gcc.*?(\d+\.\d+\.\d+)/', $version[1][0], $match)) { + return "{$head} {$match[1]}"; + } + return $head; + } + + public function isStatic(): bool + { + return PHP_OS_FAMILY === 'Linux' && LinuxUtil::isMuslDist(); + } +} diff --git a/src/StaticPHP/Toolchain/Interface/ToolchainInterface.php b/src/StaticPHP/Toolchain/Interface/ToolchainInterface.php new file mode 100644 index 000000000..34e879cf6 --- /dev/null +++ b/src/StaticPHP/Toolchain/Interface/ToolchainInterface.php @@ -0,0 +1,43 @@ + NUL && set', $output); + file_put_contents(DOWNLOAD_PATH . '/.vcenv-cache', implode("\n", $output)); + } + array_map(fn ($x) => putenv($x), $output); + } + $after = count(getenv()); + if ($after > $count) { + logger()->debug('Applied ' . ($after - $count) . ' environment variables from Visual Studio setup'); + } + } + + public function getCompilerInfo(): ?string + { + if ($vcver = getenv('VisualStudioVersion')) { + return "Visual Studio {$vcver}"; + } + return null; + } + + public function isStatic(): bool + { + return false; + } +} diff --git a/src/StaticPHP/Toolchain/MuslToolchain.php b/src/StaticPHP/Toolchain/MuslToolchain.php new file mode 100644 index 000000000..c7b39dbf6 --- /dev/null +++ b/src/StaticPHP/Toolchain/MuslToolchain.php @@ -0,0 +1,56 @@ +execWithResult("{$compiler} --version", false); + $head = pathinfo($compiler, PATHINFO_BASENAME); + if ($version[0] === 0 && preg_match('/linux-musl-cc.*(\d+.\d+.\d+)/', $version[1][0], $match)) { + return "{$head} {$match[1]}"; + } + return $head; + } + + public function isStatic(): bool + { + return true; + } +} diff --git a/src/StaticPHP/Toolchain/ToolchainManager.php b/src/StaticPHP/Toolchain/ToolchainManager.php new file mode 100644 index 000000000..688cd33a1 --- /dev/null +++ b/src/StaticPHP/Toolchain/ToolchainManager.php @@ -0,0 +1,92 @@ + LinuxUtil::isMuslDist() ? GccNativeToolchain::class : MuslToolchain::class, + 'glibc' => !LinuxUtil::isMuslDist() ? GccNativeToolchain::class : throw new WrongUsageException('SPC_LIBC must be musl for musl dist.'), + default => throw new WrongUsageException('Unsupported SPC_LIBC value: ' . $libc), + }; + } + + return match (PHP_OS_FAMILY) { + 'Linux' => ZigToolchain::class, + 'Windows' => MSVCToolchain::class, + 'Darwin' => ClangNativeToolchain::class, + default => throw new WrongUsageException('Unsupported OS family: ' . PHP_OS_FAMILY), + }; + } + + /** + * Init the toolchain and set it in the container. + */ + public static function initToolchain(): void + { + $toolchainClass = self::getToolchainClass(); + $toolchain = new $toolchainClass(); + ApplicationContext::set(ToolchainInterface::class, $toolchain); + /* @var ToolchainInterface $toolchainClass */ + $toolchain->initEnv(); + GlobalEnvManager::putenv("SPC_TOOLCHAIN={$toolchainClass}"); + } + + /** + * Perform post-initialization checks and setups for the toolchain. + */ + public static function afterInitToolchain(): void + { + if (!getenv('SPC_TOOLCHAIN')) { + throw new WrongUsageException('SPC_TOOLCHAIN was not properly set. Please contact the developers.'); + } + $musl_wrapper_lib = sprintf('/lib/ld-musl-%s.so.1', php_uname('m')); + if (SystemTarget::getLibc() === 'musl' && !ApplicationContext::get(ToolchainInterface::class)->isStatic() && !file_exists($musl_wrapper_lib)) { + throw new WrongUsageException('You are linking against musl libc dynamically, but musl libc is not installed. Please use `bin/spc doctor` to install it.'); + } + if (SystemTarget::getLibc() === 'glibc' && LinuxUtil::isMuslDist()) { + throw new WrongUsageException('You are linking against glibc dynamically, which is only supported on glibc distros.'); + } + + // init pkg-config for unix + if (SystemTarget::isUnix()) { + if (($found = PkgConfigUtil::findPkgConfig()) !== null) { + GlobalEnvManager::putenv("PKG_CONFIG={$found}"); + } elseif (!ApplicationContext::has('elephant')) { // skip pkg-config check in elephant mode :P (elephant mode is only for building pkg-config itself) + throw new WrongUsageException('Cannot find pkg-config executable. Please run `doctor` to fix this.'); + } + } + + /* @var ToolchainInterface $toolchain */ + $instance = ApplicationContext::get(ToolchainInterface::class); + $instance->afterInit(); + if (getenv('PHP_BUILD_COMPILER') === false && ($compiler_info = $instance->getCompilerInfo())) { + GlobalEnvManager::putenv("PHP_BUILD_COMPILER={$compiler_info}"); + } + } +} diff --git a/src/StaticPHP/Toolchain/ZigToolchain.php b/src/StaticPHP/Toolchain/ZigToolchain.php new file mode 100644 index 000000000..2d7c71b0c --- /dev/null +++ b/src/StaticPHP/Toolchain/ZigToolchain.php @@ -0,0 +1,108 @@ +/dev/null | grep -v '/32/' | head -n 1"); + $line = trim((string) $output); + if ($line !== '') { + $located = $line; + break; + } + } + if ($located) { + $found[] = $located; + } + } + GlobalEnvManager::putenv('SPC_EXTRA_RUNTIME_OBJECTS=' . implode(' ', $found)); + } + + public function afterInit(): void + { + GlobalEnvManager::addPathIfNotExists($this->getPath()); + f_passthru('ulimit -n 2048'); // zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough + $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: ''; + $cxxflags = getenv('SPC_DEFAULT_CXX_FLAGS') ?: ''; + $extraCflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: ''; + $cflags = trim($cflags . ' -Wno-date-time'); + $cxxflags = trim($cxxflags . ' -Wno-date-time'); + $extraCflags = trim($extraCflags . ' -Wno-date-time'); + GlobalEnvManager::putenv("SPC_DEFAULT_C_FLAGS={$cflags}"); + GlobalEnvManager::putenv("SPC_DEFAULT_CXX_FLAGS={$cxxflags}"); + GlobalEnvManager::putenv("SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS={$extraCflags}"); + GlobalEnvManager::putenv('RANLIB=zig-ranlib'); + GlobalEnvManager::putenv('OBJCOPY=zig-objcopy'); + $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + if (!str_contains($extra_libs, '-lunwind')) { + // Add unwind library if not already present + $extra_libs = trim($extra_libs . ' -lunwind'); + GlobalEnvManager::putenv("SPC_EXTRA_LIBS={$extra_libs}"); + } + $cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: ''; + $has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4'); + if (!$has_avx512) { + GlobalEnvManager::putenv('SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no'); + } + } + + public function getCompilerInfo(): ?string + { + $version = shell(false)->execWithResult('zig version', false)[1][0] ?? ''; + return trim("zig {$version}"); + } + + public function isStatic(): bool + { + // if SPC_LIBC is set, it means the target is static, remove it when 3.0 is released + if ($target = getenv('SPC_TARGET')) { + if (str_contains($target, '-macos') || str_contains($target, '-native') && PHP_OS_FAMILY === 'Darwin') { + return false; + } + if (str_contains($target, '-gnu')) { + return false; + } + if (str_contains($target, '-dynamic')) { + return false; + } + if (str_contains($target, '-musl')) { + return true; + } + if (PHP_OS_FAMILY === 'Linux') { + return LinuxUtil::isMuslDist(); + } + return true; + } + if (getenv('SPC_LIBC') === 'musl') { + return true; + } + return false; + } + + private function getPath(): string + { + return PKG_ROOT_PATH . '/zig'; + } +} diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php new file mode 100644 index 000000000..defb00ba2 --- /dev/null +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -0,0 +1,160 @@ + $dependency_overrides Override dependencies (e.g. ['php' => ['ext-gd', 'ext-curl']]) + * @param bool $include_suggests Include suggested packages in the resolution + * @param null|array &$why If provided, will be filled with a reverse dependency map + * @return array Resolved package names in order + */ + public static function resolve(array $packages, array $dependency_overrides = [], bool $include_suggests = false, ?array &$why = null): array + { + $dep_list = PackageConfig::getAll(); + $dep_list_clean = []; + // clear array for further step + foreach ($dep_list as $k => $v) { + $dep_list_clean[$k] = [ + 'depends' => PackageConfig::get($k, 'depends', []), + 'suggests' => PackageConfig::get($k, 'suggests', []), + ]; + } + + // apply dependency overrides + foreach ($dependency_overrides as $target_name => $deps) { + $dep_list_clean[$target_name]['depends'] = array_merge($dep_list_clean[$target_name]['depends'] ?? [], $deps); + } + + // mark suggests as depends + if ($include_suggests) { + foreach ($dep_list_clean as $pkg_name => $pkg_item) { + $dep_list_clean[$pkg_name]['depends'] = array_merge($pkg_item['depends'], $pkg_item['suggests']); + $dep_list_clean[$pkg_name]['suggests'] = []; + } + } + + $resolved = self::doVisitPlat($packages, $dep_list_clean); + + // Build reverse dependency map if $why is requested + if ($why !== null) { + $why = self::buildReverseDependencyMap($resolved, $dep_list_clean, $include_suggests); + } + + return $resolved; + } + + /** + * Build a reverse dependency map for the resolved packages. + * For each package that is depended upon, list which packages depend on it. + * + * @param array $resolved Resolved package names + * @param array $dep_list Dependency declaration list + * @param bool $include_suggests Whether suggests are treated as depends + * @return array Reverse dependency map [depended_pkg => [dependant1, dependant2, ...]] + */ + private static function buildReverseDependencyMap(array $resolved, array $dep_list, bool $include_suggests): array + { + $why = []; + $resolved_set = array_flip($resolved); + + foreach ($resolved as $pkg_name) { + $deps = $dep_list[$pkg_name]['depends'] ?? []; + if ($include_suggests) { + $deps = array_merge($deps, $dep_list[$pkg_name]['suggests'] ?? []); + } + + foreach ($deps as $dep) { + // Only include dependencies that are in the resolved set + if (isset($resolved_set[$dep])) { + if (!isset($why[$dep])) { + $why[$dep] = []; + } + $why[$dep][] = $pkg_name; + } + } + } + + return $why; + } + + /** + * Visitor pattern implementation for dependency resolution. + * + * @param Package[]|string[] $packages Packages list (input) + * @param array $dep_list Dependency declaration list + * @return array Resolved packages array + */ + private static function doVisitPlat(array $packages, array $dep_list): array + { + $sorted = []; + $visited = []; + foreach ($packages as $pkg) { + $pkg_name = is_string($pkg) ? $pkg : $pkg->getName(); + if (!isset($dep_list[$pkg_name])) { + throw new WrongUsageException("Package '{$pkg_name}' does not exist in config, please check your package name !"); + } + if (!isset($visited[$pkg_name])) { + self::visitPlatDeps($pkg_name, $dep_list, $visited, $sorted); + } + } + + $sorted_suggests = []; + $visited_suggests = []; + $final = []; + foreach ($packages as $pkg) { + $pkg_name = is_string($pkg) ? $pkg : $pkg->getName(); + if (!isset($visited_suggests[$pkg_name])) { + self::visitPlatAllDeps($pkg_name, $dep_list, $visited_suggests, $sorted_suggests); + } + } + foreach ($sorted_suggests as $suggest) { + if (in_array($suggest, $sorted)) { + $final[] = $suggest; + } + } + return $final; + } + + private static function visitPlatAllDeps(string $pkg_name, array $dep_list, array &$visited, array &$sorted): void + { + // 如果已经识别到了,那就不管 + if (isset($visited[$pkg_name])) { + return; + } + $visited[$pkg_name] = true; + // 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常) + foreach (array_merge($dep_list[$pkg_name]['depends'], $dep_list[$pkg_name]['suggests']) as $dep) { + self::visitPlatAllDeps($dep, $dep_list, $visited, $sorted); + } + $sorted[] = $pkg_name; + } + + private static function visitPlatDeps(string $pkg_name, array $dep_list, array &$visited, array &$sorted): void + { + // 如果已经识别到了,那就不管 + if (isset($visited[$pkg_name])) { + return; + } + $visited[$pkg_name] = true; + // 遍历该依赖的所有依赖(此处的 getLib 如果检测到当前库不存在的话,会抛出异常) + if (!isset($dep_list[$pkg_name])) { + throw new WrongUsageException("{$pkg_name} not exist !"); + } + foreach ($dep_list[$pkg_name]['depends'] as $dep) { + self::visitPlatDeps($dep, $dep_list, $visited, $sorted); + } + $sorted[] = $pkg_name; + } +} diff --git a/src/StaticPHP/Util/DirDiff.php b/src/StaticPHP/Util/DirDiff.php new file mode 100644 index 000000000..7f80f3640 --- /dev/null +++ b/src/StaticPHP/Util/DirDiff.php @@ -0,0 +1,95 @@ +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 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 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 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); + } +} diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php new file mode 100644 index 000000000..2f540d70d --- /dev/null +++ b/src/StaticPHP/Util/FileSystem.php @@ -0,0 +1,493 @@ +debug('Reading file: ' . $filename); + $r = file_get_contents(self::convertPath($filename)); + if ($r === false) { + throw new FileSystemException('Reading file ' . $filename . ' failed'); + } + return $r; + } + + /** + * Replace string content in file + * + * @param string $filename The file path + * @param mixed $search The search string + * @param mixed $replace The replacement string + * @return false|int Number of replacements or false on failure + */ + public static function replaceFileStr(string $filename, mixed $search = null, mixed $replace = null): false|int + { + return self::replaceFile($filename, REPLACE_FILE_STR, $search, $replace); + } + + /** + * Replace content in file using regex + * + * @param string $filename The file path + * @param mixed $search The regex pattern + * @param mixed $replace The replacement string + * @return false|int Number of replacements or false on failure + */ + public static function replaceFileRegex(string $filename, mixed $search = null, mixed $replace = null): false|int + { + return self::replaceFile($filename, REPLACE_FILE_PREG, $search, $replace); + } + + /** + * Replace content in file using custom callback + * + * @param string $filename The file path + * @param mixed $callback The callback function + * @return false|int Number of replacements or false on failure + */ + public static function replaceFileUser(string $filename, mixed $callback = null): false|int + { + return self::replaceFile($filename, REPLACE_FILE_USER, $callback); + } + + /** + * Get file extension from filename + * + * @param string $fn The filename + * @return string The file extension (without dot) + */ + public static function extname(string $fn): string + { + $parts = explode('.', basename($fn)); + if (count($parts) < 2) { + return ''; + } + return array_pop($parts); + } + + /** + * Find command path in system PATH (similar to which command) + * + * @param string $name The command name + * @param array $paths Optional array of paths to search + * @return null|string The full path to the command or null if not found + */ + public static function findCommandPath(string $name, array $paths = []): ?string + { + if (!$paths) { + $paths = explode(PATH_SEPARATOR, getenv('PATH')); + } + if (PHP_OS_FAMILY === 'Windows') { + foreach ($paths as $path) { + foreach (['.exe', '.bat', '.cmd'] as $suffix) { + if (file_exists($path . DIRECTORY_SEPARATOR . $name . $suffix)) { + return $path . DIRECTORY_SEPARATOR . $name . $suffix; + } + } + } + return null; + } + foreach ($paths as $path) { + if (file_exists($path . DIRECTORY_SEPARATOR . $name)) { + return $path . DIRECTORY_SEPARATOR . $name; + } + } + return null; + } + + /** + * Copy directory recursively + * + * @param string $from Source directory path + * @param string $to Destination directory path + */ + public static function copyDir(string $from, string $to): void + { + logger()->debug("Copying directory from {$from} to {$to}"); + $dst_path = FileSystem::convertPath($to); + $src_path = FileSystem::convertPath($from); + switch (PHP_OS_FAMILY) { + case 'Windows': + cmd(false)->exec('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i'); + break; + case 'Linux': + case 'Darwin': + case 'BSD': + f_passthru('cp -r "' . $src_path . '" "' . $dst_path . '"'); + break; + } + } + + /** + * Copy file from one location to another. + * This method will throw an exception if the copy operation fails. + * + * @param string $from Source file path + * @param string $to Destination file path + */ + public static function copy(string $from, string $to): bool + { + logger()->debug("Copying file from {$from} to {$to}"); + $dst_path = FileSystem::convertPath($to); + $src_path = FileSystem::convertPath($from); + if (!copy($src_path, $dst_path)) { + throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path); + } + return true; + } + + /** + * Convert path to system-specific format + * + * @param string $path The path to convert + * @return string The converted path + */ + public static function convertPath(string $path): string + { + if (str_starts_with($path, 'phar://')) { + return $path; + } + return str_replace('/', DIRECTORY_SEPARATOR, $path); + } + + /** + * Convert Windows path to MinGW format + * + * @param string $path The Windows path + * @return string The MinGW format path + */ + public static function convertWinPathToMinGW(string $path): string + { + if (preg_match('/^[A-Za-z]:/', $path)) { + $path = '/' . strtolower($path[0]) . '/' . str_replace('\\', '/', substr($path, 2)); + } + return $path; + } + + /** + * Scan directory files recursively + * + * @param string $dir Directory to scan + * @param bool $recursive Whether to scan recursively + * @param bool|string $relative Whether to return relative paths + * @param bool $include_dir Whether to include directories in result + * @return array|false Array of files or false on failure + */ + public static function scanDirFiles(string $dir, bool $recursive = true, bool|string $relative = false, bool $include_dir = false): array|false + { + $dir = self::convertPath($dir); + if (!is_dir($dir)) { + return false; + } + logger()->debug('scanning directory ' . $dir); + $scan_list = scandir($dir); + if ($scan_list === false) { + logger()->warning('Scan dir failed, cannot scan directory: ' . $dir); + return false; + } + $list = []; + // 将 relative 置为相对目录的前缀 + if ($relative === true) { + $relative = $dir; + } + // 遍历目录 + foreach ($scan_list as $v) { + // Unix 系统排除这俩目录 + if ($v == '.' || $v == '..') { + continue; + } + $sub_file = self::convertPath($dir . '/' . $v); + if (is_dir($sub_file) && $recursive) { + # 如果是 目录 且 递推 , 则递推添加下级文件 + $sub_list = self::scanDirFiles($sub_file, $recursive, $relative); + if (is_array($sub_list)) { + foreach ($sub_list as $item) { + $list[] = $item; + } + } + } elseif (is_file($sub_file) || (is_dir($sub_file) && !$recursive && $include_dir)) { + # 如果是 文件 或 (是 目录 且 不递推 且 包含目录) + if (is_string($relative) && mb_strpos($sub_file, $relative) === 0) { + $list[] = ltrim(mb_substr($sub_file, mb_strlen($relative)), '/\\'); + } elseif ($relative === false) { + $list[] = $sub_file; + } + } + } + return $list; + } + + /** + * Get PSR-4 classes from directory + * + * @param string $dir Directory to scan + * @param string $base_namespace Base namespace + * @param mixed $rule Optional filtering rule + * @param bool|string $return_path_value Whether to return path as value + * @param bool $auto_require Whether to auto-require files (useful for external plugins) + * @return array Array of class names or class=>path pairs + */ + public static function getClassesPsr4(string $dir, string $base_namespace, mixed $rule = null, bool|string $return_path_value = false, bool $auto_require = false): array + { + $classes = []; + $files = FileSystem::scanDirFiles($dir, true, true); + if ($files === false) { + throw new FileSystemException('Cannot scan dir files during get classes psr-4 from dir: ' . $dir); + } + foreach ($files as $v) { + $pathinfo = pathinfo($v); + if (($pathinfo['extension'] ?? '') == 'php') { + if ($rule === null) { + if (file_exists($dir . '/' . $pathinfo['basename'] . '.ignore')) { + continue; + } + if (mb_substr($pathinfo['basename'], 0, 7) == 'global_' || mb_substr($pathinfo['basename'], 0, 7) == 'script_') { + continue; + } + } elseif (is_callable($rule) && !$rule($dir, $pathinfo)) { + continue; + } + $dirname = $pathinfo['dirname'] == '.' ? '' : (str_replace('/', '\\', $pathinfo['dirname']) . '\\'); + $class_name = $base_namespace . '\\' . $dirname . $pathinfo['filename']; + $file_path = self::convertPath($dir . '/' . $v); + + // Auto-require file if class is not loaded (for external plugins not in composer autoload) + if ($auto_require && !class_exists($class_name, false)) { + require_once $file_path; + } + if (class_exists($class_name, false) === false) { + continue; + } + + if (is_string($return_path_value)) { + $classes[$class_name] = $return_path_value . '/' . $v; + } else { + $classes[] = $class_name; + } + } + } + return $classes; + } + + /** + * Remove directory recursively + * + * @param string $dir Directory to remove + * @return bool Success status + */ + public static function removeDir(string $dir): bool + { + $dir = self::convertPath($dir); + logger()->debug('Removing path recursively: "' . $dir . '"'); + if (!file_exists($dir)) { + logger()->debug('Scan dir failed, no such file or directory.'); + return false; + } + if (!is_dir($dir)) { + logger()->warning('Scan dir failed, not directory.'); + return false; + } + logger()->debug('scanning directory ' . $dir); + // 套上 zm_dir + $scan_list = scandir($dir); + if ($scan_list === false) { + logger()->warning('Scan dir failed, cannot scan directory: ' . $dir); + return false; + } + // 遍历目录 + foreach ($scan_list as $v) { + InteractiveTerm::advance(); + // Unix 系统排除这俩目录 + if ($v == '.' || $v == '..') { + continue; + } + $sub_file = self::convertPath($dir . '/' . $v); + if (is_dir($sub_file)) { + # 如果是 目录 且 递推 , 则递推添加下级文件 + if (!self::removeDir($sub_file)) { + return false; + } + } elseif (is_link($sub_file) || is_file($sub_file)) { + if (!unlink($sub_file)) { + $cmd = PHP_OS_FAMILY === 'Windows' ? 'del /f /q' : 'rm -f'; + f_exec("{$cmd} " . escapeshellarg($sub_file), $out, $ret); + if ($ret !== 0) { + logger()->warning('Remove file failed: ' . $sub_file); + return false; + } + } + } + } + if (is_link($dir)) { + return @unlink($dir); + } + return @rmdir($dir); + } + + /** + * Create directory recursively + * + * @param string $path Directory path to create + */ + public static function createDir(string $path): void + { + if (!is_dir($path) && !f_mkdir($path, 0755, true) && !is_dir($path)) { + throw new FileSystemException(sprintf('Unable to create dir: %s', $path)); + } + } + + /** + * Write content to file + * + * @param string $path File path + * @param mixed $content Content to write + * @param mixed ...$args Additional arguments passed to file_put_contents + * @return bool|int|string Result of file writing operation + */ + public static function writeFile(string $path, mixed $content, ...$args): bool|int|string + { + $dir = pathinfo(self::convertPath($path), PATHINFO_DIRNAME); + if (!is_dir($dir) && !mkdir($dir, 0755, true) && !is_dir($dir)) { + throw new FileSystemException('Write file failed, cannot create parent directory: ' . $dir); + } + return file_put_contents($path, $content, ...$args); + } + + /** + * Reset directory by removing and recreating it + * + * @param string $dir_name Directory name + */ + public static function resetDir(string $dir_name): void + { + $dir_name = self::convertPath($dir_name); + if (is_dir($dir_name)) { + self::removeDir($dir_name); + } + self::createDir($dir_name); + } + + /** + * Check if path is relative + * + * @param string $path Path to check + * @return bool True if path is relative + */ + public static function isRelativePath(string $path): bool + { + if (DIRECTORY_SEPARATOR === '\\') { + return !(strlen($path) > 2 && ctype_alpha($path[0]) && $path[1] === ':'); + } + return strlen($path) > 0 && $path[0] !== '/'; + } + + /** + * Replace path variables with actual values + * + * @param string $path Path with variables + * @return string Path with replaced variables + */ + public static function replacePathVariable(string $path): string + { + $replacement = [ + '{pkg_root_path}' => PKG_ROOT_PATH, + '{php_sdk_path}' => getenv('PHP_SDK_PATH') ? getenv('PHP_SDK_PATH') : WORKING_DIR . '/php-sdk-binary-tools', + '{working_dir}' => WORKING_DIR, + '{download_path}' => DOWNLOAD_PATH, + '{source_path}' => SOURCE_PATH, + ]; + return str_replace(array_keys($replacement), array_values($replacement), $path); + } + + /** + * Create backup of file + * + * @param string $path File path + * @return string Backup file path + */ + public static function backupFile(string $path): string + { + copy($path, $path . '.bak'); + return $path . '.bak'; + } + + /** + * Restore file from backup + * + * @param string $path Original file path + */ + public static function restoreBackupFile(string $path): void + { + if (!file_exists($path . '.bak')) { + throw new FileSystemException("Backup restore failed: Cannot find bak file for {$path}"); + } + copy($path . '.bak', $path); + unlink($path . '.bak'); + } + + /** + * Remove file if it exists + * + * @param string $string File path + */ + public static function removeFileIfExists(string $string): void + { + $string = self::convertPath($string); + if (file_exists($string)) { + unlink($string); + } + } + + /** + * Replace line in file that contains specific string + * + * @param string $file File path + * @param string $find String to find in line + * @param string $line New line content + * @return false|int Number of replacements or false on failure + */ + public static function replaceFileLineContainsString(string $file, string $find, string $line): false|int + { + $lines = file($file); + if ($lines === false) { + throw new FileSystemException('Cannot read file: ' . $file); + } + foreach ($lines as $key => $value) { + if (str_contains($value, $find)) { + $lines[$key] = $line . PHP_EOL; + } + } + return file_put_contents($file, implode('', $lines)); + } + + private static function replaceFile(string $filename, int $replace_type = REPLACE_FILE_STR, mixed $callback_or_search = null, mixed $to_replace = null): false|int + { + logger()->debug('Replacing file with type[' . $replace_type . ']: ' . $filename); + $file = self::readFile($filename); + switch ($replace_type) { + case REPLACE_FILE_STR: + default: + $file = str_replace($callback_or_search, $to_replace, $file); + break; + case REPLACE_FILE_PREG: + $file = preg_replace($callback_or_search, $to_replace, $file); + break; + case REPLACE_FILE_USER: + $file = $callback_or_search($file); + break; + } + return file_put_contents($filename, $file); + } +} diff --git a/src/StaticPHP/Util/GlobalEnvManager.php b/src/StaticPHP/Util/GlobalEnvManager.php new file mode 100644 index 000000000..86fcc6524 --- /dev/null +++ b/src/StaticPHP/Util/GlobalEnvManager.php @@ -0,0 +1,180 @@ + $v) { + if (getenv($k) === false) { + $default_put_list[$k] = $v; + self::putenv("{$k}={$v}"); + } + } + $os_ini = match (PHP_OS_FAMILY) { + 'Windows' => $ini['windows'] ?? [], + 'Darwin' => $ini['macos'] ?? [], + 'Linux' => $ini['linux'] ?? [], + 'BSD' => $ini['freebsd'] ?? [], + default => [], + }; + foreach ($os_ini as $k => $v) { + if (getenv($k) === false) { + $default_put_list[$k] = $v; + self::putenv("{$k}={$v}"); + } + } + + ToolchainManager::initToolchain(); + + // apply second time + $ini2 = self::readIniFile(); + + foreach ($ini2['global'] as $k => $v) { + if (isset($default_put_list[$k]) && $default_put_list[$k] !== $v) { + self::putenv("{$k}={$v}"); + } + } + $os_ini2 = match (PHP_OS_FAMILY) { + 'Windows' => $ini2['windows'] ?? [], + 'Darwin' => $ini2['macos'] ?? [], + 'Linux' => $ini2['linux'] ?? [], + 'BSD' => $ini2['freebsd'] ?? [], + default => [], + }; + foreach ($os_ini2 as $k => $v) { + if (isset($default_put_list[$k]) && $default_put_list[$k] !== $v) { + self::putenv("{$k}={$v}"); + } + } + self::$initialized = true; + } + + public static function putenv(string $val): void + { + f_putenv($val); + self::$env_cache[] = $val; + } + + public static function addPathIfNotExists(string $path): void + { + if (SystemTarget::isUnix() && !str_contains(getenv('PATH'), $path)) { + self::putenv("PATH={$path}:" . getenv('PATH')); + } elseif (SystemTarget::getTargetOS() === 'Windows' && !str_contains(getenv('PATH'), $path)) { + self::putenv("PATH={$path};" . getenv('PATH')); + } + } + + /** + * Initialize the toolchain after the environment variables are set. + * The toolchain or environment availability check is done here. + */ + public static function afterInit(): void + { + if (!filter_var(getenv('SPC_SKIP_TOOLCHAIN_CHECK'), FILTER_VALIDATE_BOOL)) { + ToolchainManager::afterInitToolchain(); + } + // test bison + if (PHP_OS_FAMILY === 'Darwin') { + if ($bison = MacOSUtil::findCommand('bison', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) { + self::putenv("BISON={$bison}"); + } + if ($yacc = MacOSUtil::findCommand('yacc', ['/opt/homebrew/opt/bison/bin', '/usr/local/opt/bison/bin'])) { + self::putenv("YACC={$yacc}"); + } + } + } + + private static function readIniFile(): array + { + // Init env.ini file, read order: + // WORKING_DIR/config/env.ini + // ROOT_DIR/config/env.ini + $ini_files = [ + WORKING_DIR . '/config/env.ini', + ROOT_DIR . '/config/env.ini', + ]; + $ini_custom = [ + WORKING_DIR . '/config/env.custom.ini', + ROOT_DIR . '/config/env.custom.ini', + ]; + $ini = null; + foreach ($ini_files as $ini_file) { + if (file_exists($ini_file)) { + $ini = parse_ini_file($ini_file, true); + break; + } + } + if ($ini === null) { + throw new WrongUsageException('env.ini not found'); + } + if ($ini === false || !isset($ini['global'])) { + throw new WrongUsageException('Failed to parse ' . $ini_file); + } + // apply custom env + foreach ($ini_custom as $ini_file) { + if (file_exists($ini_file)) { + $ini_custom = parse_ini_file($ini_file, true); + if ($ini_custom !== false) { + $ini['global'] = array_merge($ini['global'], $ini_custom['global'] ?? []); + match (PHP_OS_FAMILY) { + 'Windows' => $ini['windows'] = array_merge($ini['windows'], $ini_custom['windows'] ?? []), + 'Darwin' => $ini['macos'] = array_merge($ini['macos'], $ini_custom['macos'] ?? []), + 'Linux' => $ini['linux'] = array_merge($ini['linux'], $ini_custom['linux'] ?? []), + 'BSD' => $ini['freebsd'] = array_merge($ini['freebsd'], $ini_custom['freebsd'] ?? []), + default => null, + }; + } + break; + } + } + return $ini; + } +} diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php new file mode 100644 index 000000000..1682ed1f6 --- /dev/null +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -0,0 +1,140 @@ +getOption('no-ansi') ?? false; + $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + if ($output->isVerbose()) { + logger()->notice(strip_ansi_colors($message)); + } else { + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message)); + logger()->debug(strip_ansi_colors($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) ?? new ConsoleOutput(); + if ($output->isVerbose()) { + logger()->info(strip_ansi_colors($message)); + } else { + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message)); + logger()->debug(strip_ansi_colors($message)); + } + } + + public static function plain(string $message, string $level = 'info'): void + { + $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; + $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + if ($output->isVerbose()) { + match ($level) { + 'debug' => logger()->debug(strip_ansi_colors($message)), + 'notice' => logger()->notice(strip_ansi_colors($message)), + 'warning' => logger()->warning(strip_ansi_colors($message)), + 'error' => logger()->error(strip_ansi_colors($message)), + default => logger()->info(strip_ansi_colors($message)), + }; + } else { + $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) ?? new ConsoleOutput(); + if (!$output->isVerbose()) { + $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) ?? new ConsoleOutput(); + if ($output->isVerbose()) { + logger()->error(strip_ansi_colors($message)); + } else { + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message))); + logger()->debug(strip_ansi_colors($message)); + } + } + + public static function advance(): void + { + self::$indicator?->advance(); + } + + public static function setMessage(string $message): void + { + $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; + self::$indicator?->setMessage(($no_ansi ? 'strip_ansi_colors' : 'strval')($message)); + logger()->debug(strip_ansi_colors($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) ?? new ConsoleOutput(); + if ($output->isVerbose()) { + if ($status) { + logger()->info($message); + } else { + logger()->error($message); + } + return; + } + if (self::$indicator !== null) { + if (!$status) { + self::$indicator->finish($message, ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(' ✘'))); + } else { + self::$indicator->finish($message, ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(' ✔'))); + } + self::$indicator = null; + } + } + + public static function indicateProgress(string $message): void + { + $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; + $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + if ($output->isVerbose()) { + logger()->info(strip_ansi_colors($message)); + return; + } + if (self::$indicator !== null) { + // just reuse existing indicator, change + self::setMessage($message); + self::$indicator->advance(); + return; + } + logger()->debug(strip_ansi_colors($message)); + // 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); + } +} diff --git a/src/StaticPHP/Util/PkgConfigUtil.php b/src/StaticPHP/Util/PkgConfigUtil.php new file mode 100644 index 000000000..5efd33de9 --- /dev/null +++ b/src/StaticPHP/Util/PkgConfigUtil.php @@ -0,0 +1,124 @@ +no_php = $options['no_php'] ?? false; + $this->libs_only_deps = $options['libs_only_deps'] ?? false; + $this->absolute_libs = $options['absolute_libs'] ?? false; + } + + public function config(array $packages = [], bool $include_suggests = false): array + { + // if have php, make php as all extension's dependency + if (!$this->no_php) { + $dep_override = ['php' => array_filter($packages, fn ($y) => str_starts_with($y, 'ext-'))]; + } else { + $dep_override = []; + } + $resolved = DependencyResolver::resolve($packages, $dep_override, $include_suggests); + + $ldflags = $this->getLdflagsString(); + $cflags = $this->getIncludesString($resolved); + $libs = $this->getLibsString($resolved, !$this->absolute_libs); + + // additional OS-specific libraries (e.g. macOS -lresolv) + // embed + if ($extra_libs = SystemTarget::getRuntimeLibs()) { + $libs .= " {$extra_libs}"; + } + + $extra_env = getenv('SPC_EXTRA_LIBS'); + if (is_string($extra_env) && !empty($extra_env)) { + $libs .= " {$extra_env}"; + } + // package frameworks + if (SystemTarget::getTargetOS() === 'Darwin') { + $libs .= " {$this->getFrameworksString($resolved)}"; + } + // C++ + if ($this->hasCpp($resolved)) { + $libcpp = SystemTarget::getTargetOS() === 'Darwin' ? '-lc++' : '-lstdc++'; + $libs = str_replace($libcpp, '', $libs) . " {$libcpp}"; + } + + if ($this->libs_only_deps) { + // mimalloc must come first + if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $libs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $libs); + } + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces(getenv('LIBS') . ' ' . $libs), + ]; + } + + // embed + if (!$this->no_php) { + $libs = "-lphp {$libs} -lc"; + } + + $allLibs = getenv('LIBS') . ' ' . $libs; + + // mimalloc must come first + if (in_array('mimalloc', $resolved) && file_exists(BUILD_LIB_PATH . '/libmimalloc.a')) { + $allLibs = BUILD_LIB_PATH . '/libmimalloc.a ' . str_replace([BUILD_LIB_PATH . '/libmimalloc.a', '-lmimalloc'], ['', ''], $allLibs); + } + + return [ + 'cflags' => clean_spaces(getenv('CFLAGS') . ' ' . $cflags), + 'ldflags' => clean_spaces(getenv('LDFLAGS') . ' ' . $ldflags), + 'libs' => clean_spaces($allLibs), + ]; + } + + /** + * [Helper function] + * Get configuration for a specific extension(s) dependencies. + * + * @param array|PhpExtensionPackage $extension_packages Extension instance or list + * @return array{ + * cflags: string, + * ldflags: string, + * libs: string + * } + */ + public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array + { + if (!is_array($extension_packages)) { + $extension_packages = [$extension_packages]; + } + return $this->config( + packages: array_map(fn ($y) => $y->getName(), $extension_packages), + include_suggests: $include_suggests, + ); + } + + /** + * [Helper function] + * Get configuration for a specific library(s) dependencies. + * + * @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|LibraryPackage $lib, bool $include_suggests = false): array + { + if (!is_array($lib)) { + $lib = [$lib]; + } + $save_no_php = $this->no_php; + $this->no_php = true; + $save_libs_only_deps = $this->libs_only_deps; + $this->libs_only_deps = true; + $ret = $this->config( + 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; + return $ret; + } + + private function hasCpp(array $packages): bool + { + foreach ($packages as $package) { + $lang = PackageConfig::get($package, 'lang', 'c'); + if ($lang === 'cpp') { + return true; + } + } + return false; + } + + private function getIncludesString(array $packages): string + { + $base = BUILD_INCLUDE_PATH; + $includes = ["-I{$base}"]; + + // link with libphp + if (!$this->no_php) { + $includes = [ + ...$includes, + "-I{$base}/php", + "-I{$base}/php/main", + "-I{$base}/php/TSRM", + "-I{$base}/php/Zend", + "-I{$base}/php/ext", + ]; + } + + // parse pkg-configs + foreach ($packages as $package) { + $pc = PackageConfig::get($package, 'pkg-configs', []); + foreach ($pc as $file) { + if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$file}.pc")) { + throw new WrongUsageException("pkg-config file '{$file}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + } + } + $pc_cflags = implode(' ', $pc); + if ($pc_cflags !== '' && ($pc_cflags = PkgConfigUtil::getCflags($pc_cflags)) !== '') { + $arr = explode(' ', $pc_cflags); + $arr = array_unique($arr); + $arr = array_filter($arr, fn ($x) => !str_starts_with($x, 'SHELL:-Xarch_')); + $pc_cflags = implode(' ', $arr); + $includes[] = $pc_cflags; + } + } + $includes = array_unique($includes); + return implode(' ', $includes); + } + + private function getLdflagsString(): string + { + return '-L' . BUILD_LIB_PATH; + } + + private function getLibsString(array $packages, bool $use_short_libs = true): string + { + $lib_names = []; + $frameworks = []; + + foreach ($packages as $package) { + // add pkg-configs libs + $pkg_configs = PackageConfig::get($package, 'pkg-configs', []); + foreach ($pkg_configs as $pkg_config) { + if (!file_exists(BUILD_LIB_PATH . "/pkgconfig/{$pkg_config}.pc")) { + throw new WrongUsageException("pkg-config file '{$pkg_config}.pc' for lib [{$package}] does not exist in '" . BUILD_LIB_PATH . "/pkgconfig'. Please build it first."); + } + } + $pkg_configs = implode(' ', $pkg_configs); + if ($pkg_configs !== '') { + // static libs with dependencies come in reverse order, so reverse this too + $pc_libs = array_reverse(PkgConfigUtil::getLibsArray($pkg_configs)); + $lib_names = [...$lib_names, ...$pc_libs]; + } + // convert all static-libs to short names + $libs = array_reverse(PackageConfig::get($package, 'static-libs', [])); + foreach ($libs as $lib) { + 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; + } + } + // add frameworks for macOS + if (SystemTarget::getTargetOS() === 'Darwin') { + $frameworks = array_merge($frameworks, PackageConfig::get($package, 'frameworks', [])); + } + } + + // post-process + $lib_names = array_filter($lib_names, fn ($x) => $x !== ''); + $lib_names = array_reverse(array_unique($lib_names)); + $frameworks = array_unique($frameworks); + + // process frameworks to short_name + if (SystemTarget::getTargetOS() === 'Darwin') { + foreach ($frameworks as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $lib_names)) { + $lib_names[] = $ks; + } + } + } + + if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { + $lib_names[] = '-lcrypt'; + } + if (!$use_short_libs) { + $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); + } + return implode(' ', $lib_names); + } + + private function getShortLibName(string $lib): string + { + if (!str_starts_with($lib, 'lib') || !str_ends_with($lib, '.a')) { + return BUILD_LIB_PATH . '/' . $lib; + } + // get short name + return '-l' . substr($lib, 3, -2); + } + + private function getFullLibName(string $lib): string + { + if (!str_starts_with($lib, '-l')) { + return $lib; + } + $libname = substr($lib, 2); + $staticLib = BUILD_LIB_PATH . '/' . "lib{$libname}.a"; + if (file_exists($staticLib)) { + return $staticLib; + } + return $lib; + } + + private function getFrameworksString(array $extensions): string + { + $list = []; + foreach ($extensions as $extension) { + foreach (PackageConfig::get($extension, 'frameworks', []) as $fw) { + $ks = '-framework ' . $fw; + if (!in_array($ks, $list)) { + $list[] = $ks; + } + } + } + return implode(' ', $list); + } +} diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php new file mode 100644 index 000000000..6a16f041f --- /dev/null +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -0,0 +1,263 @@ +debug('Applying ' . ($reverse ? 'reverse ' : '') . "patch [{$patch_name}] at [{$cwd}]"); + if (FileSystem::isRelativePath($patch_name)) { + $patch_file = ROOT_DIR . "/src/globals/patch/{$patch_name}"; + } else { + $patch_file = $patch_name; + } + + if (!file_exists($patch_file)) { + throw new PatchException($patch_name, "Patch file [{$patch_file}] does not exist"); + } + + $patch_str = FileSystem::convertPath($patch_file); + if (!file_exists($patch_str)) { + throw new PatchException($patch_name, "Patch file [{$patch_str}] does not exist"); + } + + // Copy patch from phar + if (str_starts_with($patch_str, 'phar://')) { + $filename = pathinfo($patch_file, PATHINFO_BASENAME); + file_put_contents(SOURCE_PATH . "/{$filename}", file_get_contents($patch_file)); + $patch_str = FileSystem::convertPath(SOURCE_PATH . "/{$filename}"); + } + + // Detect if patch is already applied (reverse detection) + $detect_reverse = !$reverse; + $detect_cmd = 'cd ' . escapeshellarg($cwd) . ' && ' + . (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str) + . ' | patch --dry-run -p1 -s -f ' . ($detect_reverse ? '-R' : '') + . ' > ' . (PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null') . ' 2>&1'; + exec($detect_cmd, $output, $detect_status); + + if ($detect_status === 0) { + // Patch already applied + return true; + } + + // Apply patch + $apply_cmd = 'cd ' . escapeshellarg($cwd) . ' && ' + . (PHP_OS_FAMILY === 'Windows' ? 'type' : 'cat') . ' ' . escapeshellarg($patch_str) + . ' | patch -p1 ' . ($reverse ? '-R' : ''); + + exec($apply_cmd, $apply_output, $apply_status); + if ($apply_status !== 0) { + throw new PatchException($patch_name, "Patch file [{$patch_name}] failed to apply"); + } + + return true; + } + + /** + * Patch hardcoded INI values into PHP SAPI files. + * + * @param string $php_source_dir PHP source directory path + * @param array $ini Associative array of INI key-value pairs + * @return bool True if patch was applied successfully + */ + #[PatchDescription('Patch hardcoded INI values into PHP SAPI files')] + public static function patchHardcodedINI(string $php_source_dir, array $ini = []): bool + { + $sapi_files = [ + 'cli' => "{$php_source_dir}/sapi/cli/php_cli.c", + 'micro' => "{$php_source_dir}/sapi/micro/php_micro.c", + 'embed' => "{$php_source_dir}/sapi/embed/php_embed.c", + ]; + + // Build patch string + $find_str = 'const char HARDCODED_INI[] ='; + $patch_str = ''; + foreach ($ini as $key => $value) { + $patch_str .= "\"{$key}={$value}\\n\"\n"; + } + $patch_str = "const char HARDCODED_INI[] =\n{$patch_str}"; + + // Detect and restore from backup if exists + $has_backup = false; + foreach ($sapi_files as $file) { + if (file_exists("{$file}.bak")) { + $has_backup = true; + break; + } + } + if ($has_backup) { + self::unpatchHardcodedINI($php_source_dir); + } + + // Backup and patch each SAPI file + $result = true; + foreach ($sapi_files as $file) { + if (!file_exists($file)) { + continue; + } + // Backup + $result = $result && file_put_contents("{$file}.bak", file_get_contents($file)) !== false; + // Patch + FileSystem::replaceFileStr($file, $find_str, $patch_str); + } + + return $result; + } + + /** + * Restore PHP SAPI files from backup (unpatch hardcoded INI). + * + * @param string $php_source_dir PHP source directory path + * @return bool True if backup was restored successfully + */ + public static function unpatchHardcodedINI(string $php_source_dir): bool + { + $sapi_files = [ + 'cli' => "{$php_source_dir}/sapi/cli/php_cli.c", + 'micro' => "{$php_source_dir}/sapi/micro/php_micro.c", + 'embed' => "{$php_source_dir}/sapi/embed/php_embed.c", + ]; + + $has_backup = false; + foreach ($sapi_files as $file) { + if (file_exists("{$file}.bak")) { + $has_backup = true; + break; + } + } + + if (!$has_backup) { + return false; + } + + $result = true; + foreach ($sapi_files as $file) { + $backup = "{$file}.bak"; + if (file_exists($backup)) { + $result = $result && file_put_contents($file, file_get_contents($backup)) !== false; + @unlink($backup); + } + } + + 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'); + } + + public static function patchPhpSrc(?array $items = null): bool + { + $patch_dir = ROOT_DIR . '/src/globals/patch/php-src-patches'; + // in phar mode, we need to extract all the patch files + if (str_starts_with($patch_dir, 'phar://')) { + $tmp_dir = sys_get_temp_dir() . '/php-src-patches'; + FileSystem::createDir($tmp_dir); + foreach (FileSystem::scanDirFiles($patch_dir) as $file) { + FileSystem::writeFile("{$tmp_dir}/" . basename($file), file_get_contents($file)); + } + $patch_dir = $tmp_dir; + } + $php_package = PackageLoader::getTargetPackage('php'); + if (!file_exists("{$php_package->getSourceDir()}/sapi/micro/php_micro.c")) { + return false; + } + $ver_file = "{$php_package->getSourceDir()}/main/php_version.h"; + if (!file_exists($ver_file)) { + throw new PatchException('php-src patcher (original micro patches)', 'Patch failed, cannot find php source files'); + } + $version_h = FileSystem::readFile("{$php_package->getSourceDir()}/main/php_version.h"); + preg_match('/#\s*define\s+PHP_MAJOR_VERSION\s+(\d+)\s+#\s*define\s+PHP_MINOR_VERSION\s+(\d+)\s+/m', $version_h, $match); + // $ver = "{$match[1]}.{$match[2]}"; + + $major_ver = $match[1] . $match[2]; + if ($major_ver === '74') { + return false; + } + // $check = !defined('DEBUG_MODE') ? ' -q' : ''; + // f_passthru('cd ' . SOURCE_PATH . '/php-src && git checkout' . $check . ' HEAD'); + + if ($items !== null) { + $spc_micro_patches = $items; + } else { + $spc_micro_patches = getenv('SPC_MICRO_PATCHES'); + $spc_micro_patches = $spc_micro_patches === false ? [] : explode(',', $spc_micro_patches); + } + $spc_micro_patches = array_filter($spc_micro_patches, fn ($item) => trim((string) $item) !== ''); + $patch_list = $spc_micro_patches; + $patches = []; + $serial = ['80', '81', '82', '83', '84', '85']; + foreach ($patch_list as $patchName) { + if (file_exists("{$patch_dir}/{$patchName}.patch")) { + $patches[] = "{$patch_dir}/{$patchName}.patch"; + continue; + } + for ($i = array_search($major_ver, $serial, true); $i >= 0; --$i) { + $tryMajMin = $serial[$i]; + if (!file_exists("{$patch_dir}/{$patchName}_{$tryMajMin}.patch")) { + continue; + } + $patches[] = "{$patch_dir}/{$patchName}_{$tryMajMin}.patch"; + continue 2; + } + throw new PatchException('phpmicro patches', "Failed finding patch file or versioned file {$patchName} !"); + } + + foreach ($patches as $patch) { + logger()->info("Patching micro with {$patch}"); + self::patchFile($patch, $php_package->getSourceDir()); + } + + return true; + } +} diff --git a/src/StaticPHP/Util/System/LinuxUtil.php b/src/StaticPHP/Util/System/LinuxUtil.php new file mode 100644 index 000000000..79ad89ed7 --- /dev/null +++ b/src/StaticPHP/Util/System/LinuxUtil.php @@ -0,0 +1,162 @@ + 'unknown', + 'ver' => 'unknown', + 'family' => 'unknown', + ]; + switch (true) { + case file_exists('/etc/centos-release'): + $lines = file('/etc/centos-release'); + $centos = true; + goto rh; + case file_exists('/etc/redhat-release'): + $lines = file('/etc/redhat-release'); + $centos = false; + rh: + foreach ($lines as $line) { + if (preg_match('/release\s+(\d*(\.\d+)*)/', $line, $matches)) { + /* @phpstan-ignore-next-line */ + $ret['dist'] = $centos ? 'centos' : 'redhat'; + $ret['ver'] = $matches[1]; + } + } + break; + case file_exists('/etc/os-release'): + $lines = file('/etc/os-release'); + foreach ($lines as $line) { + 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]; + } + } + $ret['dist'] = trim($ret['dist'], '"\''); + $ret['ver'] = trim($ret['ver'], '"\''); + if (strcasecmp($ret['dist'], 'centos') === 0) { + $ret['dist'] = 'redhat'; + } + break; + } + return $ret; + } + + /** + * Check if current linux distro is musl-based (alpine). + */ + public static function isMuslDist(): bool + { + return static::getOSRelease()['dist'] === 'alpine'; + } + + /** + * Get CPU core count. + */ + public static function getCpuCount(): int + { + $ncpu = 1; + + if (is_file('/proc/cpuinfo')) { + $cpuinfo = file_get_contents('/proc/cpuinfo'); + preg_match_all('/^processor/m', $cpuinfo, $matches); + $ncpu = count($matches[0]); + } + + return $ncpu; + } + + /** + * Get fully-supported linux distros. + * + * @return string[] List of supported Linux distro name for doctor + */ + public static function getSupportedDistros(): array + { + return [ + // debian-like + 'debian', 'ubuntu', 'Deepin', 'neon', + // rhel-like + 'redhat', + // centos + 'centos', + // alpine + 'alpine', + // arch + 'arch', 'manjaro', + ]; + } + + /** + * 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. + */ + public static function getLibcVersionIfExists(?string $libc = null): ?string + { + if (self::$libc_version !== null) { + return self::$libc_version; + } + if ($libc === 'glibc') { + $result = shell()->execWithResult('ldd --version', false); + if ($result[0] !== 0) { + return null; + } + // get first line + $first_line = $result[1][0]; + // match ldd version: "ldd (some useless text) 2.17" match 2.17 + $pattern = '/ldd\s+\(.*?\)\s+(\d+\.\d+)/'; + if (preg_match($pattern, $first_line, $matches)) { + self::$libc_version = $matches[1]; + return self::$libc_version; + } + return null; + } + if ($libc === 'musl') { + if (self::isMuslDist()) { + $result = shell()->execWithResult('ldd 2>&1', false); + } elseif (is_file('/usr/local/musl/lib/libc.so')) { + $result = shell()->execWithResult('/usr/local/musl/lib/libc.so 2>&1', false); + } else { + $arch = php_uname('m'); + $result = shell()->execWithResult("/lib/ld-musl-{$arch}.so.1 2>&1", false); + } + // Match Version * line + // match ldd version: "Version 1.2.3" match 1.2.3 + $pattern = '/Version\s+(\d+\.\d+\.\d+)/'; + if (preg_match($pattern, $result[1][1] ?? '', $matches)) { + self::$libc_version = $matches[1]; + return self::$libc_version; + } + } + return null; + } +} diff --git a/src/StaticPHP/Util/System/MacOSUtil.php b/src/StaticPHP/Util/System/MacOSUtil.php new file mode 100644 index 000000000..5acd712f7 --- /dev/null +++ b/src/StaticPHP/Util/System/MacOSUtil.php @@ -0,0 +1,42 @@ + '--target=x86_64-apple-darwin', + 'arm64','aarch64' => '--target=arm64-apple-darwin', + default => throw new WrongUsageException('unsupported arch: ' . $arch), + }; + } +} diff --git a/src/StaticPHP/Util/System/UnixUtil.php b/src/StaticPHP/Util/System/UnixUtil.php new file mode 100644 index 000000000..8dd606188 --- /dev/null +++ b/src/StaticPHP/Util/System/UnixUtil.php @@ -0,0 +1,128 @@ +execWithResult($cmd); + if ($result[0] !== 0) { + throw new ExecutionException($cmd, 'Failed to get defined symbols from ' . $lib_file); + } + // parse shell output and filter + $defined = []; + foreach ($result[1] as $line) { + $line = trim($line); + if ($line === '' || str_ends_with($line, '.o:') || str_ends_with($line, '.o]:')) { + continue; + } + $name = strtok($line, " \t"); + if (!$name) { + continue; + } + $name = preg_replace('/@.*$/', '', $name); + if ($name !== '' && $name !== false) { + $defined[] = $name; + } + } + $defined = array_unique($defined); + sort($defined); + // export + if (SystemTarget::getTargetOS() === 'Linux') { + file_put_contents("{$lib_file}.dynsym", "{\n" . implode("\n", array_map(fn ($x) => " {$x};", $defined)) . "};\n"); + } else { + file_put_contents("{$lib_file}.dynsym", implode("\n", $defined) . "\n"); + } + } + + /** + * Get linker flag to export dynamic symbols from a static library. + * + * @param string $lib_file Static library file path (e.g. /path/to/libxxx.a) + * @return null|string Linker flag to export dynamic symbols, null if no .dynsym file found + */ + public static function getDynamicExportedSymbols(string $lib_file): ?string + { + $symbol_file = "{$lib_file}.dynsym"; + if (!is_file($symbol_file)) { + self::exportDynamicSymbols($lib_file); + } + if (!is_file($symbol_file)) { + throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available."); + } + // https://github.com/ziglang/zig/issues/24662 + if (ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + return '-Wl,--export-dynamic'; + } + // macOS + if (SystemTarget::getTargetOS() !== 'Linux') { + return "-Wl,-exported_symbols_list,{$symbol_file}"; + } + return "-Wl,--dynamic-list={$symbol_file}"; + } + + /** + * Find a command in given paths or system PATH. + * If $name is an absolute path, check if it exists. + * + * @param string $name Command name or absolute path + * @param array $paths Paths to search, if empty, use system PATH + * @return null|string Absolute path of the command if found, null otherwise + */ + public static function findCommand(string $name, array $paths = []): ?string + { + if (!$paths) { + $paths = explode(PATH_SEPARATOR, getenv('PATH')); + } + if (str_starts_with($name, '/')) { + return file_exists($name) ? $name : null; + } + foreach ($paths as $path) { + if (file_exists($path . DIRECTORY_SEPARATOR . $name)) { + return $path . DIRECTORY_SEPARATOR . $name; + } + } + return null; + } + + /** + * Make environment variable string for shell command. + * + * @param array $vars Variables, like: ["CFLAGS" => "-Ixxx"] + * @return string like: CFLAGS="-Ixxx" + */ + public static function makeEnvVarString(array $vars): string + { + $str = ''; + foreach ($vars as $key => $value) { + if ($str !== '') { + $str .= ' '; + } + $str .= $key . '=' . escapeshellarg($value); + } + return $str; + } +} diff --git a/src/StaticPHP/Util/System/WindowsUtil.php b/src/StaticPHP/Util/System/WindowsUtil.php new file mode 100644 index 000000000..a6df41564 --- /dev/null +++ b/src/StaticPHP/Util/System/WindowsUtil.php @@ -0,0 +1,109 @@ + $json[0]['installationVersion'], + 'major_version' => explode('.', $json[0]['installationVersion'])[0], + 'dir' => $json[0]['installationPath'], + ]; + } + + /** + * Get CPU count for concurrency. + */ + public static function getCpuCount(): int + { + $result = f_exec('echo %NUMBER_OF_PROCESSORS%', $out, $code); + if ($code !== 0 || !$result) { + return 1; + } + return intval($result); + } + + /** + * Create CMake toolchain file. + * + * @param null|string $cflags CFLAGS for cmake, default use '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP=' + * @param null|string $ldflags LDFLAGS for cmake, default use '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt' + */ + public static function makeCmakeToolchainFile(?string $cflags = null, ?string $ldflags = null): string + { + if ($cflags === null) { + $cflags = '/MT /Os /Ob1 /DNDEBUG /D_ACRTIMP= /D_CRTIMP='; + } + if ($ldflags === null) { + $ldflags = '/nodefaultlib:msvcrt /nodefaultlib:msvcrtd /defaultlib:libcmt'; + } + $buildroot = str_replace('\\', '\\\\', BUILD_ROOT_PATH); + $toolchain = <<getOption('with-suggested-libs')) { + $input->setOption('with-suggests', true); + } + if ($input->getOption('with-suggested-exts')) { + $input->setOption('with-suggests', true); + } + if ($input->getOption('with-libs')) { + $existing = $input->getOption('with-packages'); + $additional = $input->getOption('with-libs'); + if (!empty($existing)) { + $input->setOption('with-packages', $existing . ',' . $additional); + } else { + $input->setOption('with-packages', $additional); + } + } + } + + public static function getLegacyBuildOptions(): array + { + return [ + new InputOption('with-suggested-libs', null, InputOption::VALUE_NONE, 'Resolve and install suggested libraries as well (legacy)'), + new InputOption('with-suggested-exts', null, InputOption::VALUE_NONE, 'Resolve and install suggested extensions as well (legacy)'), + new InputOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries to install/build, comma separated (legacy)', ''), + ]; + } + + /** + * Add legacy build options for the 'php' target package. + */ + public static function addLegacyBuildOptionsForPhp(TargetPackage $package): void + { + if ($package->getName() === 'php') { + $package->addBuildOption('build-micro', null, null, 'Build micro SAPI'); + $package->addBuildOption('build-cli', null, null, 'Build cli SAPI'); + $package->addBuildOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)'); + $package->addBuildOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)'); + $package->addBuildOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)'); + $package->addBuildOption('build-cgi', null, null, 'Build cgi SAPI'); + $package->addBuildOption('build-all', null, null, 'Build all SAPI'); + } + } + + public static function beforeExtractHook(Artifact $artifact): void + { + self::emitPatchPoint(match ($artifact->getName()) { + 'php-src' => 'before-php-extract', + 'micro' => 'before-micro-extract', + default => '', + }); + } + + public static function afterExtractHook(Artifact $artifact): void + { + self::emitPatchPoint(match ($artifact->getName()) { + 'php-src' => 'after-php-extract', + 'micro' => 'after-micro-extract', + default => '', + }); + } + + public static function beforeExtsExtractHook(): void + { + self::emitPatchPoint('before-exts-extract'); + } + + public static function afterExtsExtractHook(): void + { + self::emitPatchPoint('after-exts-extract'); + } + + public static function beforeLibExtractHook(string $pkg): void + { + self::emitPatchPoint("before-library[{$pkg}]-extract"); + } + + public static function afterLibExtractHook(string $pkg): void + { + self::emitPatchPoint("after-library[{$pkg}]-extract"); + } + + public static function emitPatchPoint(string $point_name): void + { + if ($point_name === '') { + return; + } + if (!ApplicationContext::has(PackageInstaller::class)) { + return; + } + $builder = ApplicationContext::get(PackageBuilder::class); + $patch_points = $builder->getOption('with-added-patch', []); + ApplicationContext::set('patch_point', $point_name); + foreach ($patch_points as $patch_point) { + if (!file_exists($patch_point)) { + throw new WrongUsageException("Additional patch script {$patch_point} does not exist!"); + } + logger()->debug("Applying additional patch script {$patch_point}"); + + try { + require $patch_point; + } catch (InterruptException $e) { + if ($e->getCode() === 0) { + logger()->notice('Patch script ' . $patch_point . ' interrupted' . ($e->getMessage() ? (': ' . $e->getMessage()) : '.')); + } else { + logger()->error('Patch script ' . $patch_point . ' interrupted with error code [' . $e->getCode() . ']' . ($e->getMessage() ? (': ' . $e->getMessage()) : '.')); + } + } + } + ApplicationContext::set('patch_point', ''); + } +} diff --git a/src/bootstrap.php b/src/bootstrap.php new file mode 100644 index 000000000..6af814e8f --- /dev/null +++ b/src/bootstrap.php @@ -0,0 +1,64 @@ +addLogCallback(function ($level, $output) use ($log_file_fd) { + if ($log_file_fd) { + fwrite($log_file_fd, strip_ansi_colors($output) . "\n"); + } + return true; + }); +} + +// load internal registry +Registry::loadRegistry(ROOT_DIR . '/spc.registry.json'); +// load registries from environment variable SPC_REGISTRIES +Registry::loadFromEnvOrOption(); diff --git a/src/globals/defines.php b/src/globals/defines.php index 36ffba798..3e6d23605 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -2,8 +2,6 @@ declare(strict_types=1); -use ZM\Logger\ConsoleLogger; - define('WORKING_DIR', getcwd()); define('ROOT_DIR', dirname(__DIR__, 2)); putenv('WORKING_DIR=' . WORKING_DIR); @@ -79,22 +77,32 @@ const PKGCONF_PATCH_CUSTOM = 16; const PKGCONF_PATCH_ALL = 31; -// autoconf flags -const AUTOCONF_LIBS = 1; -const AUTOCONF_CFLAGS = 2; -const AUTOCONF_CPPFLAGS = 4; -const AUTOCONF_LDFLAGS = 8; -const AUTOCONF_ALL = 15; +// spc download status +const SPC_DOWNLOAD_STATUS_SKIPPED = 1; +const SPC_DOWNLOAD_STATUS_SUCCESS = 2; +const SPC_DOWNLOAD_STATUS_FAILED = 3; // spc download source type const SPC_SOURCE_ARCHIVE = 'archive'; // download as archive const SPC_SOURCE_GIT = 'git'; // download as git repository const SPC_SOURCE_LOCAL = 'local'; // download as local directory -// spc logs dir -const SPC_LOGS_DIR = WORKING_DIR . DIRECTORY_SEPARATOR . 'log'; -const SPC_OUTPUT_LOG = SPC_LOGS_DIR . DIRECTORY_SEPARATOR . 'spc.output.log'; -const SPC_SHELL_LOG = SPC_LOGS_DIR . DIRECTORY_SEPARATOR . 'spc.shell.log'; - -ConsoleLogger::$date_format = 'H:i:s'; -ConsoleLogger::$format = '[%date%] [%level_short%] %body%'; +const SPC_STATUS_EXTRACTED = 0; +const SPC_STATUS_INSTALLED = 0; +const SPC_STATUS_BUILT = 0; +const SPC_STATUS_ALREADY_EXTRACTED = 1; +const SPC_STATUS_ALREADY_INSTALLED = 1; +const SPC_STATUS_ALREADY_BUILT = 1; + +const SPC_DOWNLOAD_TYPE_DISPLAY_NAME = [ + 'bitbuckettag' => 'BitBucket', + 'filelist' => 'website', + 'git' => 'git', + 'ghrel' => 'GitHub release', + 'ghtar', 'ghtagtar' => 'GitHub tarball', + 'local' => 'local dir', + 'pie' => 'PHP Installer for Extensions', + 'url' => 'url', + 'php-release' => 'php.net', + 'custom' => 'custom downloader', +]; diff --git a/src/globals/functions.php b/src/globals/functions.php index c8c6a8d0f..93cd1ae09 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -2,14 +2,12 @@ declare(strict_types=1); -use Psr\Log\LoggerInterface; -use SPC\builder\BuilderBase; -use SPC\builder\BuilderProvider; -use SPC\exception\ExecutionException; -use SPC\exception\InterruptException; -use SPC\exception\WrongUsageException; -use SPC\util\shell\UnixShell; -use SPC\util\shell\WindowsCmd; +use StaticPHP\Exception\ExecutionException; +use StaticPHP\Exception\InterruptException; +use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Runtime\Shell\DefaultShell; +use StaticPHP\Runtime\Shell\UnixShell; +use StaticPHP\Runtime\Shell\WindowsCmd; use ZM\Logger\ConsoleLogger; /** @@ -31,7 +29,7 @@ function is_list_array(mixed $array): bool /** * Return a logger instance */ -function logger(): LoggerInterface +function logger(): ConsoleLogger { global $ob_logger; if ($ob_logger === null) { @@ -40,11 +38,6 @@ function logger(): LoggerInterface return $ob_logger; } -function is_unix(): bool -{ - return in_array(PHP_OS_FAMILY, ['Linux', 'Darwin', 'BSD']); -} - /** * Transfer architecture name to gnu triplet */ @@ -84,50 +77,22 @@ function quote(string $str, string $quote = '"'): string return $quote . $str . $quote; } -/** - * Get Family name of current OS. - */ -function osfamily2dir(): string -{ - return match (PHP_OS_FAMILY) { - /* @phpstan-ignore-next-line */ - 'Windows', 'WINNT', 'Cygwin' => 'windows', - 'Darwin' => 'macos', - 'Linux' => 'linux', - 'BSD' => 'freebsd', - default => throw new WrongUsageException('Not support os: ' . PHP_OS_FAMILY), - }; -} - -function osfamily2shortname(): string -{ - return match (PHP_OS_FAMILY) { - 'Windows' => 'win', - 'Darwin' => 'macos', - 'Linux' => 'linux', - 'BSD' => 'bsd', - default => throw new WrongUsageException('Not support os: ' . PHP_OS_FAMILY), - }; -} - function shell(?bool $debug = null): UnixShell { /* @noinspection PhpUnhandledExceptionInspection */ return new UnixShell($debug); } -function cmd(?bool $debug = null): WindowsCmd +function default_shell(): DefaultShell { /* @noinspection PhpUnhandledExceptionInspection */ - return new WindowsCmd($debug); + return new DefaultShell(); } -/** - * Get current builder. - */ -function builder(): BuilderBase +function cmd(?bool $debug = null): WindowsCmd { - return BuilderProvider::getBuilder(); + /* @noinspection PhpUnhandledExceptionInspection */ + return new WindowsCmd($debug); } /** @@ -135,7 +100,11 @@ function builder(): BuilderBase */ function patch_point(): string { - return BuilderProvider::getBuilder()->getPatchPoint(); + if (StaticPHP\DI\ApplicationContext::has('patch_point')) { + /* @phpstan-ignore-next-line */ + return StaticPHP\DI\ApplicationContext::get('patch_point'); + } + return ''; } function patch_point_interrupt(int $retcode, string $msg = ''): InterruptException @@ -272,6 +241,8 @@ function keyboard_interrupt_register(callable $callback): void if (PHP_OS_FAMILY === 'Windows') { sapi_windows_set_ctrl_handler($callback); } elseif (extension_loaded('pcntl')) { + global $_previous_sigint_handler; + $_previous_sigint_handler = pcntl_signal_get_handler(SIGINT); pcntl_signal(SIGINT, $callback); } } @@ -287,6 +258,12 @@ function keyboard_interrupt_unregister(): void if (PHP_OS_FAMILY === 'Windows') { sapi_windows_set_ctrl_handler(null); } elseif (extension_loaded('pcntl')) { + global $_previous_sigint_handler; + if ($_previous_sigint_handler !== null) { + pcntl_signal(SIGINT, $_previous_sigint_handler); + $_previous_sigint_handler = null; + return; + } pcntl_signal(SIGINT, SIG_IGN); } } @@ -294,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)); } /** @@ -317,3 +294,61 @@ function get_display_path(string $path): string } throw new WrongUsageException("Cannot convert path: {$path}"); } + +/** + * Get the global DI container instance. + * + * @deprecated Use ApplicationContext::getContainer() or dependency injection instead. + * This function is kept for backward compatibility during the migration period. + */ +function spc_container(): DI\Container +{ + return \StaticPHP\DI\ApplicationContext::getContainer(); +} + +/** + * Parse extension list from string, replace alias and filter internal extensions. + * + * @param null|array|string $ext_list Extension list, can be array or comma-separated string + * @return string[] List of extension names + */ +function parse_extension_list(array|string|null $ext_list): array +{ + // standardize and trim + $ext_list = parse_comma_list($ext_list); + // replace alias + $ls = array_map(function ($x) { + $lower = strtolower(trim($x)); + if (isset(SPC_EXTENSION_ALIAS[$lower])) { + logger()->debug("Extension [{$lower}] is an alias of [" . SPC_EXTENSION_ALIAS[$lower] . '], it will be replaced.'); + return SPC_EXTENSION_ALIAS[$lower]; + } + return $lower; + }, $ext_list); + // filter internals + return array_values(array_filter($ls, function ($x) { + if (in_array($x, SPC_INTERNAL_EXTENSIONS)) { + logger()->debug("Extension [{$x}] is an builtin extension, it will be ignored."); + return false; + } + return true; + })); +} + +/** + * Parse comma list from string. + * + * @param null|array|string $package_list Comma list, can be array or comma-separated string + * @return string[] List of items + */ +function parse_comma_list(array|string|null $package_list): array +{ + if (is_string($package_list)) { + $package_list = array_map('trim', array_filter(explode(',', $package_list))); + } + if (is_array($package_list)) { + // remove duplicates + return array_values(array_unique($package_list)); + } + return []; +} diff --git a/src/globals/internal-env.php b/src/globals/internal-env.php index 9f893df24..97df88417 100644 --- a/src/globals/internal-env.php +++ b/src/globals/internal-env.php @@ -2,14 +2,24 @@ declare(strict_types=1); -use SPC\builder\freebsd\SystemUtil as BSDSystemUtil; -use SPC\builder\linux\SystemUtil as LinuxSystemUtil; -use SPC\builder\macos\SystemUtil as MacOSSystemUtil; -use SPC\builder\windows\SystemUtil as WindowsSystemUtil; -use SPC\ConsoleApplication; -use SPC\store\FileSystem; - // static-php-cli version string +use Laravel\Prompts\ConfirmPrompt; +use Laravel\Prompts\Prompt; +use Laravel\Prompts\TextPrompt; +use StaticPHP\ConsoleApplication; +use StaticPHP\DI\ApplicationContext; +use StaticPHP\Util\FileSystem; +use StaticPHP\Util\System\LinuxUtil; +use StaticPHP\Util\System\MacOSUtil; +use StaticPHP\Util\System\WindowsUtil; +use Symfony\Component\Console\Helper\QuestionHelper; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\Console\Question\Question; + const SPC_VERSION = ConsoleApplication::VERSION; // output path for everything, other paths are defined relative to this by default define('BUILD_ROOT_PATH', FileSystem::convertPath(is_string($a = getenv('BUILD_ROOT_PATH')) ? $a : (WORKING_DIR . '/buildroot'))); @@ -23,16 +33,15 @@ define('BUILD_MODULES_PATH', FileSystem::convertPath(is_string($a = getenv('BUILD_MODULES_PATH')) ? $a : (BUILD_ROOT_PATH . '/modules'))); // pkg arch name -$_pkg_arch_name = arch2gnu(php_uname('m')) . '-' . strtolower(PHP_OS_FAMILY); +$_pkg_arch_name = getenv('EMULATE_PLATFORM') ?: (arch2gnu(php_uname('m')) . '-' . strtolower(PHP_OS_FAMILY)); define('PKG_ROOT_PATH', FileSystem::convertPath(is_string($a = getenv('PKG_ROOT_PATH')) ? $a : (WORKING_DIR . "/pkgroot/{$_pkg_arch_name}"))); define('SOURCE_PATH', FileSystem::convertPath(is_string($a = getenv('SOURCE_PATH')) ? $a : (WORKING_DIR . '/source'))); define('DOWNLOAD_PATH', FileSystem::convertPath(is_string($a = getenv('DOWNLOAD_PATH')) ? $a : (WORKING_DIR . '/downloads'))); define('CPU_COUNT', match (PHP_OS_FAMILY) { - 'Windows' => (string) WindowsSystemUtil::getCpuCount(), - 'Darwin' => (string) MacOSSystemUtil::getCpuCount(), - 'Linux' => (string) LinuxSystemUtil::getCpuCount(), - 'BSD' => (string) BSDSystemUtil::getCpuCount(), + 'Windows' => (string) WindowsUtil::getCpuCount(), + 'Darwin' => (string) MacOSUtil::getCpuCount(), + 'Linux' => (string) LinuxUtil::getCpuCount(), default => 1, }); define('GNU_ARCH', arch2gnu(php_uname('m'))); @@ -41,6 +50,11 @@ default => $_im8a }); +// logs dir +define('SPC_LOGS_DIR', FileSystem::convertPath(is_string($a = getenv('SPC_LOGS_DIR')) ? $a : (WORKING_DIR . '/log'))); +const SPC_OUTPUT_LOG = SPC_LOGS_DIR . DIRECTORY_SEPARATOR . 'spc.output.log'; +const SPC_SHELL_LOG = SPC_LOGS_DIR . DIRECTORY_SEPARATOR . 'spc.shell.log'; + // deprecated variables define('SEPARATED_PATH', [ '/' . pathinfo(BUILD_LIB_PATH)['basename'], // lib @@ -61,3 +75,31 @@ putenv('SPC_ARCH=' . php_uname('m')); putenv('GNU_ARCH=' . GNU_ARCH); putenv('MAC_ARCH=' . MAC_ARCH); + +// initialize windows prompt fallback for laravel-prompts +Prompt::fallbackWhen(PHP_OS_FAMILY === 'Windows'); +ConfirmPrompt::fallbackUsing(function (ConfirmPrompt $prompt) { + $helper = new QuestionHelper(); + $case = $prompt->default ? ' [Y/n] ' : ' [y/N] '; + $question = new ConfirmationQuestion($prompt->label . $case, $prompt->default); + if (ApplicationContext::has(InputInterface::class) && ApplicationContext::has(OutputInterface::class)) { + $input = ApplicationContext::get(InputInterface::class); + $output = ApplicationContext::get(OutputInterface::class); + } else { + $input = new ArrayInput([]); + $output = new ConsoleOutput(); + } + return $helper->ask($input, $output, $question); +}); +TextPrompt::fallbackUsing(function (TextPrompt $prompt) { + $helper = new QuestionHelper(); + $question = new Question($prompt->label . ' ', $prompt->default); + if (ApplicationContext::has(InputInterface::class) && ApplicationContext::has(OutputInterface::class)) { + $input = ApplicationContext::get(InputInterface::class); + $output = ApplicationContext::get(OutputInterface::class); + } else { + $input = new ArrayInput([]); + $output = new ConsoleOutput(); + } + return $helper->ask($input, $output, $question); +}); diff --git a/src/globals/patch/php-src-patches/Readme.md b/src/globals/patch/php-src-patches/Readme.md new file mode 100644 index 000000000..152cf6860 --- /dev/null +++ b/src/globals/patch/php-src-patches/Readme.md @@ -0,0 +1,95 @@ + +# 补丁 / Patches + +名称 Name | 平台 Platform | 可选? Optional? | 用途 Usage +--- | --- | --- | --- +phar | * | 可选 Optional | 允许micro使用压缩phar Allow micro use compressed phar +static_opcache | * | 可选 Optional | 支持静态构建opcache Support build opcache statically +macos_iconv | macOS | 可选 Optional | 支持链接到系统的iconv Support link against system iconv +static_extensions_win32 | Windows | 可选 Optional | 支持静态构建Windows其他扩展 Support build other extensions for windows +cli_checks | * | 可选 Optional | 修改PHP内核中硬编码的SAPI检查 Modify hardcoden SAPI name checks in PHP core +disable_huge_page | Linux | 可选 Optional | 禁用linux构建的max-page-size选项,缩减sfx体积(典型的, 10M+ -> 5M) Disable max-page-size for linux build,shrink sfx size (10M+ -> 5M typ.) +vcruntime140 | Windows | 必须 Nessesary | 禁用sfx启动时GetModuleHandle(vcruntime140(d).dll) Disable GetModuleHandle(vcruntime140(d).dll) at sfx start +win32 | Windows | 必须 Nessesary | 修改构建系统以静态构建 Modify build system for build sfx file +zend_stream | Windows | 必须 Nessesary | 修改构建系统以静态构建 Modify build system for build sfx file +comctl32 | Windows | 可选 Optional | 添加comctl32.dll manifest以启用[visual style](https://learn.microsoft.com/en-us/windows/win32/controls/visual-styles-overview) (会让窗口控件好看一些) Add manifest dependency for comctl32 to enable [visual style](https://learn.microsoft.com/en-us/windows/win32/controls/visual-styles-overview) (makes window control looks modern) +win32_api | Windows | 必须 Necessary | 修复一些win32 api的声明 Fix declarations of some win32 apis + +## Usage + +目前补丁不需要特定顺序,使用 + +```bash +# 在PHP源码目录 +patch -p1 < sapi/micro/patches/some_patch.patch +``` + +来打patch + +Currently, patches do not require a specific order. Use + +```bash +# at PHP source root +patch -p1 < sapi/micro/patches/some_patch.patch +``` + +to apply the patch. + +### version choose + +patch文件名为\<名称\>.patch或者\<名称\>_\<版本\>.patch,如果没有版本号,说明这个补丁支持所有目前micro支持的PHP版本 + +Patch file name is \.patch or \_\.patch. If there is no version number, it means that the patch supports all PHP versions that micro supports. + +选择等于或者低于要打补丁的PHP版本的最新版本的patch,例如要给php 8.2打patch,有 80 81 84 三个patch, 则选择81 + +Choose the latest patch that is equal to or lower than the PHP version you want to patch. For example, if you want to patch PHP 8.2, and there are patches 80 81 84, choose 81. + +所有的补丁都是给最新的修正版本使用的 + +All patches are applied to the latest patch version of its minor version. + +## Something special + +### phar.patch + +这个patch绕过PHAR对micro的文件名中包含".phar"的限制(并不会允许micro本身以外的其他文件),这使得micro文件名中不含".phar"时依然可以使用压缩过的phar + +This patch bypasses the restriction that a PHAR file must contain '.phar' in its filename when invoked with micro (it will not allow files other than the sfx to be regarded as phar). This allows micro to handle compressed phar files without a custom stub. + +有特别的stub的PHAR不需要这个补丁也可以使用 + +phar with a stub (may be a special one) do not need this patch. + +这个补丁只能在micro中使用,会导致其他SAPI编译不过 + +This patch can only be used with micro, as it causes other SAPIs to fail to build. + +### static_opcache + +静态链接opcache到PHP里,可以在其他的SAPI上用 + +This makes opcache statically linked into PHP, and it can be used for other SAPIs. + +PHP 8.3.11, 8.2.23中,opcache的config.m4发生了[变动](https://github.com/php/php-src/commit/d20d11375fa602236e1fb828f6a2236b19b43cdc),这个patch对应变动后的版本 + +The opcache's config.m4 has [changed](https://github.com/php/php-src/commit/d20d11375fa602236e1fb828f6a2236b19b43cdc) in PHP 8.3.11 and 8.2.23, and this patch corresponds to the updated version. + +### cli_checks + +绕过许多硬编码的“是不是cli”的检查 + +This bypasses many hard-coded cli SAPI name checks. + +### cli_static + +允许Windows的cli静态构建,不是给micro用的 + +This allows the Windows cli SAPI to be built fully statically. It is not a patch for micro. + +### win32_api + +修复一些win32 api的声明,避免编译警告。这些修改已经在新版本 PHP (>=8.4)中合并,但保证旧版本也能用,这些补丁仍然需要 + +This fixes declarations of some win32 apis to avoid compilation warnings. These changes have been merged into newer versions of PHP (>=8.4), but to ensure that older versions can still be used, these patches are still needed. + diff --git a/src/globals/patch/php-src-patches/cli_checks_80.patch b/src/globals/patch/php-src-patches/cli_checks_80.patch new file mode 100644 index 000000000..ff7fd4364 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_80.patch @@ -0,0 +1,174 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index bc5a6b2e23..710515b6c1 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -531,7 +531,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index fc8bb9a1b0..2fd083d912 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -4935,7 +4935,7 @@ ZEND_MINIT_FUNCTION(ffi) + + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; + + INIT_NS_CLASS_ENTRY(ce, "FFI", "Exception", NULL); + zend_ffi_exception_ce = zend_register_internal_class_ex(&ce, zend_ce_error); +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index c195ad7d2c..eef18fd10a 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2622,7 +2622,7 @@ static inline int accel_find_sapi(void) + } + } + if (ZCG(accel_directives).enable_cli && ( +- strcmp(sapi_module.name, "cli") == 0 ++ strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0)) { + return SUCCESS; + } +@@ -2916,7 +2916,7 @@ static int accel_startup(zend_extension *extension) + + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && +- (strcmp(sapi_module.name, "cli") == 0 || ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -2928,7 +2928,7 @@ static int accel_startup(zend_extension *extension) + if (accel_find_sapi() == FAILURE) { + accel_startup_ok = 0; + if (!ZCG(accel_directives).enable_cli && +- strcmp(sapi_module.name, "cli") == 0) { ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + zps_startup_failure("Opcode Caching is disabled for CLI", NULL, accelerator_remove_cb); + } else { + zps_startup_failure("Opcode Caching is only supported in Apache, FPM, FastCGI and LiteSpeed SAPIs", NULL, accelerator_remove_cb); +diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c +index d9b9d94c6f..744c715b38 100644 +--- a/ext/pcre/php_pcre.c ++++ b/ext/pcre/php_pcre.c +@@ -291,7 +291,7 @@ static PHP_GINIT_FUNCTION(pcre) /* {{{ */ + + /* If we're on the CLI SAPI, there will only be one request, so we don't need the + * cache to survive after RSHUTDOWN. */ +- pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0; ++ pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; + if (!pcre_globals->per_request_cache) { + zend_hash_init(&pcre_globals->pcre_cache, 0, NULL, php_free_pcre_cache, 1); + } +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 2930796ae7..20ad2706c7 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -721,7 +721,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if (0 == strncmp("cli", sapi_module.name, 3) || 0 == strncmp("micro", sapi_module.name, 5)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index cd91e68fd3..f270eb5a15 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -400,7 +400,7 @@ PHP_METHOD(SQLite3, loadExtension) + + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && +- (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "cli") != 0) && (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index c5743c3361..b2dd79f5c4 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") && strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index 03b55c3eac..5bb0472f76 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1136,7 +1136,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index 7bd5400760..f0a71d7915 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -480,7 +480,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(tmp_value, tmp_value_length); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +diff --git a/win32/console.c b/win32/console.c +index 7833dd97d3..1fa8e4cea9 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return !strncmp(sapi_module.name, "cli", sizeof("cli") - 1) || !strncmp(sapi_module.name, "micro", sizeof("micro") - 1); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_checks_81.patch b/src/globals/patch/php-src-patches/cli_checks_81.patch new file mode 100644 index 000000000..92d6cce5a --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_81.patch @@ -0,0 +1,183 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index cfe344e377..7e1a5ca54f 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -531,7 +531,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index 8f05686367..c155028233 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -5247,7 +5247,7 @@ ZEND_MINIT_FUNCTION(ffi) + { + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; + + zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); + +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 5f6b854d47..2b8362c412 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2830,7 +2830,7 @@ static inline int accel_find_sapi(void) + } + if (ZCG(accel_directives).enable_cli && ( + strcmp(sapi_module.name, "cli") == 0 +- || strcmp(sapi_module.name, "phpdbg") == 0)) { ++ || strcmp(sapi_module.name, "phpdbg") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + return SUCCESS; + } + } +@@ -3128,7 +3128,7 @@ static int accel_startup(zend_extension *extension) + + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && +- (strcmp(sapi_module.name, "cli") == 0 || ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -3140,7 +3140,7 @@ static int accel_startup(zend_extension *extension) + if (accel_find_sapi() == FAILURE) { + accel_startup_ok = 0; + if (!ZCG(accel_directives).enable_cli && +- strcmp(sapi_module.name, "cli") == 0) { ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + zps_startup_failure("Opcode Caching is disabled for CLI", NULL, accelerator_remove_cb); + } else { + zps_startup_failure("Opcode Caching is only supported in Apache, FPM, FastCGI and LiteSpeed SAPIs", NULL, accelerator_remove_cb); +diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c +index a8d3559ef5..1b40f94643 100644 +--- a/ext/pcre/php_pcre.c ++++ b/ext/pcre/php_pcre.c +@@ -291,7 +291,7 @@ static PHP_GINIT_FUNCTION(pcre) /* {{{ */ + + /* If we're on the CLI SAPI, there will only be one request, so we don't need the + * cache to survive after RSHUTDOWN. */ +- pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0; ++ pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; + if (!pcre_globals->per_request_cache) { + zend_hash_init(&pcre_globals->pcre_cache, 0, NULL, php_free_pcre_cache, 1); + } +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 8bf5d23df7..9af99ada0b 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -735,7 +735,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if ((strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) || 0 == strcmp("micro", sapi_module.name)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index 007eef7a74..86d75103c8 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -399,7 +399,7 @@ PHP_METHOD(SQLite3, loadExtension) + + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && +- (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "cli") != 0) && (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index 4287045511..eab0311d07 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index a57e66bd97..e9044cd34e 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1135,7 +1135,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index dc705fcdbd..a206aa11e4 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -475,7 +475,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(temporary_value); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +@@ -1340,7 +1340,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c + } + } else { + /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ +- if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) && ++ if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")) && + PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR + ) { + fprintf(stderr, "%s: %s in %s on line %" PRIu32 "\n", error_type_str, ZSTR_VAL(message), ZSTR_VAL(error_filename), error_lineno); +diff --git a/win32/console.c b/win32/console.c +index 9b48561088..a2b764cdb5 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return (strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1)) || 0 == strcmp(sapi_module.name, "micro"); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_checks_83.patch b/src/globals/patch/php-src-patches/cli_checks_83.patch new file mode 100644 index 000000000..e94250958 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_83.patch @@ -0,0 +1,192 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index dc8f9fefa3..057d76229e 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -530,7 +530,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index bbfe07576e..398373d577 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -5402,7 +5402,7 @@ ZEND_MINIT_FUNCTION(ffi) + { + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; + + zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); + +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index a21c640d91..3af0e89b21 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2822,7 +2822,7 @@ static inline zend_result accel_find_sapi(void) + } + if (ZCG(accel_directives).enable_cli && ( + strcmp(sapi_module.name, "cli") == 0 +- || strcmp(sapi_module.name, "phpdbg") == 0)) { ++ || strcmp(sapi_module.name, "phpdbg") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + return SUCCESS; + } + } +@@ -3127,7 +3127,7 @@ static int accel_startup(zend_extension *extension) + + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && +- (strcmp(sapi_module.name, "cli") == 0 || ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -3139,7 +3139,7 @@ static int accel_startup(zend_extension *extension) + if (accel_find_sapi() == FAILURE) { + accel_startup_ok = false; + if (!ZCG(accel_directives).enable_cli && +- strcmp(sapi_module.name, "cli") == 0) { ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + zps_startup_failure("Opcode Caching is disabled for CLI", NULL, accelerator_remove_cb); + } else { + zps_startup_failure("Opcode Caching is only supported in Apache, FPM, FastCGI, FrankenPHP, LiteSpeed and uWSGI SAPIs", NULL, accelerator_remove_cb); +@@ -4681,7 +4681,7 @@ static zend_result accel_finish_startup_preload_subprocess(pid_t *pid) + if (!ZCG(accel_directives).preload_user + || !*ZCG(accel_directives).preload_user) { + +- bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 ++ bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0); + + if (!sapi_requires_preload_user) { +diff --git a/ext/pcre/php_pcre.c b/ext/pcre/php_pcre.c +index 6ad0b6eb76..7c9861678f 100644 +--- a/ext/pcre/php_pcre.c ++++ b/ext/pcre/php_pcre.c +@@ -300,7 +300,7 @@ static PHP_GINIT_FUNCTION(pcre) /* {{{ */ + + /* If we're on the CLI SAPI, there will only be one request, so we don't need the + * cache to survive after RSHUTDOWN. */ +- pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0; ++ pcre_globals->per_request_cache = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; + if (!pcre_globals->per_request_cache) { + zend_hash_init(&pcre_globals->pcre_cache, 0, NULL, php_free_pcre_cache, 1); + } +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 8fbe93d648..3c14946e58 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -736,7 +736,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if ((strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) || 0 == strcmp("micro", sapi_module.name)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index badcfcc29b..70d4d5423e 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -402,7 +402,7 @@ PHP_METHOD(SQLite3, loadExtension) + + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && +- (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "cli") != 0) && (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, 0, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index 8926485025..6740163bc5 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const char *pa + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index 2d4cb42b7a..726d995dc0 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1280,7 +1280,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index 3c9c55129e..cb8fb42eea 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -486,7 +486,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(temporary_value); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +@@ -1367,7 +1367,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c + } + } else { + /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ +- if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) && ++ if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")) && + PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR + ) { + fprintf(stderr, "%s: ", error_type_str); +diff --git a/win32/console.c b/win32/console.c +index 9b48561088..a2b764cdb5 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return (strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1)) || 0 == strcmp(sapi_module.name, "micro"); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_checks_84.patch b/src/globals/patch/php-src-patches/cli_checks_84.patch new file mode 100644 index 000000000..6b8ac74e1 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_84.patch @@ -0,0 +1,191 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index dc8f9fefa3..057d76229e 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -530,7 +530,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index d797f5f93f..27cb05e3e4 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -5403,7 +5403,7 @@ ZEND_MINIT_FUNCTION(ffi) + { + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; + + zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); + +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 8d45b2ae41..35e9403a31 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2822,7 +2822,7 @@ static inline zend_result accel_find_sapi(void) + } + if (ZCG(accel_directives).enable_cli && ( + strcmp(sapi_module.name, "cli") == 0 +- || strcmp(sapi_module.name, "phpdbg") == 0)) { ++ || strcmp(sapi_module.name, "phpdbg") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + return SUCCESS; + } + } +@@ -3134,7 +3134,7 @@ static int accel_startup(zend_extension *extension) + + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && +- (strcmp(sapi_module.name, "cli") == 0 || ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -3146,7 +3146,7 @@ static int accel_startup(zend_extension *extension) + if (accel_find_sapi() == FAILURE) { + accel_startup_ok = false; + if (!ZCG(accel_directives).enable_cli && +- strcmp(sapi_module.name, "cli") == 0) { ++ (strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0)) { + zps_startup_failure("Opcode Caching is disabled for CLI", NULL, accelerator_remove_cb); + } else { + zps_startup_failure("Opcode Caching is only supported in Apache, FPM, FastCGI, FrankenPHP, LiteSpeed and uWSGI SAPIs", NULL, accelerator_remove_cb); +@@ -4685,7 +4685,7 @@ static zend_result accel_finish_startup_preload_subprocess(pid_t *pid) + if (!ZCG(accel_directives).preload_user + || !*ZCG(accel_directives).preload_user) { + +- bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 ++ bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0); + + if (!sapi_requires_preload_user) { +diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c +index 49a477998b..18fe71cce4 100644 +--- a/ext/pdo_sqlite/pdo_sqlite.c ++++ b/ext/pdo_sqlite/pdo_sqlite.c +@@ -94,6 +94,7 @@ PHP_METHOD(Pdo_Sqlite, loadExtension) + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && + (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { + zend_throw_exception_ex(php_pdo_get_exception(), 0, "Not supported in multithreaded Web servers"); +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 80ddd88f7d..7b37aaff0b 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -730,7 +730,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if ((strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) || 0 == strcmp("micro", sapi_module.name)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index 0f26593b85..5f253f0061 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -408,7 +408,7 @@ PHP_METHOD(SQLite3, loadExtension) + + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && +- (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "cli") != 0) && (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, 0, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index a5581d9ccc..98455f7b52 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index d935f9d216..bac478144d 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1274,7 +1274,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index a3acaf94b7..7ac56f9919 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -486,7 +486,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(temporary_value); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +@@ -1371,7 +1371,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c + } + } else { + /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ +- if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) && ++ if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")) && + PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR + ) { + fprintf(stderr, "%s: ", error_type_str); +diff --git a/win32/console.c b/win32/console.c +index 9b48561088..a2b764cdb5 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return (strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1)) || 0 == strcmp(sapi_module.name, "micro"); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_checks_85.patch b/src/globals/patch/php-src-patches/cli_checks_85.patch new file mode 100644 index 000000000..cfd72550c --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_85.patch @@ -0,0 +1,178 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index 4c8fc9d1..8284ac2f 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -535,7 +535,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index 10fc11f5..eb4d4175 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -5478,7 +5478,7 @@ ZEND_MINIT_FUNCTION(ffi) + { + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 1; + + zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); + +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index f597df36..ec617af7 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -2847,6 +2847,7 @@ static void zps_startup_failure(const char *reason, const char *api_reason, int + static inline bool accel_sapi_is_cli(void) + { + return strcmp(sapi_module.name, "cli") == 0 ++ || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0; + } + +@@ -3163,6 +3164,7 @@ static int accel_startup(zend_extension *extension) + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && + (strcmp(sapi_module.name, "cli") == 0 || ++ strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -4958,6 +4960,7 @@ static zend_result accel_finish_startup_preload_subprocess(pid_t *pid) + || !*ZCG(accel_directives).preload_user) { + + bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 ++ || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0); + + if (!sapi_requires_preload_user) { +diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c +index 023e35a2..6f00159a 100644 +--- a/ext/pdo_sqlite/pdo_sqlite.c ++++ b/ext/pdo_sqlite/pdo_sqlite.c +@@ -94,6 +94,7 @@ PHP_METHOD(Pdo_Sqlite, loadExtension) + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && + (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { + zend_throw_exception_ex(php_pdo_get_exception(), 0, "Not supported in multithreaded Web servers"); +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 31212999..d7705d59 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -730,7 +730,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3) || 0 == strcmp("micro", sapi_module.name)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index 21b6840a..05a7aa8e 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -413,6 +413,7 @@ PHP_METHOD(SQLite3, loadExtension) + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && + (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, 0, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index ea33ba49..083184b8 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -242,7 +242,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + } + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -258,7 +258,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -274,7 +274,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") && !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -295,7 +295,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") || strcmp(sapi_module.name, "micro")) { + if (options & REPORT_ERRORS) { + php_error_docref(NULL, E_WARNING, "Direct access to file descriptors is only available from command-line PHP"); + } +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index 690e23e0..80ab3d32 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1333,7 +1333,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index 8465b6c0..cf8f9ef0 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -580,7 +580,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(temporary_value); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +@@ -1470,7 +1470,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c + } + } else { + /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ +- if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) && ++ if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")) && + PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR + ) { + fprintf(stderr, "%s: ", error_type_str); +diff --git a/win32/console.c b/win32/console.c +index 9b485610..a2b764cd 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -111,6 +111,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return (strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1)) || 0 == strcmp(sapi_module.name, "micro"); + }/*}}}*/ + diff --git a/src/globals/patch/php-src-patches/cli_static_80.patch b/src/globals/patch/php-src-patches/cli_static_80.patch new file mode 100644 index 000000000..338873ee6 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_static_80.patch @@ -0,0 +1,24 @@ +diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c +index 0ad53e813c..a8cc1bee29 100644 +--- a/sapi/cli/php_cli.c ++++ b/sapi/cli/php_cli.c +@@ -97,7 +97,7 @@ PHPAPI extern char *php_ini_scanned_files; + + #if defined(PHP_WIN32) + #if defined(ZTS) +-ZEND_TSRMLS_CACHE_DEFINE() ++//ZEND_TSRMLS_CACHE_DEFINE() + #endif + static DWORD orig_cp = 0; + #endif +@@ -1160,6 +1160,10 @@ int main(int argc, char *argv[]) + #endif + { + #if defined(PHP_WIN32) ++ if (!php_win32_ioutil_init()) { ++ fprintf(stderr, "ioutil initialization failed"); ++ return 1; ++ } + # ifdef PHP_CLI_WIN32_NO_CONSOLE + int argc = __argc; + char **argv = __argv; diff --git a/src/globals/patch/php-src-patches/cli_static_84.patch b/src/globals/patch/php-src-patches/cli_static_84.patch new file mode 100644 index 000000000..90d8b310e --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_static_84.patch @@ -0,0 +1,23 @@ +diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c +--- a/sapi/cli/php_cli.c (revision d3bf67d44102869f340a7be0e12f4f09de0edbcf) ++++ b/sapi/cli/php_cli.c (date 1735128770216) +@@ -98,7 +98,7 @@ + + #if defined(PHP_WIN32) + #if defined(ZTS) +-ZEND_TSRMLS_CACHE_DEFINE() ++//ZEND_TSRMLS_CACHE_DEFINE() + #endif + static DWORD orig_cp = 0; + #endif +@@ -1137,6 +1137,10 @@ + #endif + { + #if defined(PHP_WIN32) ++ if (!php_win32_ioutil_init()) { ++ fprintf(stderr, "ioutil initialization failed"); ++ return 1; ++ } + # ifdef PHP_CLI_WIN32_NO_CONSOLE + int argc = __argc; + char **argv = __argv; diff --git a/src/globals/patch/php-src-patches/cli_static_85.patch b/src/globals/patch/php-src-patches/cli_static_85.patch new file mode 100644 index 000000000..83f701d00 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_static_85.patch @@ -0,0 +1,13 @@ +diff --git a/sapi/cli/php_cli.c b/sapi/cli/php_cli.c +index e212a0f7..f16e8ea9 100644 +--- a/sapi/cli/php_cli.c ++++ b/sapi/cli/php_cli.c +@@ -98,7 +98,7 @@ PHPAPI extern char *php_ini_scanned_files; + + #if defined(PHP_WIN32) + #if defined(ZTS) +-ZEND_TSRMLS_CACHE_DEFINE() ++// ZEND_TSRMLS_CACHE_DEFINE() + #endif + static DWORD orig_cp = 0; + #endif diff --git a/src/globals/patch/php-src-patches/comctl32.patch b/src/globals/patch/php-src-patches/comctl32.patch new file mode 100644 index 000000000..45b2cf1c8 --- /dev/null +++ b/src/globals/patch/php-src-patches/comctl32.patch @@ -0,0 +1,21 @@ +diff --git a/win32/build/default.manifest b/win32/build/default.manifest +index a73c2fb53d..52351251e1 100644 +--- a/win32/build/default.manifest ++++ b/win32/build/default.manifest +@@ -24,4 +24,16 @@ + true + + ++ ++ ++ ++ ++ + diff --git a/src/globals/patch/php-src-patches/disable_huge_page_80.patch b/src/globals/patch/php-src-patches/disable_huge_page_80.patch new file mode 100644 index 000000000..e671f3fb3 --- /dev/null +++ b/src/globals/patch/php-src-patches/disable_huge_page_80.patch @@ -0,0 +1,11 @@ +--- php-8.0.0/configure.ac 2020-11-25 01:04:03.000000000 +0800 ++++ php-8.0.0-micro/configure.ac 2020-11-29 20:00:13.256181206 +0800 +@@ -1005,7 +1005,7 @@ dnl Extensions post-config. + dnl ---------------------------------------------------------------------------- + + dnl Align segments on huge page boundary +-case $host_alias in ++case nope in + i[[3456]]86-*-linux-* | x86_64-*-linux-*) + AC_MSG_CHECKING(linker support for -zcommon-page-size=2097152) + save_LDFLAGS=$LDFLAGS diff --git a/src/globals/patch/php-src-patches/disable_huge_page_84.patch b/src/globals/patch/php-src-patches/disable_huge_page_84.patch new file mode 100644 index 000000000..b49dbbcdb --- /dev/null +++ b/src/globals/patch/php-src-patches/disable_huge_page_84.patch @@ -0,0 +1,13 @@ +diff --git a/configure.ac b/configure.ac +index 48778c7bd2..760c4f2670 100644 +--- a/configure.ac ++++ b/configure.ac +@@ -1127,7 +1127,7 @@ dnl Extensions post-config. + dnl ---------------------------------------------------------------------------- + + dnl Align segments on huge page boundary +-AS_CASE([$host_alias], [[i[3456]86-*-linux-* | x86_64-*-linux-*]], ++AS_CASE([nope], [[i[3456]86-*-linux-* | x86_64-*-linux-*]], + [AC_CACHE_CHECK([linker support for -zcommon-page-size=2097152], + [php_cv_have_common_page_size], [ + save_LDFLAGS=$LDFLAGS diff --git a/src/globals/patch/php-src-patches/macos_iconv_80.patch b/src/globals/patch/php-src-patches/macos_iconv_80.patch new file mode 100644 index 000000000..b8c7a329b --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_80.patch @@ -0,0 +1,23 @@ +diff --git a/build/php.m4 b/build/php.m4 +index 01b8250598..0a8c5fba53 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1963,9 +1963,7 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + if test "$PHP_ICONV" = "yes"; then +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save="$LIBS" +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC(iconv, [ + found_iconv=yes + ],[ +@@ -1974,7 +1972,6 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + found_iconv=yes + ]) + ]) +- LIBS="$LIBS_save" + fi + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/macos_iconv_81.patch b/src/globals/patch/php-src-patches/macos_iconv_81.patch new file mode 100644 index 000000000..b8c7a329b --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_81.patch @@ -0,0 +1,23 @@ +diff --git a/build/php.m4 b/build/php.m4 +index 01b8250598..0a8c5fba53 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1963,9 +1963,7 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + if test "$PHP_ICONV" = "yes"; then +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save="$LIBS" +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC(iconv, [ + found_iconv=yes + ],[ +@@ -1974,7 +1972,6 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + found_iconv=yes + ]) + ]) +- LIBS="$LIBS_save" + fi + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/macos_iconv_82.patch b/src/globals/patch/php-src-patches/macos_iconv_82.patch new file mode 100644 index 000000000..b8c7a329b --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_82.patch @@ -0,0 +1,23 @@ +diff --git a/build/php.m4 b/build/php.m4 +index 01b8250598..0a8c5fba53 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1963,9 +1963,7 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + if test "$PHP_ICONV" = "yes"; then +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save="$LIBS" +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC(iconv, [ + found_iconv=yes + ],[ +@@ -1974,7 +1972,6 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + found_iconv=yes + ]) + ]) +- LIBS="$LIBS_save" + fi + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/macos_iconv_83.patch b/src/globals/patch/php-src-patches/macos_iconv_83.patch new file mode 100644 index 000000000..b8c7a329b --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_83.patch @@ -0,0 +1,23 @@ +diff --git a/build/php.m4 b/build/php.m4 +index 01b8250598..0a8c5fba53 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1963,9 +1963,7 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + if test "$PHP_ICONV" = "yes"; then +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save="$LIBS" +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC(iconv, [ + found_iconv=yes + ],[ +@@ -1974,7 +1972,6 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + found_iconv=yes + ]) + ]) +- LIBS="$LIBS_save" + fi + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/macos_iconv_84.patch b/src/globals/patch/php-src-patches/macos_iconv_84.patch new file mode 100644 index 000000000..c362e3987 --- /dev/null +++ b/src/globals/patch/php-src-patches/macos_iconv_84.patch @@ -0,0 +1,21 @@ +diff --git a/build/php.m4 b/build/php.m4 +index e45b22b766..506be904f1 100644 +--- a/build/php.m4 ++++ b/build/php.m4 +@@ -1821,15 +1821,12 @@ AC_DEFUN([PHP_SETUP_ICONV], [ + + dnl Check libc first if no path is provided in --with-iconv. + AS_VAR_IF([PHP_ICONV], [yes], [ +- dnl Reset LIBS temporarily as it may have already been included -liconv in. +- LIBS_save=$LIBS +- LIBS= ++ LIBS="$LIBS -liconv" + AC_CHECK_FUNC([iconv], [found_iconv=yes], + [AC_CHECK_FUNC([libiconv], [ + AC_DEFINE([HAVE_LIBICONV], [1]) + found_iconv=yes + ])]) +- LIBS=$LIBS_save + ]) + + dnl Check external libs for iconv funcs. diff --git a/src/globals/patch/php-src-patches/phar_80.patch b/src/globals/patch/php-src-patches/phar_80.patch new file mode 100644 index 000000000..c33a24f5a --- /dev/null +++ b/src/globals/patch/php-src-patches/phar_80.patch @@ -0,0 +1,22 @@ +diff --git a/ext/phar/phar.c b/ext/phar/phar.c +index 2403d77a..c908a1b4 100644 +--- a/ext/phar/phar.c ++++ b/ext/phar/phar.c +@@ -3309,6 +3309,8 @@ static zend_string *phar_resolve_path(const char *filename, size_t filename_len) + return phar_find_in_include_path((char *) filename, filename_len, NULL); + } + ++char *micro_get_filename(void); ++ + static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) /* {{{ */ + { + zend_op_array *res; +@@ -3319,7 +3321,7 @@ static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) + if (!file_handle || !file_handle->filename) { + return phar_orig_compile_file(file_handle, type); + } +- 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, "://")) { + if (SUCCESS == phar_open_from_filename((char*)file_handle->filename, strlen(file_handle->filename), NULL, 0, 0, &phar, NULL)) { + if (phar->is_zip || phar->is_tar) { + zend_file_handle f = *file_handle; diff --git a/src/globals/patch/php-src-patches/phar_81.patch b/src/globals/patch/php-src-patches/phar_81.patch new file mode 100644 index 000000000..2bbdbf012 --- /dev/null +++ b/src/globals/patch/php-src-patches/phar_81.patch @@ -0,0 +1,22 @@ +diff --git a/ext/phar/phar.c b/ext/phar/phar.c +index 3c0f3eb50b..455b303a8d 100644 +--- a/ext/phar/phar.c ++++ b/ext/phar/phar.c +@@ -3295,6 +3295,8 @@ static zend_string *phar_resolve_path(zend_string *filename) + return ret; + } + ++char *micro_get_filename(void); ++ + static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) /* {{{ */ + { + zend_op_array *res; +@@ -3305,7 +3307,7 @@ static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) + if (!file_handle || !file_handle->filename) { + return phar_orig_compile_file(file_handle, type); + } +- 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), "://")) { + if (SUCCESS == phar_open_from_filename(ZSTR_VAL(file_handle->filename), ZSTR_LEN(file_handle->filename), NULL, 0, 0, &phar, NULL)) { + if (phar->is_zip || phar->is_tar) { + zend_file_handle f; diff --git a/src/globals/patch/php-src-patches/static_extensions_win32_80.patch b/src/globals/patch/php-src-patches/static_extensions_win32_80.patch new file mode 100644 index 000000000..4b47d325f --- /dev/null +++ b/src/globals/patch/php-src-patches/static_extensions_win32_80.patch @@ -0,0 +1,31 @@ +diff --git a/ext/fileinfo/config.w32 b/ext/fileinfo/config.w32 +index eefccb3d72..b231f67b23 100644 +--- a/ext/fileinfo/config.w32 ++++ b/ext/fileinfo/config.w32 +@@ -10,6 +10,6 @@ if (PHP_FILEINFO != 'no') { + readcdf.c softmagic.c der.c \ + strcasestr.c buffer.c is_csv.c"; + +- EXTENSION('fileinfo', 'fileinfo.c', true, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); ++ EXTENSION('fileinfo', 'fileinfo.c', PHP_FILEINFO_SHARED, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); + ADD_SOURCES(configure_module_dirname + '\\libmagic', LIBMAGIC_SOURCES, "fileinfo"); + } +diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 +index 9187d6bfc2..f1acc2e8b5 100644 +--- a/ext/openssl/config.w32 ++++ b/ext/openssl/config.w32 +@@ -1,12 +1,12 @@ + // vim:ft=javascript + +-ARG_WITH("openssl", "OpenSSL support", "no,shared"); ++ARG_WITH("openssl", "OpenSSL support", "no"); + + if (PHP_OPENSSL != "no") { + var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL); + + if (ret > 0) { +- EXTENSION("openssl", "openssl.c xp_ssl.c"); ++ EXTENSION("openssl", "openssl.c xp_ssl.c", PHP_OPENSSL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + AC_DEFINE("HAVE_OPENSSL_EXT", PHP_OPENSSL_SHARED ? 0 : 1, "Have openssl"); + AC_DEFINE("HAVE_OPENSSL", 1); + } diff --git a/src/globals/patch/php-src-patches/static_extensions_win32_83.patch b/src/globals/patch/php-src-patches/static_extensions_win32_83.patch new file mode 100644 index 000000000..007edd9cf --- /dev/null +++ b/src/globals/patch/php-src-patches/static_extensions_win32_83.patch @@ -0,0 +1,31 @@ +diff --git a/ext/fileinfo/config.w32 b/ext/fileinfo/config.w32 +index e42f1ce3f7..db28a68676 100644 +--- a/ext/fileinfo/config.w32 ++++ b/ext/fileinfo/config.w32 +@@ -10,6 +10,6 @@ if (PHP_FILEINFO != 'no') { + readcdf.c softmagic.c der.c \ + strcasestr.c buffer.c is_csv.c"; + +- EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', true, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); ++ EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', PHP_FILEINFO_SHARED, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); + ADD_SOURCES(configure_module_dirname + '\\libmagic', LIBMAGIC_SOURCES, "fileinfo"); + } +diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 +index 9187d6bfc2..f1acc2e8b5 100644 +--- a/ext/openssl/config.w32 ++++ b/ext/openssl/config.w32 +@@ -1,12 +1,12 @@ + // vim:ft=javascript + +-ARG_WITH("openssl", "OpenSSL support", "no,shared"); ++ARG_WITH("openssl", "OpenSSL support", "no"); + + if (PHP_OPENSSL != "no") { + var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL); + + if (ret > 0) { +- EXTENSION("openssl", "openssl.c xp_ssl.c"); ++ EXTENSION("openssl", "openssl.c xp_ssl.c", PHP_OPENSSL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + AC_DEFINE("HAVE_OPENSSL_EXT", PHP_OPENSSL_SHARED ? 0 : 1, "Have openssl"); + AC_DEFINE("HAVE_OPENSSL", 1); + } diff --git a/src/globals/patch/php-src-patches/static_extensions_win32_84.patch b/src/globals/patch/php-src-patches/static_extensions_win32_84.patch new file mode 100644 index 000000000..04246ccd4 --- /dev/null +++ b/src/globals/patch/php-src-patches/static_extensions_win32_84.patch @@ -0,0 +1,34 @@ +diff --git a/ext/fileinfo/config.w32 b/ext/fileinfo/config.w32 +index 2a42dc45..c207694f 100644 +--- a/ext/fileinfo/config.w32 ++++ b/ext/fileinfo/config.w32 +@@ -10,7 +10,7 @@ if (PHP_FILEINFO != 'no') { + readcdf.c softmagic.c der.c \ + strcasestr.c buffer.c is_csv.c"; + +- EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', true, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); ++ EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', PHP_FILEINFO_SHARED, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); + ADD_EXTENSION_DEP('fileinfo', 'pcre'); + ADD_SOURCES(configure_module_dirname + '\\libmagic', LIBMAGIC_SOURCES, "fileinfo"); + } +diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 +index 24064ec2..87ff3161 100644 +--- a/ext/openssl/config.w32 ++++ b/ext/openssl/config.w32 +@@ -1,6 +1,6 @@ + // vim:ft=javascript + +-ARG_WITH("openssl", "OpenSSL support", "no,shared"); ++ARG_WITH("openssl", "OpenSSL support", "no"); + + ARG_WITH("openssl-legacy-provider", "OPENSSL: Load legacy algorithm provider in addition to default provider", "no"); + +@@ -10,7 +10,7 @@ if (PHP_OPENSSL != "no") { + var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL); + + if (ret >= 2) { +- EXTENSION("openssl", "openssl.c openssl_pwhash.c xp_ssl.c"); ++ EXTENSION("openssl", "openssl.c openssl_pwhash.c xp_ssl.c", PHP_OPENSSL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + AC_DEFINE("HAVE_OPENSSL_EXT", 1, "Define to 1 if the PHP extension 'openssl' is available."); + if (PHP_OPENSSL_LEGACY_PROVIDER != "no") { + AC_DEFINE("LOAD_OPENSSL_LEGACY_PROVIDER", 1, "Define to 1 to load the OpenSSL legacy algorithm provider in addition to the default provider."); diff --git a/src/globals/patch/php-src-patches/static_extensions_win32_85.patch b/src/globals/patch/php-src-patches/static_extensions_win32_85.patch new file mode 100644 index 000000000..9784edbc4 --- /dev/null +++ b/src/globals/patch/php-src-patches/static_extensions_win32_85.patch @@ -0,0 +1,34 @@ +diff --git a/ext/fileinfo/config.w32 b/ext/fileinfo/config.w32 +index 2a42dc45..c207694f 100644 +--- a/ext/fileinfo/config.w32 ++++ b/ext/fileinfo/config.w32 +@@ -10,7 +10,7 @@ if (PHP_FILEINFO != 'no') { + readcdf.c softmagic.c der.c \ + strcasestr.c buffer.c is_csv.c"; + +- EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', true, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); ++ EXTENSION('fileinfo', 'fileinfo.c php_libmagic.c', PHP_FILEINFO_SHARED, "/I" + configure_module_dirname + "/libmagic /I" + configure_module_dirname); + ADD_EXTENSION_DEP('fileinfo', 'pcre'); + ADD_SOURCES(configure_module_dirname + '\\libmagic', LIBMAGIC_SOURCES, "fileinfo"); + } +diff --git a/ext/openssl/config.w32 b/ext/openssl/config.w32 +index 714f93a0..0ab6efff 100644 +--- a/ext/openssl/config.w32 ++++ b/ext/openssl/config.w32 +@@ -1,6 +1,6 @@ + // vim:ft=javascript + +-ARG_WITH("openssl", "OpenSSL support", "no,shared"); ++ARG_WITH("openssl", "OpenSSL support", "no"); + + ARG_WITH("openssl-legacy-provider", "OPENSSL: Load legacy algorithm provider in addition to default provider", "no"); + +@@ -10,7 +10,7 @@ if (PHP_OPENSSL != "no") { + var ret = SETUP_OPENSSL("openssl", PHP_OPENSSL); + + if (ret >= 2) { +- EXTENSION("openssl", "openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_ssl.c"); ++ EXTENSION("openssl", "openssl.c openssl_pwhash.c openssl_backend_common.c openssl_backend_v1.c openssl_backend_v3.c xp_ssl.c", PHP_OPENSSL_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + AC_DEFINE("HAVE_OPENSSL_EXT", 1, "Define to 1 if the PHP extension 'openssl' is available."); + if (PHP_OPENSSL_LEGACY_PROVIDER != "no") { + AC_DEFINE("LOAD_OPENSSL_LEGACY_PROVIDER", 1, "Define to 1 to load the OpenSSL legacy algorithm provider in addition to the default provider."); diff --git a/src/globals/patch/php-src-patches/static_opcache_80.patch b/src/globals/patch/php-src-patches/static_opcache_80.patch new file mode 100644 index 000000000..f83ad046a --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_80.patch @@ -0,0 +1,129 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index ad3781101b..b7133fc0c8 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index c195ad7d2c..8bb8dd78fc 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -91,7 +91,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4991,7 +4994,11 @@ static int accel_finish_startup(void) + return SUCCESS; + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index 5492fd920c..6fdb475e49 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -21,7 +21,8 @@ PHP_ARG_ENABLE([opcache-jit], + if test "$PHP_OPCACHE" != "no"; then + + dnl Always build as shared extension +- ext_shared=yes ++ dnl why? ++ dnl ext_shared=yes + + if test "$PHP_HUGE_CODE_PAGES" = "yes"; then + AC_DEFINE(HAVE_HUGE_CODE_PAGES, 1, [Define to enable copying PHP CODE pages into HUGE PAGES (experimental)]) +@@ -334,7 +335,9 @@ int main() { + Optimizer/compact_vars.c \ + Optimizer/zend_dump.c \ + $ZEND_JIT_SRC, +- shared,,-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1,,yes) ++ $ext_shared,,-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1,,yes) ++ ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) + + PHP_ADD_BUILD_DIR([$ext_builddir/Optimizer], 1) + PHP_ADD_EXTENSION_DEP(opcache, pcre) +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index fb921c73da..41de817bda 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,7 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + + if (PHP_OPCACHE_JIT == "yes") { + if (CHECK_HEADER_ADD_INCLUDE("dynasm/dasm_x86.h", "CFLAGS_OPCACHE", PHP_OPCACHE + ";ext\\opcache\\jit")) { +diff --git a/main/main.c b/main/main.c +index a40a4c8c37..ae93c8ae6c 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2016,6 +2016,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint32_t num_additional_modules) + { +@@ -2255,6 +2267,9 @@ int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_mod + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index bf88cdae44..f8f6547e39 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1535,6 +1535,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + ++ // TODO: real skip zend extensions ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); diff --git a/src/globals/patch/php-src-patches/static_opcache_81.patch b/src/globals/patch/php-src-patches/static_opcache_81.patch new file mode 100644 index 000000000..cde02c12d --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_81.patch @@ -0,0 +1,129 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index 1e71ea2069..3da32d8830 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 5f6b854d47..ea15c0d5bc 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -91,7 +91,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4808,7 +4811,11 @@ static int accel_finish_startup(void) + return SUCCESS; + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index 2a83fa2455..7b3b37182e 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -21,7 +21,8 @@ PHP_ARG_ENABLE([opcache-jit], + if test "$PHP_OPCACHE" != "no"; then + + dnl Always build as shared extension +- ext_shared=yes ++ dnl why? ++ dnl ext_shared=yes + + if test "$PHP_HUGE_CODE_PAGES" = "yes"; then + AC_DEFINE(HAVE_HUGE_CODE_PAGES, 1, [Define to enable copying PHP CODE pages into HUGE PAGES (experimental)]) +@@ -327,7 +328,9 @@ int main() { + shared_alloc_mmap.c \ + shared_alloc_posix.c \ + $ZEND_JIT_SRC, +- shared,,"-Wno-implicit-fallthrough -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ $ext_shared,,"-Wno-implicit-fallthrough -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) + + PHP_ADD_EXTENSION_DEP(opcache, pcre) + +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index 764a2edaab..95427090ce 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,7 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + + if (PHP_OPCACHE_JIT == "yes") { + if (CHECK_HEADER_ADD_INCLUDE("dynasm/dasm_x86.h", "CFLAGS_OPCACHE", PHP_OPCACHE + ";ext\\opcache\\jit")) { +diff --git a/main/main.c b/main/main.c +index 8c16f01b11..0560348a06 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2011,6 +2011,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_modules, uint32_t num_additional_modules) + { +@@ -2253,6 +2265,9 @@ int php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_mod + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 1a2dfe43b4..ae405f035a 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1535,6 +1535,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + ++ // TODO: real skip zend extensions ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); diff --git a/src/globals/patch/php-src-patches/static_opcache_82.patch b/src/globals/patch/php-src-patches/static_opcache_82.patch new file mode 100644 index 000000000..03e04dd2c --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_82.patch @@ -0,0 +1,129 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index 1e71ea20..77895167 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 9bcd035c..7bc01614 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -93,7 +93,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4792,7 +4795,11 @@ static int accel_finish_startup(void) + return SUCCESS; + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index b3929382..8607ff25 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -21,7 +21,8 @@ PHP_ARG_ENABLE([opcache-jit], + if test "$PHP_OPCACHE" != "no"; then + + dnl Always build as shared extension +- ext_shared=yes ++ dnl why? ++ dnl ext_shared=yes + + if test "$PHP_HUGE_CODE_PAGES" = "yes"; then + AC_DEFINE(HAVE_HUGE_CODE_PAGES, 1, [Define to enable copying PHP CODE pages into HUGE PAGES (experimental)]) +@@ -336,7 +337,9 @@ int main(void) { + shared_alloc_mmap.c \ + shared_alloc_posix.c \ + $ZEND_JIT_SRC, +- shared,,"$PHP_OPCACHE_CFLAGS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ $ext_shared,,"$PHP_OPCACHE_CFLAGS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) + + PHP_ADD_EXTENSION_DEP(opcache, pcre) + +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index 764a2eda..95427090 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,7 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + + if (PHP_OPCACHE_JIT == "yes") { + if (CHECK_HEADER_ADD_INCLUDE("dynasm/dasm_x86.h", "CFLAGS_OPCACHE", PHP_OPCACHE + ";ext\\opcache\\jit")) { +diff --git a/main/main.c b/main/main.c +index 0adecd10..ee89ebfb 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2048,6 +2048,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_module) + { +@@ -2293,6 +2305,9 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 4eece379..59b7bd5c 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1534,6 +1534,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + ++ // TODO: real skip zend extensions ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); \ No newline at end of file diff --git a/src/globals/patch/php-src-patches/static_opcache_83.patch b/src/globals/patch/php-src-patches/static_opcache_83.patch new file mode 100644 index 000000000..ae4541ef8 --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_83.patch @@ -0,0 +1,130 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index 1e71ea2069..3da32d8830 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index ec33c69eb2..b8ce7e3eca 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -93,7 +93,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4814,7 +4817,11 @@ static int accel_finish_startup(void) + #endif /* ZEND_WIN32 */ + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index 2a83fa2455..7b3b37182e 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -27,7 +27,8 @@ + if test "$PHP_OPCACHE" != "no"; then + + dnl Always build as shared extension +- ext_shared=yes ++ dnl why? ++ dnl ext_shared=yes + + if test "$PHP_HUGE_CODE_PAGES" = "yes"; then + AC_DEFINE(HAVE_HUGE_CODE_PAGES, 1, [Define to enable copying PHP CODE pages into HUGE PAGES (experimental)]) +@@ -319,8 +320,10 @@ + shared_alloc_mmap.c \ + shared_alloc_posix.c \ + $ZEND_JIT_SRC, +- shared,,"$PHP_OPCACHE_CFLAGS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) ++ $ext_shared,,"$PHP_OPCACHE_CFLAGS -DZEND_ENABLE_STATIC_TSRMLS_CACHE=1",,yes) + ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) ++ + PHP_ADD_EXTENSION_DEP(opcache, pcre) + + if test "$have_shm_ipc" != "yes" && test "$have_shm_mmap_posix" != "yes" && test "$have_shm_mmap_anon" != "yes"; then +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index 764a2edaab..95427090ce 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,7 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + + if (PHP_OPCACHE_JIT == "yes") { + if (CHECK_HEADER_ADD_INCLUDE("dynasm/dasm_x86.h", "CFLAGS_OPCACHE", PHP_OPCACHE + ";ext\\opcache\\jit")) { +diff --git a/main/main.c b/main/main.c +index 6fdfbce13e..bcccfad6e3 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2012,6 +2012,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_module) + { +@@ -2196,6 +2208,9 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 359c751b7b..01068efcf6 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1534,6 +1534,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + ++ // TODO: real skip zend extensions ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); diff --git a/src/globals/patch/php-src-patches/static_opcache_84.patch b/src/globals/patch/php-src-patches/static_opcache_84.patch new file mode 100644 index 000000000..65841eda7 --- /dev/null +++ b/src/globals/patch/php-src-patches/static_opcache_84.patch @@ -0,0 +1,146 @@ +diff --git a/build/order_by_dep.awk b/build/order_by_dep.awk +index 1e71ea20..3da32d88 100644 +--- a/build/order_by_dep.awk ++++ b/build/order_by_dep.awk +@@ -37,6 +37,11 @@ function get_module_index(name, i) + function do_deps(mod_idx, module_name, mod_name_len, dep, ext, val, depidx) + { + module_name = mods[mod_idx]; ++ # TODO: real skip zend extension ++ if (module_name == "opcache") { ++ delete mods[mod_idx]; ++ return; ++ } + mod_name_len = length(module_name); + + for (ext in mod_deps) { +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index 3e8bdea9..4a784945 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -97,7 +97,10 @@ typedef int gid_t; + #include + #endif + ++#ifdef COMPILE_DL_OPCACHE ++// micro: avoid symbol conflict + ZEND_EXTENSION(); ++#endif + + #ifndef ZTS + zend_accel_globals accel_globals; +@@ -4828,7 +4831,11 @@ static zend_result accel_finish_startup(void) + #endif /* ZEND_WIN32 */ + } + ++#ifdef COMPILE_DL_OPCACHE + ZEND_EXT_API zend_extension zend_extension_entry = { ++#else ++zend_extension opcache_zend_extension_entry = { ++#endif + ACCELERATOR_PRODUCT_NAME, /* name */ + PHP_VERSION, /* version */ + "Zend Technologies", /* author */ +diff --git a/ext/opcache/config.m4 b/ext/opcache/config.m4 +index 8f6d5ab7..19530321 100644 +--- a/ext/opcache/config.m4 ++++ b/ext/opcache/config.m4 +@@ -26,8 +26,8 @@ PHP_ARG_WITH([capstone], + [no]) + + if test "$PHP_OPCACHE" != "no"; then +- dnl Always build as shared extension. +- ext_shared=yes ++ dnl Always build as shared extension. (micro patches: no, we need static) ++ dnl ext_shared=yes + + AS_VAR_IF([PHP_HUGE_CODE_PAGES], [yes], + [AC_DEFINE([HAVE_HUGE_CODE_PAGES], [1], +@@ -343,6 +343,7 @@ int main(void) { + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1 $JIT_CFLAGS],, + [yes]) + ++ AC_DEFINE(HAVE_OPCACHE, 1, [opcache enabled]) + PHP_ADD_EXTENSION_DEP(opcache, date) + PHP_ADD_EXTENSION_DEP(opcache, pcre) + +diff --git a/ext/opcache/config.w32 b/ext/opcache/config.w32 +index d0af7258..a054e6c8 100644 +--- a/ext/opcache/config.w32 ++++ b/ext/opcache/config.w32 +@@ -16,8 +16,9 @@ if (PHP_OPCACHE != "no") { + zend_persist_calc.c \ + zend_file_cache.c \ + zend_shared_alloc.c \ +- shared_alloc_win32.c", true, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); ++ shared_alloc_win32.c", PHP_OPCACHE_SHARED, "/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + ++ AC_DEFINE('HAVE_OPCACHE', 1, 'opcache enabled'); + ADD_EXTENSION_DEP('opcache', 'date'); + ADD_EXTENSION_DEP('opcache', 'hash'); + ADD_EXTENSION_DEP('opcache', 'pcre'); +diff --git a/ext/opcache/jit/ir/ir_gdb.c b/ext/opcache/jit/ir/ir_gdb.c +index ecaf8803..a8275466 100644 +--- a/ext/opcache/jit/ir/ir_gdb.c ++++ b/ext/opcache/jit/ir/ir_gdb.c +@@ -504,11 +504,11 @@ typedef struct _ir_gdbjit_descriptor { + extern ir_gdbjit_descriptor __jit_debug_descriptor; + void __jit_debug_register_code(void); + #else +-ir_gdbjit_descriptor __jit_debug_descriptor = { ++static ir_gdbjit_descriptor __jit_debug_descriptor = { + 1, IR_GDBJIT_NOACTION, NULL, NULL + }; + +-IR_NEVER_INLINE void __jit_debug_register_code(void) ++static IR_NEVER_INLINE void __jit_debug_register_code(void) + { + __asm__ __volatile__(""); + } +diff --git a/main/main.c b/main/main.c +index 0b38f303..b2cb9d4a 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -2099,6 +2099,18 @@ void dummy_invalid_parameter_handler( + } + #endif + ++// this can be moved to other place ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++extern zend_extension opcache_zend_extension_entry; ++extern void zend_register_extension(zend_extension *new_extension, void *handle); ++ ++int zend_load_static_extensions(void) ++{ ++ zend_register_extension(&opcache_zend_extension_entry, NULL /*opcache cannot be unloaded*/); ++ return 0; ++} ++#endif ++ + /* {{{ php_module_startup */ + zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additional_module) + { +@@ -2283,6 +2295,9 @@ zend_result php_module_startup(sapi_module_struct *sf, zend_module_entry *additi + ahead of all other internals + */ + php_ini_register_extensions(); ++#if defined(HAVE_OPCACHE) && !defined(COMPILE_DL_OPCACHE) ++ zend_load_static_extensions(); ++#endif + zend_startup_modules(); + + /* start Zend extensions */ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 1a4ddbff..f47090b7 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -1531,7 +1531,8 @@ function EXTENSION(extname, file_list, shared, cflags, dllname, obj_dir) + } + } + } +- ++ // micro: skip zend opcache ++ if (extname != 'opcache') + extension_module_ptrs += '\tphpext_' + extname + '_ptr,\r\n'; + + DEFINE('CFLAGS_' + EXT + '_OBJ', '$(CFLAGS_PHP) $(CFLAGS_' + EXT + ')'); diff --git a/src/globals/patch/php-src-patches/static_opcache_85.patch b/src/globals/patch/php-src-patches/static_opcache_85.patch new file mode 100644 index 000000000..e69de29bb diff --git a/src/globals/patch/php-src-patches/vcruntime140_74.patch b/src/globals/patch/php-src-patches/vcruntime140_74.patch new file mode 100644 index 000000000..842ffa8b3 --- /dev/null +++ b/src/globals/patch/php-src-patches/vcruntime140_74.patch @@ -0,0 +1,12 @@ +--- a/main/main.c 2020-09-29 10:17:07.000000000 +0000 ++++ b/main/main.c 2020-11-19 07:57:40.769785000 +0000 +@@ -2172,7 +2172,8 @@ int php_module_startup(sapi_module_struc + #endif + + #ifdef PHP_WIN32 +-# if PHP_LINKER_MAJOR == 14 ++// fucked here ++# if false && PHP_LINKER_MAJOR == 14 + /* Extend for other CRT if needed. */ + # if PHP_DEBUG + # define PHP_VCRUNTIME "vcruntime140d.dll" diff --git a/src/globals/patch/php-src-patches/vcruntime140_80.patch b/src/globals/patch/php-src-patches/vcruntime140_80.patch new file mode 100644 index 000000000..1430d63d9 --- /dev/null +++ b/src/globals/patch/php-src-patches/vcruntime140_80.patch @@ -0,0 +1,11 @@ +--- php-8.0.0-src/win32/winutil.c 2020-11-24 17:04:03.000000000 +0000 ++++ php-8.0.0-micro/win32/winutil.c 2020-12-03 07:59:22.177745800 +0000 +@@ -484,7 +484,7 @@ + /* Expect a CRT module handle */ + PHP_WINUTIL_API BOOL php_win32_crt_compatible(char **err) + {/*{{{*/ +-#if PHP_LINKER_MAJOR == 14 ++#if false && PHP_LINKER_MAJOR == 14 + /* Extend for other CRT if needed. */ + # if PHP_DEBUG + const char *crt_name = "vcruntime140d.dll"; diff --git a/src/globals/patch/php-src-patches/win32_74.patch b/src/globals/patch/php-src-patches/win32_74.patch new file mode 100644 index 000000000..c4a9d37e3 --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_74.patch @@ -0,0 +1,21 @@ +diff -pru /mnt/c/Users/dixyes/Desktop/phiwrapaper/phibatsh/build/php-7.4.11-src/win32/build/confutils.js win32/build/confutils.js +--- a/win32/build/confutils.js 2020-09-29 10:17:06.000000000 +0000 ++++ b/win32/build/confutils.js 2020-11-20 10:07:21.642064000 +0000 +@@ -3413,7 +3413,7 @@ function toolset_setup_common_libs() + function toolset_setup_build_mode() + { + if (PHP_DEBUG == "yes") { +- ADD_FLAG("CFLAGS", "/LDd /MDd /W3 /Od /D _DEBUG /D ZEND_DEBUG=1 " + ++ ADD_FLAG("CFLAGS", "/LDd /MTd /W3 /Od /D _DEBUG /D ZEND_DEBUG=1 " + + (X64?"/Zi":"/ZI")); + ADD_FLAG("LDFLAGS", "/debug"); + // Avoid problems when linking to release libraries that use the release +@@ -3425,7 +3425,7 @@ function toolset_setup_build_mode() + ADD_FLAG("CFLAGS", "/Zi"); + ADD_FLAG("LDFLAGS", "/incremental:no /debug /opt:ref,icf"); + } +- ADD_FLAG("CFLAGS", "/LD /MD /W3"); ++ ADD_FLAG("CFLAGS", "/LD /MT /W3"); + if (PHP_SANITIZER == "yes" && CLANG_TOOLSET) { + ADD_FLAG("CFLAGS", "/Od /D NDebug /D NDEBUG /D ZEND_WIN32_NEVER_INLINE /D ZEND_DEBUG=0"); + } else { diff --git a/src/globals/patch/php-src-patches/win32_80.patch b/src/globals/patch/php-src-patches/win32_80.patch new file mode 100644 index 000000000..4333bffd7 --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_80.patch @@ -0,0 +1,21 @@ +diff -urN php-8.0.0-src/win32/build/confutils.js php-8.0.0-micro/win32/build/confutils.js +--- php-8.0.0-src/win32/build/confutils.js 2020-11-24 17:04:03.000000000 +0000 ++++ php-8.0.0-micro/win32/build/confutils.js 2020-12-03 06:16:12.949921700 +0000 +@@ -3407,7 +3407,7 @@ + function toolset_setup_build_mode() + { + if (PHP_DEBUG == "yes") { +- ADD_FLAG("CFLAGS", "/LDd /MDd /Od /D _DEBUG /D ZEND_DEBUG=1 " + ++ ADD_FLAG("CFLAGS", "/LDd /MTd /Od /D _DEBUG /D ZEND_DEBUG=1 " + + (X64?"/Zi":"/ZI")); + ADD_FLAG("LDFLAGS", "/debug"); + // Avoid problems when linking to release libraries that use the release +@@ -3419,7 +3419,7 @@ + ADD_FLAG("CFLAGS", "/Zi"); + ADD_FLAG("LDFLAGS", "/incremental:no /debug /opt:ref,icf"); + } +- ADD_FLAG("CFLAGS", "/LD /MD"); ++ ADD_FLAG("CFLAGS", "/LD /MT"); + if (PHP_SANITIZER == "yes" && CLANG_TOOLSET) { + ADD_FLAG("CFLAGS", "/Od /D NDebug /D NDEBUG /D ZEND_WIN32_NEVER_INLINE /D ZEND_DEBUG=0"); + } else { diff --git a/src/globals/patch/php-src-patches/win32_82.patch b/src/globals/patch/php-src-patches/win32_82.patch new file mode 100644 index 000000000..d6c4de042 --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_82.patch @@ -0,0 +1,22 @@ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index dc6675c6d2..587d4022a6 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -3454,7 +3454,7 @@ function toolset_setup_common_libs() + function toolset_setup_build_mode() + { + if (PHP_DEBUG == "yes") { +- ADD_FLAG("CFLAGS", "/LDd /MDd /Od /D _DEBUG /D ZEND_DEBUG=1 " + ++ ADD_FLAG("CFLAGS", "/LDd /MTd /Od /D _DEBUG /D ZEND_DEBUG=1 " + + (TARGET_ARCH == 'x86'?"/ZI":"/Zi")); + ADD_FLAG("LDFLAGS", "/debug"); + // Avoid problems when linking to release libraries that use the release +@@ -3466,7 +3466,7 @@ function toolset_setup_build_mode() + ADD_FLAG("CFLAGS", "/Zi"); + ADD_FLAG("LDFLAGS", "/incremental:no /debug /opt:ref,icf"); + } +- ADD_FLAG("CFLAGS", "/LD /MD"); ++ ADD_FLAG("CFLAGS", "/LD /MT"); + if (PHP_SANITIZER == "yes" && CLANG_TOOLSET) { + ADD_FLAG("CFLAGS", "/Od /D NDebug /D NDEBUG /D ZEND_WIN32_NEVER_INLINE /D ZEND_DEBUG=0"); + } else { diff --git a/src/globals/patch/php-src-patches/win32_85.patch b/src/globals/patch/php-src-patches/win32_85.patch new file mode 100644 index 000000000..33d061844 --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_85.patch @@ -0,0 +1,22 @@ +diff --git a/win32/build/confutils.js b/win32/build/confutils.js +index 0f97a1a2..4797967d 100644 +--- a/win32/build/confutils.js ++++ b/win32/build/confutils.js +@@ -3450,7 +3450,7 @@ function toolset_setup_common_libs() + function toolset_setup_build_mode() + { + if (PHP_DEBUG == "yes") { +- ADD_FLAG("CFLAGS", "/MDd /Od /U NDebug /U NDEBUG /D ZEND_DEBUG=1 " + ++ ADD_FLAG("CFLAGS", "/MTd /Od /U NDebug /U NDEBUG /D ZEND_DEBUG=1 " + + (TARGET_ARCH == 'x86'?"/ZI":"/Zi")); + ADD_FLAG("LDFLAGS", "/debug"); + // Avoid problems when linking to release libraries that use the release +@@ -3462,7 +3462,7 @@ function toolset_setup_build_mode() + ADD_FLAG("CFLAGS", "/Zi"); + ADD_FLAG("LDFLAGS", "/incremental:no /debug /opt:ref,icf"); + } +- ADD_FLAG("CFLAGS", "/MD"); ++ ADD_FLAG("CFLAGS", "/MT"); + if (PHP_SANITIZER == "yes") { + if (VS_TOOLSET) { + ADD_FLAG("CFLAGS", "/Ox /U NDebug /U NDEBUG /D ZEND_DEBUG=1"); diff --git a/src/globals/patch/php-src-patches/win32_api_80.patch b/src/globals/patch/php-src-patches/win32_api_80.patch new file mode 100644 index 000000000..82d834f2c --- /dev/null +++ b/src/globals/patch/php-src-patches/win32_api_80.patch @@ -0,0 +1,88 @@ +diff --git a/win32/dllmain.c b/win32/dllmain.c +index a507f1e1..ab625bf3 100644 +--- a/win32/dllmain.c ++++ b/win32/dllmain.c +@@ -38,20 +38,6 @@ BOOL WINAPI DllMain(HINSTANCE inst, DWORD reason, LPVOID dummy) + switch (reason) + { + case DLL_PROCESS_ATTACH: +- /* +- * We do not need to check the return value of php_win32_init_gettimeofday() +- * because the symbol bare minimum symbol we need is always available on our +- * lowest supported platform. +- * +- * On Windows 8 or greater, we use a more precise symbol to obtain the system +- * time, which is dynamically. The fallback allows us to proper support +- * Vista/7/Server 2003 R2/Server 2008/Server 2008 R2. +- * +- * Instead simply initialize the global in win32/time.c for gettimeofday() +- * use later on +- */ +- php_win32_init_gettimeofday(); +- + ret = ret && php_win32_ioutil_init(); + if (!ret) { + fprintf(stderr, "ioutil initialization failed"); +diff --git a/win32/time.c b/win32/time.c +index d1fe5145..57db914e 100644 +--- a/win32/time.c ++++ b/win32/time.c +@@ -23,42 +23,13 @@ + #include + #include "php_win32_globals.h" + +-typedef VOID (WINAPI *MyGetSystemTimeAsFileTime)(LPFILETIME lpSystemTimeAsFileTime); +- +-static MyGetSystemTimeAsFileTime timefunc = NULL; +- +-#ifdef PHP_EXPORTS +-static zend_always_inline MyGetSystemTimeAsFileTime get_time_func(void) +-{/*{{{*/ +- MyGetSystemTimeAsFileTime timefunc = NULL; +- HMODULE hMod = GetModuleHandle("kernel32.dll"); +- +- if (hMod) { +- /* Max possible resolution <1us, win8/server2012 */ +- timefunc = (MyGetSystemTimeAsFileTime)GetProcAddress(hMod, "GetSystemTimePreciseAsFileTime"); +- } +- +- if(!timefunc) { +- /* 100ns blocks since 01-Jan-1641 */ +- timefunc = (MyGetSystemTimeAsFileTime) GetSystemTimeAsFileTime; +- } +- +- return timefunc; +-}/*}}}*/ +- +-void php_win32_init_gettimeofday(void) +-{/*{{{*/ +- timefunc = get_time_func(); +-}/*}}}*/ +-#endif +- + static zend_always_inline int getfilesystemtime(struct timeval *tv) + {/*{{{*/ + FILETIME ft; + unsigned __int64 ff = 0; + ULARGE_INTEGER fft; + +- timefunc(&ft); ++ GetSystemTimePreciseAsFileTime(&ft); + + /* + * Do not cast a pointer to a FILETIME structure to either a +diff --git a/win32/time.h b/win32/time.h +index 51090ccf..77d1cbfd 100644 +--- a/win32/time.h ++++ b/win32/time.h +@@ -54,10 +54,4 @@ PHPAPI int nanosleep( const struct timespec * rqtp, struct timespec * rmtp ); + + PHPAPI int usleep(unsigned int useconds); + +-#ifdef PHP_EXPORTS +-/* This symbols are needed only for the DllMain, but should not be exported +- or be available when used with PHP binaries. */ +-void php_win32_init_gettimeofday(void); +-#endif +- + #endif diff --git a/src/globals/patch/php-src-patches/win32_api_84.patch b/src/globals/patch/php-src-patches/win32_api_84.patch new file mode 100644 index 000000000..e69de29bb diff --git a/src/globals/patch/php-src-patches/zend_stream.patch b/src/globals/patch/php-src-patches/zend_stream.patch new file mode 100644 index 000000000..09295afd2 --- /dev/null +++ b/src/globals/patch/php-src-patches/zend_stream.patch @@ -0,0 +1,12 @@ +--- php-8.0.0-src/Zend/zend_stream.c 2020-11-24 17:04:03.000000000 +0000 ++++ php-8.0.0-micro/Zend/zend_stream.c 2020-12-03 07:01:36.375355300 +0000 +@@ -23,7 +23,9 @@ + #include "zend_compile.h" + #include "zend_stream.h" + ++#if !defined(_CRT_INTERNAL_NONSTDC_NAMES) || !_CRT_INTERNAL_NONSTDC_NAMES + ZEND_DLIMPORT int isatty(int fd); ++#endif + + static ssize_t zend_stream_stdio_reader(void *handle, char *buf, size_t len) /* {{{ */ + { diff --git a/src/globals/scripts/zig-cc.sh b/src/globals/scripts/zig-cc.sh new file mode 100755 index 000000000..56ae95055 --- /dev/null +++ b/src/globals/scripts/zig-cc.sh @@ -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[@]}" diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 1d31d5202..f44914ece 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,9 +13,9 @@ // 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_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 @@ // 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' => '', }; diff --git a/tests/SPC/GlobalDefinesTest.php b/tests/SPC/GlobalDefinesTest.php deleted file mode 100644 index 956b35e25..000000000 --- a/tests/SPC/GlobalDefinesTest.php +++ /dev/null @@ -1,25 +0,0 @@ -assertTrue(defined('WORKING_DIR')); - } - - public function testInternalEnv(): void - { - require __DIR__ . '/../../src/globals/internal-env.php'; - $this->assertTrue(defined('GNU_ARCH')); - } -} diff --git a/tests/SPC/GlobalFunctionsTest.php b/tests/SPC/GlobalFunctionsTest.php deleted file mode 100644 index 6a8bd7388..000000000 --- a/tests/SPC/GlobalFunctionsTest.php +++ /dev/null @@ -1,33 +0,0 @@ -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); - } -} diff --git a/tests/SPC/builder/BuilderTest.php b/tests/SPC/builder/BuilderTest.php deleted file mode 100644 index b4b6258fd..000000000 --- a/tests/SPC/builder/BuilderTest.php +++ /dev/null @@ -1,246 +0,0 @@ -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 = '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'); - } -} diff --git a/tests/SPC/builder/ExtensionTest.php b/tests/SPC/builder/ExtensionTest.php deleted file mode 100644 index 89462065c..000000000 --- a/tests/SPC/builder/ExtensionTest.php +++ /dev/null @@ -1,94 +0,0 @@ -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()); - } -} diff --git a/tests/SPC/builder/linux/SystemUtilTest.php b/tests/SPC/builder/linux/SystemUtilTest.php deleted file mode 100644 index 01d555d81..000000000 --- a/tests/SPC/builder/linux/SystemUtilTest.php +++ /dev/null @@ -1,60 +0,0 @@ -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'])); - } -} diff --git a/tests/SPC/builder/macos/SystemUtilTest.php b/tests/SPC/builder/macos/SystemUtilTest.php deleted file mode 100644 index 4af764a74..000000000 --- a/tests/SPC/builder/macos/SystemUtilTest.php +++ /dev/null @@ -1,31 +0,0 @@ -assertIsInt(SystemUtil::getCpuCount()); - } - - public function testGetArchCFlags() - { - $this->assertEquals('--target=x86_64-apple-darwin', SystemUtil::getArchCFlags('x86_64')); - } -} diff --git a/tests/SPC/builder/unix/UnixSystemUtilTest.php b/tests/SPC/builder/unix/UnixSystemUtilTest.php deleted file mode 100644 index e17574af6..000000000 --- a/tests/SPC/builder/unix/UnixSystemUtilTest.php +++ /dev/null @@ -1,42 +0,0 @@ - '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'])); - } -} diff --git a/tests/SPC/doctor/CheckListHandlerTest.php b/tests/SPC/doctor/CheckListHandlerTest.php deleted file mode 100644 index c6191e30d..000000000 --- a/tests/SPC/doctor/CheckListHandlerTest.php +++ /dev/null @@ -1,24 +0,0 @@ -getValidCheckList(); - foreach ($id as $item) { - $this->assertInstanceOf('SPC\doctor\AsCheckItem', $item); - } - } -} diff --git a/tests/SPC/globals/GlobalFunctionsTest.php b/tests/SPC/globals/GlobalFunctionsTest.php deleted file mode 100644 index 818f9686d..000000000 --- a/tests/SPC/globals/GlobalFunctionsTest.php +++ /dev/null @@ -1,94 +0,0 @@ -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'); - } -} diff --git a/tests/SPC/store/ConfigTest.php b/tests/SPC/store/ConfigTest.php deleted file mode 100644 index 84701cd4c..000000000 --- a/tests/SPC/store/ConfigTest.php +++ /dev/null @@ -1,68 +0,0 @@ -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())); - } -} diff --git a/tests/SPC/store/CurlHookTest.php b/tests/SPC/store/CurlHookTest.php deleted file mode 100644 index 8b950da64..000000000 --- a/tests/SPC/store/CurlHookTest.php +++ /dev/null @@ -1,29 +0,0 @@ -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); - } -} diff --git a/tests/SPC/store/DownloaderTest.php b/tests/SPC/store/DownloaderTest.php deleted file mode 100644 index 749fb3856..000000000 --- a/tests/SPC/store/DownloaderTest.php +++ /dev/null @@ -1,113 +0,0 @@ -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="(?filelist-(?[^"]+)\.tar\.xz)"/', - ]); - $this->assertIsArray($filelist); - $this->assertEquals('filelist-4.7.0.tar.xz', $filelist[1]); - } -} diff --git a/tests/SPC/store/FileSystemTest.php b/tests/SPC/store/FileSystemTest.php deleted file mode 100644 index 13d2893f3..000000000 --- a/tests/SPC/store/FileSystemTest.php +++ /dev/null @@ -1,176 +0,0 @@ -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'); - } -} diff --git a/tests/SPC/util/ConfigValidatorTest.php b/tests/SPC/util/ConfigValidatorTest.php deleted file mode 100644 index aba611a42..000000000 --- a/tests/SPC/util/ConfigValidatorTest.php +++ /dev/null @@ -1,767 +0,0 @@ - [ - '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="(?.*\.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); - } - } -} diff --git a/tests/SPC/util/DependencyUtilTest.php b/tests/SPC/util/DependencyUtilTest.php deleted file mode 100644 index 468f6eb2c..000000000 --- a/tests/SPC/util/DependencyUtilTest.php +++ /dev/null @@ -1,113 +0,0 @@ -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']); - } -} diff --git a/tests/SPC/util/GlobalEnvManagerTest.php b/tests/SPC/util/GlobalEnvManagerTest.php deleted file mode 100644 index 1d9ed1083..000000000 --- a/tests/SPC/util/GlobalEnvManagerTest.php +++ /dev/null @@ -1,143 +0,0 @@ -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'], - ]; - } -} diff --git a/tests/SPC/util/LicenseDumperTest.php b/tests/SPC/util/LicenseDumperTest.php deleted file mode 100644 index ed914296c..000000000 --- a/tests/SPC/util/LicenseDumperTest.php +++ /dev/null @@ -1,110 +0,0 @@ - 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']; - } -} diff --git a/tests/SPC/util/PkgConfigUtilTest.php b/tests/SPC/util/PkgConfigUtilTest.php deleted file mode 100644 index 41de6d70e..000000000 --- a/tests/SPC/util/PkgConfigUtilTest.php +++ /dev/null @@ -1,210 +0,0 @@ -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); - } -} diff --git a/tests/SPC/util/SPCConfigUtilTest.php b/tests/SPC/util/SPCConfigUtilTest.php deleted file mode 100644 index c4b1427a2..000000000 --- a/tests/SPC/util/SPCConfigUtilTest.php +++ /dev/null @@ -1,74 +0,0 @@ -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); - } - } -} diff --git a/tests/SPC/util/SPCTargetTest.php b/tests/SPC/util/SPCTargetTest.php deleted file mode 100644 index 665b4045b..000000000 --- a/tests/SPC/util/SPCTargetTest.php +++ /dev/null @@ -1,106 +0,0 @@ -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'); - } -} diff --git a/tests/SPC/util/TestBase.php b/tests/SPC/util/TestBase.php deleted file mode 100644 index fd82ccfbf..000000000 --- a/tests/SPC/util/TestBase.php +++ /dev/null @@ -1,100 +0,0 @@ -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]; - } -} diff --git a/tests/SPC/util/UnixShellTest.php b/tests/SPC/util/UnixShellTest.php deleted file mode 100644 index f65e5a406..000000000 --- a/tests/SPC/util/UnixShellTest.php +++ /dev/null @@ -1,184 +0,0 @@ -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'], - ]; - } -} diff --git a/tests/SPC/util/WindowsCmdTest.php b/tests/SPC/util/WindowsCmdTest.php deleted file mode 100644 index fb4ee3f2e..000000000 --- a/tests/SPC/util/WindowsCmdTest.php +++ /dev/null @@ -1,68 +0,0 @@ -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'], - ]; - } -} diff --git a/tests/StaticPHP/Config/ArtifactConfigTest.php b/tests/StaticPHP/Config/ArtifactConfigTest.php new file mode 100644 index 000000000..dc3964881 --- /dev/null +++ b/tests/StaticPHP/Config/ArtifactConfigTest.php @@ -0,0 +1,303 @@ +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([]); + } + + /** @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([]); + } + + 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); + } +} diff --git a/tests/StaticPHP/Config/ConfigTypeTest.php b/tests/StaticPHP/Config/ConfigTypeTest.php new file mode 100644 index 000000000..0990931ef --- /dev/null +++ b/tests/StaticPHP/Config/ConfigTypeTest.php @@ -0,0 +1,196 @@ +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)); + } +} diff --git a/tests/StaticPHP/Config/ConfigValidatorTest.php b/tests/StaticPHP/Config/ConfigValidatorTest.php new file mode 100644 index 000000000..ae5544ae1 --- /dev/null +++ b/tests/StaticPHP/Config/ConfigValidatorTest.php @@ -0,0 +1,627 @@ +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); + } +} diff --git a/tests/StaticPHP/Config/PackageConfigTest.php b/tests/StaticPHP/Config/PackageConfigTest.php new file mode 100644 index 000000000..4072e39e9 --- /dev/null +++ b/tests/StaticPHP/Config/PackageConfigTest.php @@ -0,0 +1,434 @@ +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([]); + } + + 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([]); + } + + 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); + } +} diff --git a/tests/StaticPHP/DI/ApplicationContextTest.php b/tests/StaticPHP/DI/ApplicationContextTest.php new file mode 100644 index 000000000..3da63f47c --- /dev/null +++ b/tests/StaticPHP/DI/ApplicationContextTest.php @@ -0,0 +1,433 @@ +assertInstanceOf(Container::class, $container); + $this->assertSame($container, ApplicationContext::getContainer()); + } + + public function testInitializeWithDebugMode(): void + { + ApplicationContext::initialize(['debug' => true]); + + $this->assertTrue(ApplicationContext::isDebug()); + } + + public function testInitializeWithoutDebugMode(): void + { + ApplicationContext::initialize(['debug' => false]); + + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testInitializeWithCustomDefinitions(): void + { + $customValue = 'test_value'; + ApplicationContext::initialize([ + 'definitions' => [ + 'test.service' => $customValue, + ], + ]); + + $this->assertEquals($customValue, ApplicationContext::get('test.service')); + } + + public function testInitializeThrowsExceptionWhenAlreadyInitialized(): void + { + ApplicationContext::initialize(); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('ApplicationContext already initialized'); + ApplicationContext::initialize(); + } + + public function testGetContainerAutoInitializes(): void + { + // Don't call initialize + $container = ApplicationContext::getContainer(); + + $this->assertInstanceOf(Container::class, $container); + } + + public function testGetReturnsServiceFromContainer(): void + { + ApplicationContext::initialize([ + 'definitions' => [ + 'test.key' => 'test_value', + ], + ]); + + $this->assertEquals('test_value', ApplicationContext::get('test.key')); + } + + public function testGetWithClassType(): void + { + ApplicationContext::initialize(); + + $container = ApplicationContext::get(Container::class); + $this->assertInstanceOf(Container::class, $container); + } + + public function testGetContainerInterface(): void + { + ApplicationContext::initialize(); + + $container = ApplicationContext::get(ContainerInterface::class); + $this->assertInstanceOf(ContainerInterface::class, $container); + } + + public function testHasReturnsTrueForExistingService(): void + { + ApplicationContext::initialize([ + 'definitions' => [ + 'test.service' => 'value', + ], + ]); + + $this->assertTrue(ApplicationContext::has('test.service')); + } + + public function testHasReturnsFalseForNonExistingService(): void + { + ApplicationContext::initialize(); + + $this->assertFalse(ApplicationContext::has('non.existing.service')); + } + + public function testSetAddsServiceToContainer(): void + { + ApplicationContext::initialize(); + + ApplicationContext::set('dynamic.service', 'dynamic_value'); + + $this->assertTrue(ApplicationContext::has('dynamic.service')); + $this->assertEquals('dynamic_value', ApplicationContext::get('dynamic.service')); + } + + public function testSetOverridesExistingService(): void + { + ApplicationContext::initialize([ + 'definitions' => [ + 'test.service' => 'original', + ], + ]); + + ApplicationContext::set('test.service', 'updated'); + + $this->assertEquals('updated', ApplicationContext::get('test.service')); + } + + public function testBindCommandContextSetsInputAndOutput(): void + { + ApplicationContext::initialize(); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $output->method('isDebug')->willReturn(false); + + ApplicationContext::bindCommandContext($input, $output); + + $this->assertSame($input, ApplicationContext::get(InputInterface::class)); + $this->assertSame($output, ApplicationContext::get(OutputInterface::class)); + } + + public function testBindCommandContextSetsDebugMode(): void + { + ApplicationContext::initialize(); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $output->method('isDebug')->willReturn(true); + + ApplicationContext::bindCommandContext($input, $output); + + $this->assertTrue(ApplicationContext::isDebug()); + } + + public function testBindCommandContextWithNonDebugOutput(): void + { + ApplicationContext::initialize(); + + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $output->method('isDebug')->willReturn(false); + + ApplicationContext::bindCommandContext($input, $output); + + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testGetInvokerReturnsCallbackInvoker(): void + { + ApplicationContext::initialize(); + + $invoker = ApplicationContext::getInvoker(); + + $this->assertInstanceOf(CallbackInvoker::class, $invoker); + } + + public function testGetInvokerReturnsSameInstance(): void + { + ApplicationContext::initialize(); + + $invoker1 = ApplicationContext::getInvoker(); + $invoker2 = ApplicationContext::getInvoker(); + + $this->assertSame($invoker1, $invoker2); + } + + public function testGetInvokerAutoInitializesContainer(): void + { + // Don't call initialize + $invoker = ApplicationContext::getInvoker(); + + $this->assertInstanceOf(CallbackInvoker::class, $invoker); + } + + public function testInvokeCallsCallback(): void + { + ApplicationContext::initialize(); + + $called = false; + $callback = function () use (&$called) { + $called = true; + return 'result'; + }; + + $result = ApplicationContext::invoke($callback); + + $this->assertTrue($called); + $this->assertEquals('result', $result); + } + + public function testInvokeWithContext(): void + { + ApplicationContext::initialize(); + + $callback = function (string $param) { + return $param; + }; + + $result = ApplicationContext::invoke($callback, ['param' => 'test_value']); + + $this->assertEquals('test_value', $result); + } + + public function testInvokeWithDependencyInjection(): void + { + ApplicationContext::initialize(); + + $callback = function (Container $container) { + return $container; + }; + + $result = ApplicationContext::invoke($callback); + + $this->assertInstanceOf(Container::class, $result); + } + + public function testInvokeWithArrayCallback(): void + { + ApplicationContext::initialize(); + + $object = new class { + public function method(): string + { + return 'called'; + } + }; + + $result = ApplicationContext::invoke([$object, 'method']); + + $this->assertEquals('called', $result); + } + + public function testIsDebugDefaultsFalse(): void + { + ApplicationContext::initialize(); + + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testSetDebugChangesDebugMode(): void + { + ApplicationContext::initialize(); + + ApplicationContext::setDebug(true); + $this->assertTrue(ApplicationContext::isDebug()); + + ApplicationContext::setDebug(false); + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testResetClearsContainer(): void + { + ApplicationContext::initialize(); + ApplicationContext::set('test.service', 'value'); + + ApplicationContext::reset(); + + // After reset, container should be reinitialized + $this->assertFalse(ApplicationContext::has('test.service')); + } + + public function testResetClearsInvoker(): void + { + ApplicationContext::initialize(); + $invoker1 = ApplicationContext::getInvoker(); + + ApplicationContext::reset(); + + $invoker2 = ApplicationContext::getInvoker(); + $this->assertNotSame($invoker1, $invoker2); + } + + public function testResetClearsDebugMode(): void + { + ApplicationContext::initialize(['debug' => true]); + $this->assertTrue(ApplicationContext::isDebug()); + + ApplicationContext::reset(); + + // After reset and reinit, debug should be false by default + ApplicationContext::initialize(); + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testResetAllowsReinitialize(): void + { + ApplicationContext::initialize(); + ApplicationContext::reset(); + + // Should not throw exception + $container = ApplicationContext::initialize(['debug' => true]); + + $this->assertInstanceOf(Container::class, $container); + $this->assertTrue(ApplicationContext::isDebug()); + } + + public function testCallbackInvokerIsAvailableInContainer(): void + { + ApplicationContext::initialize(); + + $invoker = ApplicationContext::get(CallbackInvoker::class); + + $this->assertInstanceOf(CallbackInvoker::class, $invoker); + } + + public function testMultipleGetCallsReturnSameContainer(): void + { + $container1 = ApplicationContext::getContainer(); + $container2 = ApplicationContext::getContainer(); + + $this->assertSame($container1, $container2); + } + + public function testInitializeWithEmptyOptions(): void + { + $container = ApplicationContext::initialize([]); + + $this->assertInstanceOf(Container::class, $container); + $this->assertFalse(ApplicationContext::isDebug()); + } + + public function testInitializeWithNullDefinitions(): void + { + $container = ApplicationContext::initialize(['definitions' => null]); + + $this->assertInstanceOf(Container::class, $container); + } + + public function testInitializeWithEmptyDefinitions(): void + { + $container = ApplicationContext::initialize(['definitions' => []]); + + $this->assertInstanceOf(Container::class, $container); + } + + public function testSetBeforeInitializeAutoInitializes(): void + { + // Don't call initialize + ApplicationContext::set('test.service', 'value'); + + $this->assertEquals('value', ApplicationContext::get('test.service')); + } + + public function testHasBeforeInitializeAutoInitializes(): void + { + // Don't call initialize, should auto-initialize + $result = ApplicationContext::has(Container::class); + + $this->assertTrue($result); + } + + public function testGetBeforeInitializeAutoInitializes(): void + { + // Don't call initialize + $container = ApplicationContext::get(Container::class); + + $this->assertInstanceOf(Container::class, $container); + } + + public function testInvokerSingletonConsistency(): void + { + // Test fix for issue #3 and #4 - Invoker instance consistency + ApplicationContext::initialize(); + + $invoker1 = ApplicationContext::getInvoker(); + $invoker2 = ApplicationContext::get(CallbackInvoker::class); + + // Both should return the same instance + $this->assertSame($invoker1, $invoker2); + } + + public function testInvokerSingletonConsistencyAfterReset(): void + { + ApplicationContext::initialize(); + $invoker1 = ApplicationContext::getInvoker(); + + ApplicationContext::reset(); + ApplicationContext::initialize(); + + $invoker2 = ApplicationContext::getInvoker(); + $invoker3 = ApplicationContext::get(CallbackInvoker::class); + + // After reset, should be new instance + $this->assertNotSame($invoker1, $invoker2); + // But getInvoker() and container should still be consistent + $this->assertSame($invoker2, $invoker3); + } +} diff --git a/tests/StaticPHP/DI/CallbackInvokerTest.php b/tests/StaticPHP/DI/CallbackInvokerTest.php new file mode 100644 index 000000000..751a70eb9 --- /dev/null +++ b/tests/StaticPHP/DI/CallbackInvokerTest.php @@ -0,0 +1,629 @@ +container = new Container(); + $this->invoker = new CallbackInvoker($this->container); + } + + public function testInvokeSimpleCallbackWithoutParameters(): void + { + $callback = function () { + return 'result'; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals('result', $result); + } + + public function testInvokeCallbackWithContextByTypeName(): void + { + $callback = function (string $param) { + return $param; + }; + + $result = $this->invoker->invoke($callback, ['string' => 'test_value']); + + $this->assertEquals('test_value', $result); + } + + public function testInvokeCallbackWithContextByParameterName(): void + { + $callback = function (string $myParam) { + return $myParam; + }; + + $result = $this->invoker->invoke($callback, ['myParam' => 'test_value']); + + $this->assertEquals('test_value', $result); + } + + public function testInvokeCallbackWithContextByTypeNameTakesPrecedence(): void + { + $callback = function (string $myParam) { + return $myParam; + }; + + // Type name should take precedence over parameter name + $result = $this->invoker->invoke($callback, [ + 'string' => 'by_type', + 'myParam' => 'by_name', + ]); + + $this->assertEquals('by_type', $result); + } + + public function testInvokeCallbackWithContainerResolution(): void + { + $this->container->set('test.service', 'service_value'); + + $callback = function (string $testService) { + return $testService; + }; + + // Should not resolve from container as 'test.service' is not a type + // Will try default value or null + $this->expectException(\RuntimeException::class); + $this->invoker->invoke($callback); + } + + public function testInvokeCallbackWithClassTypeFromContainer(): void + { + $testObject = new \stdClass(); + $testObject->value = 'test'; + $this->container->set(\stdClass::class, $testObject); + + $callback = function (\stdClass $obj) { + return $obj->value; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals('test', $result); + } + + public function testInvokeCallbackWithDefaultValue(): void + { + $callback = function (string $param = 'default_value') { + return $param; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals('default_value', $result); + } + + public function testInvokeCallbackWithNullableParameter(): void + { + $callback = function (?string $param) { + return $param ?? 'was_null'; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals('was_null', $result); + } + + public function testInvokeCallbackThrowsExceptionForUnresolvableParameter(): void + { + $callback = function (string $required) { + return $required; + }; + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("Cannot resolve parameter 'required' of type 'string'"); + $this->invoker->invoke($callback); + } + + public function testInvokeCallbackThrowsExceptionForNonExistentClass(): void + { + // This test uses UnresolvableTestClass which has required constructor params + // Container.has() will return true but get() will throw InvalidDefinition + // So we test that container exceptions bubble up + $callback = function (UnresolvableTestClass $obj) { + return $obj; + }; + + $this->expectException(\Throwable::class); + $this->invoker->invoke($callback); + } + + public function testInvokeCallbackWithMultipleParameters(): void + { + $callback = function (string $first, int $second, bool $third) { + return [$first, $second, $third]; + }; + + $result = $this->invoker->invoke($callback, [ + 'first' => 'value1', + 'second' => 42, + 'third' => true, + ]); + + $this->assertEquals(['value1', 42, true], $result); + } + + public function testInvokeCallbackWithMixedResolutionSources(): void + { + $this->container->set(\stdClass::class, new \stdClass()); + + $callback = function ( + \stdClass $fromContainer, + string $fromContext, + int $withDefault = 100 + ) { + return [$fromContainer, $fromContext, $withDefault]; + }; + + $result = $this->invoker->invoke($callback, ['fromContext' => 'context_value']); + + $this->assertInstanceOf(\stdClass::class, $result[0]); + $this->assertEquals('context_value', $result[1]); + $this->assertEquals(100, $result[2]); + } + + public function testExpandContextHierarchyWithObject(): void + { + // Create a simple parent-child relationship + $childClass = new \ArrayObject(['key' => 'value']); + + $callback = function (\ArrayObject $obj) { + return $obj; + }; + + $result = $this->invoker->invoke($callback, [get_class($childClass) => $childClass]); + + $this->assertSame($childClass, $result); + } + + public function testExpandContextHierarchyWithInterface(): void + { + $object = new class implements \Countable { + public function count(): int + { + return 42; + } + }; + + $callback = function (\Countable $countable) { + return $countable->count(); + }; + + $result = $this->invoker->invoke($callback, [get_class($object) => $object]); + + $this->assertEquals(42, $result); + } + + public function testExpandContextHierarchyWithMultipleInterfaces(): void + { + $object = new class implements \Countable, \IteratorAggregate { + public function count(): int + { + return 5; + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator([]); + } + }; + + $callback = function (\Countable $c, \IteratorAggregate $i) { + return [$c->count(), $i]; + }; + + $result = $this->invoker->invoke($callback, ['obj' => $object]); + + $this->assertEquals(5, $result[0]); + $this->assertInstanceOf(\IteratorAggregate::class, $result[1]); + } + + public function testInvokeWithArrayCallback(): void + { + $testClass = new class { + public function method(string $param): string + { + return 'called_' . $param; + } + }; + + $result = $this->invoker->invoke([$testClass, 'method'], ['param' => 'test']); + + $this->assertEquals('called_test', $result); + } + + public function testInvokeWithStaticMethod(): void + { + $testClass = new class { + public static function staticMethod(string $param): string + { + return 'static_' . $param; + } + }; + + $className = get_class($testClass); + $result = $this->invoker->invoke([$className, 'staticMethod'], ['param' => 'value']); + + $this->assertEquals('static_value', $result); + } + + public function testInvokeWithCallableString(): void + { + $callback = 'Tests\StaticPHP\DI\testFunction'; + + if (!function_exists($callback)) { + eval('namespace Tests\StaticPHP\DI; function testFunction(string $param) { return "func_" . $param; }'); + } + + $result = $this->invoker->invoke($callback, ['param' => 'test']); + + $this->assertEquals('func_test', $result); + } + + public function testInvokeWithNoTypeHintedParameter(): void + { + $callback = function ($param) { + return $param; + }; + + $result = $this->invoker->invoke($callback, ['param' => 'value']); + + $this->assertEquals('value', $result); + } + + public function testInvokeWithNoTypeHintedParameterReturnsNull(): void + { + // Parameters without type hints are implicitly nullable in PHP + $callback = function ($param) { + return $param; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertNull($result); + } + + public function testInvokeWithNoTypeHintAndValueInContext(): void + { + $callback = function ($param) { + return $param; + }; + + $result = $this->invoker->invoke($callback, ['param' => 'value']); + + $this->assertEquals('value', $result); + } + + public function testInvokeWithBuiltinTypes(): void + { + $callback = function ( + string $str, + int $num, + float $decimal, + bool $flag, + array $arr + ) { + return compact('str', 'num', 'decimal', 'flag', 'arr'); + }; + + $result = $this->invoker->invoke($callback, [ + 'str' => 'test', + 'num' => 42, + 'decimal' => 3.14, + 'flag' => true, + 'arr' => [1, 2, 3], + ]); + + $this->assertEquals([ + 'str' => 'test', + 'num' => 42, + 'decimal' => 3.14, + 'flag' => true, + 'arr' => [1, 2, 3], + ], $result); + } + + public function testInvokeWithEmptyContext(): void + { + $callback = function () { + return 'no_params'; + }; + + $result = $this->invoker->invoke($callback, []); + + $this->assertEquals('no_params', $result); + } + + public function testInvokePreservesCallbackReturnValue(): void + { + $callback = function () { + return ['key' => 'value', 'number' => 123]; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertEquals(['key' => 'value', 'number' => 123], $result); + } + + public function testInvokeWithNullReturnValue(): void + { + $callback = function () { + return null; + }; + + $result = $this->invoker->invoke($callback); + + $this->assertNull($result); + } + + public function testInvokeWithObjectInContext(): void + { + $obj = new \stdClass(); + $obj->value = 'test'; + + $callback = function (\stdClass $param) { + return $param->value; + }; + + $result = $this->invoker->invoke($callback, ['param' => $obj]); + + $this->assertEquals('test', $result); + } + + public function testInvokeWithInheritanceInContext(): void + { + $exception = new \RuntimeException('test message'); + + $callback = function (\Exception $e) { + return $e->getMessage(); + }; + + // RuntimeException should be resolved as Exception via hierarchy expansion + $result = $this->invoker->invoke($callback, ['exc' => $exception]); + + $this->assertEquals('test message', $result); + } + + public function testInvokeContextValueOverridesContainer(): void + { + $containerObj = new \stdClass(); + $containerObj->source = 'container'; + $this->container->set(\stdClass::class, $containerObj); + + $contextObj = new \stdClass(); + $contextObj->source = 'context'; + + $callback = function (\stdClass $obj) { + return $obj->source; + }; + + // Context should override container + $result = $this->invoker->invoke($callback, [\stdClass::class => $contextObj]); + + $this->assertEquals('context', $result); + } + + public function testInvokeWithDefaultValueNotUsedWhenContextProvided(): void + { + $callback = function (string $param = 'default') { + return $param; + }; + + $result = $this->invoker->invoke($callback, ['param' => 'from_context']); + + $this->assertEquals('from_context', $result); + } + + public function testInvokeWithMixedNullableAndRequired(): void + { + $callback = function (string $required, ?string $optional) { + return [$required, $optional]; + }; + + $result = $this->invoker->invoke($callback, ['required' => 'value']); + + $this->assertEquals(['value', null], $result); + } + + public function testInvokeWithComplexObjectHierarchy(): void + { + // Use built-in PHP classes with inheritance + // ArrayIterator extends IteratorIterator implements ArrayAccess, SeekableIterator, Countable, Serializable + $arrayIterator = new \ArrayIterator(['test' => 'value']); + + // Test that the object can be resolved via interface (Countable) + $callback1 = function (\Countable $test) { + return $test->count(); + }; + + $result1 = $this->invoker->invoke($callback1, ['obj' => $arrayIterator]); + $this->assertEquals(1, $result1); + + // Test that the object can be resolved via another interface (Iterator) + $callback2 = function (\Iterator $test) { + return $test; + }; + + $result2 = $this->invoker->invoke($callback2, ['obj' => $arrayIterator]); + $this->assertInstanceOf(\ArrayIterator::class, $result2); + + // Test that the object can be resolved via concrete class + $callback3 = function (\ArrayIterator $test) { + return $test; + }; + + $result3 = $this->invoker->invoke($callback3, ['obj' => $arrayIterator]); + $this->assertSame($arrayIterator, $result3); + } + + public function testInvokeWithNonObjectContextValues(): void + { + $callback = function (string $str, int $num, array $arr, bool $flag) { + return compact('str', 'num', 'arr', 'flag'); + }; + + $context = [ + 'str' => 'hello', + 'num' => 999, + 'arr' => ['a', 'b'], + 'flag' => false, + ]; + + $result = $this->invoker->invoke($callback, $context); + + $this->assertEquals($context, $result); + } + + public function testInvokeParameterOrderMatters(): void + { + $callback = function (string $first, string $second, string $third) { + return [$first, $second, $third]; + }; + + $result = $this->invoker->invoke($callback, [ + 'first' => 'A', + 'second' => 'B', + 'third' => 'C', + ]); + + $this->assertEquals(['A', 'B', 'C'], $result); + } + + public function testInvokeWithUnionTypeThrowsException(): void + { + if (PHP_VERSION_ID < 80000) { + $this->markTestSkipped('Union types require PHP 8.0+'); + } + + $callback = eval('return function (string|int $param) { return $param; };'); + + // Union types are not ReflectionNamedType, should not be resolved from container + $this->expectException(\RuntimeException::class); + $this->invoker->invoke($callback); + } + + public function testInvokeWithCallableType(): void + { + $callback = function (callable $fn) { + return $fn(); + }; + + $result = $this->invoker->invoke($callback, [ + 'fn' => fn () => 'called', + ]); + + $this->assertEquals('called', $result); + } + + public function testInvokeWithIterableType(): void + { + $callback = function (iterable $items) { + $result = []; + foreach ($items as $item) { + $result[] = $item; + } + return $result; + }; + + $result = $this->invoker->invoke($callback, [ + 'items' => [1, 2, 3], + ]); + + $this->assertEquals([1, 2, 3], $result); + } + + public function testInvokeWithObjectType(): void + { + $callback = function (object $obj) { + return get_class($obj); + }; + + $testObj = new \stdClass(); + $result = $this->invoker->invoke($callback, ['obj' => $testObj]); + + $this->assertEquals('stdClass', $result); + } + + public function testInvokeWithContainerExceptionFallsThrough(): void + { + // Test fix for issue #1 - Container exceptions should be caught + // and fall through to other resolution strategies + $callback = function (?UnresolvableTestClass $obj = null) { + return $obj; + }; + + // Should use default value (null) instead of throwing container exception + $result = $this->invoker->invoke($callback); + + $this->assertNull($result); + } + + public function testInvokeWithContainerExceptionAndNoFallback(): void + { + // When there's no fallback (no default, not nullable), should throw RuntimeException + $callback = function (UnresolvableTestClass $obj) { + return $obj; + }; + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage("Cannot resolve parameter 'obj'"); + + $this->invoker->invoke($callback); + } + + public function testExpandContextHierarchyPerformance(): void + { + // Test fix for issue #2 - Should not create duplicate ReflectionClass + // This is more of a code quality test, ensuring the fix doesn't break functionality + $obj = new \ArrayIterator(['a', 'b', 'c']); + + $callback = function ( + \ArrayIterator $asArrayIterator, + \Traversable $asTraversable, + \Countable $asCountable + ) { + return [ + get_class($asArrayIterator), + get_class($asTraversable), + get_class($asCountable), + ]; + }; + + $result = $this->invoker->invoke($callback, ['obj' => $obj]); + + $this->assertEquals([ + 'ArrayIterator', + 'ArrayIterator', + 'ArrayIterator', + ], $result); + } +} diff --git a/tests/StaticPHP/Registry/ArtifactLoaderTest.php b/tests/StaticPHP/Registry/ArtifactLoaderTest.php new file mode 100644 index 000000000..75370dfe8 --- /dev/null +++ b/tests/StaticPHP/Registry/ArtifactLoaderTest.php @@ -0,0 +1,440 @@ +tempDir = sys_get_temp_dir() . '/artifact_loader_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset ArtifactLoader and ArtifactConfig state + $reflection = new \ReflectionClass(ArtifactLoader::class); + $property = $reflection->getProperty('artifacts'); + $property->setAccessible(true); + $property->setValue(null, null); + + $configReflection = new \ReflectionClass(ArtifactConfig::class); + $configProperty = $configReflection->getProperty('artifact_configs'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, []); + } + + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset ArtifactLoader and ArtifactConfig state + $reflection = new \ReflectionClass(ArtifactLoader::class); + $property = $reflection->getProperty('artifacts'); + $property->setAccessible(true); + $property->setValue(null, null); + + $configReflection = new \ReflectionClass(ArtifactConfig::class); + $configProperty = $configReflection->getProperty('artifact_configs'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, []); + } + + public function testInitArtifactInstancesOnlyRunsOnce(): void + { + $this->createTestArtifactConfig('test-artifact'); + + ArtifactLoader::initArtifactInstances(); + ArtifactLoader::initArtifactInstances(); + + // Should only initialize once + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertInstanceOf(Artifact::class, $artifact); + } + + public function testGetArtifactInstanceReturnsNullForNonExistent(): void + { + ArtifactLoader::initArtifactInstances(); + $artifact = ArtifactLoader::getArtifactInstance('non-existent-artifact'); + $this->assertNull($artifact); + } + + public function testGetArtifactInstanceReturnsArtifact(): void + { + $this->createTestArtifactConfig('test-artifact'); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertInstanceOf(Artifact::class, $artifact); + } + + public function testLoadFromClassWithCustomSourceAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomSource('test-artifact')] + public function customSource(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidCustomSourceArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomSource('non-existent-artifact')] + public function customSource(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[CustomSource]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithCustomBinaryAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomBinary('test-artifact', ['linux-x86_64', 'macos-aarch64'])] + public function customBinary(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidCustomBinaryArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomBinary('non-existent-artifact', ['linux-x86_64'])] + public function customBinary(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[CustomBinary]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithSourceExtractAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[SourceExtract('test-artifact')] + public function sourceExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidSourceExtractArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[SourceExtract('non-existent-artifact')] + public function sourceExtract(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[SourceExtract]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithBinaryExtractAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[BinaryExtract('test-artifact')] + public function binaryExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassWithBinaryExtractAttributeAndPlatforms(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[BinaryExtract('test-artifact', platforms: ['linux-x86_64', 'darwin-aarch64'])] + public function binaryExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidBinaryExtractArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[BinaryExtract('non-existent-artifact')] + public function binaryExtract(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[BinaryExtract]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithAfterSourceExtractAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterSourceExtract('test-artifact')] + public function afterSourceExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidAfterSourceExtractArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterSourceExtract('non-existent-artifact')] + public function afterSourceExtract(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[AfterSourceExtract]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithAfterBinaryExtractAttribute(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterBinaryExtract('test-artifact')] + public function afterBinaryExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassWithAfterBinaryExtractAttributeAndPlatforms(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterBinaryExtract('test-artifact', platforms: ['linux-x86_64'])] + public function afterBinaryExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromClassThrowsExceptionForInvalidAfterBinaryExtractArtifact(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[AfterBinaryExtract('non-existent-artifact')] + public function afterBinaryExtract(): void {} + }; + + $this->expectException(ValidationException::class); + $this->expectExceptionMessage("Artifact 'non-existent-artifact' not found for #[AfterBinaryExtract]"); + + ArtifactLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassWithMultipleAttributes(): void + { + $this->createTestArtifactConfig('test-artifact-1'); + $this->createTestArtifactConfig('test-artifact-2'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomSource('test-artifact-1')] + public function customSource(): void {} + + #[CustomBinary('test-artifact-2', ['linux-x86_64'])] + public function customBinary(): void {} + + #[SourceExtract('test-artifact-1')] + public function sourceExtract(): void {} + + #[AfterSourceExtract('test-artifact-2')] + public function afterSourceExtract(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact1 = ArtifactLoader::getArtifactInstance('test-artifact-1'); + $artifact2 = ArtifactLoader::getArtifactInstance('test-artifact-2'); + $this->assertNotNull($artifact1); + $this->assertNotNull($artifact2); + } + + public function testLoadFromClassIgnoresNonPublicMethods(): void + { + $this->createTestArtifactConfig('test-artifact'); + ArtifactLoader::initArtifactInstances(); + + $class = new class { + #[CustomSource('test-artifact')] + public function publicCustomSource(): void {} + + #[CustomSource('test-artifact')] + private function privateCustomSource(): void {} + + #[CustomSource('test-artifact')] + protected function protectedCustomSource(): void {} + }; + + // Should only process public method + ArtifactLoader::loadFromClass(get_class($class)); + + $artifact = ArtifactLoader::getArtifactInstance('test-artifact'); + $this->assertNotNull($artifact); + } + + public function testLoadFromPsr4DirLoadsAllClasses(): void + { + $this->createTestArtifactConfig('test-artifact'); + + // Create a PSR-4 directory structure + $psr4Dir = $this->tempDir . '/ArtifactClasses'; + mkdir($psr4Dir, 0755, true); + + // Create test class file + $classContent = 'assertNotNull($artifact); + } + + public function testLoadFromClassWithNoAttributes(): void + { + ArtifactLoader::initArtifactInstances(); + + $class = new class { + public function regularMethod(): void {} + }; + + // Should not throw exception + ArtifactLoader::loadFromClass(get_class($class)); + + // Verify no side effects + $this->assertTrue(true); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } + + private function createTestArtifactConfig(string $name): void + { + $reflection = new \ReflectionClass(ArtifactConfig::class); + $property = $reflection->getProperty('artifact_configs'); + $property->setAccessible(true); + $configs = $property->getValue(); + $configs[$name] = [ + 'type' => 'source', + 'url' => 'https://example.com/test.tar.gz', + ]; + $property->setValue(null, $configs); + } +} diff --git a/tests/StaticPHP/Registry/DoctorLoaderTest.php b/tests/StaticPHP/Registry/DoctorLoaderTest.php new file mode 100644 index 000000000..7817a880b --- /dev/null +++ b/tests/StaticPHP/Registry/DoctorLoaderTest.php @@ -0,0 +1,374 @@ +tempDir = sys_get_temp_dir() . '/doctor_loader_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset DoctorLoader state + $reflection = new \ReflectionClass(DoctorLoader::class); + $property = $reflection->getProperty('doctor_items'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('fix_items'); + $property->setAccessible(true); + $property->setValue(null, []); + } + + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset DoctorLoader state + $reflection = new \ReflectionClass(DoctorLoader::class); + $property = $reflection->getProperty('doctor_items'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('fix_items'); + $property->setAccessible(true); + $property->setValue(null, []); + } + + public function testGetDoctorItemsReturnsEmptyArrayInitially(): void + { + $this->assertEmpty(DoctorLoader::getDoctorItems()); + } + + public function testLoadFromClassWithCheckItemAttribute(): void + { + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $items); + $this->assertInstanceOf(CheckItem::class, $items[0][0]); + $this->assertEquals('test-check', $items[0][0]->item_name); + $this->assertEquals(1, $items[0][0]->level); + } + + public function testLoadFromClassWithMultipleCheckItems(): void + { + $class = new class { + #[CheckItem('check-1', level: 2)] + public function check1(): void {} + + #[CheckItem('check-2', level: 1)] + public function check2(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(2, $items); + } + + public function testLoadFromClassSortsByLevelDescending(): void + { + $class = new class { + #[CheckItem('low-priority', level: 1)] + public function lowCheck(): void {} + + #[CheckItem('high-priority', level: 5)] + public function highCheck(): void {} + + #[CheckItem('medium-priority', level: 3)] + public function mediumCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(3, $items); + // Should be sorted by level descending: 5, 3, 1 + $this->assertEquals(5, $items[0][0]->level); + $this->assertEquals(3, $items[1][0]->level); + $this->assertEquals(1, $items[2][0]->level); + } + + public function testLoadFromClassWithoutSorting(): void + { + $class = new class { + #[CheckItem('check-1', level: 1)] + public function check1(): void {} + + #[CheckItem('check-2', level: 5)] + public function check2(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class), false); + + $items = DoctorLoader::getDoctorItems(); + // Without sorting, items should be in order they were added + $this->assertCount(2, $items); + } + + public function testLoadFromClassWithFixItemAttribute(): void + { + $class = new class { + #[FixItem('test-fix')] + public function testFix(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $fixItem = DoctorLoader::getFixItem('test-fix'); + $this->assertNotNull($fixItem); + $this->assertTrue(is_callable($fixItem)); + } + + public function testLoadFromClassWithMultipleFixItems(): void + { + $class = new class { + #[FixItem('fix-1')] + public function fix1(): void {} + + #[FixItem('fix-2')] + public function fix2(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $this->assertNotNull(DoctorLoader::getFixItem('fix-1')); + $this->assertNotNull(DoctorLoader::getFixItem('fix-2')); + } + + public function testGetFixItemReturnsNullForNonExistent(): void + { + $this->assertNull(DoctorLoader::getFixItem('non-existent-fix')); + } + + public function testLoadFromClassWithOptionalCheckOnClass(): void + { + // Note: OptionalCheck expects an array, not a callable directly + // This test verifies the structure even though we can't easily test with anonymous classes + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $items); + // Second element is the optional check callback (null if not set) + $this->assertIsArray($items[0]); + } + + public function testLoadFromClassWithOptionalCheckOnMethod(): void + { + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $items); + } + + public function testLoadFromClassSetsCallbackCorrectly(): void + { + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): string + { + return 'test-result'; + } + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $items); + + // Test that the callback is set correctly + $callback = $items[0][0]->callback; + $this->assertIsCallable($callback); + $this->assertEquals('test-result', call_user_func($callback)); + } + + public function testLoadFromClassWithBothCheckAndFixItems(): void + { + $class = new class { + #[CheckItem('test-check', level: 1)] + public function testCheck(): void {} + + #[FixItem('test-fix')] + public function testFix(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $checkItems = DoctorLoader::getDoctorItems(); + $this->assertCount(1, $checkItems); + + $fixItem = DoctorLoader::getFixItem('test-fix'); + $this->assertNotNull($fixItem); + } + + public function testLoadFromClassMultipleTimesAccumulatesItems(): void + { + $class1 = new class { + #[CheckItem('check-1', level: 1)] + public function check1(): void {} + }; + + $class2 = new class { + #[CheckItem('check-2', level: 2)] + public function check2(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class1)); + DoctorLoader::loadFromClass(get_class($class2)); + + $items = DoctorLoader::getDoctorItems(); + $this->assertCount(2, $items); + } + + public function testLoadFromPsr4DirLoadsAllClasses(): void + { + // Create a PSR-4 directory structure + $psr4Dir = $this->tempDir . '/DoctorClasses'; + mkdir($psr4Dir, 0755, true); + + // Create test class file 1 + $classContent1 = 'assertGreaterThanOrEqual(0, count($items)); + } + + public function testLoadFromPsr4DirSortsItemsByLevel(): void + { + // Create a PSR-4 directory structure + $psr4Dir = $this->tempDir . '/DoctorClasses'; + mkdir($psr4Dir, 0755, true); + + $classContent = '= 2) { + $this->assertGreaterThanOrEqual($items[1][0]->level, $items[0][0]->level); + } + } + + public function testLoadFromClassIgnoresNonPublicMethods(): void + { + $class = new class { + #[CheckItem('public-check', level: 1)] + public function publicCheck(): void {} + + #[CheckItem('private-check', level: 1)] + private function privateCheck(): void {} + + #[CheckItem('protected-check', level: 1)] + protected function protectedCheck(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + $items = DoctorLoader::getDoctorItems(); + // Should only load public methods + $this->assertCount(1, $items); + $this->assertEquals('public-check', $items[0][0]->item_name); + } + + public function testLoadFromClassWithNoAttributes(): void + { + $class = new class { + public function regularMethod(): void {} + }; + + DoctorLoader::loadFromClass(get_class($class)); + + // Should not add any items + $items = DoctorLoader::getDoctorItems(); + $this->assertEmpty($items); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } +} diff --git a/tests/StaticPHP/Registry/PackageLoaderTest.php b/tests/StaticPHP/Registry/PackageLoaderTest.php new file mode 100644 index 000000000..a40c79faf --- /dev/null +++ b/tests/StaticPHP/Registry/PackageLoaderTest.php @@ -0,0 +1,581 @@ +tempDir = sys_get_temp_dir() . '/package_loader_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset PackageLoader state + $reflection = new \ReflectionClass(PackageLoader::class); + + $property = $reflection->getProperty('packages'); + $property->setAccessible(true); + $property->setValue(null, null); + + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('after_stages'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('loaded_classes'); + $property->setAccessible(true); + $property->setValue(null, []); + + // Reset PackageConfig state + $configReflection = new \ReflectionClass(PackageConfig::class); + $configProperty = $configReflection->getProperty('package_configs'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, []); + } + + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset PackageLoader state + $reflection = new \ReflectionClass(PackageLoader::class); + + $property = $reflection->getProperty('packages'); + $property->setAccessible(true); + $property->setValue(null, null); + + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('after_stages'); + $property->setAccessible(true); + $property->setValue(null, []); + + $property = $reflection->getProperty('loaded_classes'); + $property->setAccessible(true); + $property->setValue(null, []); + + // Reset PackageConfig state + $configReflection = new \ReflectionClass(PackageConfig::class); + $configProperty = $configReflection->getProperty('package_configs'); + $configProperty->setAccessible(true); + $configProperty->setValue(null, []); + } + + public function testInitPackageInstancesOnlyRunsOnce(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + + PackageLoader::initPackageInstances(); + PackageLoader::initPackageInstances(); + + // Should only initialize once + $this->assertTrue(PackageLoader::hasPackage('test-lib')); + } + + public function testInitPackageInstancesCreatesLibraryPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-lib'); + $this->assertInstanceOf(LibraryPackage::class, $package); + } + + public function testInitPackageInstancesCreatesPhpExtensionPackage(): void + { + $this->createTestPackageConfig('test-ext', 'php-extension'); + + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-ext'); + $this->assertInstanceOf(PhpExtensionPackage::class, $package); + } + + public function testInitPackageInstancesCreatesTargetPackage(): void + { + $this->createTestPackageConfig('test-target', 'target'); + + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-target'); + $this->assertInstanceOf(TargetPackage::class, $package); + } + + public function testInitPackageInstancesCreatesVirtualTargetPackage(): void + { + $this->createTestPackageConfig('test-virtual-target', 'virtual-target'); + + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-virtual-target'); + $this->assertInstanceOf(TargetPackage::class, $package); + } + + public function testInitPackageInstancesThrowsExceptionForUnknownType(): void + { + $this->createTestPackageConfig('test-unknown', 'unknown-type'); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('has unknown type'); + + PackageLoader::initPackageInstances(); + } + + public function testHasPackageReturnsTrueForExistingPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $this->assertTrue(PackageLoader::hasPackage('test-lib')); + } + + public function testHasPackageReturnsFalseForNonExistingPackage(): void + { + PackageLoader::initPackageInstances(); + + $this->assertFalse(PackageLoader::hasPackage('non-existent')); + } + + public function testGetPackageReturnsPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-lib'); + $this->assertInstanceOf(LibraryPackage::class, $package); + } + + public function testGetPackageThrowsExceptionForNonExistingPackage(): void + { + PackageLoader::initPackageInstances(); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('not found'); + + PackageLoader::getPackage('non-existent'); + } + + public function testGetTargetPackageReturnsTargetPackage(): void + { + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getTargetPackage('test-target'); + $this->assertInstanceOf(TargetPackage::class, $package); + } + + public function testGetTargetPackageThrowsExceptionForNonTargetPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('is not a TargetPackage'); + + PackageLoader::getTargetPackage('test-lib'); + } + + public function testGetLibraryPackageReturnsLibraryPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getLibraryPackage('test-lib'); + $this->assertInstanceOf(LibraryPackage::class, $package); + } + + public function testGetLibraryPackageThrowsExceptionForNonLibraryPackage(): void + { + $this->createTestPackageConfig('ext-test-ext', 'php-extension'); + PackageLoader::initPackageInstances(); + + $this->expectException(WrongUsageException::class); + $this->expectExceptionMessage('is not a LibraryPackage'); + + PackageLoader::getLibraryPackage('ext-test-ext'); + } + + public function testGetPackagesReturnsAllPackages(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + $this->createTestPackageConfig('test-ext', 'php-extension'); + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $packages = iterator_to_array(PackageLoader::getPackages()); + $this->assertCount(3, $packages); + } + + public function testGetPackagesWithTypeFilterReturnsFilteredPackages(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + $this->createTestPackageConfig('test-ext', 'php-extension'); + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $packages = iterator_to_array(PackageLoader::getPackages('library')); + $this->assertCount(1, $packages); + $this->assertArrayHasKey('test-lib', $packages); + } + + public function testGetPackagesWithArrayTypeFilterReturnsFilteredPackages(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + $this->createTestPackageConfig('test-ext', 'php-extension'); + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $packages = iterator_to_array(PackageLoader::getPackages(['library', 'target'])); + $this->assertCount(2, $packages); + $this->assertArrayHasKey('test-lib', $packages); + $this->assertArrayHasKey('test-target', $packages); + } + + public function testLoadFromClassWithLibraryAttribute(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $class = new #[Library('test-lib')] class {}; + + PackageLoader::loadFromClass(get_class($class)); + + $this->assertTrue(PackageLoader::hasPackage('test-lib')); + } + + public function testLoadFromClassWithExtensionAttribute(): void + { + $this->createTestPackageConfig('ext-test-ext', 'php-extension'); + PackageLoader::initPackageInstances(); + + $class = new #[Extension('ext-test-ext')] class {}; + + PackageLoader::loadFromClass(get_class($class)); + + $this->assertTrue(PackageLoader::hasPackage('ext-test-ext')); + } + + public function testLoadFromClassWithTargetAttribute(): void + { + $this->createTestPackageConfig('test-target', 'target'); + PackageLoader::initPackageInstances(); + + $class = new #[Target('test-target')] class {}; + + PackageLoader::loadFromClass(get_class($class)); + + $this->assertTrue(PackageLoader::hasPackage('test-target')); + } + + public function testLoadFromClassThrowsExceptionForUndefinedPackage(): void + { + PackageLoader::initPackageInstances(); + + $class = new #[Library('undefined-lib')] class {}; + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('not defined in config'); + + PackageLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassThrowsExceptionForTypeMismatch(): void + { + $this->createTestPackageConfig('ext-test-lib', 'library'); + PackageLoader::initPackageInstances(); + + // Try to load with Extension attribute but config says library + $class = new #[Extension('ext-test-lib')] class {}; + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('type mismatch'); + + PackageLoader::loadFromClass(get_class($class)); + } + + public function testLoadFromClassSkipsDuplicateClasses(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $className = get_class(new #[Library('test-lib')] class {}); + + // Load twice + PackageLoader::loadFromClass($className); + PackageLoader::loadFromClass($className); + + // Should not throw exception + $this->assertTrue(PackageLoader::hasPackage('test-lib')); + } + + public function testLoadFromClassWithNoPackageAttribute(): void + { + PackageLoader::initPackageInstances(); + + $class = new class { + public function regularMethod(): void {} + }; + + // Should not throw exception + PackageLoader::loadFromClass(get_class($class)); + + // Verify no side effects + $this->assertTrue(true); + } + + public function testCheckLoadedStageEventsThrowsExceptionForUnknownPackage(): void + { + PackageLoader::initPackageInstances(); + + // Manually add a before_stage for non-existent package + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'non-existent-package' => [ + 'stage-name' => [[fn () => null, null]], + ], + ]); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('unknown package'); + + PackageLoader::checkLoadedStageEvents(); + } + + public function testCheckLoadedStageEventsThrowsExceptionForUnknownStage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + // Add a build function for current OS so the stage validation is triggered + $package = PackageLoader::getPackage('test-lib'); + $package->addBuildFunction(PHP_OS_FAMILY, fn () => null); + + // Manually add a before_stage for non-existent stage + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-lib' => [ + 'non-existent-stage' => [[fn () => null, null]], + ], + ]); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('is not registered'); + + PackageLoader::checkLoadedStageEvents(); + } + + public function testCheckLoadedStageEventsThrowsExceptionForUnknownOnlyWhenPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + $package = PackageLoader::getPackage('test-lib'); + $package->addStage('test-stage', fn () => null); + + // Manually add a before_stage with unknown only_when_package_resolved + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-lib' => [ + 'test-stage' => [[fn () => null, 'non-existent-package']], + ], + ]); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('unknown only_when_package_resolved package'); + + PackageLoader::checkLoadedStageEvents(); + } + + public function testCheckLoadedStageEventsDoesNotThrowForNonCurrentOSPackage(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + PackageLoader::initPackageInstances(); + + // Add a build function for a different OS (not current OS) + $package = PackageLoader::getPackage('test-lib'); + $otherOS = PHP_OS_FAMILY === 'Windows' ? 'Linux' : 'Windows'; + $package->addBuildFunction($otherOS, fn () => null); + + // Manually add a before_stage for 'build' stage + // This should NOT throw an exception because the package has no build function for current OS + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-lib' => [ + 'build' => [[fn () => null, null]], + ], + ]); + + // This should not throw an exception + PackageLoader::checkLoadedStageEvents(); + + $this->assertTrue(true); // If we get here, the test passed + } + + public function testGetBeforeStageCallbacksReturnsCallbacks(): void + { + PackageLoader::initPackageInstances(); + + // Manually add some before_stage callbacks + $callback1 = fn () => 'callback1'; + $callback2 = fn () => 'callback2'; + + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('before_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-package' => [ + 'test-stage' => [ + [$callback1, null], + [$callback2, null], + ], + ], + ]); + + $callbacks = iterator_to_array(PackageLoader::getBeforeStageCallbacks('test-package', 'test-stage')); + $this->assertCount(2, $callbacks); + } + + public function testGetAfterStageCallbacksReturnsCallbacks(): void + { + PackageLoader::initPackageInstances(); + + // Manually add some after_stage callbacks + $callback1 = fn () => 'callback1'; + $callback2 = fn () => 'callback2'; + + $reflection = new \ReflectionClass(PackageLoader::class); + $property = $reflection->getProperty('after_stages'); + $property->setAccessible(true); + $property->setValue(null, [ + 'test-package' => [ + 'test-stage' => [ + [$callback1, null], + [$callback2, null], + ], + ], + ]); + + $callbacks = PackageLoader::getAfterStageCallbacks('test-package', 'test-stage'); + $this->assertCount(2, $callbacks); + } + + public function testGetBeforeStageCallbacksReturnsEmptyForNonExistentPackage(): void + { + PackageLoader::initPackageInstances(); + + $callbacks = iterator_to_array(PackageLoader::getBeforeStageCallbacks('non-existent', 'stage')); + $this->assertEmpty($callbacks); + } + + public function testGetAfterStageCallbacksReturnsEmptyForNonExistentPackage(): void + { + PackageLoader::initPackageInstances(); + + $callbacks = PackageLoader::getAfterStageCallbacks('non-existent', 'stage'); + $this->assertEmpty($callbacks); + } + + public function testRegisterAllDefaultStagesRegistersForPhpExtensions(): void + { + $this->createTestPackageConfig('test-ext', 'php-extension'); + PackageLoader::initPackageInstances(); + + PackageLoader::registerAllDefaultStages(); + + $package = PackageLoader::getPackage('test-ext'); + $this->assertInstanceOf(PhpExtensionPackage::class, $package); + // Default stages should be registered (we can't easily verify this without accessing internal state) + } + + public function testLoadFromPsr4DirLoadsAllClasses(): void + { + $this->createTestPackageConfig('test-lib', 'library'); + + // Create a PSR-4 directory structure + $psr4Dir = $this->tempDir . '/PackageClasses'; + mkdir($psr4Dir, 0755, true); + + // Create test class file + $classContent = 'assertTrue(PackageLoader::hasPackage('test-lib')); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } + + private function createTestPackageConfig(string $name, string $type): void + { + $reflection = new \ReflectionClass(PackageConfig::class); + $property = $reflection->getProperty('package_configs'); + $property->setAccessible(true); + $configs = $property->getValue(); + $configs[$name] = [ + 'type' => $type, + 'deps' => [], + ]; + $property->setValue(null, $configs); + } +} diff --git a/tests/StaticPHP/Registry/RegistryTest.php b/tests/StaticPHP/Registry/RegistryTest.php new file mode 100644 index 000000000..a7dbd8761 --- /dev/null +++ b/tests/StaticPHP/Registry/RegistryTest.php @@ -0,0 +1,378 @@ +tempDir = sys_get_temp_dir() . '/registry_test_' . uniqid(); + mkdir($this->tempDir, 0755, true); + + // Reset Registry state + Registry::reset(); + } + + protected function tearDown(): void + { + parent::tearDown(); + // Clean up temp directory + if (is_dir($this->tempDir)) { + $this->removeDirectory($this->tempDir); + } + + // Reset Registry state + Registry::reset(); + } + + public function testLoadRegistryWithValidJsonFile(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'test-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + Registry::loadRegistry($registryFile); + + $this->assertContains('test-registry', Registry::getLoadedRegistries()); + } + + public function testLoadRegistryWithValidYamlFile(): void + { + $registryFile = $this->tempDir . '/test-registry.yaml'; + $registryContent = "name: test-registry-yaml\npackage:\n config: []"; + file_put_contents($registryFile, $registryContent); + + Registry::loadRegistry($registryFile); + + $this->assertContains('test-registry-yaml', Registry::getLoadedRegistries()); + } + + public function testLoadRegistryWithValidYmlFile(): void + { + $registryFile = $this->tempDir . '/test-registry.yml'; + $registryContent = "name: test-registry-yml\npackage:\n config: []"; + file_put_contents($registryFile, $registryContent); + + Registry::loadRegistry($registryFile); + + $this->assertContains('test-registry-yml', Registry::getLoadedRegistries()); + } + + public function testLoadRegistryThrowsExceptionForNonExistentFile(): void + { + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('Failed to read registry file'); + + Registry::loadRegistry($this->tempDir . '/non-existent.json'); + } + + public function testLoadRegistryThrowsExceptionForUnsupportedFormat(): void + { + $registryFile = $this->tempDir . '/test-registry.txt'; + file_put_contents($registryFile, 'invalid content'); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('Unsupported registry file format'); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryThrowsExceptionForInvalidJson(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + file_put_contents($registryFile, 'invalid json content'); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('Invalid registry format'); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryThrowsExceptionForMissingName(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'package' => [], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage("Registry 'name' is missing or invalid"); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryThrowsExceptionForEmptyName(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => '', + 'package' => [], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage("Registry 'name' is missing or invalid"); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryThrowsExceptionForNonStringName(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 123, + 'package' => [], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + $this->expectException(RegistryException::class); + $this->expectExceptionMessage("Registry 'name' is missing or invalid"); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistrySkipsDuplicateRegistry(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'duplicate-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + // Load first time + Registry::loadRegistry($registryFile); + $this->assertCount(1, Registry::getLoadedRegistries()); + + // Load second time - should skip + Registry::loadRegistry($registryFile); + $this->assertCount(1, Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionWithNullRegistries(): void + { + // Should not throw exception when null is passed and env is not set + Registry::loadFromEnvOrOption(null); + $this->assertEmpty(Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionWithEmptyString(): void + { + Registry::loadFromEnvOrOption(''); + $this->assertEmpty(Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionWithSingleRegistry(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'env-test-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + Registry::loadFromEnvOrOption($registryFile); + + $this->assertContains('env-test-registry', Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionWithMultipleRegistries(): void + { + $registryFile1 = $this->tempDir . '/test-registry-1.json'; + $registryData1 = [ + 'name' => 'env-test-registry-1', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile1, json_encode($registryData1)); + + $registryFile2 = $this->tempDir . '/test-registry-2.json'; + $registryData2 = [ + 'name' => 'env-test-registry-2', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile2, json_encode($registryData2)); + + Registry::loadFromEnvOrOption($registryFile1 . ':' . $registryFile2); + + $this->assertContains('env-test-registry-1', Registry::getLoadedRegistries()); + $this->assertContains('env-test-registry-2', Registry::getLoadedRegistries()); + } + + public function testLoadFromEnvOrOptionIgnoresNonExistentFiles(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'env-test-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + // Mix existing and non-existing files + Registry::loadFromEnvOrOption($registryFile . ':' . $this->tempDir . '/non-existent.json'); + + // Should only load the existing one + $this->assertCount(1, Registry::getLoadedRegistries()); + $this->assertContains('env-test-registry', Registry::getLoadedRegistries()); + } + + public function testGetLoadedRegistriesReturnsEmptyArrayInitially(): void + { + $this->assertEmpty(Registry::getLoadedRegistries()); + } + + public function testGetLoadedRegistriesReturnsCorrectList(): void + { + $registryFile1 = $this->tempDir . '/test-registry-1.json'; + $registryData1 = [ + 'name' => 'registry-1', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile1, json_encode($registryData1)); + + $registryFile2 = $this->tempDir . '/test-registry-2.json'; + $registryData2 = [ + 'name' => 'registry-2', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile2, json_encode($registryData2)); + + Registry::loadRegistry($registryFile1); + Registry::loadRegistry($registryFile2); + + $loaded = Registry::getLoadedRegistries(); + $this->assertCount(2, $loaded); + $this->assertContains('registry-1', $loaded); + $this->assertContains('registry-2', $loaded); + } + + public function testResetClearsLoadedRegistries(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'test-registry', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + Registry::loadRegistry($registryFile); + $this->assertNotEmpty(Registry::getLoadedRegistries()); + + Registry::reset(); + $this->assertEmpty(Registry::getLoadedRegistries()); + } + + public function testLoadRegistryWithAutoloadPath(): void + { + // Create a test autoload file + $autoloadFile = $this->tempDir . '/vendor/autoload.php'; + mkdir(dirname($autoloadFile), 0755, true); + file_put_contents($autoloadFile, 'tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'autoload-test-registry', + 'autoload' => 'vendor/autoload.php', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + // Should not throw exception + Registry::loadRegistry($registryFile); + + $this->assertContains('autoload-test-registry', Registry::getLoadedRegistries()); + } + + public function testLoadRegistryWithNonExistentAutoloadPath(): void + { + $registryFile = $this->tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'autoload-missing-test-registry', + 'autoload' => 'vendor/non-existent-autoload.php', + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + // Should throw exception when path doesn't exist + $this->expectException(RegistryException::class); + $this->expectExceptionMessage('Path does not exist'); + + Registry::loadRegistry($registryFile); + } + + public function testLoadRegistryWithAbsoluteAutoloadPath(): void + { + // Create a test autoload file with absolute path + $autoloadFile = $this->tempDir . '/vendor/autoload.php'; + mkdir(dirname($autoloadFile), 0755, true); + file_put_contents($autoloadFile, 'tempDir . '/test-registry.json'; + $registryData = [ + 'name' => 'absolute-autoload-test-registry', + 'autoload' => $autoloadFile, + 'package' => [ + 'config' => [], + ], + ]; + file_put_contents($registryFile, json_encode($registryData)); + + Registry::loadRegistry($registryFile); + + $this->assertContains('absolute-autoload-test-registry', Registry::getLoadedRegistries()); + } + + private function removeDirectory(string $dir): void + { + if (!is_dir($dir)) { + return; + } + $files = array_diff(scandir($dir), ['.', '..']); + foreach ($files as $file) { + $path = $dir . '/' . $file; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($dir); + } +} diff --git a/tests/assets/filelist.gz b/tests/assets/filelist.gz deleted file mode 100644 index fb566ebf7..000000000 Binary files a/tests/assets/filelist.gz and /dev/null differ diff --git a/tests/assets/github_api_AOMediaCodec_libavif_releases.json.gz b/tests/assets/github_api_AOMediaCodec_libavif_releases.json.gz deleted file mode 100644 index 31e0b1261..000000000 Binary files a/tests/assets/github_api_AOMediaCodec_libavif_releases.json.gz and /dev/null differ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index dcb4316eb..ba9173323 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,13 +1,9 @@ setLevel(LogLevel::ERROR);