mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-02 22:35:43 +08:00
Compare commits
52 Commits
2.7.10
...
v3-feat/te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
020a30315d | ||
|
|
bde1440617 | ||
|
|
78375632b4 | ||
|
|
f68adc3256 | ||
|
|
4a968757ba | ||
|
|
bcaef59a15 | ||
|
|
b0f630f95f | ||
|
|
ac01867e9c | ||
|
|
808aed2a66 | ||
|
|
e004d10861 | ||
|
|
0db26be826 | ||
|
|
a4bd2a79a9 | ||
|
|
7b16f683fc | ||
|
|
78234ef147 | ||
|
|
80128edd39 | ||
|
|
b384345723 | ||
|
|
f4bb0263f6 | ||
|
|
321f2e13e8 | ||
|
|
11e7a590c8 | ||
|
|
20e0711747 | ||
|
|
80d922ab3b | ||
|
|
a1cadecc54 | ||
|
|
127c935106 | ||
|
|
eab105965d | ||
|
|
abd6c2fa3a | ||
|
|
df6c27c98d | ||
|
|
3ff762c4c8 | ||
|
|
6775cb4674 | ||
|
|
88b86d3eaf | ||
|
|
dbc6dbee53 | ||
|
|
baddd60113 | ||
|
|
2f09ace82f | ||
|
|
d3b0f5de79 | ||
|
|
9ad7147155 | ||
|
|
106b55d4e7 | ||
|
|
93a697ebbf | ||
|
|
7fa6fd08d4 | ||
|
|
52553fb5ed | ||
|
|
c925914925 | ||
|
|
d16f5a972c | ||
|
|
ee46c1c387 | ||
|
|
64fde5fd8c | ||
|
|
dc5bf6dc98 | ||
|
|
20892ab194 | ||
|
|
e9d3f7e7eb | ||
|
|
2f8570b59e | ||
|
|
71d803d36f | ||
|
|
daa87e1350 | ||
|
|
c38f174a6b | ||
|
|
9903c2294c | ||
|
|
14bfb4198a | ||
|
|
f6c818d3c0 |
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -2,7 +2,7 @@ name: Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
branches: [ "main", "v3" ]
|
||||
types: [ opened, synchronize, reopened ]
|
||||
paths:
|
||||
- 'src/**'
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,8 +1,8 @@
|
||||
.idea
|
||||
runtime/
|
||||
docker/libraries/
|
||||
docker/extensions/
|
||||
docker/source/
|
||||
/runtime/
|
||||
/docker/libraries/
|
||||
/docker/extensions/
|
||||
/docker/source/
|
||||
|
||||
# Vendor files
|
||||
/vendor/**
|
||||
|
||||
@@ -69,6 +69,6 @@ return (new PhpCsFixer\Config())
|
||||
'php_unit_data_provider_method_order' => false,
|
||||
])
|
||||
->setFinder(
|
||||
PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/SPC'])
|
||||
PhpCsFixer\Finder::create()->in([__DIR__ . '/src', __DIR__ . '/tests/StaticPHP'])
|
||||
)
|
||||
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect());
|
||||
|
||||
23
bin/spc
23
bin/spc
@@ -1,13 +1,9 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
use SPC\ConsoleApplication;
|
||||
use SPC\exception\ExceptionHandler;
|
||||
|
||||
// Load custom php if exists
|
||||
if (PHP_OS_FAMILY !== 'Windows' && PHP_BINARY !== (__DIR__ . '/php') && file_exists(__DIR__ . '/php') && is_executable(__DIR__ . '/php')) {
|
||||
pcntl_exec(__DIR__ . '/php', $argv);
|
||||
}
|
||||
use StaticPHP\ConsoleApplication;
|
||||
use StaticPHP\Exception\ExceptionHandler;
|
||||
use StaticPHP\Exception\SPCException;
|
||||
|
||||
if (file_exists(dirname(__DIR__) . '/vendor/autoload.php')) {
|
||||
// Current: ./bin (git/project mode)
|
||||
@@ -17,11 +13,6 @@ if (file_exists(dirname(__DIR__) . '/vendor/autoload.php')) {
|
||||
require_once dirname(__DIR__, 3) . '/autoload.php';
|
||||
}
|
||||
|
||||
// 防止 Micro 打包状态下不支持中文的显示(虽然这个项目目前好像没输出过中文?)
|
||||
if (PHP_OS_FAMILY === 'Windows' && Phar::running()) {
|
||||
exec('CHCP 65001');
|
||||
}
|
||||
|
||||
// Print deprecation notice on PHP < 8.4, use red and highlight background
|
||||
if (PHP_VERSION_ID < 80400) {
|
||||
echo "\e[43mDeprecation Notice: PHP < 8.4 is deprecated, please upgrade your PHP version.\e[0m\n";
|
||||
@@ -29,7 +20,11 @@ if (PHP_VERSION_ID < 80400) {
|
||||
|
||||
try {
|
||||
(new ConsoleApplication())->run();
|
||||
} catch (Exception $e) {
|
||||
ExceptionHandler::getInstance()->handle($e);
|
||||
} catch (SPCException $e) {
|
||||
ExceptionHandler::handleSPCException($e);
|
||||
exit(1);
|
||||
} catch (\Throwable $e) {
|
||||
ExceptionHandler::handleDefaultException($e);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
4
bin/spc-debug
Executable file
4
bin/spc-debug
Executable file
@@ -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" "$@"
|
||||
@@ -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": [
|
||||
|
||||
997
composer.lock
generated
997
composer.lock
generated
File diff suppressed because it is too large
Load Diff
1057
config/artifact.json
Normal file
1057
config/artifact.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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++
|
||||
|
||||
1541
config/pkg.ext.json
Normal file
1541
config/pkg.ext.json
Normal file
File diff suppressed because it is too large
Load Diff
105
config/pkg.json
105
config/pkg.json
@@ -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"
|
||||
}
|
||||
}
|
||||
992
config/pkg.lib.json
Normal file
992
config/pkg.lib.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
95
config/pkg.target.json
Normal file
95
config/pkg.target.json
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"vswhere": {
|
||||
"type": "target",
|
||||
"artifact": "vswhere"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -771,9 +771,8 @@
|
||||
]
|
||||
},
|
||||
"libwebp": {
|
||||
"type": "ghtagtar",
|
||||
"repo": "webmproject/libwebp",
|
||||
"match": "v1\\.\\d+\\.\\d+$",
|
||||
"type": "url",
|
||||
"url": "https://github.com/webmproject/libwebp/archive/refs/tags/v1.3.2.tar.gz",
|
||||
"provide-pre-built": true,
|
||||
"license": {
|
||||
"type": "file",
|
||||
@@ -781,10 +780,8 @@
|
||||
}
|
||||
},
|
||||
"libxml2": {
|
||||
"type": "ghtagtar",
|
||||
"repo": "GNOME/libxml2",
|
||||
"match": "v2\\.\\d+\\.\\d+$",
|
||||
"provide-pre-built": false,
|
||||
"type": "url",
|
||||
"url": "https://github.com/GNOME/libxml2/archive/refs/tags/v2.12.5.tar.gz",
|
||||
"license": {
|
||||
"type": "file",
|
||||
"path": "Copyright"
|
||||
@@ -1172,8 +1169,9 @@
|
||||
}
|
||||
},
|
||||
"xdebug": {
|
||||
"type": "pie",
|
||||
"repo": "xdebug/xdebug",
|
||||
"type": "url",
|
||||
"url": "https://pecl.php.net/get/xdebug",
|
||||
"filename": "xdebug.tgz",
|
||||
"license": {
|
||||
"type": "file",
|
||||
"path": "LICENSE"
|
||||
|
||||
@@ -17,3 +17,4 @@ parameters:
|
||||
- ./src/globals/ext-tests/swoole.php
|
||||
- ./src/globals/ext-tests/swoole.phpt
|
||||
- ./src/globals/test-extensions.php
|
||||
- ./src/SPC/
|
||||
|
||||
32
spc.registry.json
Normal file
32
spc.registry.json
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/Package/Artifact/go_xcaddy.php
Normal file
94
src/Package/Artifact/go_xcaddy.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Artifact;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
|
||||
use StaticPHP\Attribute\Artifact\CustomBinary;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
class go_xcaddy
|
||||
{
|
||||
#[CustomBinary('go-xcaddy', [
|
||||
'linux-x86_64',
|
||||
'linux-aarch64',
|
||||
'macos-x86_64',
|
||||
'macos-aarch64',
|
||||
])]
|
||||
public function downBinary(ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
$pkgroot = PKG_ROOT_PATH;
|
||||
$name = SystemTarget::getCurrentPlatformString();
|
||||
$arch = match (explode('-', $name)[1]) {
|
||||
'x86_64' => '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\">.*?<tt>([a-f0-9]{64})<\\/tt>/s";
|
||||
if (preg_match($pattern, $page, $matches)) {
|
||||
$hash = $matches[1];
|
||||
} else {
|
||||
throw new DownloaderException("Failed to find download hash for Go {$version} {$os}-{$arch}");
|
||||
}
|
||||
|
||||
$url = "https://go.dev/dl/{$version}.{$os}-{$arch}.tar.gz";
|
||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "{$version}.{$os}-{$arch}.tar.gz";
|
||||
default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
|
||||
// verify hash
|
||||
$file_hash = hash_file('sha256', $path);
|
||||
if ($file_hash !== $hash) {
|
||||
throw new 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");
|
||||
}
|
||||
}
|
||||
98
src/Package/Artifact/zig.php
Normal file
98
src/Package/Artifact/zig.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Artifact;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
|
||||
use StaticPHP\Attribute\Artifact\CustomBinary;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
|
||||
class zig
|
||||
{
|
||||
#[CustomBinary('zig', [
|
||||
'linux-x86_64',
|
||||
'linux-aarch64',
|
||||
'macos-x86_64',
|
||||
'macos-aarch64',
|
||||
])]
|
||||
public function downBinary(ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
$index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry());
|
||||
$index_json = json_decode($index_json ?: '', true);
|
||||
$latest_version = null;
|
||||
foreach ($index_json as $version => $data) {
|
||||
$latest_version = $version;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!$latest_version) {
|
||||
throw new DownloaderException('Could not determine latest Zig version');
|
||||
}
|
||||
$zig_arch = SystemTarget::getTargetArch();
|
||||
$zig_os = match (SystemTarget::getTargetOS()) {
|
||||
'Windows' => 'win',
|
||||
'Darwin' => 'macos',
|
||||
'Linux' => 'linux',
|
||||
default => throw new DownloaderException('Unsupported OS for Zig: ' . SystemTarget::getTargetOS()),
|
||||
};
|
||||
$platform_key = "{$zig_arch}-{$zig_os}";
|
||||
if (!isset($index_json[$latest_version][$platform_key])) {
|
||||
throw new DownloaderException("No download available for {$platform_key} in Zig version {$latest_version}");
|
||||
}
|
||||
$download_info = $index_json[$latest_version][$platform_key];
|
||||
$url = $download_info['tarball'];
|
||||
$sha256 = $download_info['shasum'];
|
||||
$filename = basename($url);
|
||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
||||
default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry());
|
||||
// verify hash
|
||||
$file_hash = hash_file('sha256', $path);
|
||||
if ($file_hash !== $sha256) {
|
||||
throw new DownloaderException("Hash mismatch for downloaded Zig binary. Expected {$sha256}, got {$file_hash}");
|
||||
}
|
||||
return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version);
|
||||
}
|
||||
|
||||
#[AfterBinaryExtract('zig', [
|
||||
'linux-x86_64',
|
||||
'linux-aarch64',
|
||||
'macos-x86_64',
|
||||
'macos-aarch64',
|
||||
])]
|
||||
public function postExtractZig(string $target_path): void
|
||||
{
|
||||
$files = ['zig', 'zig-cc', 'zig-c++', 'zig-ar', 'zig-ld.lld', 'zig-ranlib', 'zig-objcopy'];
|
||||
$all_exist = true;
|
||||
foreach ($files as $file) {
|
||||
if (!file_exists("{$target_path}/{$file}")) {
|
||||
$all_exist = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($all_exist) {
|
||||
return;
|
||||
}
|
||||
|
||||
$script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh';
|
||||
$script_content = file_get_contents($script_path);
|
||||
|
||||
file_put_contents("{$target_path}/zig-cc", $script_content);
|
||||
chmod("{$target_path}/zig-cc", 0755);
|
||||
|
||||
$script_content = str_replace('zig cc', 'zig c++', $script_content);
|
||||
file_put_contents("{$target_path}/zig-c++", $script_content);
|
||||
file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@");
|
||||
file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@");
|
||||
file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@");
|
||||
file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@");
|
||||
chmod("{$target_path}/zig-c++", 0755);
|
||||
chmod("{$target_path}/zig-ar", 0755);
|
||||
chmod("{$target_path}/zig-ld.lld", 0755);
|
||||
chmod("{$target_path}/zig-ranlib", 0755);
|
||||
chmod("{$target_path}/zig-objcopy", 0755);
|
||||
}
|
||||
}
|
||||
121
src/Package/Command/SwitchPhpVersionCommand.php
Normal file
121
src/Package/Command/SwitchPhpVersionCommand.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Command;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactCache;
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\DownloaderOptions;
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
#[AsCommand('switch-php-version', description: 'Switch downloaded PHP version')]
|
||||
class SwitchPhpVersionCommand extends BaseCommand
|
||||
{
|
||||
protected bool $no_motd = true;
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->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("<error>Invalid PHP version '{$php_ver}'!</error>");
|
||||
$this->output->writeln('<comment>Supported formats: 7.4, 8.0, 8.1, 8.2, 8.3, 8.4, or specific version like 8.4.5</comment>');
|
||||
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("<info>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('<info>Removing extracted PHP source directory...</info>');
|
||||
InteractiveTerm::indicateProgress('Removing: ' . $source_dir);
|
||||
FileSystem::removeDir($source_dir);
|
||||
InteractiveTerm::finish('Removed: ' . $source_dir);
|
||||
}
|
||||
|
||||
// Download new PHP source
|
||||
$this->output->writeln("<info>Downloading PHP {$php_ver} source...</info>");
|
||||
|
||||
$this->input->setOption('with-php', $php_ver);
|
||||
|
||||
$downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->input->getOptions());
|
||||
$downloader = new ArtifactDownloader($downloaderOptions);
|
||||
|
||||
// Get php-src artifact from php package
|
||||
$php_package = PackageLoader::getPackage('php');
|
||||
$artifact = $php_package->getArtifact();
|
||||
|
||||
if ($artifact === null) {
|
||||
$this->output->writeln('<error>Failed to get php-src artifact!</error>');
|
||||
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("<info>Successfully switched to PHP {$new_version}!</info>");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
37
src/Package/Extension/readline.php
Normal file
37
src/Package/Extension/readline.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Extension;
|
||||
|
||||
use Package\Target\php;
|
||||
use StaticPHP\Attribute\Package\AfterStage;
|
||||
use StaticPHP\Attribute\Package\BeforeStage;
|
||||
use StaticPHP\Attribute\Package\Extension;
|
||||
use StaticPHP\Attribute\PatchDescription;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Toolchain\Interface\ToolchainInterface;
|
||||
use StaticPHP\Util\SourcePatcher;
|
||||
|
||||
#[Extension('readline')]
|
||||
class readline
|
||||
{
|
||||
#[BeforeStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')]
|
||||
#[PatchDescription('Fix readline static build with musl')]
|
||||
public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
|
||||
{
|
||||
if ($toolchain->isStatic()) {
|
||||
$php_src = $installer->getBuildPackage('php')->getSourceDir();
|
||||
SourcePatcher::patchFile('musl_static_readline.patch', $php_src);
|
||||
}
|
||||
}
|
||||
|
||||
#[AfterStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')]
|
||||
public function afterMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
|
||||
{
|
||||
if ($toolchain->isStatic()) {
|
||||
$php_src = $installer->getBuildPackage('php')->getSourceDir();
|
||||
SourcePatcher::patchFile('musl_static_readline.patch', $php_src, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/Package/Library/imap.php
Normal file
25
src/Package/Library/imap.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Library;
|
||||
|
||||
use Package\Target\php;
|
||||
use StaticPHP\Attribute\Package\AfterStage;
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
use StaticPHP\Attribute\PatchDescription;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
#[Library('imap')]
|
||||
class imap
|
||||
{
|
||||
#[AfterStage('php', [php::class, 'patchEmbedScripts'], 'imap')]
|
||||
#[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')]
|
||||
public function afterPatchScripts(): void
|
||||
{
|
||||
if (SystemTarget::getLibc() === 'glibc') {
|
||||
FileSystem::replaceFileRegex(BUILD_BIN_PATH . '/php-config', '/^libs="(.*)"$/m', 'libs="$1 -lcrypt"');
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/Package/Library/libedit.php
Normal file
37
src/Package/Library/libedit.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Library;
|
||||
|
||||
use StaticPHP\Attribute\Package\BeforeStage;
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
#[Library('libedit')]
|
||||
class libedit extends LibraryPackage
|
||||
{
|
||||
#[BeforeStage(stage: 'build')]
|
||||
public function patchBeforeBuild(): void
|
||||
{
|
||||
FileSystem::replaceFileRegex(
|
||||
"{$this->getSourceDir()}/src/sys.h",
|
||||
'|//#define\s+strl|',
|
||||
'#define strl'
|
||||
);
|
||||
}
|
||||
|
||||
#[BuildFor('Darwin')]
|
||||
#[BuildFor('Linux')]
|
||||
public function build(): void
|
||||
{
|
||||
UnixAutoconfExecutor::create($this)
|
||||
->appendEnv(['CFLAGS' => '-D__STDC_ISO_10646__=201103L'])
|
||||
->configure()
|
||||
->make();
|
||||
$this->patchPkgconfPrefix(['libedit.pc']);
|
||||
}
|
||||
}
|
||||
27
src/Package/Library/libiconv.php
Normal file
27
src/Package/Library/libiconv.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Library;
|
||||
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
|
||||
|
||||
#[Library('libiconv')]
|
||||
class libiconv
|
||||
{
|
||||
#[BuildFor('Darwin')]
|
||||
public function build(LibraryPackage $package): void
|
||||
{
|
||||
UnixAutoconfExecutor::create($package)
|
||||
->configure(
|
||||
'--enable-extra-encodings',
|
||||
'--enable-year2038',
|
||||
)
|
||||
->make('install-lib', with_install: false)
|
||||
->make('install-lib', with_install: false, dir: "{$package->getSourceDir()}/libcharset");
|
||||
$package->patchLaDependencyPrefix();
|
||||
}
|
||||
}
|
||||
54
src/Package/Library/libxml2.php
Normal file
54
src/Package/Library/libxml2.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Library;
|
||||
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Runtime\Executor\UnixCMakeExecutor;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
#[Library('libxml2')]
|
||||
class libxml2
|
||||
{
|
||||
#[BuildFor('Darwin')]
|
||||
public function build(LibraryPackage $package): void
|
||||
{
|
||||
$cmake = UnixCMakeExecutor::create($package)
|
||||
->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'
|
||||
);
|
||||
}
|
||||
}
|
||||
61
src/Package/Library/ncurses.php
Normal file
61
src/Package/Library/ncurses.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Library;
|
||||
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
|
||||
use StaticPHP\Toolchain\Interface\ToolchainInterface;
|
||||
use StaticPHP\Util\DirDiff;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
#[Library('ncurses')]
|
||||
class ncurses
|
||||
{
|
||||
#[BuildFor('Darwin')]
|
||||
#[BuildFor('Linux')]
|
||||
public function build(LibraryPackage $package, ToolchainInterface $toolchain): void
|
||||
{
|
||||
$dirdiff = new DirDiff(BUILD_BIN_PATH);
|
||||
|
||||
UnixAutoconfExecutor::create($package)
|
||||
->appendEnv([
|
||||
'LDFLAGS' => $toolchain->isStatic() ? '-static' : '',
|
||||
])
|
||||
->configure(
|
||||
'--enable-overwrite',
|
||||
'--with-curses-h',
|
||||
'--enable-pc-files',
|
||||
'--enable-echo',
|
||||
'--disable-widec',
|
||||
'--with-normal',
|
||||
'--with-ticlib',
|
||||
'--without-tests',
|
||||
'--without-dlsym',
|
||||
'--without-debug',
|
||||
'--enable-symlinks',
|
||||
"--bindir={$package->getBinDir()}",
|
||||
"--includedir={$package->getIncludeDir()}",
|
||||
"--libdir={$package->getLibDir()}",
|
||||
"--prefix={$package->getBuildRootPath()}",
|
||||
)
|
||||
->make();
|
||||
$new_files = $dirdiff->getIncrementFiles(true);
|
||||
foreach ($new_files as $file) {
|
||||
@unlink(BUILD_BIN_PATH . '/' . $file);
|
||||
}
|
||||
|
||||
shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo');
|
||||
shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo');
|
||||
|
||||
$pkgconf_list = ['form.pc', 'menu.pc', 'ncurses++.pc', 'ncurses.pc', 'panel.pc', 'tic.pc'];
|
||||
$package->patchPkgconfPrefix($pkgconf_list);
|
||||
|
||||
foreach ($pkgconf_list as $pkgconf) {
|
||||
FileSystem::replaceFileStr("{$package->getLibDir()}/pkgconfig/{$pkgconf}", "-L{$package->getLibDir()}", '-L${libdir}');
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Package/Library/postgresql.php
Normal file
23
src/Package/Library/postgresql.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Library;
|
||||
|
||||
use Package\Target\php;
|
||||
use StaticPHP\Attribute\Package\BeforeStage;
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
use StaticPHP\Attribute\PatchDescription;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
|
||||
#[Library('postgresql')]
|
||||
class postgresql
|
||||
{
|
||||
#[BeforeStage('php', [php::class, 'configureForUnix'], 'postgresql')]
|
||||
#[PatchDescription('Patch to avoid explicit_bzero detection issues on some systems')]
|
||||
public function patchBeforePHPConfigure(TargetPackage $package): void
|
||||
{
|
||||
shell()->cd($package->getSourceDir())
|
||||
->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure');
|
||||
}
|
||||
}
|
||||
3
src/Package/README.md
Normal file
3
src/Package/README.md
Normal file
@@ -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.
|
||||
24
src/Package/Target/go_xcaddy.php
Normal file
24
src/Package/Target/go_xcaddy.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Target;
|
||||
|
||||
use StaticPHP\Attribute\Package\InitPackage;
|
||||
use StaticPHP\Attribute\Package\Target;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
|
||||
#[Target('go-xcaddy')]
|
||||
class go_xcaddy
|
||||
{
|
||||
#[InitPackage]
|
||||
public function init(): void
|
||||
{
|
||||
if (is_dir(PKG_ROOT_PATH . '/go-xcaddy/bin')) {
|
||||
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '/go-xcaddy/bin');
|
||||
GlobalEnvManager::putenv('GOROOT=' . PKG_ROOT_PATH . '/go-xcaddy');
|
||||
GlobalEnvManager::putenv('GOBIN=' . PKG_ROOT_PATH . '/go-xcaddy/bin');
|
||||
GlobalEnvManager::putenv('GOPATH=' . PKG_ROOT_PATH . '/go-xcaddy/go');
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Package/Target/micro.php
Normal file
22
src/Package/Target/micro.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Target;
|
||||
|
||||
use StaticPHP\Attribute\Package\BeforeStage;
|
||||
use StaticPHP\Attribute\Package\Target;
|
||||
use StaticPHP\Attribute\PatchDescription;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
#[Target('php-micro')]
|
||||
class micro
|
||||
{
|
||||
#[BeforeStage('php', [php::class, 'makeEmbedForUnix'], 'php-micro')]
|
||||
#[PatchDescription('Patch Makefile to build only libphp.la for embedding')]
|
||||
public function patchBeforeEmbed(TargetPackage $package): void
|
||||
{
|
||||
FileSystem::replaceFileStr("{$package->getSourceDir()}/Makefile", 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la');
|
||||
}
|
||||
}
|
||||
684
src/Package/Target/php.php
Normal file
684
src/Package/Target/php.php
Normal file
@@ -0,0 +1,684 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Target;
|
||||
|
||||
use StaticPHP\Attribute\Package\BeforeStage;
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\Info;
|
||||
use StaticPHP\Attribute\Package\InitPackage;
|
||||
use StaticPHP\Attribute\Package\ResolveBuild;
|
||||
use StaticPHP\Attribute\Package\Stage;
|
||||
use StaticPHP\Attribute\Package\Target;
|
||||
use StaticPHP\Attribute\Package\Validate;
|
||||
use StaticPHP\Attribute\PatchDescription;
|
||||
use StaticPHP\Config\PackageConfig;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Package\Package;
|
||||
use StaticPHP\Package\PackageBuilder;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Package\PhpExtensionPackage;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
use StaticPHP\Registry\ArtifactLoader;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Toolchain\Interface\ToolchainInterface;
|
||||
use StaticPHP\Toolchain\ToolchainManager;
|
||||
use StaticPHP\Util\DirDiff;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\SourcePatcher;
|
||||
use StaticPHP\Util\SPCConfigUtil;
|
||||
use StaticPHP\Util\System\UnixUtil;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
#[Target('php')]
|
||||
#[Target('php-cli')]
|
||||
#[Target('php-fpm')]
|
||||
#[Target('php-micro')]
|
||||
#[Target('php-cgi')]
|
||||
#[Target('php-embed')]
|
||||
#[Target('frankenphp')]
|
||||
class php extends TargetPackage
|
||||
{
|
||||
public static function getPHPVersionID(): int
|
||||
{
|
||||
$artifact = ArtifactLoader::getArtifactInstance('php-src');
|
||||
if (!file_exists("{$artifact->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);
|
||||
}
|
||||
|
||||
#[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')]
|
||||
#[PatchDescription('Patch configure.ac for musl and musl-toolchain')]
|
||||
#[PatchDescription('Let php m4 tools use static pkg-config')]
|
||||
public function patchBeforeBuildconf(TargetPackage $package): void
|
||||
{
|
||||
// patch configure.ac for musl and musl-toolchain
|
||||
$musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl';
|
||||
FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac');
|
||||
FileSystem::replaceFileStr(
|
||||
SOURCE_PATH . '/php-src/configure.ac',
|
||||
'if command -v ldd >/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 patchEmbedScripts(): void
|
||||
{
|
||||
// patch phpize
|
||||
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
|
||||
logger()->debug('Patching phpize prefix');
|
||||
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
|
||||
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
|
||||
$this->setOutput('phpize script path for embed SAPI', BUILD_BIN_PATH . '/phpize');
|
||||
}
|
||||
// patch php-config
|
||||
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
|
||||
logger()->debug('Patching php-config prefix and libs order');
|
||||
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
|
||||
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
|
||||
// move mimalloc to the beginning of libs
|
||||
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
|
||||
// move lstdc++ to the end of libs
|
||||
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
|
||||
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
|
||||
$this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename libphp.so to libphp-<release>.so if -release is set in LDFLAGS.
|
||||
*/
|
||||
private function processLibphpSoFile(string $libphpSo, PackageInstaller $installer): void
|
||||
{
|
||||
$ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: '';
|
||||
$libDir = BUILD_LIB_PATH;
|
||||
$modulesDir = BUILD_MODULES_PATH;
|
||||
$realLibName = 'libphp.so';
|
||||
$cwd = getcwd();
|
||||
|
||||
if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) {
|
||||
$release = $matches[1];
|
||||
$realLibName = "libphp-{$release}.so";
|
||||
$libphpRelease = "{$libDir}/{$realLibName}";
|
||||
if (!file_exists($libphpRelease) && file_exists($libphpSo)) {
|
||||
rename($libphpSo, $libphpRelease);
|
||||
}
|
||||
if (file_exists($libphpRelease)) {
|
||||
chdir($libDir);
|
||||
if (file_exists($libphpSo)) {
|
||||
unlink($libphpSo);
|
||||
}
|
||||
symlink($realLibName, 'libphp.so');
|
||||
shell()->exec(sprintf(
|
||||
'patchelf --set-soname %s %s',
|
||||
escapeshellarg($realLibName),
|
||||
escapeshellarg($libphpRelease)
|
||||
));
|
||||
}
|
||||
if (is_dir($modulesDir)) {
|
||||
chdir($modulesDir);
|
||||
foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) {
|
||||
if (!$ext->isBuildShared()) {
|
||||
continue;
|
||||
}
|
||||
$name = $ext->getName();
|
||||
$versioned = "{$name}-{$release}.so";
|
||||
$unversioned = "{$name}.so";
|
||||
$src = "{$modulesDir}/{$versioned}";
|
||||
$dst = "{$modulesDir}/{$unversioned}";
|
||||
if (is_file($src)) {
|
||||
rename($src, $dst);
|
||||
shell()->exec(sprintf(
|
||||
'patchelf --set-soname %s %s',
|
||||
escapeshellarg($unversioned),
|
||||
escapeshellarg($dst)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
chdir($cwd);
|
||||
}
|
||||
|
||||
$target = "{$libDir}/{$realLibName}";
|
||||
if (file_exists($target)) {
|
||||
[, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target));
|
||||
$output = implode("\n", $output);
|
||||
if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) {
|
||||
$currentSoname = $sonameMatch[1];
|
||||
if ($currentSoname !== basename($target)) {
|
||||
shell()->exec(sprintf(
|
||||
'patchelf --set-soname %s %s',
|
||||
escapeshellarg(basename($target)),
|
||||
escapeshellarg($target)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/Package/Target/pkgconfig.php
Normal file
45
src/Package/Target/pkgconfig.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Target;
|
||||
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\InitPackage;
|
||||
use StaticPHP\Attribute\Package\Target;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
|
||||
use StaticPHP\Toolchain\Interface\ToolchainInterface;
|
||||
|
||||
#[Target('pkg-config')]
|
||||
class pkgconfig
|
||||
{
|
||||
#[InitPackage]
|
||||
public function resolveBuild(): void
|
||||
{
|
||||
ApplicationContext::set('elephant', true);
|
||||
}
|
||||
|
||||
#[BuildFor('Linux')]
|
||||
#[BuildFor('Darwin')]
|
||||
public function build(TargetPackage $package, ToolchainInterface $toolchain): void
|
||||
{
|
||||
UnixAutoconfExecutor::create($package)
|
||||
->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');
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ use Symfony\Component\Console\Application;
|
||||
*/
|
||||
final class ConsoleApplication extends Application
|
||||
{
|
||||
public const string VERSION = '2.7.10';
|
||||
public const string VERSION = '3.0.0-dev';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -5,8 +5,6 @@ declare(strict_types=1);
|
||||
namespace SPC\builder\extension;
|
||||
|
||||
use SPC\builder\Extension;
|
||||
use SPC\toolchain\ToolchainManager;
|
||||
use SPC\toolchain\ZigToolchain;
|
||||
use SPC\util\CustomExt;
|
||||
|
||||
#[CustomExt('imagick')]
|
||||
@@ -21,9 +19,7 @@ class imagick extends Extension
|
||||
protected function splitLibsIntoStaticAndShared(string $allLibs): array
|
||||
{
|
||||
[$static, $shared] = parent::splitLibsIntoStaticAndShared($allLibs);
|
||||
if (ToolchainManager::getToolchainClass() !== ZigToolchain::class &&
|
||||
(str_contains(getenv('PATH'), 'rh/devtoolset') || str_contains(getenv('PATH'), 'rh/gcc-toolset'))
|
||||
) {
|
||||
if (str_contains(getenv('PATH'), 'rh/devtoolset') || str_contains(getenv('PATH'), 'rh/gcc-toolset')) {
|
||||
$static .= ' -l:libstdc++.a';
|
||||
$shared = str_replace('-lstdc++', '', $shared);
|
||||
}
|
||||
|
||||
@@ -24,9 +24,4 @@ class mongodb extends Extension
|
||||
$arg .= $this->builder->getLib('zlib') ? ' --with-mongodb-zlib=yes ' : ' --with-mongodb-zlib=bundled ';
|
||||
return clean_spaces($arg);
|
||||
}
|
||||
|
||||
public function getExtraEnv(): array
|
||||
{
|
||||
return ['CFLAGS' => '-std=c17'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class pgsql extends Extension
|
||||
protected function getExtraEnv(): array
|
||||
{
|
||||
return [
|
||||
'CFLAGS' => '-std=c17 -Wno-int-conversion',
|
||||
'CFLAGS' => '-Wno-int-conversion',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ class SystemUtil
|
||||
{
|
||||
return [
|
||||
// debian-like
|
||||
'debian', 'ubuntu', 'Deepin',
|
||||
'debian', 'ubuntu', 'Deepin', 'neon',
|
||||
// rhel-like
|
||||
'redhat',
|
||||
// centos
|
||||
|
||||
@@ -6,8 +6,6 @@ namespace SPC\builder\linux\library;
|
||||
|
||||
use SPC\builder\linux\SystemUtil;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\toolchain\GccNativeToolchain;
|
||||
use SPC\toolchain\ToolchainManager;
|
||||
use SPC\util\executor\UnixAutoconfExecutor;
|
||||
use SPC\util\SPCTarget;
|
||||
|
||||
@@ -17,19 +15,26 @@ class liburing extends LinuxLibraryBase
|
||||
|
||||
public function patchBeforeBuild(): bool
|
||||
{
|
||||
if (SystemUtil::isMuslDist()) {
|
||||
FileSystem::replaceFileStr($this->source_dir . '/configure', 'realpath -s', 'realpath');
|
||||
return true;
|
||||
if (!SystemUtil::isMuslDist()) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
FileSystem::replaceFileStr($this->source_dir . '/configure', 'realpath -s', 'realpath');
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function build(): void
|
||||
{
|
||||
$use_libc = ToolchainManager::getToolchainClass() !== GccNativeToolchain::class || version_compare(SPCTarget::getLibcVersion(), '2.30', '>=');
|
||||
$use_libc = SPCTarget::getLibc() !== 'glibc' || version_compare(SPCTarget::getLibcVersion(), '2.30', '>=');
|
||||
$make = UnixAutoconfExecutor::create($this);
|
||||
|
||||
if ($use_libc) {
|
||||
if (!$use_libc) {
|
||||
$make->appendEnv([
|
||||
'CC' => 'gcc', // libc-less version fails to compile with clang or zig
|
||||
'CXX' => 'g++',
|
||||
'AR' => 'ar',
|
||||
'LD' => 'ld',
|
||||
]);
|
||||
} else {
|
||||
$make->appendEnv([
|
||||
'CFLAGS' => '-D_GNU_SOURCE',
|
||||
]);
|
||||
@@ -46,7 +51,7 @@ class liburing extends LinuxLibraryBase
|
||||
$use_libc ? '--use-libc' : '',
|
||||
)
|
||||
->configure()
|
||||
->make('library ENABLE_SHARED=0', 'install ENABLE_SHARED=0', with_clean: false);
|
||||
->make('library', 'install ENABLE_SHARED=0', with_clean: false);
|
||||
|
||||
$this->patchPkgconfPrefix();
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ class openssl extends LinuxLibraryBase
|
||||
"{$zlib_extra}" .
|
||||
'enable-pie ' .
|
||||
'no-legacy ' .
|
||||
'no-tests ' .
|
||||
"linux-{$arch}"
|
||||
)
|
||||
->exec('make clean')
|
||||
|
||||
@@ -10,12 +10,7 @@ trait gmp
|
||||
{
|
||||
protected function build(): void
|
||||
{
|
||||
UnixAutoconfExecutor::create($this)
|
||||
->appendEnv([
|
||||
'CFLAGS' => '-std=c17',
|
||||
])
|
||||
->configure()
|
||||
->make();
|
||||
UnixAutoconfExecutor::create($this)->configure()->make();
|
||||
$this->patchPkgconfPrefix(['gmp.pc']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,17 +29,13 @@ trait libjxl
|
||||
);
|
||||
|
||||
if (ToolchainManager::getToolchainClass() === ZigToolchain::class) {
|
||||
$cflags = getenv('SPC_DEFAULT_C_FLAGS') ?: getenv('CFLAGS') ?: '';
|
||||
$has_avx512 = str_contains($cflags, '-mavx512') || str_contains($cflags, '-march=x86-64-v4');
|
||||
if (!$has_avx512) {
|
||||
$cmake->addConfigureArgs(
|
||||
'-DCXX_MAVX512F_SUPPORTED:BOOL=FALSE',
|
||||
'-DCXX_MAVX512DQ_SUPPORTED:BOOL=FALSE',
|
||||
'-DCXX_MAVX512CD_SUPPORTED:BOOL=FALSE',
|
||||
'-DCXX_MAVX512BW_SUPPORTED:BOOL=FALSE',
|
||||
'-DCXX_MAVX512VL_SUPPORTED:BOOL=FALSE'
|
||||
);
|
||||
}
|
||||
$cmake->addConfigureArgs(
|
||||
'-DCXX_MAVX512F_SUPPORTED:BOOL=FALSE',
|
||||
'-DCXX_MAVX512DQ_SUPPORTED:BOOL=FALSE',
|
||||
'-DCXX_MAVX512CD_SUPPORTED:BOOL=FALSE',
|
||||
'-DCXX_MAVX512BW_SUPPORTED:BOOL=FALSE',
|
||||
'-DCXX_MAVX512VL_SUPPORTED:BOOL=FALSE'
|
||||
);
|
||||
}
|
||||
|
||||
$cmake->build();
|
||||
|
||||
@@ -5,26 +5,13 @@ declare(strict_types=1);
|
||||
namespace SPC\builder\unix\library;
|
||||
|
||||
use SPC\util\executor\UnixCMakeExecutor;
|
||||
use SPC\util\SPCTarget;
|
||||
|
||||
trait libwebp
|
||||
{
|
||||
protected function build(): void
|
||||
{
|
||||
UnixCMakeExecutor::create($this)
|
||||
->addConfigureArgs(
|
||||
'-DWEBP_BUILD_EXTRAS=OFF',
|
||||
'-DWEBP_BUILD_ANIM_UTILS=OFF',
|
||||
'-DWEBP_BUILD_CWEBP=OFF',
|
||||
'-DWEBP_BUILD_DWEBP=OFF',
|
||||
'-DWEBP_BUILD_GIF2WEBP=OFF',
|
||||
'-DWEBP_BUILD_IMG2WEBP=OFF',
|
||||
'-DWEBP_BUILD_VWEBP=OFF',
|
||||
'-DWEBP_BUILD_WEBPINFO=OFF',
|
||||
'-DWEBP_BUILD_WEBPMUX=OFF',
|
||||
'-DWEBP_BUILD_FUZZTEST=OFF',
|
||||
SPCTarget::getLibcVersion() === '2.31' && GNU_ARCH === 'x86_64' ? '-DWEBP_ENABLE_SIMD=OFF' : '' // fix an edge bug for debian 11 with gcc 10
|
||||
)
|
||||
->addConfigureArgs('-DWEBP_BUILD_EXTRAS=ON')
|
||||
->build();
|
||||
// patch pkgconfig
|
||||
$this->patchPkgconfPrefix(patch_option: PKGCONF_PATCH_PREFIX | PKGCONF_PATCH_LIBDIR);
|
||||
|
||||
@@ -16,7 +16,6 @@ trait ncurses
|
||||
|
||||
UnixAutoconfExecutor::create($this)
|
||||
->appendEnv([
|
||||
'CFLAGS' => '-std=c17',
|
||||
'LDFLAGS' => SPCTarget::isStatic() ? '-static' : '',
|
||||
])
|
||||
->configure(
|
||||
@@ -30,7 +29,7 @@ trait ncurses
|
||||
'--without-tests',
|
||||
'--without-dlsym',
|
||||
'--without-debug',
|
||||
'--enable-symlinks',
|
||||
'-enable-symlinks',
|
||||
"--bindir={$this->getBinDir()}",
|
||||
"--includedir={$this->getIncludeDir()}",
|
||||
"--libdir={$this->getLibDir()}",
|
||||
|
||||
@@ -112,7 +112,7 @@ class LinuxToolCheckList
|
||||
public function fixBuildTools(array $distro, array $missing): bool
|
||||
{
|
||||
$install_cmd = match ($distro['dist']) {
|
||||
'ubuntu', 'debian', 'Deepin' => 'apt-get install -y',
|
||||
'ubuntu', 'debian', 'Deepin', 'neon' => 'apt-get install -y',
|
||||
'alpine' => 'apk add',
|
||||
'redhat' => 'dnf install -y',
|
||||
'centos' => 'yum install -y',
|
||||
@@ -128,7 +128,7 @@ class LinuxToolCheckList
|
||||
logger()->warning('Current user (' . $user . ') is not root, using sudo for running command (may require password input)');
|
||||
}
|
||||
|
||||
$is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin']);
|
||||
$is_debian = in_array($distro['dist'], ['debian', 'ubuntu', 'Deepin', 'neon']);
|
||||
$to_install = $is_debian ? str_replace('xz', 'xz-utils', $missing) : $missing;
|
||||
// debian, alpine libtool -> libtoolize
|
||||
$to_install = str_replace('libtoolize', 'libtool', $to_install);
|
||||
|
||||
@@ -572,44 +572,6 @@ class FileSystem
|
||||
return file_put_contents($file, implode('', $lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function moveFileOrDir(string $source, string $dest): void
|
||||
{
|
||||
$source = self::convertPath($source);
|
||||
$dest = self::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)) {
|
||||
self::copyDir($source, $dest);
|
||||
self::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 static function extractArchive(string $filename, string $target): void
|
||||
{
|
||||
// Create base dir
|
||||
@@ -686,6 +648,44 @@ class FileSystem
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = self::convertPath($source);
|
||||
$dest = self::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)) {
|
||||
self::copyDir($source, $dest);
|
||||
self::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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzip file with stripping top-level directory
|
||||
*/
|
||||
|
||||
@@ -25,7 +25,6 @@ class SourcePatcher
|
||||
FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']);
|
||||
FileSystem::addSourceExtractHook('sqlsrv', [__CLASS__, 'patchSQLSRVWin32']);
|
||||
FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVWin32']);
|
||||
FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVPhp85']);
|
||||
FileSystem::addSourceExtractHook('yaml', [__CLASS__, 'patchYamlWin32']);
|
||||
FileSystem::addSourceExtractHook('libyaml', [__CLASS__, 'patchLibYaml']);
|
||||
FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchImapLicense']);
|
||||
@@ -433,23 +432,6 @@ class SourcePatcher
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the compilation issue of pdo_sqlsrv with php 8.5
|
||||
*/
|
||||
public static function patchSQLSRVPhp85(): bool
|
||||
{
|
||||
$source_dir = SOURCE_PATH . '/php-src/ext/pdo_sqlsrv';
|
||||
if (!file_exists($source_dir . '/config.m4') && is_dir($source_dir . '/source/pdo_sqlsrv')) {
|
||||
FileSystem::moveFileOrDir($source_dir . '/LICENSE', $source_dir . '/source/pdo_sqlsrv/LICENSE');
|
||||
FileSystem::moveFileOrDir($source_dir . '/source/shared', $source_dir . '/source/pdo_sqlsrv/shared');
|
||||
FileSystem::moveFileOrDir($source_dir . '/source/pdo_sqlsrv', SOURCE_PATH . '/pdo_sqlsrv');
|
||||
FileSystem::removeDir($source_dir);
|
||||
FileSystem::moveFileOrDir(SOURCE_PATH . '/pdo_sqlsrv', $source_dir);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function patchYamlWin32(): bool
|
||||
{
|
||||
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/yaml/config.w32', "lib.substr(lib.length - 6, 6) == '_a.lib'", "lib.substr(lib.length - 6, 6) == '_a.lib' || 'yes' == 'yes'");
|
||||
|
||||
@@ -48,10 +48,10 @@ class GoXcaddy extends CustomPackage
|
||||
'macos' => 'darwin',
|
||||
default => throw new \InvalidArgumentException('Unsupported OS: ' . $name),
|
||||
};
|
||||
[$go_version] = explode("\n", Downloader::curlExec('https://go.dev/VERSION?m=text'));
|
||||
$go_version = '1.25.0';
|
||||
$config = [
|
||||
'type' => 'url',
|
||||
'url' => "https://go.dev/dl/{$go_version}.{$os}-{$arch}.tar.gz",
|
||||
'url' => "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz",
|
||||
];
|
||||
Downloader::downloadPackage($name, $config, $force);
|
||||
}
|
||||
|
||||
544
src/StaticPHP/Artifact/Artifact.php
Normal file
544
src/StaticPHP/Artifact/Artifact.php
Normal file
@@ -0,0 +1,544 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact;
|
||||
|
||||
use StaticPHP\Config\ArtifactConfig;
|
||||
use StaticPHP\Config\ConfigValidator;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
class Artifact
|
||||
{
|
||||
public const int FETCH_PREFER_SOURCE = 0;
|
||||
|
||||
public const int FETCH_PREFER_BINARY = 1;
|
||||
|
||||
public const int FETCH_ONLY_SOURCE = 2;
|
||||
|
||||
public const int FETCH_ONLY_BINARY = 3;
|
||||
|
||||
protected ?array $config;
|
||||
|
||||
/** @var null|callable Bind custom source fetcher callback */
|
||||
protected mixed $custom_source_callback = null;
|
||||
|
||||
/** @var array<string, callable> 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<callable> After source extract hooks */
|
||||
protected array $after_source_extract_callbacks = [];
|
||||
|
||||
/** @var array<array{callback: callable, platforms: string[]}> 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 and hash
|
||||
$target_path = $extract_config['path'];
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
305
src/StaticPHP/Artifact/ArtifactCache.php
Normal file
305
src/StaticPHP/Artifact/ArtifactCache.php
Normal file
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact;
|
||||
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
class ArtifactCache
|
||||
{
|
||||
/**
|
||||
* @var array<string, array{
|
||||
* source: null|array{
|
||||
* lock_type: 'binary'|'source',
|
||||
* cache_type: 'archive'|'file'|'git'|'local',
|
||||
* filename?: string,
|
||||
* dirname?: string,
|
||||
* extract: null|'&custom'|string,
|
||||
* hash: null|string
|
||||
* },
|
||||
* binary: array{
|
||||
* windows-x86_64?: null|array{
|
||||
* lock_type: 'binary'|'source',
|
||||
* cache_type: 'archive'|'file'|'git'|'local',
|
||||
* filename?: string,
|
||||
* dirname?: string,
|
||||
* extract: null|'&custom'|string,
|
||||
* hash: null|string,
|
||||
* version?: null|string
|
||||
* }
|
||||
* }
|
||||
* }>
|
||||
*/
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
676
src/StaticPHP/Artifact/ArtifactDownloader.php
Normal file
676
src/StaticPHP/Artifact/ArtifactDownloader.php
Normal file
@@ -0,0 +1,676 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact;
|
||||
|
||||
use Psr\Log\LogLevel;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Artifact\Downloader\Type\BitBucketTag;
|
||||
use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface;
|
||||
use StaticPHP\Artifact\Downloader\Type\FileList;
|
||||
use StaticPHP\Artifact\Downloader\Type\Git;
|
||||
use StaticPHP\Artifact\Downloader\Type\GitHubRelease;
|
||||
use StaticPHP\Artifact\Downloader\Type\GitHubTarball;
|
||||
use StaticPHP\Artifact\Downloader\Type\HostedPackageBin;
|
||||
use StaticPHP\Artifact\Downloader\Type\LocalDir;
|
||||
use StaticPHP\Artifact\Downloader\Type\PhpRelease;
|
||||
use StaticPHP\Artifact\Downloader\Type\PIE;
|
||||
use StaticPHP\Artifact\Downloader\Type\Url;
|
||||
use StaticPHP\Artifact\Downloader\Type\ValidatorInterface;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
use StaticPHP\Exception\ExecutionException;
|
||||
use StaticPHP\Exception\SPCException;
|
||||
use StaticPHP\Exception\ValidationException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Registry\ArtifactLoader;
|
||||
use StaticPHP\Runtime\Shell\Shell;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
/**
|
||||
* Artifact Downloader class
|
||||
*/
|
||||
class ArtifactDownloader
|
||||
{
|
||||
/** @var array<string, class-string<DownloadTypeInterface>> */
|
||||
public const array DOWNLOADERS = [
|
||||
'bitbuckettag' => BitBucketTag::class,
|
||||
'filelist' => FileList::class,
|
||||
'git' => Git::class,
|
||||
'ghrel' => GitHubRelease::class,
|
||||
'ghtar' => GitHubTarball::class,
|
||||
'ghtagtar' => GitHubTarball::class,
|
||||
'local' => LocalDir::class,
|
||||
'pie' => PIE::class,
|
||||
'url' => Url::class,
|
||||
'php-release' => PhpRelease::class,
|
||||
'hosted' => HostedPackageBin::class,
|
||||
];
|
||||
|
||||
/** @var array<string, Artifact> Artifact objects */
|
||||
protected array $artifacts = [];
|
||||
|
||||
/** @var int Parallel process number (1 and 0 as single-threaded mode) */
|
||||
protected int $parallel = 1;
|
||||
|
||||
protected int $retry = 0;
|
||||
|
||||
/** @var array<string, string> Override custom download urls from options */
|
||||
protected array $custom_urls = [];
|
||||
|
||||
/** @var array<string, array{0: string, 1: string}> Override custom git options from options ([branch, git url]) */
|
||||
protected array $custom_gits = [];
|
||||
|
||||
/** @var array<string, string> 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<string, int> Specific fetch preference */
|
||||
protected array $fetch_prefs = [];
|
||||
|
||||
/** @var array<string>|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<string>,
|
||||
* custom-git?: array<string>,
|
||||
* custom-local?: array<string>,
|
||||
* 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<Artifact|string> $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<array{display: string, lock: string, config: 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
635
src/StaticPHP/Artifact/ArtifactExtractor.php
Normal file
635
src/StaticPHP/Artifact/ArtifactExtractor.php
Normal file
@@ -0,0 +1,635 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact;
|
||||
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\FileSystemException;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Package\Package;
|
||||
use StaticPHP\Registry\ArtifactLoader;
|
||||
use StaticPHP\Runtime\Shell\Shell;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
|
||||
/**
|
||||
* ArtifactExtractor is responsible for extracting downloaded artifacts to their target locations.
|
||||
*
|
||||
* Extraction rules for source:
|
||||
* 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): selective extraction (file mapping)
|
||||
*
|
||||
* Extraction rules for binary:
|
||||
* 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
|
||||
*/
|
||||
class ArtifactExtractor
|
||||
{
|
||||
/** @var array<string, bool> 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<Package> $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<string,string> $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 = "{$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);
|
||||
|
||||
FileSystem::createDir($target);
|
||||
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
// Use 7za.exe for Windows
|
||||
$is_txz = str_ends_with($filename, '.txz') || str_ends_with($filename, '.tar.xz');
|
||||
default_shell()->execute7zExtract($filename, $target, $is_txz);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unix-like systems: determine compression type
|
||||
if (str_ends_with($filename, '.tar.gz') || str_ends_with($filename, '.tgz')) {
|
||||
default_shell()->executeTarExtract($filename, $target, 'gz');
|
||||
} elseif (str_ends_with($filename, '.tar.bz2')) {
|
||||
default_shell()->executeTarExtract($filename, $target, 'bz2');
|
||||
} elseif (str_ends_with($filename, '.tar.xz') || str_ends_with($filename, '.txz')) {
|
||||
default_shell()->executeTarExtract($filename, $target, 'xz');
|
||||
} elseif (str_ends_with($filename, '.tar')) {
|
||||
default_shell()->executeTarExtract($filename, $target, 'none');
|
||||
} elseif (str_ends_with($filename, '.zip')) {
|
||||
// Zip requires special handling for strip-components
|
||||
$this->unzipWithStrip($filename, $target);
|
||||
} elseif (str_ends_with($filename, '.exe')) {
|
||||
// exe just copy to target
|
||||
$dest_file = FileSystem::convertPath("{$target}/" . basename($filename));
|
||||
FileSystem::copy($filename, $dest_file);
|
||||
} else {
|
||||
throw new FileSystemException("Unknown archive format: {$filename}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzip file with stripping top-level directory.
|
||||
*/
|
||||
protected function unzipWithStrip(string $zip_file, string $extract_path): void
|
||||
{
|
||||
$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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
];
|
||||
return str_replace(array_keys($replacement), array_values($replacement), $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Move file or directory, handling cross-device scenarios
|
||||
* Uses rename() if possible, falls back to copy+delete for cross-device moves
|
||||
*
|
||||
* @param string $source Source path
|
||||
* @param string $dest Destination path
|
||||
*/
|
||||
private static function moveFileOrDir(string $source, string $dest): void
|
||||
{
|
||||
$source = FileSystem::convertPath($source);
|
||||
$dest = FileSystem::convertPath($dest);
|
||||
|
||||
// Check if source and dest are on the same device to avoid cross-device rename errors
|
||||
$source_stat = @stat($source);
|
||||
$dest_parent = dirname($dest);
|
||||
$dest_stat = @stat($dest_parent);
|
||||
|
||||
// Only use rename if on same device
|
||||
if ($source_stat !== false && $dest_stat !== false && $source_stat['dev'] === $dest_stat['dev']) {
|
||||
if (@rename($source, $dest)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to copy + delete for cross-device moves or if rename failed
|
||||
if (is_dir($source)) {
|
||||
FileSystem::copyDir($source, $dest);
|
||||
FileSystem::removeDir($source);
|
||||
} else {
|
||||
if (!copy($source, $dest)) {
|
||||
throw new FileSystemException("Failed to copy file from {$source} to {$dest}");
|
||||
}
|
||||
if (!unlink($source)) {
|
||||
throw new FileSystemException("Failed to remove source file: {$source}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function copyFile(string $source_file, string $target_path): void
|
||||
{
|
||||
FileSystem::createDir(dirname($target_path));
|
||||
FileSystem::copy(FileSystem::convertPath($source_file), $target_path);
|
||||
}
|
||||
}
|
||||
146
src/StaticPHP/Artifact/Downloader/DownloadResult.php
Normal file
146
src/StaticPHP/Artifact/Downloader/DownloadResult.php
Normal file
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader;
|
||||
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
class DownloadResult
|
||||
{
|
||||
/**
|
||||
* @param string $cache_type Type of cache: 'archive', 'git', or 'local'
|
||||
* @param null|string $filename Filename for archive type
|
||||
* @param null|string $dirname Directory name for git/local type
|
||||
* @param mixed $extract Extraction path or configuration
|
||||
* @param bool $verified Whether the download has been verified (hash check)
|
||||
* @param null|string $version Version of the downloaded artifact (e.g., "1.2.3", "v2.0.0")
|
||||
* @param array $metadata Additional metadata (e.g., commit hash, release notes, etc.)
|
||||
*/
|
||||
private function __construct(
|
||||
public readonly string $cache_type,
|
||||
public readonly array $config,
|
||||
public readonly ?string $filename = null,
|
||||
public readonly ?string $dirname = null,
|
||||
public mixed $extract = null,
|
||||
public bool $verified = false,
|
||||
public readonly ?string $version = null,
|
||||
public readonly array $metadata = [],
|
||||
) {
|
||||
switch ($this->cache_type) {
|
||||
case 'archive':
|
||||
$this->filename !== null ?: throw new DownloaderException('Archive 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 {
|
||||
return new self('archive', config: $config, filename: $filename, extract: $extract, 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])
|
||||
);
|
||||
}
|
||||
}
|
||||
41
src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php
Normal file
41
src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
|
||||
/** bitbuckettag */
|
||||
class BitBucketTag implements DownloadTypeInterface
|
||||
{
|
||||
public const string BITBUCKET_API_URL = 'https://api.bitbucket.org/2.0/repositories/{repo}/refs/tags';
|
||||
|
||||
public const string BITBUCKET_DOWNLOAD_URL = 'https://bitbucket.org/{repo}/get/{version}.tar.gz';
|
||||
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
logger()->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=("?)(?<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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
|
||||
interface DownloadTypeInterface
|
||||
{
|
||||
/**
|
||||
* @param string $name Download item name
|
||||
* @param array $config Input configuration for the download
|
||||
* @param ArtifactDownloader $downloader Downloader instance
|
||||
*/
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult;
|
||||
}
|
||||
46
src/StaticPHP/Artifact/Downloader/Type/FileList.php
Normal file
46
src/StaticPHP/Artifact/Downloader/Type/FileList.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
|
||||
/** filelist */
|
||||
class FileList implements DownloadTypeInterface
|
||||
{
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
logger()->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);
|
||||
}
|
||||
}
|
||||
22
src/StaticPHP/Artifact/Downloader/Type/Git.php
Normal file
22
src/StaticPHP/Artifact/Downloader/Type/Git.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
|
||||
/** git */
|
||||
class Git implements DownloadTypeInterface
|
||||
{
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
$path = DOWNLOAD_PATH . "/{$name}";
|
||||
logger()->debug("Cloning git repository for {$name} from {$config['url']}");
|
||||
$shallow = !$downloader->getOption('no-shallow-clone', false);
|
||||
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);
|
||||
}
|
||||
}
|
||||
118
src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php
Normal file
118
src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
|
||||
/** ghrel */
|
||||
class GitHubRelease implements DownloadTypeInterface, ValidatorInterface
|
||||
{
|
||||
use GitHubTokenSetupTrait;
|
||||
|
||||
public const string API_URL = 'https://api.github.com/repos/{repo}/releases';
|
||||
|
||||
public const string ASSET_URL = 'https://api.github.com/repos/{repo}/releases/assets/{id}';
|
||||
|
||||
private string $sha256 = '';
|
||||
|
||||
private ?string $version = null;
|
||||
|
||||
public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true): array
|
||||
{
|
||||
logger()->debug("Fetching {$name} GitHub releases from {$repo}");
|
||||
$url = str_replace('{repo}', $repo, self::API_URL);
|
||||
$headers = $this->getGitHubTokenHeaders();
|
||||
$data2 = default_shell()->executeCurl($url, headers: $headers);
|
||||
$data = json_decode($data2 ?: '', true);
|
||||
if (!is_array($data)) {
|
||||
throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}");
|
||||
}
|
||||
$releases = [];
|
||||
foreach ($data as $release) {
|
||||
if ($prefer_stable && $release['prerelease'] === true) {
|
||||
continue;
|
||||
}
|
||||
$releases[] = $release;
|
||||
}
|
||||
return $releases;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest GitHub release assets for a given repository.
|
||||
* match_asset is provided, only return the asset that matches the regex.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
78
src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php
Normal file
78
src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php
Normal file
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
|
||||
/** ghtar */
|
||||
/** ghtagtar */
|
||||
class GitHubTarball implements DownloadTypeInterface
|
||||
{
|
||||
use GitHubTokenSetupTrait;
|
||||
|
||||
public const string API_URL = 'https://api.github.com/repos/{repo}/{rel_type}';
|
||||
|
||||
private ?string $version = null;
|
||||
|
||||
/**
|
||||
* Get the GitHub tarball URL for a given repository and release type.
|
||||
* If match_url is provided, only return the tarball that matches the regex.
|
||||
* Otherwise, return the first tarball found.
|
||||
*/
|
||||
public function getGitHubTarballInfo(string $name, string $repo, string $rel_type, bool $prefer_stable = true, ?string $match_url = null, ?string $basename = null): array
|
||||
{
|
||||
$url = str_replace(['{repo}', '{rel_type}'], [$repo, $rel_type], self::API_URL);
|
||||
$data = default_shell()->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=("?)(?<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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
trait GitHubTokenSetupTrait
|
||||
{
|
||||
public function getGitHubTokenHeaders(): array
|
||||
{
|
||||
return self::getGitHubTokenHeadersStatic();
|
||||
}
|
||||
|
||||
public static function getGitHubTokenHeadersStatic(): array
|
||||
{
|
||||
// GITHUB_TOKEN support
|
||||
if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) {
|
||||
logger()->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 [];
|
||||
}
|
||||
}
|
||||
63
src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php
Normal file
63
src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
|
||||
class HostedPackageBin implements DownloadTypeInterface
|
||||
{
|
||||
use GitHubTokenSetupTrait;
|
||||
|
||||
public const string BASE_REPO = 'static-php/package-bin';
|
||||
|
||||
public const array ASSET_MATCHES = [
|
||||
'linux' => '{name}-{arch}-{os}-{libc}-{libcver}.txz',
|
||||
'darwin' => '{name}-{arch}-{os}.txz',
|
||||
'windows' => '{name}-{arch}-{os}.tgz',
|
||||
];
|
||||
|
||||
private static array $release_info = [];
|
||||
|
||||
public static function getReleaseInfo(): array
|
||||
{
|
||||
if (empty(self::$release_info)) {
|
||||
$rel = (new GitHubRelease())->getGitHubReleases('hosted', self::BASE_REPO);
|
||||
if (empty($rel)) {
|
||||
throw new DownloaderException('No releases found for hosted package-bin');
|
||||
}
|
||||
self::$release_info = $rel[0];
|
||||
}
|
||||
return self::$release_info;
|
||||
}
|
||||
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
$info = self::getReleaseInfo();
|
||||
$replace = [
|
||||
'{name}' => $name,
|
||||
'{arch}' => SystemTarget::getTargetArch(),
|
||||
'{os}' => strtolower(SystemTarget::getTargetOS()),
|
||||
'{libc}' => SystemTarget::getLibc() ?? 'default',
|
||||
'{libcver}' => SystemTarget::getLibcVersion() ?? 'default',
|
||||
];
|
||||
$find_str = str_replace(array_keys($replace), array_values($replace), self::ASSET_MATCHES[strtolower(SystemTarget::getTargetOS())]);
|
||||
foreach ($info['assets'] as $asset) {
|
||||
if ($asset['name'] === $find_str) {
|
||||
$download_url = $asset['browser_download_url'];
|
||||
$filename = $asset['name'];
|
||||
$version = ltrim($info['tag_name'], 'v');
|
||||
logger()->debug("Downloading hosted package-bin {$name} version {$version} from GitHub: {$download_url}");
|
||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
||||
$headers = $this->getGitHubTokenHeaders();
|
||||
default_shell()->executeCurlDownload($download_url, $path, headers: $headers, retries: $downloader->getRetry());
|
||||
return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version);
|
||||
}
|
||||
}
|
||||
throw new DownloaderException("No matching asset found for hosted package-bin {$name}: {$find_str}");
|
||||
}
|
||||
}
|
||||
18
src/StaticPHP/Artifact/Downloader/Type/LocalDir.php
Normal file
18
src/StaticPHP/Artifact/Downloader/Type/LocalDir.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
|
||||
/** local */
|
||||
class LocalDir implements DownloadTypeInterface
|
||||
{
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
logger()->debug("Using local source directory for {$name} from {$config['dirname']}");
|
||||
return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null);
|
||||
}
|
||||
}
|
||||
47
src/StaticPHP/Artifact/Downloader/Type/PIE.php
Normal file
47
src/StaticPHP/Artifact/Downloader/Type/PIE.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
|
||||
/** pie */
|
||||
class PIE implements DownloadTypeInterface
|
||||
{
|
||||
public const string PACKAGIST_URL = 'https://repo.packagist.org/p2/';
|
||||
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
$packagist_url = self::PACKAGIST_URL . "{$config['repo']}.json";
|
||||
logger()->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);
|
||||
}
|
||||
}
|
||||
76
src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php
Normal file
76
src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
|
||||
class PhpRelease implements DownloadTypeInterface, ValidatorInterface
|
||||
{
|
||||
public const string PHP_API = 'https://www.php.net/releases/index.php?json&version={version}';
|
||||
|
||||
public const string DOWNLOAD_URL = 'https://www.php.net/distributions/php-{version}.tar.xz';
|
||||
|
||||
private ?string $sha256 = '';
|
||||
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
$phpver = $downloader->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;
|
||||
}
|
||||
}
|
||||
23
src/StaticPHP/Artifact/Downloader/Type/Url.php
Normal file
23
src/StaticPHP/Artifact/Downloader/Type/Url.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
|
||||
/** url */
|
||||
class Url implements DownloadTypeInterface
|
||||
{
|
||||
public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult
|
||||
{
|
||||
$url = $config['url'];
|
||||
$filename = $config['filename'] ?? basename($url);
|
||||
$path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename;
|
||||
logger()->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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
|
||||
interface ValidatorInterface
|
||||
{
|
||||
public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool;
|
||||
}
|
||||
103
src/StaticPHP/Artifact/DownloaderOptions.php
Normal file
103
src/StaticPHP/Artifact/DownloaderOptions.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Artifact;
|
||||
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
/**
|
||||
* Downloader options definition and extraction helper.
|
||||
* Used to share download-related options between DownloadCommand and BuildTargetCommand.
|
||||
*/
|
||||
class DownloaderOptions
|
||||
{
|
||||
/**
|
||||
* Option keys used by the downloader.
|
||||
*/
|
||||
private const array OPTION_KEYS = [
|
||||
'with-php',
|
||||
'parallel',
|
||||
'retry',
|
||||
'prefer-source',
|
||||
'prefer-binary',
|
||||
'prefer-pre-built',
|
||||
'source-only',
|
||||
'binary-only',
|
||||
'ignore-cache',
|
||||
'ignore-cache-sources',
|
||||
'no-alt',
|
||||
'no-shallow-clone',
|
||||
'custom-url',
|
||||
'custom-git',
|
||||
'custom-local',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns all downloader-related InputOption definitions.
|
||||
*
|
||||
* @param string $prefix Optional prefix for option names (e.g., 'dl' becomes '--dl-parallel')
|
||||
* @return InputOption[]
|
||||
*/
|
||||
public static function getConsoleOptions(string $prefix = ''): array
|
||||
{
|
||||
$p = $prefix ? "{$prefix}-" : '';
|
||||
$shortP = $prefix ? null : 'P'; // Disable short options when using prefix
|
||||
$shortR = $prefix ? null : 'R';
|
||||
$shortp = $prefix ? null : 'p';
|
||||
$shortU = $prefix ? null : 'U';
|
||||
$shortG = $prefix ? null : 'G';
|
||||
$shortL = $prefix ? null : 'L';
|
||||
|
||||
return [
|
||||
// php version option
|
||||
new InputOption("{$p}with-php", null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format (default 8.4)', '8.4'),
|
||||
|
||||
// download preference options
|
||||
new InputOption("{$p}prefer-source", null, InputOption::VALUE_OPTIONAL, 'Prefer source downloads when both source and binary are available', false),
|
||||
new InputOption("{$p}prefer-binary", $shortp, InputOption::VALUE_OPTIONAL, 'Prefer binary downloads when both source and binary are available', false),
|
||||
new InputOption("{$p}source-only", null, InputOption::VALUE_OPTIONAL, 'Only download source artifacts, skip binary artifacts', false),
|
||||
new InputOption("{$p}binary-only", null, InputOption::VALUE_OPTIONAL, 'Only download binary artifacts, skip source artifacts', false),
|
||||
|
||||
// download behavior options
|
||||
new InputOption("{$p}parallel", $shortP, InputOption::VALUE_REQUIRED, 'Number of parallel downloads (default 1)', '1'),
|
||||
new InputOption("{$p}retry", $shortR, InputOption::VALUE_REQUIRED, 'Number of download retries on failure (default 0)', '0'),
|
||||
new InputOption("{$p}ignore-cache", null, InputOption::VALUE_OPTIONAL, 'Ignore some caches when downloading, comma separated, e.g "php-src,curl,openssl"', false),
|
||||
new InputOption("{$p}no-alt", null, null, 'Do not use alternative mirror download artifacts for sources'),
|
||||
new InputOption("{$p}no-shallow-clone", null, null, 'Do not clone shallowly repositories when downloading sources'),
|
||||
|
||||
// custom overrides
|
||||
new InputOption("{$p}custom-url", $shortU, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom source download url, e.g "php-src:https://example.com/php.tar.gz"'),
|
||||
new InputOption("{$p}custom-git", $shortG, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom source git url, e.g "php-src:master:https://github.com/php/php-src.git"'),
|
||||
new InputOption("{$p}custom-local", $shortL, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Specify custom local source path, e.g "php-src:/path/to/php-src"'),
|
||||
|
||||
// deprecated options (for backward compatibility)
|
||||
new InputOption("{$p}prefer-pre-built", null, null, 'Deprecated, use `--' . $p . 'prefer-binary` instead'),
|
||||
new InputOption("{$p}ignore-cache-sources", null, InputOption::VALUE_OPTIONAL, 'Deprecated, use `--' . $p . 'ignore-cache` instead', false),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract downloader-related options from console input options array.
|
||||
* Handles both prefixed and non-prefixed options.
|
||||
*
|
||||
* @param array $allOptions All options from InputInterface::getOptions()
|
||||
* @param string $prefix The prefix used when defining options (empty for DownloadCommand)
|
||||
* @return array Options array suitable for ArtifactDownloader constructor
|
||||
*/
|
||||
public static function extractFromConsoleOptions(array $allOptions, string $prefix = ''): array
|
||||
{
|
||||
$result = [];
|
||||
$p = $prefix ? "{$prefix}-" : '';
|
||||
|
||||
foreach (self::OPTION_KEYS as $key) {
|
||||
$prefixedKey = $p . $key;
|
||||
if (array_key_exists($prefixedKey, $allOptions)) {
|
||||
// Store with non-prefixed key for ArtifactDownloader
|
||||
$result[$key] = $allOptions[$prefixedKey];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
63
src/StaticPHP/Attribute/Artifact/AfterBinaryExtract.php
Normal file
63
src/StaticPHP/Attribute/Artifact/AfterBinaryExtract.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Artifact;
|
||||
|
||||
/**
|
||||
* Attribute to register a hook that runs after binary extraction completes.
|
||||
*
|
||||
* This is useful for post-extraction tasks like:
|
||||
* - Setting executable permissions
|
||||
* - Creating symlinks
|
||||
* - Platform-specific binary setup
|
||||
* - Verifying binary integrity
|
||||
*
|
||||
* The callback method signature should be:
|
||||
* ```php
|
||||
* function(string $target_path, string $platform): void
|
||||
* ```
|
||||
*
|
||||
* - `$target_path`: The directory where binary was extracted
|
||||
* - `$platform`: The current platform string (e.g., 'linux-x86_64')
|
||||
*
|
||||
* Multiple hooks can be registered for the same artifact using IS_REPEATABLE.
|
||||
* Use the `$platforms` parameter to filter which platforms the hook should run on.
|
||||
*
|
||||
* @example
|
||||
* ```php
|
||||
* #[AfterBinaryExtract('zig')]
|
||||
* public function setupZig(string $target_path, string $platform): void
|
||||
* {
|
||||
* // Setup zig after extraction (runs on all platforms)
|
||||
* chmod("{$target_path}/zig", 0755);
|
||||
* }
|
||||
*
|
||||
* #[AfterBinaryExtract('pkg-config', ['linux-x86_64', 'linux-aarch64'])]
|
||||
* public function setupPkgConfigLinux(string $target_path): void
|
||||
* {
|
||||
* // Linux-specific setup for pkg-config
|
||||
* symlink("{$target_path}/bin/pkg-config", "/usr/local/bin/pkg-config");
|
||||
* }
|
||||
*
|
||||
* #[AfterBinaryExtract('openssl', ['darwin-aarch64'])]
|
||||
* public function patchOpensslMacM1(string $target_path): void
|
||||
* {
|
||||
* // macOS ARM64 specific patches
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
readonly class AfterBinaryExtract
|
||||
{
|
||||
/**
|
||||
* @param string $artifact_name The name of the artifact this hook applies to
|
||||
* @param string[] $platforms Platform filters (empty array means all platforms).
|
||||
* Valid values: 'linux-x86_64', 'linux-aarch64', 'darwin-x86_64',
|
||||
* 'darwin-aarch64', 'windows-x86_64', etc.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $artifact_name,
|
||||
public array $platforms = []
|
||||
) {}
|
||||
}
|
||||
50
src/StaticPHP/Attribute/Artifact/AfterSourceExtract.php
Normal file
50
src/StaticPHP/Attribute/Artifact/AfterSourceExtract.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Artifact;
|
||||
|
||||
/**
|
||||
* Attribute to register a hook that runs after source extraction completes.
|
||||
*
|
||||
* This is useful for post-extraction tasks like:
|
||||
* - Patching source files
|
||||
* - Removing unnecessary directories (tests, docs, etc.)
|
||||
* - Applying platform-specific fixes
|
||||
* - Renaming or reorganizing files
|
||||
*
|
||||
* The callback method signature should be:
|
||||
* ```php
|
||||
* function(string $target_path): void
|
||||
* ```
|
||||
*
|
||||
* - `$target_path`: The directory where source was extracted
|
||||
*
|
||||
* Multiple hooks can be registered for the same artifact using IS_REPEATABLE.
|
||||
*
|
||||
* @example
|
||||
* ```php
|
||||
* #[AfterSourceExtract('php-src')]
|
||||
* public function patchPhpSrc(string $target_path): void
|
||||
* {
|
||||
* // Apply patches after php-src is extracted
|
||||
* FileSystem::replaceFileStr("{$target_path}/configure", 'old', 'new');
|
||||
* }
|
||||
*
|
||||
* #[AfterSourceExtract('openssl')]
|
||||
* public function cleanupOpenssl(string $target_path): void
|
||||
* {
|
||||
* // Remove unnecessary directories
|
||||
* FileSystem::removeDir("{$target_path}/test");
|
||||
* FileSystem::removeDir("{$target_path}/fuzz");
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
readonly class AfterSourceExtract
|
||||
{
|
||||
/**
|
||||
* @param string $artifact_name The name of the artifact this hook applies to
|
||||
*/
|
||||
public function __construct(public string $artifact_name) {}
|
||||
}
|
||||
45
src/StaticPHP/Attribute/Artifact/BinaryExtract.php
Normal file
45
src/StaticPHP/Attribute/Artifact/BinaryExtract.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Artifact;
|
||||
|
||||
/**
|
||||
* Attribute to completely take over the binary extraction process for an artifact.
|
||||
*
|
||||
* When this attribute is applied to a method, the standard extraction logic is bypassed,
|
||||
* and the annotated method is responsible for extracting the binary files.
|
||||
*
|
||||
* The callback method signature should be:
|
||||
* ```php
|
||||
* function(Artifact $artifact, string $source_file, string $target_path, string $platform): void
|
||||
* ```
|
||||
*
|
||||
* - `$artifact`: The artifact instance being extracted
|
||||
* - `$source_file`: Path to the downloaded archive or directory
|
||||
* - `$target_path`: The resolved target extraction path from config
|
||||
* - `$platform`: The current platform string (e.g., 'linux-x86_64', 'darwin-aarch64')
|
||||
*
|
||||
* @example
|
||||
* ```php
|
||||
* #[BinaryExtract('special-tool', ['linux-x86_64', 'linux-aarch64'])]
|
||||
* public function extractSpecialTool(Artifact $artifact, string $source_file, string $target_path): void
|
||||
* {
|
||||
* // Custom binary extraction logic
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
readonly class BinaryExtract
|
||||
{
|
||||
/**
|
||||
* @param string $artifact_name The name of the artifact this extraction handler applies to
|
||||
* @param string[] $platforms Platform filters (empty array means all platforms).
|
||||
* Valid values: 'linux-x86_64', 'linux-aarch64', 'darwin-x86_64',
|
||||
* 'darwin-aarch64', 'windows-x86_64', etc.
|
||||
*/
|
||||
public function __construct(
|
||||
public string $artifact_name,
|
||||
public array $platforms = []
|
||||
) {}
|
||||
}
|
||||
11
src/StaticPHP/Attribute/Artifact/CustomBinary.php
Normal file
11
src/StaticPHP/Attribute/Artifact/CustomBinary.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Artifact;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class CustomBinary
|
||||
{
|
||||
public function __construct(public string $artifact_name, public array $support_os) {}
|
||||
}
|
||||
11
src/StaticPHP/Attribute/Artifact/CustomSource.php
Normal file
11
src/StaticPHP/Attribute/Artifact/CustomSource.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Artifact;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class CustomSource
|
||||
{
|
||||
public function __construct(public string $artifact_name) {}
|
||||
}
|
||||
39
src/StaticPHP/Attribute/Artifact/SourceExtract.php
Normal file
39
src/StaticPHP/Attribute/Artifact/SourceExtract.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Artifact;
|
||||
|
||||
/**
|
||||
* Attribute to completely take over the source extraction process for an artifact.
|
||||
*
|
||||
* When this attribute is applied to a method, the standard extraction logic is bypassed,
|
||||
* and the annotated method is responsible for extracting the source files.
|
||||
*
|
||||
* The callback method signature should be:
|
||||
* ```php
|
||||
* function(Artifact $artifact, string $source_file, string $target_path): void
|
||||
* ```
|
||||
*
|
||||
* - `$artifact`: The artifact instance being extracted
|
||||
* - `$source_file`: Path to the downloaded archive or directory
|
||||
* - `$target_path`: The resolved target extraction path from config
|
||||
*
|
||||
* @example
|
||||
* ```php
|
||||
* #[SourceExtract('weird-package')]
|
||||
* public function extractWeirdPackage(Artifact $artifact, string $source_file, string $target_path): void
|
||||
* {
|
||||
* // Custom extraction logic
|
||||
* shell_exec("tar -xf {$source_file} -C {$target_path} --strip-components=2");
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
readonly class SourceExtract
|
||||
{
|
||||
/**
|
||||
* @param string $artifact_name The name of the artifact this extraction handler applies to
|
||||
*/
|
||||
public function __construct(public string $artifact_name) {}
|
||||
}
|
||||
18
src/StaticPHP/Attribute/Doctor/CheckItem.php
Normal file
18
src/StaticPHP/Attribute/Doctor/CheckItem.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Doctor;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class CheckItem
|
||||
{
|
||||
public mixed $callback = null;
|
||||
|
||||
public function __construct(
|
||||
public string $item_name,
|
||||
public ?string $limit_os = null,
|
||||
public int $level = 100,
|
||||
public bool $manual = false,
|
||||
) {}
|
||||
}
|
||||
14
src/StaticPHP/Attribute/Doctor/FixItem.php
Normal file
14
src/StaticPHP/Attribute/Doctor/FixItem.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Doctor;
|
||||
|
||||
/**
|
||||
* Indicate a method is a fix item for doctor check.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
class FixItem
|
||||
{
|
||||
public function __construct(public string $name) {}
|
||||
}
|
||||
11
src/StaticPHP/Attribute/Doctor/OptionalCheck.php
Normal file
11
src/StaticPHP/Attribute/Doctor/OptionalCheck.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Doctor;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_METHOD)]
|
||||
class OptionalCheck
|
||||
{
|
||||
public function __construct(public array $check) {}
|
||||
}
|
||||
14
src/StaticPHP/Attribute/Package/AfterStage.php
Normal file
14
src/StaticPHP/Attribute/Package/AfterStage.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
/**
|
||||
* Indicates that the annotated method should be executed after a specific stage of the build process for a given package.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
readonly class AfterStage
|
||||
{
|
||||
public function __construct(public string $package_name, public array|string $stage, public ?string $only_when_package_resolved = null) {}
|
||||
}
|
||||
19
src/StaticPHP/Attribute/Package/BeforeStage.php
Normal file
19
src/StaticPHP/Attribute/Package/BeforeStage.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
/**
|
||||
* Indicates that the annotated method should be executed before a specific stage of the build process for a given package.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class BeforeStage
|
||||
{
|
||||
public readonly array|string $stage;
|
||||
|
||||
public function __construct(public string $package_name = '', array|callable|string $stage = '', public ?string $only_when_package_resolved = null)
|
||||
{
|
||||
$this->stage = $stage;
|
||||
}
|
||||
}
|
||||
17
src/StaticPHP/Attribute/Package/BuildFor.php
Normal file
17
src/StaticPHP/Attribute/Package/BuildFor.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
/**
|
||||
* Mark a method as building for a specific OS.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
readonly class BuildFor
|
||||
{
|
||||
/**
|
||||
* @param 'Darwin'|'Linux'|'Windows' $os The operating system to build for PHP_OS_FAMILY
|
||||
*/
|
||||
public function __construct(public string $os) {}
|
||||
}
|
||||
14
src/StaticPHP/Attribute/Package/CustomPhpConfigureArg.php
Normal file
14
src/StaticPHP/Attribute/Package/CustomPhpConfigureArg.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
/**
|
||||
* Indicates a custom configure argument for PHP build process.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
readonly class CustomPhpConfigureArg
|
||||
{
|
||||
public function __construct(public string $os = '') {}
|
||||
}
|
||||
19
src/StaticPHP/Attribute/Package/Extension.php
Normal file
19
src/StaticPHP/Attribute/Package/Extension.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
/**
|
||||
* Indicates that the annotated class defines a PHP extension.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
|
||||
class Extension
|
||||
{
|
||||
public function __construct(public string $name)
|
||||
{
|
||||
if (!str_starts_with($name, 'ext-')) {
|
||||
$this->name = "ext-{$name}";
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/StaticPHP/Attribute/Package/Info.php
Normal file
8
src/StaticPHP/Attribute/Package/Info.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class Info {}
|
||||
8
src/StaticPHP/Attribute/Package/InitPackage.php
Normal file
8
src/StaticPHP/Attribute/Package/InitPackage.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
class InitPackage {}
|
||||
14
src/StaticPHP/Attribute/Package/Library.php
Normal file
14
src/StaticPHP/Attribute/Package/Library.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
/**
|
||||
* Indicates that the annotated class defines a library package.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
|
||||
readonly class Library
|
||||
{
|
||||
public function __construct(public string $name) {}
|
||||
}
|
||||
11
src/StaticPHP/Attribute/Package/ResolveBuild.php
Normal file
11
src/StaticPHP/Attribute/Package/ResolveBuild.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
/**
|
||||
* Indicates that the annotated method is responsible for initializing the build process for a target package.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD)]
|
||||
class ResolveBuild {}
|
||||
14
src/StaticPHP/Attribute/Package/Stage.php
Normal file
14
src/StaticPHP/Attribute/Package/Stage.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
/**
|
||||
* Indicates that the annotated method defines a specific stage in a package.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
readonly class Stage
|
||||
{
|
||||
public function __construct(public ?string $function = null) {}
|
||||
}
|
||||
14
src/StaticPHP/Attribute/Package/Target.php
Normal file
14
src/StaticPHP/Attribute/Package/Target.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
/**
|
||||
* Indicates that the annotated class defines a target.
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
|
||||
readonly class Target
|
||||
{
|
||||
public function __construct(public string $name) {}
|
||||
}
|
||||
8
src/StaticPHP/Attribute/Package/Validate.php
Normal file
8
src/StaticPHP/Attribute/Package/Validate.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute\Package;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
class Validate {}
|
||||
11
src/StaticPHP/Attribute/PatchDescription.php
Normal file
11
src/StaticPHP/Attribute/PatchDescription.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Attribute;
|
||||
|
||||
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
|
||||
readonly class PatchDescription
|
||||
{
|
||||
public function __construct(public string $description) {}
|
||||
}
|
||||
182
src/StaticPHP/Command/BaseCommand.php
Normal file
182
src/StaticPHP/Command/BaseCommand.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\ExceptionHandler;
|
||||
use StaticPHP\Exception\SPCException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
abstract class BaseCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The message of the day (MOTD) displayed when the command is run.
|
||||
* You can customize this to show your application's name and version if you are using SPC in vendor mode.
|
||||
*/
|
||||
public static string $motd = ' ____ _ _ _ ____ _ _ ____
|
||||
/ ___|| |_ __ _| |_(_) ___| _ \| | | | _ \
|
||||
\___ \| __/ _` | __| |/ __| |_) | |_| | |_) |
|
||||
___) | || (_| | |_| | (__| __/| _ | __/
|
||||
|____/ \__\__,_|\__|_|\___|_| |_| |_|_| {version}
|
||||
|
||||
';
|
||||
|
||||
protected bool $no_motd = false;
|
||||
|
||||
protected InputInterface $input;
|
||||
|
||||
protected OutputInterface $output;
|
||||
|
||||
public function __construct(?string $name = null)
|
||||
{
|
||||
parent::__construct($name);
|
||||
$this->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_VERY_VERBOSE;
|
||||
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;
|
||||
}
|
||||
}
|
||||
29
src/StaticPHP/Command/BuildLibsCommand.php
Normal file
29
src/StaticPHP/Command/BuildLibsCommand.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
#[AsCommand('build:libs')]
|
||||
class BuildLibsCommand extends BaseCommand
|
||||
{
|
||||
public function configure()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
58
src/StaticPHP/Command/BuildTargetCommand.php
Normal file
58
src/StaticPHP/Command/BuildTargetCommand.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\Artifact\DownloaderOptions;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
class BuildTargetCommand extends BaseCommand
|
||||
{
|
||||
public function __construct(private readonly string $target, ?string $description = null)
|
||||
{
|
||||
parent::__construct("build:{$target}");
|
||||
if ($target === 'php') {
|
||||
$this->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("<info>✔ BUILD SUCCESSFUL ({$usedtime} s)</info>");
|
||||
$this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
|
||||
|
||||
$installer->printBuildPackageOutputs();
|
||||
|
||||
return static::SUCCESS;
|
||||
}
|
||||
}
|
||||
34
src/StaticPHP/Command/Dev/IsInstalledCommand.php
Normal file
34
src/StaticPHP/Command/Dev/IsInstalledCommand.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command\Dev;
|
||||
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
|
||||
#[AsCommand('dev:is-installed', 'Check if a package is installed correctly', ['is-installed'], true)]
|
||||
class IsInstalledCommand extends BaseCommand
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->no_motd = true;
|
||||
$this->addArgument('package', InputArgument::REQUIRED, 'The package name to check installation status');
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$installer = new PackageInstaller();
|
||||
$package = $this->input->getArgument('package');
|
||||
$installer->addInstallPackage($package);
|
||||
$installed = $installer->isPackageInstalled($package);
|
||||
if ($installed) {
|
||||
$this->output->writeln("<info>Package [{$package}] is installed correctly.</info>");
|
||||
return static::SUCCESS;
|
||||
}
|
||||
$this->output->writeln("<error>Package [{$package}] is not installed.</error>");
|
||||
return static::FAILURE;
|
||||
}
|
||||
}
|
||||
33
src/StaticPHP/Command/Dev/ShellCommand.php
Normal file
33
src/StaticPHP/Command/Dev/ShellCommand.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command\Dev;
|
||||
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand('dev:shell')]
|
||||
class ShellCommand extends BaseCommand
|
||||
{
|
||||
public function handle(): int
|
||||
{
|
||||
// need to init global env first
|
||||
GlobalEnvManager::afterInit();
|
||||
|
||||
$this->output->writeln("Entering interactive shell. Type 'exit' to leave.");
|
||||
|
||||
if (SystemTarget::isUnix()) {
|
||||
passthru('PS1=\'[StaticPHP] > \' /bin/bash', $code);
|
||||
return $code;
|
||||
}
|
||||
if (SystemTarget::getTargetOS() === 'Windows') {
|
||||
passthru('cmd.exe', $code);
|
||||
return $code;
|
||||
}
|
||||
$this->output->writeln('<error>Unsupported OS for shell command.</error>');
|
||||
return static::FAILURE;
|
||||
}
|
||||
}
|
||||
34
src/StaticPHP/Command/DoctorCommand.php
Normal file
34
src/StaticPHP/Command/DoctorCommand.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\Doctor\Doctor;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand('doctor', 'Diagnose whether the current environment can compile normally')]
|
||||
class DoctorCommand extends BaseCommand
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->addOption('auto-fix', null, InputOption::VALUE_OPTIONAL, 'Automatically fix failed items (if possible)', false);
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$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('<info>Doctor check complete !</info>');
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
return static::FAILURE;
|
||||
}
|
||||
}
|
||||
119
src/StaticPHP/Command/DownloadCommand.php
Normal file
119
src/StaticPHP/Command/DownloadCommand.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\DownloaderOptions;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Util\DependencyResolver;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand('download')]
|
||||
class DownloadCommand extends BaseCommand
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->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('<info>Download completed in ' . $elapsed . ' s.</info>');
|
||||
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;
|
||||
}
|
||||
}
|
||||
111
src/StaticPHP/Command/ExtractCommand.php
Normal file
111
src/StaticPHP/Command/ExtractCommand.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactCache;
|
||||
use StaticPHP\Artifact\ArtifactExtractor;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Registry\ArtifactLoader;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Util\DependencyResolver;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
#[AsCommand('extract')]
|
||||
class ExtractCommand extends BaseCommand
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->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("<error>Artifact '{$name}' not found.</error>");
|
||||
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('<comment>No artifacts specified. Use artifact names or --for-extensions/--for-libs/--for-packages options.</comment>');
|
||||
$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;
|
||||
}
|
||||
}
|
||||
27
src/StaticPHP/Command/InstallPackageCommand.php
Normal file
27
src/StaticPHP/Command/InstallPackageCommand.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand('install-pkg', 'Install additional package', ['i', 'install-package'])]
|
||||
class InstallPackageCommand extends BaseCommand
|
||||
{
|
||||
public function configure()
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
||||
58
src/StaticPHP/Command/SPCConfigCommand.php
Normal file
58
src/StaticPHP/Command/SPCConfigCommand.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command;
|
||||
|
||||
use StaticPHP\Util\SPCConfigUtil;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
#[AsCommand('spc-config', 'Build dependencies')]
|
||||
class SPCConfigCommand extends BaseCommand
|
||||
{
|
||||
protected bool $no_motd = true;
|
||||
|
||||
public function configure(): void
|
||||
{
|
||||
$this->addArgument('extensions', InputArgument::OPTIONAL, 'The extensions will be compiled, comma separated');
|
||||
$this->addOption('with-libs', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', '');
|
||||
$this->addOption('with-packages', null, InputOption::VALUE_REQUIRED, 'add additional libraries, comma separated', '');
|
||||
$this->addOption('with-suggested-libs', 'L', null, 'Build with suggested libs for selected exts and libs');
|
||||
$this->addOption('with-suggests', null, null, 'Build with suggested packages for selected exts and libs');
|
||||
$this->addOption('with-suggested-exts', 'E', null, 'Build with suggested extensions for selected exts');
|
||||
$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;
|
||||
}
|
||||
}
|
||||
68
src/StaticPHP/Config/ArtifactConfig.php
Normal file
68
src/StaticPHP/Config/ArtifactConfig.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Config;
|
||||
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
|
||||
class ArtifactConfig
|
||||
{
|
||||
private static array $artifact_configs = [];
|
||||
|
||||
public static function loadFromDir(string $dir): void
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
throw new WrongUsageException("Directory {$dir} does not exist, cannot load artifact config.");
|
||||
}
|
||||
$files = glob("{$dir}/artifact.*.json");
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $file) {
|
||||
self::loadFromFile($file);
|
||||
}
|
||||
}
|
||||
if (file_exists("{$dir}/artifact.json")) {
|
||||
self::loadFromFile("{$dir}/artifact.json");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load artifact configurations from a specified JSON file.
|
||||
*/
|
||||
public static function loadFromFile(string $file): void
|
||||
{
|
||||
$content = @file_get_contents($file);
|
||||
if ($content === false) {
|
||||
throw new WrongUsageException("Failed to read artifact config file: {$file}");
|
||||
}
|
||||
$data = json_decode($content, true);
|
||||
if (!is_array($data)) {
|
||||
throw new WrongUsageException("Invalid JSON format in artifact config file: {$file}");
|
||||
}
|
||||
ConfigValidator::validateAndLintArtifacts(basename($file), $data);
|
||||
foreach ($data as $artifact_name => $config) {
|
||||
self::$artifact_configs[$artifact_name] = $config;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all loaded artifact configurations.
|
||||
*
|
||||
* @return array<string, 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;
|
||||
}
|
||||
}
|
||||
52
src/StaticPHP/Config/ConfigType.php
Normal file
52
src/StaticPHP/Config/ConfigType.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Config;
|
||||
|
||||
enum ConfigType
|
||||
{
|
||||
public const string LIST_ARRAY = 'list_array';
|
||||
|
||||
public const string ASSOC_ARRAY = 'assoc_array';
|
||||
|
||||
public const string STRING = 'string';
|
||||
|
||||
public const string BOOL = 'bool';
|
||||
|
||||
public const array PACKAGE_TYPES = [
|
||||
'library',
|
||||
'php-extension',
|
||||
'target',
|
||||
'virtual-target',
|
||||
];
|
||||
|
||||
public static function validateLicenseField(mixed $value): bool
|
||||
{
|
||||
if (is_list_array($value)) {
|
||||
foreach ($value as $item) {
|
||||
if (!self::validateLicenseField($item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!is_assoc_array($value)) {
|
||||
return false;
|
||||
}
|
||||
if (!isset($value['type'])) {
|
||||
return false;
|
||||
}
|
||||
return match ($value['type']) {
|
||||
'file' => 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user