mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-13 11:55:36 +08:00
Compare commits
31 Commits
v3-feat/te
...
dd5762fbd3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd5762fbd3 | ||
|
|
1707c679e8 | ||
|
|
acd0e2b23a | ||
|
|
c1c31a730b | ||
|
|
910f10a1dd | ||
|
|
fefcbf4029 | ||
|
|
88d135a4e5 | ||
|
|
f8952da2a3 | ||
|
|
63c7aa8d38 | ||
|
|
355da340b4 | ||
|
|
a4fd618a10 | ||
|
|
9a91aecb28 | ||
|
|
8bf2f42596 | ||
|
|
4e841cfc67 | ||
|
|
da8b7c2bc4 | ||
|
|
c4cec15c18 | ||
|
|
e3f9894331 | ||
|
|
6d292b4c54 | ||
|
|
f6b47ad810 | ||
|
|
7c8b40a49a | ||
|
|
48fbeab7e4 | ||
|
|
eb0a36e379 | ||
|
|
4bbe56dd9f | ||
|
|
fe0b983f6c | ||
|
|
2080407283 | ||
|
|
dc05ad23c9 | ||
|
|
458af6ac78 | ||
|
|
66d3af518b | ||
|
|
2901d32ba7 | ||
|
|
794d92c9d8 | ||
|
|
97e337cea2 |
@@ -1,44 +1,44 @@
|
||||
{
|
||||
"pre-push": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer analyse"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pre-commit": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer cs-fix -- --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php}",
|
||||
"conditions": [
|
||||
{
|
||||
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType",
|
||||
"args": ["php"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"post-change": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer install",
|
||||
"options": [],
|
||||
"conditions": [
|
||||
{
|
||||
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any",
|
||||
"args": [
|
||||
[
|
||||
"composer.json",
|
||||
"composer.lock"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
{
|
||||
"pre-push": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "php vendor/bin/phpstan analyse --memory-limit 300M"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pre-commit": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "php vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff {$STAGED_FILES|of-type:php} --sequential",
|
||||
"conditions": [
|
||||
{
|
||||
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType",
|
||||
"args": ["php"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"post-change": {
|
||||
"enabled": true,
|
||||
"actions": [
|
||||
{
|
||||
"action": "composer install",
|
||||
"options": [],
|
||||
"conditions": [
|
||||
{
|
||||
"exec": "\\CaptainHook\\App\\Hook\\Condition\\FileChanged\\Any",
|
||||
"args": [
|
||||
[
|
||||
"composer.json",
|
||||
"composer.lock"
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,131 +1,4 @@
|
||||
{
|
||||
"vswhere": {
|
||||
"binary": {
|
||||
"windows-x86_64": {
|
||||
"type": "url",
|
||||
"url": "https://github.com/microsoft/vswhere/releases/download/3.1.7/vswhere.exe",
|
||||
"extract": "{pkg_root_path}/bin/vswhere.exe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"musl-wrapper": {
|
||||
"source": "https://musl.libc.org/releases/musl-1.2.5.tar.gz"
|
||||
},
|
||||
"php-src": {
|
||||
"source": {
|
||||
"type": "php-release"
|
||||
}
|
||||
},
|
||||
"php-sdk-binary-tools": {
|
||||
"binary": {
|
||||
"windows-x86_64": {
|
||||
"type": "git",
|
||||
"rev": "master",
|
||||
"url": "https://github.com/php/php-sdk-binary-tools.git",
|
||||
"extract": "{php_sdk_path}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"go-xcaddy": {
|
||||
"binary": "custom"
|
||||
},
|
||||
"musl-toolchain": {
|
||||
"binary": {
|
||||
"linux-x86_64": {
|
||||
"type": "url",
|
||||
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz",
|
||||
"extract": "{pkg_root_path}/musl-toolchain"
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"type": "url",
|
||||
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz",
|
||||
"extract": "{pkg_root_path}/musl-toolchain"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pkg-config": {
|
||||
"source": "https://dl.static-php.dev/static-php-cli/deps/pkg-config/pkg-config-0.29.2.tar.gz",
|
||||
"binary": {
|
||||
"linux-x86_64": {
|
||||
"type": "ghrel",
|
||||
"repo": "static-php/static-php-cli-hosted",
|
||||
"match": "pkg-config-aarch64-linux-musl-1.2.5.txz",
|
||||
"extract": {
|
||||
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
|
||||
}
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"type": "ghrel",
|
||||
"repo": "static-php/static-php-cli-hosted",
|
||||
"match": "pkg-config-x86_64-linux-musl-1.2.5.txz",
|
||||
"extract": {
|
||||
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
|
||||
}
|
||||
},
|
||||
"macos-x86_64": {
|
||||
"type": "ghrel",
|
||||
"repo": "static-php/static-php-cli-hosted",
|
||||
"match": "pkg-config-x86_64-darwin.txz",
|
||||
"extract": {
|
||||
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
|
||||
}
|
||||
},
|
||||
"macos-aarch64": {
|
||||
"type": "ghrel",
|
||||
"repo": "static-php/static-php-cli-hosted",
|
||||
"match": "pkg-config-aarch64-darwin.txz",
|
||||
"extract": "{pkg_root_path}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"strawberry-perl": {
|
||||
"binary": {
|
||||
"windows-x86_64": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip"
|
||||
}
|
||||
},
|
||||
"upx": {
|
||||
"binary": {
|
||||
"linux-x86_64": {
|
||||
"type": "ghrel",
|
||||
"repo": "upx/upx",
|
||||
"match": "upx.+-amd64_linux\\.tar\\.xz",
|
||||
"extract": {
|
||||
"upx": "{pkg_root_path}/bin/upx"
|
||||
}
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"type": "ghrel",
|
||||
"repo": "upx/upx",
|
||||
"match": "upx.+-arm64_linux\\.tar\\.xz",
|
||||
"extract": {
|
||||
"upx": "{pkg_root_path}/bin/upx"
|
||||
}
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"type": "ghrel",
|
||||
"repo": "upx/upx",
|
||||
"match": "upx.+-win64\\.zip",
|
||||
"extract": {
|
||||
"upx.exe": "{pkg_root_path}/bin/upx.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"zig": {
|
||||
"binary": "custom"
|
||||
},
|
||||
"nasm": {
|
||||
"binary": {
|
||||
"windows-x86_64": {
|
||||
"type": "url",
|
||||
"url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip",
|
||||
"extract": {
|
||||
"nasm.exe": "{php_sdk_path}/bin/nasm.exe",
|
||||
"ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"amqp": {
|
||||
"source": {
|
||||
"type": "url",
|
||||
@@ -391,11 +264,14 @@
|
||||
"repo": "guanzhi/GmSSL"
|
||||
}
|
||||
},
|
||||
"go-xcaddy": {
|
||||
"binary": "custom"
|
||||
},
|
||||
"grpc": {
|
||||
"binary": "hosted",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"rev": "v1.75.x",
|
||||
"regex": "v(?<version>1.\\d+).x",
|
||||
"url": "https://github.com/grpc/grpc.git"
|
||||
}
|
||||
},
|
||||
@@ -729,13 +605,6 @@
|
||||
"extract": "php-src/ext/memcached"
|
||||
}
|
||||
},
|
||||
"mimalloc": {
|
||||
"source": {
|
||||
"type": "ghtagtar",
|
||||
"repo": "microsoft/mimalloc",
|
||||
"match": "v2\\.\\d\\.[^3].*"
|
||||
}
|
||||
},
|
||||
"micro": {
|
||||
"source": {
|
||||
"type": "git",
|
||||
@@ -744,6 +613,13 @@
|
||||
"url": "https://github.com/static-php/phpmicro"
|
||||
}
|
||||
},
|
||||
"mimalloc": {
|
||||
"source": {
|
||||
"type": "ghtagtar",
|
||||
"repo": "microsoft/mimalloc",
|
||||
"match": "v2\\.\\d\\.[^3].*"
|
||||
}
|
||||
},
|
||||
"mongodb": {
|
||||
"source": {
|
||||
"type": "ghrel",
|
||||
@@ -761,6 +637,35 @@
|
||||
"extract": "php-src/ext/msgpack"
|
||||
}
|
||||
},
|
||||
"musl-toolchain": {
|
||||
"binary": {
|
||||
"linux-x86_64": {
|
||||
"type": "url",
|
||||
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/x86_64-musl-toolchain.tgz",
|
||||
"extract": "{pkg_root_path}/musl-toolchain"
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"type": "url",
|
||||
"url": "https://dl.static-php.dev/static-php-cli/deps/musl-toolchain/aarch64-musl-toolchain.tgz",
|
||||
"extract": "{pkg_root_path}/musl-toolchain"
|
||||
}
|
||||
}
|
||||
},
|
||||
"musl-wrapper": {
|
||||
"source": "https://musl.libc.org/releases/musl-1.2.5.tar.gz"
|
||||
},
|
||||
"nasm": {
|
||||
"binary": {
|
||||
"windows-x86_64": {
|
||||
"type": "url",
|
||||
"url": "https://dl.static-php.dev/static-php-cli/deps/nasm/nasm-2.16.01-win64.zip",
|
||||
"extract": {
|
||||
"nasm.exe": "{php_sdk_path}/bin/nasm.exe",
|
||||
"ndisasm.exe": "{php_sdk_path}/bin/ndisasm.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ncurses": {
|
||||
"binary": "hosted",
|
||||
"source": {
|
||||
@@ -846,6 +751,56 @@
|
||||
"extract": "php-src/ext/pdo_sqlsrv"
|
||||
}
|
||||
},
|
||||
"php-sdk-binary-tools": {
|
||||
"binary": {
|
||||
"windows-x86_64": {
|
||||
"type": "git",
|
||||
"rev": "master",
|
||||
"url": "https://github.com/php/php-sdk-binary-tools.git",
|
||||
"extract": "{php_sdk_path}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"php-src": {
|
||||
"source": {
|
||||
"type": "php-release"
|
||||
}
|
||||
},
|
||||
"pkg-config": {
|
||||
"binary": {
|
||||
"linux-x86_64": {
|
||||
"type": "ghrel",
|
||||
"repo": "static-php/static-php-cli-hosted",
|
||||
"match": "pkg-config-aarch64-linux-musl-1.2.5.txz",
|
||||
"extract": {
|
||||
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
|
||||
}
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"type": "ghrel",
|
||||
"repo": "static-php/static-php-cli-hosted",
|
||||
"match": "pkg-config-x86_64-linux-musl-1.2.5.txz",
|
||||
"extract": {
|
||||
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
|
||||
}
|
||||
},
|
||||
"macos-x86_64": {
|
||||
"type": "ghrel",
|
||||
"repo": "static-php/static-php-cli-hosted",
|
||||
"match": "pkg-config-x86_64-darwin.txz",
|
||||
"extract": {
|
||||
"bin/pkg-config": "{pkg_root_path}/bin/pkg-config"
|
||||
}
|
||||
},
|
||||
"macos-aarch64": {
|
||||
"type": "ghrel",
|
||||
"repo": "static-php/static-php-cli-hosted",
|
||||
"match": "pkg-config-aarch64-darwin.txz",
|
||||
"extract": "{pkg_root_path}"
|
||||
}
|
||||
},
|
||||
"source": "https://dl.static-php.dev/static-php-cli/deps/pkg-config/pkg-config-0.29.2.tar.gz"
|
||||
},
|
||||
"postgresql": {
|
||||
"source": {
|
||||
"type": "ghtagtar",
|
||||
@@ -946,6 +901,15 @@
|
||||
"extract": "php-src/ext/sqlsrv"
|
||||
}
|
||||
},
|
||||
"strawberry-perl": {
|
||||
"binary": {
|
||||
"windows-x86_64": {
|
||||
"type": "url",
|
||||
"url": "https://github.com/StrawberryPerl/Perl-Dist-Strawberry/releases/download/SP_5380_5361/strawberry-perl-5.38.0.1-64bit-portable.zip",
|
||||
"extract": "{pkg_root_path}/strawberry-perl"
|
||||
}
|
||||
}
|
||||
},
|
||||
"swoole": {
|
||||
"source": {
|
||||
"type": "ghtar",
|
||||
@@ -978,6 +942,43 @@
|
||||
"version": "2.3.12"
|
||||
}
|
||||
},
|
||||
"upx": {
|
||||
"binary": {
|
||||
"linux-x86_64": {
|
||||
"type": "ghrel",
|
||||
"repo": "upx/upx",
|
||||
"match": "upx.+-amd64_linux\\.tar\\.xz",
|
||||
"extract": {
|
||||
"upx": "{pkg_root_path}/bin/upx"
|
||||
}
|
||||
},
|
||||
"linux-aarch64": {
|
||||
"type": "ghrel",
|
||||
"repo": "upx/upx",
|
||||
"match": "upx.+-arm64_linux\\.tar\\.xz",
|
||||
"extract": {
|
||||
"upx": "{pkg_root_path}/bin/upx"
|
||||
}
|
||||
},
|
||||
"windows-x86_64": {
|
||||
"type": "ghrel",
|
||||
"repo": "upx/upx",
|
||||
"match": "upx.+-win64\\.zip",
|
||||
"extract": {
|
||||
"upx.exe": "{pkg_root_path}/bin/upx.exe"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"vswhere": {
|
||||
"binary": {
|
||||
"windows-x86_64": {
|
||||
"type": "url",
|
||||
"url": "https://github.com/microsoft/vswhere/releases/download/3.1.7/vswhere.exe",
|
||||
"extract": "{pkg_root_path}/bin/vswhere.exe"
|
||||
}
|
||||
}
|
||||
},
|
||||
"watcher": {
|
||||
"source": {
|
||||
"type": "ghtar",
|
||||
@@ -1037,6 +1038,9 @@
|
||||
"extract": "php-src/ext/yaml"
|
||||
}
|
||||
},
|
||||
"zig": {
|
||||
"binary": "custom"
|
||||
},
|
||||
"zlib": {
|
||||
"binary": "hosted",
|
||||
"source": {
|
||||
|
||||
1182
config/pkg.ext.json
1182
config/pkg.ext.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,55 +1,5 @@
|
||||
{
|
||||
"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",
|
||||
@@ -59,37 +9,90 @@
|
||||
"php-embed",
|
||||
"go-xcaddy",
|
||||
"libxml2"
|
||||
]
|
||||
],
|
||||
"type": "virtual-target"
|
||||
},
|
||||
"go-xcaddy": {
|
||||
"type": "target",
|
||||
"artifact": "go-xcaddy",
|
||||
"static-bins": [
|
||||
"xcaddy"
|
||||
]
|
||||
],
|
||||
"type": "target"
|
||||
},
|
||||
"musl-toolchain": {
|
||||
"type": "target",
|
||||
"artifact": "musl-toolchain"
|
||||
},
|
||||
"strawberry-perl": {
|
||||
"type": "target",
|
||||
"artifact": "strawberry-perl"
|
||||
},
|
||||
"upx": {
|
||||
"type": "target",
|
||||
"artifact": "upx"
|
||||
},
|
||||
"zig": {
|
||||
"type": "target",
|
||||
"artifact": "zig"
|
||||
"artifact": "musl-toolchain",
|
||||
"type": "target"
|
||||
},
|
||||
"nasm": {
|
||||
"type": "target",
|
||||
"artifact": "nasm"
|
||||
"artifact": "nasm",
|
||||
"type": "target"
|
||||
},
|
||||
"php": {
|
||||
"artifact": "php-src",
|
||||
"depends@macos": [
|
||||
"libxml2"
|
||||
],
|
||||
"type": "target"
|
||||
},
|
||||
"php-cgi": {
|
||||
"depends": [
|
||||
"php"
|
||||
],
|
||||
"type": "virtual-target"
|
||||
},
|
||||
"php-cli": {
|
||||
"depends": [
|
||||
"php"
|
||||
],
|
||||
"type": "virtual-target"
|
||||
},
|
||||
"php-embed": {
|
||||
"depends": [
|
||||
"php"
|
||||
],
|
||||
"type": "virtual-target"
|
||||
},
|
||||
"php-fpm": {
|
||||
"depends": [
|
||||
"php"
|
||||
],
|
||||
"type": "virtual-target"
|
||||
},
|
||||
"php-micro": {
|
||||
"artifact": "micro",
|
||||
"depends": [
|
||||
"php"
|
||||
],
|
||||
"type": "virtual-target"
|
||||
},
|
||||
"php-sdk-binary-tools": {
|
||||
"type": "target",
|
||||
"artifact": "php-sdk-binary-tools"
|
||||
"artifact": "php-sdk-binary-tools",
|
||||
"type": "target"
|
||||
},
|
||||
"pkg-config": {
|
||||
"artifact": "pkg-config",
|
||||
"static-bins": [
|
||||
"pkg-config"
|
||||
],
|
||||
"type": "target"
|
||||
},
|
||||
"strawberry-perl": {
|
||||
"artifact": "strawberry-perl",
|
||||
"type": "target"
|
||||
},
|
||||
"upx": {
|
||||
"artifact": "upx",
|
||||
"type": "target"
|
||||
},
|
||||
"vswhere": {
|
||||
"artifact": "vswhere",
|
||||
"static-bins@windows": [
|
||||
"vswhere.exe"
|
||||
],
|
||||
"type": "target"
|
||||
},
|
||||
"zig": {
|
||||
"artifact": "zig",
|
||||
"type": "target"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
parameters:
|
||||
reportUnmatchedIgnoredErrors: false
|
||||
level: 4
|
||||
phpVersion: 80400
|
||||
paths:
|
||||
- ./src/
|
||||
ignoreErrors:
|
||||
|
||||
28
skeleton-test.php
Normal file
28
skeleton-test.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use StaticPHP\Runtime\Executor\UnixCMakeExecutor;
|
||||
use StaticPHP\Skeleton\ArtifactGenerator;
|
||||
use StaticPHP\Skeleton\ExecutorGenerator;
|
||||
use StaticPHP\Skeleton\PackageGenerator;
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
$package_generator = new PackageGenerator('foo', 'library')
|
||||
->addDependency('bar')
|
||||
->addStaticLib('libfoo.a', 'unix')
|
||||
->addStaticLib('libfoo.a', 'unix')
|
||||
->addArtifact($artifact_generator = new ArtifactGenerator('foo')->setSource(['type' => 'url', 'url' => 'https://example.com/foo.tar.gz']))
|
||||
->enableBuild(['Darwin', 'Linux'], 'build')
|
||||
->addFunctionExecutorBinding('build', new ExecutorGenerator(UnixCMakeExecutor::class));
|
||||
|
||||
$pkg_config = $package_generator->generateConfigArray();
|
||||
$artifact_config = $artifact_generator->generateConfigArray();
|
||||
|
||||
echo '===== pkg.json =====' . PHP_EOL;
|
||||
echo json_encode($pkg_config, 64 | 128 | 256) . PHP_EOL;
|
||||
echo '===== artifact.json =====' . PHP_EOL;
|
||||
echo json_encode($artifact_config, 64 | 128 | 256) . PHP_EOL;
|
||||
echo '===== php code for package =====' . PHP_EOL;
|
||||
echo $package_generator->generatePackageClassFile('Package\Library');
|
||||
@@ -14,7 +14,7 @@ use StaticPHP\Util\FileSystem;
|
||||
#[Library('imap')]
|
||||
class imap
|
||||
{
|
||||
#[AfterStage('php', [php::class, 'patchEmbedScripts'], 'imap')]
|
||||
#[AfterStage('php', [php::class, 'patchUnixEmbedScripts'], 'imap')]
|
||||
#[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')]
|
||||
public function afterPatchScripts(): void
|
||||
{
|
||||
|
||||
24
src/Package/Library/onig.php
Normal file
24
src/Package/Library/onig.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?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\WindowsCMakeExecutor;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
#[Library('onig')]
|
||||
class onig
|
||||
{
|
||||
#[BuildFor('Windows')]
|
||||
public function buildWin(LibraryPackage $package): void
|
||||
{
|
||||
WindowsCMakeExecutor::create($package)
|
||||
->addConfigureArgs('-DMSVC_STATIC_RUNTIME=ON')
|
||||
->build();
|
||||
FileSystem::copy("{$package->getLibDir()}\\onig.lib", "{$package->getLibDir()}\\onig_a.lib");
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,16 @@ declare(strict_types=1);
|
||||
|
||||
namespace Package\Target;
|
||||
|
||||
use Package\Target\php\unix;
|
||||
use Package\Target\php\windows;
|
||||
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;
|
||||
@@ -27,16 +25,11 @@ 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')]
|
||||
@@ -47,6 +40,9 @@ use ZM\Logger\ConsoleColor;
|
||||
#[Target('frankenphp')]
|
||||
class php extends TargetPackage
|
||||
{
|
||||
use unix;
|
||||
use windows;
|
||||
|
||||
public static function getPHPVersionID(): int
|
||||
{
|
||||
$artifact = ArtifactLoader::getArtifactInstance('php-src');
|
||||
@@ -241,337 +237,6 @@ class php extends TargetPackage
|
||||
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 = [];
|
||||
@@ -592,93 +257,4 @@ class php extends TargetPackage
|
||||
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)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
450
src/Package/Target/php/unix.php
Normal file
450
src/Package/Target/php/unix.php
Normal file
@@ -0,0 +1,450 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Target\php;
|
||||
|
||||
use StaticPHP\Attribute\Package\BeforeStage;
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\Stage;
|
||||
use StaticPHP\Attribute\PatchDescription;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCException;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Package\PackageBuilder;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Package\PhpExtensionPackage;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Toolchain\Interface\ToolchainInterface;
|
||||
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 ZM\Logger\ConsoleColor;
|
||||
|
||||
trait unix
|
||||
{
|
||||
#[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 patchUnixEmbedScripts(): void
|
||||
{
|
||||
// patch phpize
|
||||
if (file_exists(BUILD_BIN_PATH . '/phpize')) {
|
||||
logger()->debug('Patching phpize prefix');
|
||||
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'");
|
||||
FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#');
|
||||
$this->setOutput('phpize script path for embed SAPI', BUILD_BIN_PATH . '/phpize');
|
||||
}
|
||||
// patch php-config
|
||||
if (file_exists(BUILD_BIN_PATH . '/php-config')) {
|
||||
logger()->debug('Patching php-config prefix and libs order');
|
||||
$php_config_str = FileSystem::readFile(BUILD_BIN_PATH . '/php-config');
|
||||
$php_config_str = str_replace('prefix=""', 'prefix="' . BUILD_ROOT_PATH . '"', $php_config_str);
|
||||
// move mimalloc to the beginning of libs
|
||||
$php_config_str = preg_replace('/(libs=")(.*?)\s*(' . preg_quote(BUILD_LIB_PATH, '/') . '\/mimalloc\.o)\s*(.*?)"/', '$1$3 $2 $4"', $php_config_str);
|
||||
// move lstdc++ to the end of libs
|
||||
$php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str);
|
||||
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
|
||||
$this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Seek php-src/config.log when building PHP, add it to exception.
|
||||
*/
|
||||
protected function seekPhpSrcLogFileOnException(callable $callback, string $source_dir): void
|
||||
{
|
||||
try {
|
||||
$callback();
|
||||
} catch (SPCException $e) {
|
||||
if (file_exists("{$source_dir}/config.log")) {
|
||||
$e->addExtraLogFile('php-src config.log', 'php-src.config.log');
|
||||
copy("{$source_dir}/config.log", SPC_LOGS_DIR . '/php-src.config.log');
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename libphp.so to libphp-<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)
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
232
src/Package/Target/php/windows.php
Normal file
232
src/Package/Target/php/windows.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Package\Target\php;
|
||||
|
||||
use StaticPHP\Attribute\Package\BeforeStage;
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\Stage;
|
||||
use StaticPHP\Attribute\PatchDescription;
|
||||
use StaticPHP\Exception\PatchException;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Package\PackageBuilder;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\SourcePatcher;
|
||||
use StaticPHP\Util\System\WindowsUtil;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
trait windows
|
||||
{
|
||||
#[Stage]
|
||||
public function buildconfForWindows(TargetPackage $package): void
|
||||
{
|
||||
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf.bat'));
|
||||
V2CompatLayer::emitPatchPoint('before-php-buildconf');
|
||||
cmd()->cd($package->getSourceDir())->exec('.\buildconf.bat');
|
||||
}
|
||||
|
||||
#[Stage]
|
||||
public function configureForWindows(TargetPackage $package, PackageInstaller $installer): void
|
||||
{
|
||||
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure.bat'));
|
||||
V2CompatLayer::emitPatchPoint('before-php-configure');
|
||||
$args = [
|
||||
'--disable-all',
|
||||
"--with-php-build={$package->getBuildRootPath()}",
|
||||
"--with-extra-includes={$package->getIncludeDir()}",
|
||||
"--with-extra-libs={$package->getLibDir()}",
|
||||
];
|
||||
// sapis
|
||||
$cli = $installer->isPackageResolved('php-cli');
|
||||
$cgi = $installer->isPackageResolved('php-cgi');
|
||||
$micro = $installer->isPackageResolved('php-micro');
|
||||
$args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no';
|
||||
$args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no';
|
||||
$args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no';
|
||||
|
||||
// zts
|
||||
$args[] = $package->getBuildOption('enable-zts', false) ? '--enable-zts=yes' : '--enable-zts=no';
|
||||
// opcache-jit
|
||||
$args[] = !$package->getBuildOption('disable-opcache-jit', false) ? '--enable-opcache-jit=yes' : '--enable-opcache-jit=no';
|
||||
// micro win32
|
||||
if ($micro && $package->getBuildOption('enable-micro-win32', false)) {
|
||||
$args[] = '--enable-micro-win32=yes';
|
||||
}
|
||||
// config-file-scan-dir
|
||||
if ($option = $package->getBuildOption('with-config-file-scan-dir', false)) {
|
||||
$args[] = "--with-config-file-scan-dir={$option}";
|
||||
}
|
||||
// micro logo
|
||||
if ($micro && ($logo = $this->getBuildOption('with-micro-logo')) !== null) {
|
||||
$args[] = "--enable-micro-logo={$logo}";
|
||||
copy($logo, SOURCE_PATH . '\php-src\\' . $logo);
|
||||
}
|
||||
$args = implode(' ', $args);
|
||||
$static_extension_str = $this->makeStaticExtensionString($installer);
|
||||
cmd()->cd($package->getSourceDir())->exec(".\\configure.bat {$args} {$static_extension_str}");
|
||||
}
|
||||
|
||||
#[BeforeStage('php', [self::class, 'makeCliForWindows'])]
|
||||
#[PatchDescription('Patch Windows Makefile for CLI target')]
|
||||
public function patchCLITarget(TargetPackage $package): void
|
||||
{
|
||||
// search Makefile code line contains "$(BUILD_DIR)\php.exe:"
|
||||
$content = FileSystem::readFile("{$package->getSourceDir()}\\Makefile");
|
||||
$lines = explode("\r\n", $content);
|
||||
$line_num = 0;
|
||||
$found = false;
|
||||
foreach ($lines as $v) {
|
||||
if (str_contains($v, '$(BUILD_DIR)\php.exe:')) {
|
||||
$found = $line_num;
|
||||
break;
|
||||
}
|
||||
++$line_num;
|
||||
}
|
||||
if ($found === false) {
|
||||
throw new PatchException('Windows Makefile patching for php.exe target', 'Cannot patch windows CLI Makefile, Makefile does not contain "$(BUILD_DIR)\php.exe:" line');
|
||||
}
|
||||
$lines[$line_num] = '$(BUILD_DIR)\php.exe: generated_files $(DEPS_CLI) $(PHP_GLOBAL_OBJS) $(CLI_GLOBAL_OBJS) $(STATIC_EXT_OBJS) $(ASM_OBJS) $(BUILD_DIR)\php.exe.res $(BUILD_DIR)\php.exe.manifest';
|
||||
$lines[$line_num + 1] = "\t" . '"$(LINK)" /nologo $(PHP_GLOBAL_OBJS_RESP) $(CLI_GLOBAL_OBJS_RESP) $(STATIC_EXT_OBJS_RESP) $(STATIC_EXT_LIBS) $(ASM_OBJS) $(LIBS) $(LIBS_CLI) $(BUILD_DIR)\php.exe.res /out:$(BUILD_DIR)\php.exe $(LDFLAGS) $(LDFLAGS_CLI) /ltcg /nodefaultlib:msvcrt /nodefaultlib:msvcrtd /ignore:4286';
|
||||
FileSystem::writeFile("{$package->getSourceDir()}\\Makefile", implode("\r\n", $lines));
|
||||
}
|
||||
|
||||
#[Stage]
|
||||
public function makeCliForWindows(TargetPackage $package, PackageBuilder $builder): void
|
||||
{
|
||||
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('php.exe'));
|
||||
|
||||
// extra lib
|
||||
$extra_libs = getenv('SPC_EXTRA_LIBS') ?: '';
|
||||
|
||||
// Add debug symbols for release build if --no-strip is specified
|
||||
// We need to modify CFLAGS to replace /Ox with /Zi and add /DEBUG to LDFLAGS
|
||||
$debug_overrides = '';
|
||||
if ($package->getBuildOption('no-strip', false)) {
|
||||
// Read current CFLAGS from Makefile and replace optimization flags
|
||||
$makefile_content = file_get_contents("{$package->getSourceDir()}\\Makefile");
|
||||
if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) {
|
||||
$cflags = $matches[1];
|
||||
// Replace /Ox (full optimization) with /Zi (debug info) and /Od (disable optimization)
|
||||
// Keep optimization for speed: /O2 /Zi instead of /Od /Zi
|
||||
$cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags);
|
||||
$debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO" "LDFLAGS_CLI=/DEBUG" ';
|
||||
}
|
||||
}
|
||||
|
||||
cmd()->cd($package->getSourceDir())
|
||||
->exec("nmake /nologo {$debug_overrides}LIBS_CLI=\"ws2_32.lib shell32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php.exe");
|
||||
|
||||
$this->deployWindowsBinary($builder, $package, 'php-cli');
|
||||
}
|
||||
|
||||
#[Stage]
|
||||
public function makeForWindows(TargetPackage $package, PackageInstaller $installer): void
|
||||
{
|
||||
V2CompatLayer::emitPatchPoint('before-php-make');
|
||||
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('nmake clean'));
|
||||
cmd()->cd($package->getSourceDir())->exec('nmake clean');
|
||||
|
||||
if ($installer->isPackageResolved('php-cli')) {
|
||||
$package->runStage([$this, 'makeCliForWindows']);
|
||||
}
|
||||
if ($installer->isPackageResolved('php-cgi')) {
|
||||
$package->runStage([$this, 'makeCgiForWindows']);
|
||||
}
|
||||
if ($installer->isPackageResolved('php-micro')) {
|
||||
$package->runStage([$this, 'makeMicroForWindows']);
|
||||
}
|
||||
}
|
||||
|
||||
#[BuildFor('Windows')]
|
||||
public function buildWin(TargetPackage $package): void
|
||||
{
|
||||
if ($package->getName() !== 'php') {
|
||||
return;
|
||||
}
|
||||
|
||||
$package->runStage([$this, 'buildconfForWindows']);
|
||||
$package->runStage([$this, 'configureForWindows']);
|
||||
$package->runStage([$this, 'makeForWindows']);
|
||||
}
|
||||
|
||||
#[BeforeStage('php', [self::class, 'buildconfForWindows'])]
|
||||
#[PatchDescription('Patch SPC_MICRO_PATCHES defined patches')]
|
||||
#[PatchDescription('Fix PHP 8.1 static build bug on Windows')]
|
||||
#[PatchDescription('Fix PHP Visual Studio version detection')]
|
||||
public function patchBeforeBuildconfForWindows(TargetPackage $package): void
|
||||
{
|
||||
// php-src patches from micro
|
||||
SourcePatcher::patchPhpSrc();
|
||||
|
||||
// php 8.1 bug
|
||||
if ($this->getPHPVersionID() >= 80100 && $this->getPHPVersionID() < 80200) {
|
||||
logger()->info('Patching PHP 8.1 windows Fiber bug');
|
||||
FileSystem::replaceFileStr(
|
||||
"{$package->getSourceDir()}\\win32\\build\\config.w32",
|
||||
"ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');",
|
||||
"ADD_FLAG('ASM_OBJS', '$(BUILD_DIR)\\\\Zend\\\\jump_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj $(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');"
|
||||
);
|
||||
FileSystem::replaceFileStr(
|
||||
"{$package->getSourceDir()}\\win32\\build\\config.w32",
|
||||
"ADD_FLAG('LDFLAGS', '$(BUILD_DIR)\\\\Zend\\\\make_' + FIBER_ASM_ARCH + '_ms_pe_masm.obj');",
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
// Fix PHP VS version
|
||||
// get vs version
|
||||
$vc = WindowsUtil::findVisualStudio();
|
||||
if ($vc === false) {
|
||||
$vc_matches = ['unknown', 'unknown'];
|
||||
} else {
|
||||
$vc_matches = match ($vc['major_version']) {
|
||||
'17' => ['VS17', 'Visual C++ 2022'],
|
||||
'16' => ['VS16', 'Visual C++ 2019'],
|
||||
default => ['unknown', 'unknown'],
|
||||
};
|
||||
}
|
||||
// patch php-src/win32/build/confutils.js
|
||||
FileSystem::replaceFileStr(
|
||||
"{$package->getSourceDir()}\\win32\\build\\confutils.js",
|
||||
'var name = "unknown";',
|
||||
"var name = short ? \"{$vc_matches[0]}\" : \"{$vc_matches[1]}\";return name;"
|
||||
);
|
||||
|
||||
// patch micro win32
|
||||
if ($package->getBuildOption('enable-micro-win32') && !file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) {
|
||||
copy("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", "{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak");
|
||||
FileSystem::replaceFileStr("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c", '#include "php_variables.h"', '#include "php_variables.h"' . "\n#define PHP_MICRO_WIN32_NO_CONSOLE 1");
|
||||
} else {
|
||||
if (file_exists("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak")) {
|
||||
rename("{$package->getSourceDir()}\\sapi\\micro\\php_micro.c.win32bak", "{$package->getSourceDir()}\\sapi\\micro\\php_micro.c");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function deployWindowsBinary(PackageBuilder $builder, TargetPackage $package, string $sapi): void
|
||||
{
|
||||
$rel_type = 'Release'; // TODO: Debug build support
|
||||
$ts = $builder->getOption('enable-zts') ? '_TS' : '';
|
||||
$debug_dir = BUILD_ROOT_PATH . '\debug';
|
||||
$src = match ($sapi) {
|
||||
'php-cli' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'php.exe', 'php.pdb'],
|
||||
'php-micro' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'micro.sfx', 'micro.pdb'],
|
||||
'php-cgi' => ["{$package->getSourceDir()}\\x64\\{$rel_type}{$ts}", 'php-cgi.exe', 'php-cgi.pdb'],
|
||||
default => throw new SPCInternalException("Deployment does not accept type {$sapi}"),
|
||||
};
|
||||
$src_file = "{$src[0]}\\{$src[1]}";
|
||||
$dst_file = BUILD_BIN_PATH . '\\' . basename($src_file);
|
||||
|
||||
$builder->deployBinary($src_file, $dst_file);
|
||||
|
||||
// make debug info file path
|
||||
if ($builder->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) {
|
||||
FileSystem::copy("{$src[0]}\\{$src[2]}", "{$debug_dir}\\{$src[2]}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,11 +167,22 @@ class Artifact
|
||||
return false;
|
||||
}
|
||||
|
||||
// For standalone mode, check directory and hash
|
||||
// For standalone mode, check directory or file and hash
|
||||
$target_path = $extract_config['path'];
|
||||
|
||||
if (!is_dir($target_path)) {
|
||||
return false;
|
||||
// Check if target is a file or directory
|
||||
$is_file_target = !is_dir($target_path) && (pathinfo($target_path, PATHINFO_EXTENSION) !== '');
|
||||
|
||||
if ($is_file_target) {
|
||||
// For single file extraction (e.g., vswhere.exe)
|
||||
if (!file_exists($target_path)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// For directory extraction
|
||||
if (!is_dir($target_path)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$compare_hash) {
|
||||
@@ -320,7 +331,7 @@ class Artifact
|
||||
* For merge mode, returns the base path.
|
||||
* For standalone mode, returns the specific directory.
|
||||
*/
|
||||
public function getBinaryDir(): string
|
||||
public function getBinaryDir(): ?string
|
||||
{
|
||||
$config = $this->getBinaryExtractConfig();
|
||||
return $config['path'];
|
||||
|
||||
@@ -329,8 +329,7 @@ class ArtifactDownloader
|
||||
}
|
||||
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;
|
||||
InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}\n", true);
|
||||
}
|
||||
}
|
||||
} catch (SPCException $e) {
|
||||
|
||||
@@ -293,7 +293,7 @@ class ArtifactExtractor
|
||||
// Process file mappings
|
||||
foreach ($file_map as $src_pattern => $dst_path) {
|
||||
$dst_path = $this->replacePathVariables($dst_path);
|
||||
$src_full = "{$temp_path}/{$src_pattern}";
|
||||
$src_full = FileSystem::convertPath("{$temp_path}/{$src_pattern}");
|
||||
|
||||
// Handle glob patterns
|
||||
if (str_contains($src_pattern, '*')) {
|
||||
@@ -460,40 +460,36 @@ class ArtifactExtractor
|
||||
$target = FileSystem::convertPath($target);
|
||||
$filename = FileSystem::convertPath($filename);
|
||||
|
||||
FileSystem::createDir($target);
|
||||
$extname = FileSystem::extname($filename);
|
||||
|
||||
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}");
|
||||
if ($extname !== 'exe' && !is_dir($target)) {
|
||||
FileSystem::createDir($target);
|
||||
}
|
||||
match (SystemTarget::getTargetOS()) {
|
||||
'Windows' => match ($extname) {
|
||||
'tar' => default_shell()->executeTarExtract($filename, $target, 'none'),
|
||||
'xz', 'txz', 'gz', 'tgz', 'bz2' => default_shell()->execute7zExtract($filename, $target),
|
||||
'zip' => $this->unzipWithStrip($filename, $target),
|
||||
'exe' => $this->copyFile($filename, $target),
|
||||
default => throw new FileSystemException("Unknown archive format: {$filename}"),
|
||||
},
|
||||
'Linux', 'Darwin' => match ($extname) {
|
||||
'tar' => default_shell()->executeTarExtract($filename, $target, 'none'),
|
||||
'gz', 'tgz' => default_shell()->executeTarExtract($filename, $target, 'gz'),
|
||||
'bz2' => default_shell()->executeTarExtract($filename, $target, 'bz2'),
|
||||
'xz', 'txz' => default_shell()->executeTarExtract($filename, $target, 'xz'),
|
||||
'zip' => $this->unzipWithStrip($filename, $target),
|
||||
'exe' => $this->copyFile($filename, $target),
|
||||
default => throw new FileSystemException("Unknown archive format: {$filename}"),
|
||||
},
|
||||
default => throw new SPCInternalException('Unsupported OS for archive extraction')
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzip file with stripping top-level directory.
|
||||
*/
|
||||
protected function unzipWithStrip(string $zip_file, string $extract_path): void
|
||||
protected function unzipWithStrip(string $zip_file, string $extract_path): bool
|
||||
{
|
||||
$temp_dir = FileSystem::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16)));
|
||||
$zip_file = FileSystem::convertPath($zip_file);
|
||||
@@ -572,6 +568,8 @@ class ArtifactExtractor
|
||||
|
||||
// Clean up temp directory
|
||||
FileSystem::removeDir($temp_dir);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -585,6 +583,7 @@ class ArtifactExtractor
|
||||
'{source_path}' => SOURCE_PATH,
|
||||
'{download_path}' => DOWNLOAD_PATH,
|
||||
'{working_dir}' => WORKING_DIR,
|
||||
'{php_sdk_path}' => getenv('PHP_SDK_PATH') ?: '',
|
||||
];
|
||||
return str_replace(array_keys($replacement), array_values($replacement), $path);
|
||||
}
|
||||
@@ -627,9 +626,9 @@ class ArtifactExtractor
|
||||
}
|
||||
}
|
||||
|
||||
private function copyFile(string $source_file, string $target_path): void
|
||||
private function copyFile(string $source_file, string $target_path): bool
|
||||
{
|
||||
FileSystem::createDir(dirname($target_path));
|
||||
FileSystem::copy(FileSystem::convertPath($source_file), $target_path);
|
||||
return FileSystem::copy(FileSystem::convertPath($source_file), $target_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ class DownloadResult
|
||||
) {
|
||||
switch ($this->cache_type) {
|
||||
case 'archive':
|
||||
$this->filename !== null ?: throw new DownloaderException('Archive download result must have a filename.');
|
||||
case 'file':
|
||||
$this->filename !== null ?: throw new DownloaderException('Archive/file download result must have a filename.');
|
||||
$fn = FileSystem::isRelativePath($this->filename) ? (DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $this->filename) : $this->filename;
|
||||
file_exists($fn) ?: throw new DownloaderException("Downloaded archive file does not exist: {$fn}");
|
||||
break;
|
||||
@@ -60,7 +61,20 @@ class DownloadResult
|
||||
?string $version = null,
|
||||
array $metadata = []
|
||||
): DownloadResult {
|
||||
return new self('archive', config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata);
|
||||
// judge if it is archive or just a pure file
|
||||
$cache_type = self::isArchiveFile($filename) ? 'archive' : 'file';
|
||||
return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata);
|
||||
}
|
||||
|
||||
public static function file(
|
||||
string $filename,
|
||||
array $config,
|
||||
bool $verified = false,
|
||||
?string $version = null,
|
||||
array $metadata = []
|
||||
): DownloadResult {
|
||||
$cache_type = self::isArchiveFile($filename) ? 'archive' : 'file';
|
||||
return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,4 +157,16 @@ class DownloadResult
|
||||
array_merge($this->metadata, [$key => $value])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check
|
||||
*/
|
||||
private static function isArchiveFile(string $filename): bool
|
||||
{
|
||||
$archive_extensions = [
|
||||
'zip', 'tar', 'tar.gz', 'tgz', 'tar.bz2', 'tbz2', 'tar.xz', 'txz', 'rar', '7z',
|
||||
];
|
||||
$lower_filename = strtolower($filename);
|
||||
return array_any($archive_extensions, fn ($ext) => str_ends_with($lower_filename, '.' . $ext));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ namespace StaticPHP\Artifact\Downloader\Type;
|
||||
|
||||
use StaticPHP\Artifact\ArtifactDownloader;
|
||||
use StaticPHP\Artifact\Downloader\DownloadResult;
|
||||
use StaticPHP\Exception\DownloaderException;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
/** git */
|
||||
class Git implements DownloadTypeInterface
|
||||
@@ -15,8 +17,55 @@ class Git implements DownloadTypeInterface
|
||||
$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);
|
||||
|
||||
// direct branch clone
|
||||
if (isset($config['rev'])) {
|
||||
default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null);
|
||||
$version = "dev-{$config['rev']}";
|
||||
return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version);
|
||||
}
|
||||
if (!isset($config['regex'])) {
|
||||
throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.');
|
||||
}
|
||||
|
||||
// regex matches branch first, we need to fetch all refs in emptyfirst
|
||||
$gitdir = sys_get_temp_dir() . '/' . $name;
|
||||
FileSystem::resetDir($gitdir);
|
||||
$shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false);
|
||||
$result = $shell->cd($gitdir)
|
||||
->exec(SPC_GIT_EXEC . ' init')
|
||||
->exec(SPC_GIT_EXEC . ' remote add origin ' . escapeshellarg($config['url']))
|
||||
->execWithResult(SPC_GIT_EXEC . ' ls-remote origin');
|
||||
if ($result[0] !== 0) {
|
||||
throw new DownloaderException("Failed to ls-remote from {$config['url']}");
|
||||
}
|
||||
$refs = $result[1];
|
||||
$matched_version_branch = [];
|
||||
$matched_count = 0;
|
||||
|
||||
$regex = '/^' . $config['regex'] . '$/';
|
||||
foreach ($refs as $ref) {
|
||||
$matches = null;
|
||||
if (preg_match('/^[0-9a-f]{40}\s+refs\/heads\/(.+)$/', $ref, $matches)) {
|
||||
++$matched_count;
|
||||
$branch = $matches[1];
|
||||
if (preg_match($regex, $branch, $vermatch) && isset($vermatch['version'])) {
|
||||
$matched_version_branch[$vermatch['version']] = $vermatch[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
// sort versions
|
||||
uksort($matched_version_branch, function ($a, $b) {
|
||||
return version_compare($b, $a);
|
||||
});
|
||||
if (!empty($matched_version_branch)) {
|
||||
// use the highest version
|
||||
$version = array_key_first($matched_version_branch);
|
||||
$branch = $matched_version_branch[$version];
|
||||
logger()->info("Matched version {$version} from branch {$branch} for {$name}");
|
||||
default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null);
|
||||
return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version);
|
||||
}
|
||||
throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches).");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ abstract class BaseCommand extends Command
|
||||
\___ \| __/ _` | __| |/ __| |_) | |_| | |_) |
|
||||
___) | || (_| | |_| | (__| __/| _ | __/
|
||||
|____/ \__\__,_|\__|_|\___|_| |_| |_|_| {version}
|
||||
|
||||
';
|
||||
|
||||
protected bool $no_motd = false;
|
||||
@@ -71,7 +70,7 @@ abstract class BaseCommand extends Command
|
||||
$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;
|
||||
$this->output->writeln($this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +93,7 @@ abstract class BaseCommand extends Command
|
||||
}
|
||||
|
||||
// Set debug mode in ApplicationContext
|
||||
$isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE;
|
||||
$isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG;
|
||||
ApplicationContext::setDebug($isDebug);
|
||||
|
||||
// show raw argv list for logger()->debug
|
||||
|
||||
43
src/StaticPHP/Command/Dev/EnvCommand.php
Normal file
43
src/StaticPHP/Command/Dev/EnvCommand.php
Normal file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command\Dev;
|
||||
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand('dev:env', 'Returns the internally defined environment variables')]
|
||||
class EnvCommand extends BaseCommand
|
||||
{
|
||||
public function configure(): void
|
||||
{
|
||||
$this->addArgument('env', InputArgument::OPTIONAL, 'The environment variable to show, if not set, all will be shown');
|
||||
}
|
||||
|
||||
public function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->no_motd = true;
|
||||
parent::initialize($input, $output);
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$env = $this->getArgument('env');
|
||||
if (($val = getenv($env)) === false) {
|
||||
$this->output->writeln("<error>Environment variable '{$env}' is not set.</error>");
|
||||
return static::FAILURE;
|
||||
}
|
||||
if (is_array($val)) {
|
||||
foreach ($val as $k => $v) {
|
||||
$this->output->writeln("<info>{$k}={$v}</info>");
|
||||
}
|
||||
return static::SUCCESS;
|
||||
}
|
||||
$this->output->writeln("<info>{$val}</info>");
|
||||
return static::SUCCESS;
|
||||
}
|
||||
}
|
||||
402
src/StaticPHP/Command/Dev/SkeletonCommand.php
Normal file
402
src/StaticPHP/Command/Dev/SkeletonCommand.php
Normal file
@@ -0,0 +1,402 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command\Dev;
|
||||
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Registry\Registry;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Skeleton\ArtifactGenerator;
|
||||
use StaticPHP\Skeleton\PackageGenerator;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\select;
|
||||
use function Laravel\Prompts\text;
|
||||
|
||||
#[AsCommand('dev:skel', 'Generate a skeleton for a StaticPHP package')]
|
||||
class SkeletonCommand extends BaseCommand
|
||||
{
|
||||
public function handle(): int
|
||||
{
|
||||
// Only available for non-Windows systems for now
|
||||
// Only available when spc loading mode is SPC_MODE_VENDOR, SPC_MODE_SOURCE
|
||||
if (spc_mode(SPC_MODE_PHAR)) {
|
||||
$this->output->writeln('<error>The dev:skel command is not available in phar mode.</error>');
|
||||
return 1;
|
||||
}
|
||||
if (SystemTarget::getTargetOS() === 'Windows') {
|
||||
$this->output->writeln('<error>The dev:skel command is not available on Windows systems.</error>');
|
||||
return 1;
|
||||
}
|
||||
|
||||
$this->runMainMenu();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public function validatePackageName(string $name): ?string
|
||||
{
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
|
||||
return 'Library name can only contain letters, numbers, underscores, and hyphens.';
|
||||
}
|
||||
// must start with a letter
|
||||
if (!preg_match('/^[a-zA-Z]/', $name)) {
|
||||
return 'Library name must start with a letter.';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function runMainMenu(): void
|
||||
{
|
||||
$main = select('Please select the skeleton option', [
|
||||
'library' => 'Create a new library package',
|
||||
'target' => 'Create a new target package',
|
||||
'php-extension' => 'Create a new PHP extension',
|
||||
'q' => 'Exit',
|
||||
]);
|
||||
$generator = match ($main) {
|
||||
'library' => $this->runCreateLib(),
|
||||
'target' => $this->runCreateTarget(),
|
||||
'php-extension' => $this->runCreateExt(),
|
||||
'q' => exit(0),
|
||||
default => null,
|
||||
};
|
||||
$write = $generator->writeAll();
|
||||
$this->output->writeln("<info>Package config in: {$write['package_config']}</info>");
|
||||
$this->output->writeln("<info>Artifact config in: {$write['artifact_config']}</info>");
|
||||
$this->output->writeln('<comment>Package class:</comment>');
|
||||
$this->output->writeln($write['package_class_content']);
|
||||
}
|
||||
|
||||
private function runCreateLib(): PackageGenerator
|
||||
{
|
||||
// init empty
|
||||
$static_libs = '';
|
||||
$headers = '';
|
||||
$static_bins = '';
|
||||
$pkg_configs = '';
|
||||
|
||||
// ask name
|
||||
$package_name = text('Please enter your library name', placeholder: 'e.g. pcre2', validate: [$this, 'validatePackageName']);
|
||||
|
||||
// ask OS
|
||||
$os = select("[{$package_name}] On which OS family do you want to build this library?", [
|
||||
'unix' => 'Both Linux and Darwin (unix-like OS)',
|
||||
'linux' => 'Linux only',
|
||||
'macos' => 'Darwin(macOS) only',
|
||||
'windows' => 'Windows only',
|
||||
'all' => 'All supported OS (' . implode(', ', SUPPORTED_OS_FAMILY) . ')',
|
||||
]);
|
||||
|
||||
$produce = select("[{$package_name}] What does this library produce?", [
|
||||
'static_libs' => 'Static Libraries (.a/.lib)',
|
||||
'headers' => 'Header Files (.h)',
|
||||
'static_bins' => 'Static Binaries (executables)',
|
||||
'pkg_configs' => 'Pkg-Config files (.pc)',
|
||||
'all' => 'All of the above',
|
||||
]);
|
||||
|
||||
if ($produce === 'all' || $produce === 'static_libs') {
|
||||
$static_libs = text(
|
||||
'Please enter the names of the static libraries produced',
|
||||
placeholder: 'e.g. libpcre2.a, libbar.a',
|
||||
default: str_starts_with($package_name, 'lib') ? "{$package_name}.a" : "lib{$package_name}.a",
|
||||
validate: function ($value) {
|
||||
$names = array_map('trim', explode(',', $value));
|
||||
if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) {
|
||||
return 'Library names can only contain letters, numbers, underscores, hyphens, and dots.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hint: 'Separate multiple names with commas'
|
||||
);
|
||||
}
|
||||
if ($produce === 'all' || $produce === 'headers') {
|
||||
$headers = text(
|
||||
'Please enter the names of the header files produced',
|
||||
placeholder: 'e.g. foo.h, bar.h',
|
||||
default: str_starts_with($package_name, 'lib') ? str_replace('lib', '', $package_name) . '.h' : $package_name . '.h',
|
||||
validate: function ($value) {
|
||||
$names = array_map('trim', explode(',', $value));
|
||||
if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) {
|
||||
return 'Header file names can only contain letters, numbers, underscores, hyphens, and dots.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hint: 'Separate multiple names with commas, directories are allowed (e.g. openssl directory)'
|
||||
);
|
||||
}
|
||||
if ($produce === 'all' || $produce === 'static_bins') {
|
||||
$static_bins = text(
|
||||
'Please enter the names of the static binaries produced',
|
||||
placeholder: 'e.g. foo, bar',
|
||||
default: $package_name,
|
||||
validate: function ($value) {
|
||||
$names = array_map('trim', explode(',', $value));
|
||||
if (array_any($names, fn ($name) => !preg_match('/^[a-zA-Z0-9_.-]+$/', $name))) {
|
||||
return 'Binary names can only contain letters, numbers, underscores, hyphens, and dots.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hint: 'Separate multiple names with commas'
|
||||
);
|
||||
}
|
||||
if ($produce === 'all' || $produce === 'pkg_configs') {
|
||||
$pkg_configs = text(
|
||||
'Please enter the names of the pkg-config files produced',
|
||||
placeholder: 'e.g. foo.pc, bar.pc',
|
||||
default: str_starts_with($package_name, 'lib') ? str_replace('lib', '', $package_name) . '.pc' : $package_name . '.pc',
|
||||
validate: function ($value) {
|
||||
if (!str_ends_with($value, '.pc')) {
|
||||
return 'Pkg-config file names must end with .pc extension.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hint: 'Separate multiple names with commas'
|
||||
);
|
||||
}
|
||||
|
||||
if ($headers === '' && $static_bins === '' && $static_libs === '' && $pkg_configs === '') {
|
||||
$this->output->writeln('<error>You must specify at least one of static libraries, header files, or static binaries produced.</error>');
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// ask source
|
||||
$artifact_generator = $this->runCreateArtifact($package_name, true, false, null);
|
||||
$package_generator = new PackageGenerator($package_name, 'library');
|
||||
// set artifact
|
||||
$package_generator = $package_generator->addArtifact($artifact_generator);
|
||||
// set os
|
||||
$package_generator = match ($os) {
|
||||
'unix' => $package_generator->enableBuild(['Darwin', 'Linux'], 'build'),
|
||||
'linux' => $package_generator->enableBuild(['Linux'], 'build'),
|
||||
'macos' => $package_generator->enableBuild(['Darwin'], 'build'),
|
||||
'windows' => $package_generator->enableBuild(['Windows'], 'build'),
|
||||
'all' => $package_generator->enableBuild(SUPPORTED_OS_FAMILY, 'build'),
|
||||
default => $package_generator,
|
||||
};
|
||||
// set produce
|
||||
if ($static_libs !== '') {
|
||||
$lib_names = array_map('trim', explode(',', $static_libs));
|
||||
foreach ($lib_names as $lib_name) {
|
||||
$package_generator = $package_generator->addStaticLib($lib_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os));
|
||||
}
|
||||
}
|
||||
if ($headers !== '') {
|
||||
$header_names = array_map('trim', explode(',', $headers));
|
||||
foreach ($header_names as $header_name) {
|
||||
$package_generator = $package_generator->addHeaderFile($header_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os));
|
||||
}
|
||||
}
|
||||
if ($static_bins !== '') {
|
||||
$bin_names = array_map('trim', explode(',', $static_bins));
|
||||
foreach ($bin_names as $bin_name) {
|
||||
$package_generator = $package_generator->addStaticBin($bin_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os));
|
||||
}
|
||||
}
|
||||
if ($pkg_configs !== '') {
|
||||
$pc_names = array_map('trim', explode(',', $pkg_configs));
|
||||
foreach ($pc_names as $pc_name) {
|
||||
$package_generator = $package_generator->addPkgConfigFile($pc_name, $os === 'all' ? 'all' : ($os === 'unix' ? 'unix' : $os));
|
||||
}
|
||||
}
|
||||
// ask for package config writing selection, same as artifact
|
||||
$package_configs = Registry::getLoadedPackageConfigs();
|
||||
$package_config_file = select("[{$package_name}] Please select the package config file to write the package config to", $package_configs);
|
||||
return $package_generator->setConfigFile($package_config_file);
|
||||
}
|
||||
|
||||
private function runCreateArtifact(
|
||||
string $package_name,
|
||||
?bool $create_source,
|
||||
?bool $create_binary,
|
||||
string|true|null $default_extract_dir = true
|
||||
): ArtifactGenerator {
|
||||
$artifact = new ArtifactGenerator($package_name);
|
||||
|
||||
if ($create_source === null) {
|
||||
$create_source = confirm("[{$package_name}] Do you want to create a source artifact?");
|
||||
}
|
||||
|
||||
if (!$create_source) {
|
||||
goto binary;
|
||||
}
|
||||
|
||||
$source_type = select("[{$package_name}] Where is the source code located?", SPC_DOWNLOAD_TYPE_DISPLAY_NAME);
|
||||
|
||||
$source_config = $this->askDownloadTypeConfig($package_name, $source_type, $default_extract_dir, 'source');
|
||||
$artifact = $artifact->setSource($source_config);
|
||||
|
||||
binary:
|
||||
if ($create_binary === null) {
|
||||
$create_binary = confirm("[{$package_name}] Do you want to create a binary artifact?");
|
||||
}
|
||||
|
||||
if (!$create_binary) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
$binary_fix = [
|
||||
'macos-x86_64' => null,
|
||||
'macos-aarch64' => null,
|
||||
'linux-x86_64' => null,
|
||||
'linux-aarch64' => null,
|
||||
'windows-x86_64' => null,
|
||||
];
|
||||
while (($os = select("[{$package_name}] Please configure the binary downloading options for OS", [
|
||||
'macos-x86_64' => 'macos-x86_64' . ($binary_fix['macos-x86_64'] ? ' (done)' : ''),
|
||||
'macos-aarch64' => 'macos-aarch64' . ($binary_fix['macos-aarch64'] ? ' (done)' : ''),
|
||||
'linux-x86_64' => 'linux-x86_64' . ($binary_fix['linux-x86_64'] ? ' (done)' : ''),
|
||||
'linux-aarch64' => 'linux-aarch64' . ($binary_fix['linux-aarch64'] ? ' (done)' : ''),
|
||||
'windows-x86_64' => 'windows-x86_64' . ($binary_fix['windows-x86_64'] ? ' (done)' : ''),
|
||||
'copy' => 'Duplicate from another OS',
|
||||
'finish' => 'Submit',
|
||||
])) !== 'finish') {
|
||||
$source_type = select("[{$package_name}] Where is the binary for {$os} located?", SPC_DOWNLOAD_TYPE_DISPLAY_NAME);
|
||||
$source_config = $this->askDownloadTypeConfig($package_name, $source_type, $default_extract_dir, 'binary');
|
||||
// set to artifact
|
||||
$artifact = $artifact->setBinary($os, $source_config);
|
||||
$binary_fix[$os] = true;
|
||||
}
|
||||
|
||||
end:
|
||||
|
||||
// generate config files, select existing package config file to write
|
||||
$artifact_configs = Registry::getLoadedArtifactConfigs();
|
||||
$artifact_config_file = select("[{$package_name}] Please select the artifact config file to write the artifact config to", $artifact_configs);
|
||||
return $artifact->setConfigFile($artifact_config_file);
|
||||
}
|
||||
|
||||
private function runCreateTarget(): PackageGenerator
|
||||
{
|
||||
throw new WrongUsageException('Not implemented');
|
||||
}
|
||||
|
||||
private function runCreateExt(): PackageGenerator
|
||||
{
|
||||
throw new WrongUsageException('Not implemented');
|
||||
}
|
||||
|
||||
private function askDownloadTypeConfig(string $package_name, int|string $source_type, bool|string|null $default_extract_dir, string $config_type): array
|
||||
{
|
||||
$source_config = ['type' => $source_type];
|
||||
switch ($source_type) {
|
||||
case 'bitbuckettag':
|
||||
$source_config['repo'] = text("[{$package_name}] Please enter the BitBucket repository (e.g. user/repo)");
|
||||
break;
|
||||
case 'filelist':
|
||||
$source_config['url'] = text(
|
||||
"[{$package_name}] Please enter the file index website URL",
|
||||
placeholder: 'e.g. https://ftp.gnu.org/pub/gnu/gettext/',
|
||||
hint: 'Make sure the target url is a directory listing page like ftp.gnu.org.'
|
||||
);
|
||||
$source_config['regex'] = text(
|
||||
"[{$package_name}] Please enter the regex pattern to match the archive file",
|
||||
placeholder: 'e.g. /gettext-(\d+\.\d+(\.\d+)?)\.tar\.gz/',
|
||||
default: "/href=\"(?<file>{$package_name}-(?<version>[^\"]+)\\.tar\\.gz)\"/",
|
||||
hint: 'Make sure the regex contains a capturing group for the version number.'
|
||||
);
|
||||
break;
|
||||
case 'git':
|
||||
$source_config['url'] = text(
|
||||
"[{$package_name}] Please enter the Git repository URL",
|
||||
validate: function ($value) {
|
||||
if (!filter_var($value, FILTER_VALIDATE_URL) && !preg_match('/^(git|ssh|http|https|git@[-\w.]+):(\/\/)?(.*?)(\.git)(\/?|#[-\d\w._]+?)$/', $value)) {
|
||||
return 'Please enter a valid Git repository URL.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hint: 'e.g. https://github.com/user/repo.git'
|
||||
);
|
||||
$source_config['rev'] = text(
|
||||
"[{$package_name}] Please enter the Git revision (branch, tag, or commit hash)",
|
||||
default: 'main',
|
||||
hint: 'e.g. main, master, v1.0.0, or a commit hash'
|
||||
);
|
||||
break;
|
||||
case 'ghrel':
|
||||
$source_config['repo'] = text("[{$package_name}] Please enter the GitHub repository (e.g. user/repo)");
|
||||
$source_config['match'] = text(
|
||||
"[{$package_name}] Please enter the regex pattern to match the source archive file",
|
||||
placeholder: 'e.g. /foo-(\d+\.\d+(\.\d+)?)\.tar\.gz/',
|
||||
default: "{$package_name}-.+\\.tar\\.gz",
|
||||
);
|
||||
break;
|
||||
case 'ghtar':
|
||||
case 'ghtagtar':
|
||||
$source_config['repo'] = text("[{$package_name}] Please enter the GitHub repository (e.g. user/repo)");
|
||||
$source_config['prefer-stable'] = confirm("[{$package_name}] Do you want to prefer stable releases?");
|
||||
if ($source_type === 'ghtagtar' && confirm('Do you want to match tags with a specific pattern?', default: false)) {
|
||||
$source_config['match'] = text(
|
||||
"[{$package_name}] Please enter the regex pattern to match tags",
|
||||
placeholder: 'e.g. v(\d+\.\d+(\.\d+)?)',
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'local':
|
||||
$source_config['dirname'] = text(
|
||||
"[{$package_name}] Please enter the local directory path",
|
||||
validate: function ($value) {
|
||||
if (trim($value) === '') {
|
||||
return 'Local source directory cannot be empty.';
|
||||
}
|
||||
if (!is_dir($value)) {
|
||||
return 'The specified local source directory does not exist.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'pie':
|
||||
$source_config['repo'] = text(
|
||||
"[{$package_name}] Please enter the PIE repository name",
|
||||
placeholder: 'e.g. user/repo',
|
||||
);
|
||||
break;
|
||||
case 'url':
|
||||
$source_config['url'] = text(
|
||||
"[{$package_name}] Please enter the file download URL",
|
||||
validate: function ($value) {
|
||||
if (!filter_var($value, FILTER_VALIDATE_URL)) {
|
||||
return 'Please enter a valid URL.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
break;
|
||||
case 'custom':
|
||||
break;
|
||||
}
|
||||
// ask extract dir if is true
|
||||
if ($default_extract_dir === true) {
|
||||
if (confirm('Do you want to specify a custom extract directory?')) {
|
||||
$extract_hint = match ($config_type) {
|
||||
'source' => 'the source will be from the `source/` dir by default',
|
||||
'binary' => 'the binary will be from the `pkgroot/{arch}-{os}/` dir by default',
|
||||
default => '',
|
||||
};
|
||||
$default_extract_dir = text(
|
||||
"[{$package_name}] Please enter the source extract directory",
|
||||
validate: function ($value) {
|
||||
if (trim($value) === '') {
|
||||
return 'Extract directory cannot be empty.';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hint: 'You can use relative path, ' . $extract_hint . '.'
|
||||
);
|
||||
} else {
|
||||
$default_extract_dir = null;
|
||||
}
|
||||
}
|
||||
if ($default_extract_dir !== null) {
|
||||
$source_config['extract'] = $default_extract_dir;
|
||||
}
|
||||
|
||||
// return config
|
||||
return $source_config;
|
||||
}
|
||||
}
|
||||
49
src/StaticPHP/Command/Dev/SortConfigCommand.php
Normal file
49
src/StaticPHP/Command/Dev/SortConfigCommand.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Command\Dev;
|
||||
|
||||
use StaticPHP\Command\BaseCommand;
|
||||
use StaticPHP\Registry\Registry;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
#[AsCommand('dev:sort-config', 'Sort artifact configuration files alphabetically')]
|
||||
class SortConfigCommand extends BaseCommand
|
||||
{
|
||||
public function handle(): int
|
||||
{
|
||||
// get loaded configs
|
||||
$loded_configs = Registry::getLoadedArtifactConfigs();
|
||||
foreach ($loded_configs as $file) {
|
||||
$this->sortConfigFile($file);
|
||||
}
|
||||
$loaded_pkg_configs = Registry::getLoadedPackageConfigs();
|
||||
foreach ($loaded_pkg_configs as $file) {
|
||||
$this->sortConfigFile($file);
|
||||
}
|
||||
return static::SUCCESS;
|
||||
}
|
||||
|
||||
private function sortConfigFile(mixed $file): void
|
||||
{
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
$this->output->writeln("Failed to read artifact config file: {$file}");
|
||||
return;
|
||||
}
|
||||
$data = json_decode($content, true);
|
||||
if (!is_array($data)) {
|
||||
$this->output->writeln("Invalid JSON format in artifact config file: {$file}");
|
||||
return;
|
||||
}
|
||||
ksort($data);
|
||||
foreach ($data as $artifact_name => &$config) {
|
||||
ksort($config);
|
||||
}
|
||||
unset($config);
|
||||
$new_content = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
|
||||
file_put_contents($file, $new_content);
|
||||
$this->output->writeln("Sorted artifact config file: {$file}");
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ class DoctorCommand extends BaseCommand
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
f_putenv('SPC_SKIP_TOOLCHAIN_CHECK=yes');
|
||||
$fix_policy = match ($this->input->getOption('auto-fix')) {
|
||||
'never' => FIX_POLICY_DIE,
|
||||
true, null => FIX_POLICY_AUTOFIX,
|
||||
|
||||
@@ -5,33 +5,38 @@ declare(strict_types=1);
|
||||
namespace StaticPHP\Config;
|
||||
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Registry\Registry;
|
||||
|
||||
class ArtifactConfig
|
||||
{
|
||||
private static array $artifact_configs = [];
|
||||
|
||||
public static function loadFromDir(string $dir): void
|
||||
public static function loadFromDir(string $dir, string $registry_name): array
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
throw new WrongUsageException("Directory {$dir} does not exist, cannot load artifact config.");
|
||||
}
|
||||
$loaded = [];
|
||||
$files = glob("{$dir}/artifact.*.json");
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $file) {
|
||||
self::loadFromFile($file);
|
||||
self::loadFromFile($file, $registry_name);
|
||||
$loaded[] = $file;
|
||||
}
|
||||
}
|
||||
if (file_exists("{$dir}/artifact.json")) {
|
||||
self::loadFromFile("{$dir}/artifact.json");
|
||||
self::loadFromFile("{$dir}/artifact.json", $registry_name);
|
||||
$loaded[] = "{$dir}/artifact.json";
|
||||
}
|
||||
return $loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load artifact configurations from a specified JSON file.
|
||||
*/
|
||||
public static function loadFromFile(string $file): void
|
||||
public static function loadFromFile(string $file, string $registry_name): string
|
||||
{
|
||||
$content = @file_get_contents($file);
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
throw new WrongUsageException("Failed to read artifact config file: {$file}");
|
||||
}
|
||||
@@ -42,7 +47,9 @@ class ArtifactConfig
|
||||
ConfigValidator::validateAndLintArtifacts(basename($file), $data);
|
||||
foreach ($data as $artifact_name => $config) {
|
||||
self::$artifact_configs[$artifact_name] = $config;
|
||||
Registry::_bindArtifactConfigFile($artifact_name, $registry_name, $file);
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -79,7 +79,7 @@ class ConfigValidator
|
||||
|
||||
public const array ARTIFACT_TYPE_FIELDS = [ // [required_fields, optional_fields]
|
||||
'filelist' => [['url', 'regex'], ['extract']],
|
||||
'git' => [['url', 'rev'], ['extract', 'submodules']],
|
||||
'git' => [['url'], ['extract', 'submodules', 'rev', 'regex']],
|
||||
'ghtagtar' => [['repo'], ['extract', 'prefer-stable', 'match']],
|
||||
'ghtar' => [['repo'], ['extract', 'prefer-stable', 'match']],
|
||||
'ghrel' => [['repo', 'match'], ['extract', 'prefer-stable']],
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace StaticPHP\Config;
|
||||
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Registry\Registry;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
|
||||
class PackageConfig
|
||||
@@ -15,20 +16,24 @@ class PackageConfig
|
||||
* Load package configurations from a specified directory.
|
||||
* It will look for files matching the pattern 'pkg.*.json' and 'pkg.json'.
|
||||
*/
|
||||
public static function loadFromDir(string $dir): void
|
||||
public static function loadFromDir(string $dir, string $registry_name): array
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config.");
|
||||
}
|
||||
$loaded = [];
|
||||
$files = glob("{$dir}/pkg.*.json");
|
||||
if (is_array($files)) {
|
||||
foreach ($files as $file) {
|
||||
self::loadFromFile($file);
|
||||
self::loadFromFile($file, $registry_name);
|
||||
$loaded[] = $file;
|
||||
}
|
||||
}
|
||||
if (file_exists("{$dir}/pkg.json")) {
|
||||
self::loadFromFile("{$dir}/pkg.json");
|
||||
self::loadFromFile("{$dir}/pkg.json", $registry_name);
|
||||
$loaded[] = "{$dir}/pkg.json";
|
||||
}
|
||||
return $loaded;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,9 +41,9 @@ class PackageConfig
|
||||
*
|
||||
* @param string $file the path to the json package configuration file
|
||||
*/
|
||||
public static function loadFromFile(string $file): void
|
||||
public static function loadFromFile(string $file, string $registry_name): string
|
||||
{
|
||||
$content = @file_get_contents($file);
|
||||
$content = file_get_contents($file);
|
||||
if ($content === false) {
|
||||
throw new WrongUsageException("Failed to read package config file: {$file}");
|
||||
}
|
||||
@@ -49,7 +54,9 @@ class PackageConfig
|
||||
ConfigValidator::validateAndLintPackages(basename($file), $data);
|
||||
foreach ($data as $pkg_name => $config) {
|
||||
self::$package_configs[$pkg_name] = $config;
|
||||
Registry::_bindPackageConfigFile($pkg_name, $registry_name, $file);
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,8 +6,11 @@ namespace StaticPHP;
|
||||
|
||||
use StaticPHP\Command\BuildLibsCommand;
|
||||
use StaticPHP\Command\BuildTargetCommand;
|
||||
use StaticPHP\Command\Dev\EnvCommand;
|
||||
use StaticPHP\Command\Dev\IsInstalledCommand;
|
||||
use StaticPHP\Command\Dev\ShellCommand;
|
||||
use StaticPHP\Command\Dev\SkeletonCommand;
|
||||
use StaticPHP\Command\Dev\SortConfigCommand;
|
||||
use StaticPHP\Command\DoctorCommand;
|
||||
use StaticPHP\Command\DownloadCommand;
|
||||
use StaticPHP\Command\ExtractCommand;
|
||||
@@ -26,12 +29,12 @@ class ConsoleApplication extends Application
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct('static-php-cli', self::VERSION);
|
||||
parent::__construct('StaticPHP', self::VERSION);
|
||||
|
||||
require_once ROOT_DIR . '/src/bootstrap.php';
|
||||
|
||||
// check registry
|
||||
Registry::checkLoadedRegistries();
|
||||
// resolve registry
|
||||
Registry::resolve();
|
||||
|
||||
/**
|
||||
* @var string $name
|
||||
@@ -57,6 +60,9 @@ class ConsoleApplication extends Application
|
||||
// dev commands
|
||||
new ShellCommand(),
|
||||
new IsInstalledCommand(),
|
||||
new EnvCommand(),
|
||||
new SkeletonCommand(),
|
||||
new SortConfigCommand(),
|
||||
]);
|
||||
|
||||
// add additional commands from registries
|
||||
|
||||
@@ -83,7 +83,7 @@ class ApplicationContext
|
||||
*
|
||||
* @param class-string<T> $id Service identifier
|
||||
*
|
||||
* @return T
|
||||
* @return null|T
|
||||
*/
|
||||
public static function get(string $id): mixed
|
||||
{
|
||||
|
||||
@@ -130,6 +130,7 @@ readonly class Doctor
|
||||
$this->output?->writeln('<error>Fix failed: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
} catch (\Throwable $e) {
|
||||
logger()->debug('Error: ' . $e->getMessage() . " at {$e->getFile()}:{$e->getLine()}\n" . $e->getTraceAsString());
|
||||
$this->output?->writeln('<error>Fix failed with an unexpected error: ' . $e->getMessage() . '</error>');
|
||||
return false;
|
||||
} finally {
|
||||
|
||||
@@ -10,7 +10,7 @@ use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
class OSCheck
|
||||
{
|
||||
#[CheckItem('if current OS are supported', level: 1000)]
|
||||
#[CheckItem('if current OS is supported', level: 1000)]
|
||||
public function checkOS(): ?CheckResult
|
||||
{
|
||||
if (!in_array(PHP_OS_FAMILY, ['Darwin', 'Linux', 'Windows'])) {
|
||||
|
||||
142
src/StaticPHP/Doctor/Item/WindowsToolCheck.php
Normal file
142
src/StaticPHP/Doctor/Item/WindowsToolCheck.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Doctor\Item;
|
||||
|
||||
use StaticPHP\Attribute\Doctor\CheckItem;
|
||||
use StaticPHP\Attribute\Doctor\FixItem;
|
||||
use StaticPHP\Attribute\Doctor\OptionalCheck;
|
||||
use StaticPHP\Doctor\CheckResult;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Toolchain\ToolchainManager;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\System\WindowsUtil;
|
||||
|
||||
#[OptionalCheck([self::class, 'optional'])]
|
||||
class WindowsToolCheck
|
||||
{
|
||||
public static function optional(): bool
|
||||
{
|
||||
return SystemTarget::getTargetOS() === 'Windows';
|
||||
}
|
||||
|
||||
#[CheckItem('if vswhere is installed', level: 999)]
|
||||
public function findVSWhere(): ?CheckResult
|
||||
{
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('vswhere');
|
||||
$is_installed = $installer->isPackageInstalled('vswhere');
|
||||
if ($is_installed) {
|
||||
return CheckResult::ok();
|
||||
}
|
||||
return CheckResult::fail('vswhere is not installed', 'install-vswhere');
|
||||
}
|
||||
|
||||
#[CheckItem('if Visual Studio is installed', level: 998)]
|
||||
public function findVS(): ?CheckResult
|
||||
{
|
||||
$a = WindowsUtil::findVisualStudio();
|
||||
if ($a !== false) {
|
||||
return CheckResult::ok("{$a['version']} at {$a['dir']}");
|
||||
}
|
||||
return CheckResult::fail('Visual Studio with C++ tools is not installed. Please install Visual Studio with C++ tools.');
|
||||
}
|
||||
|
||||
#[CheckItem('if git associated command exists', level: 997)]
|
||||
public function checkGitPatch(): ?CheckResult
|
||||
{
|
||||
if (WindowsUtil::findCommand('patch.exe') === null) {
|
||||
return CheckResult::fail('Git patch (minGW command) not found in path. You need to add "C:\Program Files\Git\usr\bin" in Path.');
|
||||
}
|
||||
return CheckResult::ok();
|
||||
}
|
||||
|
||||
#[CheckItem('if php-sdk-binary-tools are downloaded', limit_os: 'Windows', level: 996)]
|
||||
public function checkSDK(): ?CheckResult
|
||||
{
|
||||
if (!file_exists(getenv('PHP_SDK_PATH') . DIRECTORY_SEPARATOR . 'phpsdk-starter.bat')) {
|
||||
return CheckResult::fail('php-sdk-binary-tools not downloaded', 'install-php-sdk');
|
||||
}
|
||||
return CheckResult::ok(getenv('PHP_SDK_PATH'));
|
||||
}
|
||||
|
||||
#[CheckItem('if nasm installed', level: 995)]
|
||||
public function checkNasm(): ?CheckResult
|
||||
{
|
||||
if (($a = WindowsUtil::findCommand('nasm.exe')) === null) {
|
||||
return CheckResult::fail('nasm.exe not found in path.', 'install-nasm');
|
||||
}
|
||||
return CheckResult::ok($a);
|
||||
}
|
||||
|
||||
#[CheckItem('if perl(strawberry) installed', limit_os: 'Windows', level: 994)]
|
||||
public function checkPerl(): ?CheckResult
|
||||
{
|
||||
if (($path = WindowsUtil::findCommand('perl.exe')) === null) {
|
||||
return CheckResult::fail('perl not found in path.', 'install-perl');
|
||||
}
|
||||
if (!str_contains(implode('', cmd()->execWithResult(quote($path) . ' -v', false)[1]), 'MSWin32')) {
|
||||
return CheckResult::fail($path . ' is not built for msvc.', 'install-perl');
|
||||
}
|
||||
return CheckResult::ok($path);
|
||||
}
|
||||
|
||||
#[CheckItem('if environment is properly set up', level: 1)]
|
||||
public function checkenv(): ?CheckResult
|
||||
{
|
||||
// manually trigger after init
|
||||
try {
|
||||
ToolchainManager::afterInitToolchain();
|
||||
} catch (\Exception $e) {
|
||||
return CheckResult::fail('Environment setup failed: ' . $e->getMessage());
|
||||
}
|
||||
$required_cmd = ['cl.exe', 'link.exe', 'lib.exe', 'dumpbin.exe', 'msbuild.exe', 'nmake.exe'];
|
||||
foreach ($required_cmd as $cmd) {
|
||||
if (WindowsUtil::findCommand($cmd) === null) {
|
||||
return CheckResult::fail("{$cmd} not found in path. Please make sure Visual Studio with C++ tools is properly installed.");
|
||||
}
|
||||
}
|
||||
return CheckResult::ok();
|
||||
}
|
||||
|
||||
#[FixItem('install-perl')]
|
||||
public function installPerl(): bool
|
||||
{
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('strawberry-perl');
|
||||
$installer->run(false);
|
||||
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl');
|
||||
return true;
|
||||
}
|
||||
|
||||
#[FixItem('install-php-sdk')]
|
||||
public function installSDK(): bool
|
||||
{
|
||||
FileSystem::removeDir(getenv('PHP_SDK_PATH'));
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('php-sdk-binary-tools');
|
||||
$installer->run(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
#[FixItem('install-nasm')]
|
||||
public function installNasm(): bool
|
||||
{
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('nasm');
|
||||
$installer->run(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
#[FixItem('install-vswhere')]
|
||||
public function installVSWhere(): bool
|
||||
{
|
||||
$installer = new PackageInstaller();
|
||||
$installer->addInstallPackage('vswhere');
|
||||
$installer->run(false);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -190,7 +190,7 @@ class ExceptionHandler
|
||||
$line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT);
|
||||
fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL);
|
||||
if ($output_log) {
|
||||
InteractiveTerm::plain(ConsoleColor::red($line) . '');
|
||||
InteractiveTerm::plain(ConsoleColor::red($line) . '', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,14 @@ abstract class Package
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the package has a build function for the current OS.
|
||||
*/
|
||||
public function hasBuildFunctionForCurrentOS(): bool
|
||||
{
|
||||
return isset($this->build_functions[PHP_OS_FAMILY]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of the package.
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,6 @@ use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Runtime\Shell\Shell;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\System\LinuxUtil;
|
||||
|
||||
@@ -27,9 +26,6 @@ class PackageBuilder
|
||||
{
|
||||
ApplicationContext::set(PackageBuilder::class, $this);
|
||||
|
||||
// apply build toolchain envs
|
||||
GlobalEnvManager::afterInit();
|
||||
|
||||
$this->concurrency = (int) getenv('SPC_CONCURRENCY') ?: 1;
|
||||
}
|
||||
|
||||
@@ -103,7 +99,7 @@ class PackageBuilder
|
||||
|
||||
// ignore copy to self
|
||||
if (realpath($src) !== realpath($dst)) {
|
||||
shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg($dst));
|
||||
FileSystem::copy($src, $dst);
|
||||
}
|
||||
|
||||
// file exist
|
||||
@@ -115,7 +111,7 @@ class PackageBuilder
|
||||
$this->extractDebugInfo($dst);
|
||||
|
||||
// strip
|
||||
if (!$this->getOption('no-strip')) {
|
||||
if (!$this->getOption('no-strip') && SystemTarget::isUnix()) {
|
||||
$this->stripBinary($dst);
|
||||
}
|
||||
|
||||
@@ -127,6 +123,9 @@ class PackageBuilder
|
||||
}
|
||||
logger()->info("Compressing {$dst} with UPX");
|
||||
shell()->exec(getenv('UPX_EXEC') . " --best {$dst}");
|
||||
} elseif ($upx_option && SystemTarget::getTargetOS() === 'Windows' && $executable) {
|
||||
logger()->info("Compressing {$dst} with UPX");
|
||||
shell()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst));
|
||||
}
|
||||
|
||||
return $dst;
|
||||
@@ -140,12 +139,13 @@ class PackageBuilder
|
||||
public function extractDebugInfo(string $binary_path): string
|
||||
{
|
||||
$target_dir = BUILD_ROOT_PATH . '/debug';
|
||||
FileSystem::createDir($target_dir);
|
||||
$basename = basename($binary_path);
|
||||
$debug_file = "{$target_dir}/{$basename}" . (SystemTarget::getTargetOS() === 'Darwin' ? '.dwarf' : '.debug');
|
||||
if (SystemTarget::getTargetOS() === 'Darwin') {
|
||||
FileSystem::createDir($target_dir);
|
||||
shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}");
|
||||
} elseif (SystemTarget::getTargetOS() === 'Linux') {
|
||||
FileSystem::createDir($target_dir);
|
||||
if ($eu_strip = LinuxUtil::findCommand('eu-strip')) {
|
||||
shell()
|
||||
->exec("{$eu_strip} -f {$debug_file} {$binary_path}")
|
||||
@@ -156,7 +156,8 @@ class PackageBuilder
|
||||
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
|
||||
}
|
||||
} else {
|
||||
throw new SPCInternalException('extractDebugInfo is only supported on Linux and macOS');
|
||||
logger()->debug('extractDebugInfo is only supported on Linux and macOS');
|
||||
return '';
|
||||
}
|
||||
return $debug_file;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ use StaticPHP\Artifact\DownloaderOptions;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\WrongUsageException;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
use StaticPHP\Runtime\SystemTarget;
|
||||
use StaticPHP\Util\DependencyResolver;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\V2CompatLayer;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
@@ -119,6 +121,9 @@ class PackageInstaller
|
||||
*/
|
||||
public function run(bool $interactive = true, bool $disable_delay_msg = false): void
|
||||
{
|
||||
// apply build toolchain envs
|
||||
GlobalEnvManager::afterInit();
|
||||
|
||||
if (empty($this->packages)) {
|
||||
// resolve input, make dependency graph
|
||||
$this->resolvePackages();
|
||||
@@ -128,11 +133,13 @@ class PackageInstaller
|
||||
// show install or build options in terminal with beautiful output
|
||||
$this->printInstallerInfo();
|
||||
|
||||
InteractiveTerm::notice('Build process will start after 2s ...');
|
||||
InteractiveTerm::notice('Build process will start after 2s ...' . PHP_EOL);
|
||||
sleep(2);
|
||||
echo PHP_EOL;
|
||||
}
|
||||
|
||||
// Early validation: check if packages can be built or installed before downloading
|
||||
$this->validatePackagesBeforeBuild();
|
||||
|
||||
// check download
|
||||
if ($this->download) {
|
||||
$downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl');
|
||||
@@ -199,8 +206,6 @@ class PackageInstaller
|
||||
if ($interactive) {
|
||||
InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : ''));
|
||||
}
|
||||
} elseif ($package->getType() === 'library') {
|
||||
throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,6 +540,23 @@ class PackageInstaller
|
||||
}
|
||||
}
|
||||
|
||||
private function validatePackagesBeforeBuild(): void
|
||||
{
|
||||
foreach ($this->packages as $package) {
|
||||
if ($package->getType() !== 'library') {
|
||||
continue;
|
||||
}
|
||||
$is_to_build = $this->isBuildPackage($package);
|
||||
$has_build_stage = $package instanceof LibraryPackage && $package->hasStage('build');
|
||||
$should_use_binary = $package instanceof LibraryPackage && ($package->getArtifact()?->shouldUseBinary() ?? false);
|
||||
|
||||
// Check if package can neither be built nor installed
|
||||
if (!$is_to_build && !$should_use_binary && !$has_build_stage) {
|
||||
throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS: " . SystemTarget::getCurrentPlatformString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function performAfterInstallActions(Package $package): void
|
||||
{
|
||||
// ----------- perform post-install actions from extracted .package.{pkg_name}.postinstall.json -----------
|
||||
|
||||
@@ -306,7 +306,9 @@ class PackageLoader
|
||||
}
|
||||
}
|
||||
// check stage exists
|
||||
if (!$pkg->hasStage($stage_name)) {
|
||||
// Skip validation if the package has no build function for current OS
|
||||
// (e.g., libedit has BeforeStage for 'build' but only BuildFor('Darwin'/'Linux'))
|
||||
if (!$pkg->hasStage($stage_name) && $pkg->hasBuildFunctionForCurrentOS()) {
|
||||
throw new RegistryException("Package stage [{$stage_name}] is not registered in package [{$package_name}].");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,39 @@ use Symfony\Component\Yaml\Yaml;
|
||||
|
||||
class Registry
|
||||
{
|
||||
/** @var string[] List of loaded registry names */
|
||||
/** @var string[] List of loaded registries */
|
||||
private static array $loaded_registries = [];
|
||||
|
||||
/** @var array<string, array> Loaded registry configs */
|
||||
private static array $registry_configs = [];
|
||||
|
||||
private static array $loaded_package_configs = [];
|
||||
|
||||
private static array $loaded_artifact_configs = [];
|
||||
|
||||
/** @var array<string, array{registry: string, config: string}> Maps of package and artifact names to their registry config file paths (for reverse lookup) */
|
||||
private static array $package_reversed_registry_files = [];
|
||||
|
||||
private static array $artifact_reversed_registry_files = [];
|
||||
|
||||
/**
|
||||
* Get the current registry configuration.
|
||||
* "Current" depends on SPC load mode
|
||||
*/
|
||||
public static function getRegistryConfig(?string $registry_name = null): array
|
||||
{
|
||||
if ($registry_name === null && spc_mode(SPC_MODE_SOURCE)) {
|
||||
return self::$registry_configs['internal'];
|
||||
}
|
||||
if ($registry_name !== null && isset(self::$registry_configs[$registry_name])) {
|
||||
return self::$registry_configs[$registry_name];
|
||||
}
|
||||
if ($registry_name === null) {
|
||||
throw new RegistryException('No registry name specified.');
|
||||
}
|
||||
throw new RegistryException("Registry '{$registry_name}' is not loaded.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a registry from file path.
|
||||
* This method handles external registries that may not be in composer autoload.
|
||||
@@ -48,12 +78,14 @@ class Registry
|
||||
return;
|
||||
}
|
||||
self::$loaded_registries[] = $registry_name;
|
||||
self::$registry_configs[$registry_name] = $data;
|
||||
self::$registry_configs[$registry_name]['_file'] = $registry_file;
|
||||
|
||||
logger()->debug("Loading registry '{$registry_name}' from file: {$registry_file}");
|
||||
|
||||
// Load composer autoload if specified (for external registries with their own dependencies)
|
||||
if (isset($data['autoload']) && is_string($data['autoload'])) {
|
||||
$autoload_path = self::fullpath($data['autoload'], dirname($registry_file));
|
||||
$autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file));
|
||||
if (file_exists($autoload_path)) {
|
||||
logger()->debug("Loading external autoload from: {$autoload_path}");
|
||||
require_once $autoload_path;
|
||||
@@ -65,7 +97,7 @@ class Registry
|
||||
// load doctor items from PSR-4 directories
|
||||
if (isset($data['doctor']['psr-4']) && is_assoc_array($data['doctor']['psr-4'])) {
|
||||
foreach ($data['doctor']['psr-4'] as $namespace => $path) {
|
||||
$path = self::fullpath($path, dirname($registry_file));
|
||||
$path = FileSystem::fullpath($path, dirname($registry_file));
|
||||
DoctorLoader::loadFromPsr4Dir($path, $namespace, $auto_require);
|
||||
}
|
||||
}
|
||||
@@ -83,11 +115,11 @@ class Registry
|
||||
// load package configs
|
||||
if (isset($data['package']['config']) && is_array($data['package']['config'])) {
|
||||
foreach ($data['package']['config'] as $path) {
|
||||
$path = self::fullpath($path, dirname($registry_file));
|
||||
$path = FileSystem::fullpath($path, dirname($registry_file));
|
||||
if (is_file($path)) {
|
||||
PackageConfig::loadFromFile($path);
|
||||
self::$loaded_package_configs[] = PackageConfig::loadFromFile($path, $registry_name);
|
||||
} elseif (is_dir($path)) {
|
||||
PackageConfig::loadFromDir($path);
|
||||
self::$loaded_package_configs = array_merge(self::$loaded_package_configs, PackageConfig::loadFromDir($path, $registry_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,11 +127,11 @@ class Registry
|
||||
// load artifact configs
|
||||
if (isset($data['artifact']['config']) && is_array($data['artifact']['config'])) {
|
||||
foreach ($data['artifact']['config'] as $path) {
|
||||
$path = self::fullpath($path, dirname($registry_file));
|
||||
$path = FileSystem::fullpath($path, dirname($registry_file));
|
||||
if (is_file($path)) {
|
||||
ArtifactConfig::loadFromFile($path);
|
||||
self::$loaded_artifact_configs[] = ArtifactConfig::loadFromFile($path, $registry_name);
|
||||
} elseif (is_dir($path)) {
|
||||
ArtifactConfig::loadFromDir($path);
|
||||
self::$loaded_package_configs = array_merge(self::$loaded_package_configs, ArtifactConfig::loadFromDir($path, $registry_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -107,7 +139,7 @@ class Registry
|
||||
// load packages from PSR-4 directories
|
||||
if (isset($data['package']['psr-4']) && is_assoc_array($data['package']['psr-4'])) {
|
||||
foreach ($data['package']['psr-4'] as $namespace => $path) {
|
||||
$path = self::fullpath($path, dirname($registry_file));
|
||||
$path = FileSystem::fullpath($path, dirname($registry_file));
|
||||
PackageLoader::loadFromPsr4Dir($path, $namespace, $auto_require);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +157,7 @@ class Registry
|
||||
// load artifacts from PSR-4 directories
|
||||
if (isset($data['artifact']['psr-4']) && is_assoc_array($data['artifact']['psr-4'])) {
|
||||
foreach ($data['artifact']['psr-4'] as $namespace => $path) {
|
||||
$path = self::fullpath($path, dirname($registry_file));
|
||||
$path = FileSystem::fullpath($path, dirname($registry_file));
|
||||
ArtifactLoader::loadFromPsr4Dir($path, $namespace, $auto_require);
|
||||
}
|
||||
}
|
||||
@@ -143,7 +175,7 @@ class Registry
|
||||
// load additional commands from PSR-4 directories
|
||||
if (isset($data['command']['psr-4']) && is_assoc_array($data['command']['psr-4'])) {
|
||||
foreach ($data['command']['psr-4'] as $namespace => $path) {
|
||||
$path = self::fullpath($path, dirname($registry_file));
|
||||
$path = FileSystem::fullpath($path, dirname($registry_file));
|
||||
$classes = FileSystem::getClassesPsr4($path, $namespace, auto_require: $auto_require);
|
||||
$instances = array_map(fn ($x) => new $x(), $classes);
|
||||
ConsoleApplication::_addAdditionalCommands($instances);
|
||||
@@ -187,7 +219,12 @@ class Registry
|
||||
}
|
||||
}
|
||||
|
||||
public static function checkLoadedRegistries(): void
|
||||
/**
|
||||
* Resolve loaded registries.
|
||||
* This method finalizes the loading process by registering default stages
|
||||
* and validating stage events.
|
||||
*/
|
||||
public static function resolve(): void
|
||||
{
|
||||
// Register default stages for all PhpExtensionPackage instances
|
||||
// This must be done after all registries are loaded to ensure custom stages take precedence
|
||||
@@ -217,6 +254,52 @@ class Registry
|
||||
self::$loaded_registries = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a package name to its registry config file for reverse lookup.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function _bindPackageConfigFile(string $package_name, string $registry_name, string $config_file): void
|
||||
{
|
||||
self::$package_reversed_registry_files[$package_name] = [
|
||||
'registry' => $registry_name,
|
||||
'config' => $config_file,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind an artifact name to its registry config file for reverse lookup.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public static function _bindArtifactConfigFile(string $artifact_name, string $registry_name, string $config_file): void
|
||||
{
|
||||
self::$artifact_reversed_registry_files[$artifact_name] = [
|
||||
'registry' => $registry_name,
|
||||
'config' => $config_file,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPackageConfigInfo(string $package_name): ?array
|
||||
{
|
||||
return self::$package_reversed_registry_files[$package_name] ?? null;
|
||||
}
|
||||
|
||||
public static function getArtifactConfigInfo(string $artifact_name): ?array
|
||||
{
|
||||
return self::$artifact_reversed_registry_files[$artifact_name] ?? null;
|
||||
}
|
||||
|
||||
public static function getLoadedPackageConfigs(): array
|
||||
{
|
||||
return self::$loaded_package_configs;
|
||||
}
|
||||
|
||||
public static function getLoadedArtifactConfigs(): array
|
||||
{
|
||||
return self::$loaded_artifact_configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a class entry from the classes array.
|
||||
* Supports two formats:
|
||||
@@ -253,7 +336,7 @@ class Registry
|
||||
|
||||
// If file path is provided, require it
|
||||
if ($file_path !== null) {
|
||||
$full_path = self::fullpath($file_path, $base_path);
|
||||
$full_path = FileSystem::fullpath($file_path, $base_path);
|
||||
require_once $full_path;
|
||||
return;
|
||||
}
|
||||
@@ -266,21 +349,4 @@ class Registry
|
||||
" 3. Provide file path in classes map: \"{$class}\": \"path/to/file.php\""
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return full path, resolving relative paths against a base path.
|
||||
*
|
||||
* @param string $path Input path (relative or absolute)
|
||||
* @param string $relative_path_base Base path for relative paths
|
||||
*/
|
||||
private static function fullpath(string $path, string $relative_path_base): string
|
||||
{
|
||||
if (FileSystem::isRelativePath($path)) {
|
||||
$path = $relative_path_base . DIRECTORY_SEPARATOR . $path;
|
||||
}
|
||||
if (!file_exists($path)) {
|
||||
throw new RegistryException("Path does not exist: {$path}");
|
||||
}
|
||||
return FileSystem::convertPath($path);
|
||||
}
|
||||
}
|
||||
|
||||
218
src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php
Normal file
218
src/StaticPHP/Runtime/Executor/WindowsCMakeExecutor.php
Normal file
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Executor;
|
||||
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Package\PackageBuilder;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Runtime\Shell\WindowsCmd;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
use StaticPHP\Util\InteractiveTerm;
|
||||
use StaticPHP\Util\System\WindowsUtil;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
class WindowsCMakeExecutor extends Executor
|
||||
{
|
||||
protected WindowsCmd $cmd;
|
||||
|
||||
protected array $configure_args = [];
|
||||
|
||||
protected array $ignore_args = [];
|
||||
|
||||
protected ?string $build_dir = null;
|
||||
|
||||
protected ?array $custom_default_args = null;
|
||||
|
||||
protected int $steps = 2;
|
||||
|
||||
protected bool $reset = true;
|
||||
|
||||
protected PackageBuilder $builder;
|
||||
|
||||
protected PackageInstaller $installer;
|
||||
|
||||
public function __construct(protected LibraryPackage $package)
|
||||
{
|
||||
parent::__construct($this->package);
|
||||
$this->builder = ApplicationContext::get(PackageBuilder::class);
|
||||
$this->installer = ApplicationContext::get(PackageInstaller::class);
|
||||
$this->initCmd();
|
||||
|
||||
// judge that this package has artifact.source and defined build stage
|
||||
if (!$this->package->hasStage('build')) {
|
||||
throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined.");
|
||||
}
|
||||
}
|
||||
|
||||
public function build(): static
|
||||
{
|
||||
$this->initBuildDir();
|
||||
|
||||
if ($this->reset) {
|
||||
FileSystem::resetDir($this->build_dir);
|
||||
}
|
||||
|
||||
// configure
|
||||
if ($this->steps >= 1) {
|
||||
$args = array_merge($this->configure_args, $this->getDefaultCMakeArgs());
|
||||
$args = array_diff($args, $this->ignore_args);
|
||||
$configure_args = implode(' ', $args);
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName() . ' (cmake configure)'));
|
||||
$this->cmd->exec("cmake {$configure_args}");
|
||||
}
|
||||
|
||||
// make
|
||||
if ($this->steps >= 2) {
|
||||
InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName() . ' (cmake build)'));
|
||||
$this->cmd->cd($this->build_dir)->exec("cmake --build {$this->build_dir} --config Release --target install -j{$this->builder->concurrency}");
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add optional package configuration.
|
||||
* This method checks if a package is available and adds the corresponding arguments to the CMake configuration.
|
||||
*
|
||||
* @param string $name package name to check
|
||||
* @param \Closure|string $true_args arguments to use if the package is available (allow closure, returns string)
|
||||
* @param string $false_args arguments to use if the package is not available
|
||||
* @return $this
|
||||
*/
|
||||
public function optionalPackage(string $name, \Closure|string $true_args, string $false_args = ''): static
|
||||
{
|
||||
if ($get = $this->installer->getResolvedPackages()[$name] ?? null) {
|
||||
logger()->info("Building package [{$this->package->getName()}] with {$name} support");
|
||||
$args = $true_args instanceof \Closure ? $true_args($get) : $true_args;
|
||||
} else {
|
||||
logger()->info("Building package [{$this->package->getName()}] without {$name} support");
|
||||
$args = $false_args;
|
||||
}
|
||||
$this->addConfigureArgs($args);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add configure args.
|
||||
*/
|
||||
public function addConfigureArgs(...$args): static
|
||||
{
|
||||
$this->configure_args = [...$this->configure_args, ...$args];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove some configure args, to bypass the configure option checking for some libs.
|
||||
*/
|
||||
public function removeConfigureArgs(...$args): static
|
||||
{
|
||||
$this->ignore_args = [...$this->ignore_args, ...$args];
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEnv(array $env): static
|
||||
{
|
||||
$this->cmd->setEnv($env);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function appendEnv(array $env): static
|
||||
{
|
||||
$this->cmd->appendEnv($env);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* To build steps.
|
||||
*
|
||||
* @param int $step Step number, accept 1-3
|
||||
* @return $this
|
||||
*/
|
||||
public function toStep(int $step): static
|
||||
{
|
||||
$this->steps = $step;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom CMake build directory.
|
||||
*
|
||||
* @param string $dir custom CMake build directory
|
||||
*/
|
||||
public function setBuildDir(string $dir): static
|
||||
{
|
||||
$this->build_dir = $dir;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom default args.
|
||||
*/
|
||||
public function setCustomDefaultArgs(...$args): static
|
||||
{
|
||||
$this->custom_default_args = $args;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the reset status.
|
||||
* If we set it to false, it will not clean and create the specified cmake working directory.
|
||||
*/
|
||||
public function setReset(bool $reset): static
|
||||
{
|
||||
$this->reset = $reset;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configure argument string.
|
||||
*/
|
||||
public function getConfigureArgsString(): string
|
||||
{
|
||||
return implode(' ', array_merge($this->configure_args, $this->getDefaultCMakeArgs()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default CMake args.
|
||||
*/
|
||||
private function getDefaultCMakeArgs(): array
|
||||
{
|
||||
return $this->custom_default_args ?? [
|
||||
'-A x64',
|
||||
'-DCMAKE_BUILD_TYPE=Release',
|
||||
'-DBUILD_SHARED_LIBS=OFF',
|
||||
'-DBUILD_STATIC_LIBS=ON',
|
||||
"-DCMAKE_TOOLCHAIN_FILE={$this->makeCmakeToolchainFile()}",
|
||||
'-DCMAKE_INSTALL_PREFIX=' . escapeshellarg($this->package->getBuildRootPath()),
|
||||
'-B ' . escapeshellarg(FileSystem::convertPath($this->build_dir)),
|
||||
];
|
||||
}
|
||||
|
||||
private function makeCmakeToolchainFile(): string
|
||||
{
|
||||
if (file_exists(SOURCE_PATH . '\toolchain.cmake')) {
|
||||
return SOURCE_PATH . '\toolchain.cmake';
|
||||
}
|
||||
return WindowsUtil::makeCmakeToolchainFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the CMake build directory.
|
||||
* If the directory is not set, it defaults to the package's source directory with '/build' appended.
|
||||
*/
|
||||
private function initBuildDir(): void
|
||||
{
|
||||
if ($this->build_dir === null) {
|
||||
$this->build_dir = "{$this->package->getSourceDir()}\\build";
|
||||
}
|
||||
}
|
||||
|
||||
private function initCmd(): void
|
||||
{
|
||||
$this->cmd = cmd()->cd($this->package->getSourceDir());
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ class DefaultShell extends Shell
|
||||
$cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}";
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
$result = $this->passthru($cmd, console_output: false, capture_output: true, throw_on_error: false);
|
||||
$result = $this->passthru($cmd, capture_output: true, throw_on_error: false);
|
||||
$ret = $result['code'];
|
||||
$output = $result['output'];
|
||||
if ($ret !== 0) {
|
||||
@@ -96,15 +96,15 @@ class DefaultShell extends Shell
|
||||
$cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}");
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[GIT CLONE] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput);
|
||||
if ($submodules !== null) {
|
||||
$depth_flag = $shallow ? '--depth 1' : '';
|
||||
foreach ($submodules as $submodule) {
|
||||
$submodule = escapeshellarg($submodule);
|
||||
$submodule_cmd = clean_spaces("cd {$path_arg} && {$git} submodule update --init {$depth_flag} {$submodule}");
|
||||
$submodule_cmd = clean_spaces("{$git} submodule update --init {$depth_flag} {$submodule}");
|
||||
$this->logCommandInfo($submodule_cmd);
|
||||
logger()->debug("[GIT SUBMODULE] {$submodule_cmd}");
|
||||
$this->passthru($submodule_cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($submodule_cmd, $this->console_putput, cwd: $path_arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +117,7 @@ class DefaultShell extends Shell
|
||||
* @param string $compression Compression type: 'gz', 'bz2', 'xz', or 'none'
|
||||
* @param int $strip Number of leading components to strip (default: 1)
|
||||
*/
|
||||
public function executeTarExtract(string $archive_path, string $target_path, string $compression, int $strip = 1): void
|
||||
public function executeTarExtract(string $archive_path, string $target_path, string $compression, int $strip = 1): bool
|
||||
{
|
||||
$archive_arg = escapeshellarg(FileSystem::convertPath($archive_path));
|
||||
$target_arg = escapeshellarg(FileSystem::convertPath($target_path));
|
||||
@@ -135,7 +135,8 @@ class DefaultShell extends Shell
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[TAR EXTRACT] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,7 +155,7 @@ class DefaultShell extends Shell
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[UNZIP] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,9 +163,8 @@ class DefaultShell extends Shell
|
||||
*
|
||||
* @param string $archive_path Path to the archive file
|
||||
* @param string $target_path Path to extract to
|
||||
* @param bool $is_txz Whether this is a .txz/.tar.xz file that needs double extraction
|
||||
*/
|
||||
public function execute7zExtract(string $archive_path, string $target_path, bool $is_txz = false): void
|
||||
public function execute7zExtract(string $archive_path, string $target_path): bool
|
||||
{
|
||||
$sdk_path = getenv('PHP_SDK_PATH');
|
||||
if ($sdk_path === false) {
|
||||
@@ -177,15 +177,19 @@ class DefaultShell extends Shell
|
||||
|
||||
$mute = $this->console_putput ? '' : ' > NUL';
|
||||
|
||||
if ($is_txz) {
|
||||
// txz/tar.xz contains a tar file inside, extract twice
|
||||
$cmd = "{$_7z} x {$archive_arg} -so | {$_7z} x -si -ttar -o{$target_arg} -y{$mute}";
|
||||
} else {
|
||||
$cmd = "{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}";
|
||||
}
|
||||
$run = function ($cmd) {
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[7Z EXTRACT] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput);
|
||||
};
|
||||
|
||||
$this->logCommandInfo($cmd);
|
||||
logger()->debug("[7Z EXTRACT] {$cmd}");
|
||||
$this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true);
|
||||
$extname = FileSystem::extname($archive_path);
|
||||
match ($extname) {
|
||||
'tar' => $this->executeTarExtract($archive_path, $target_path, 'none'),
|
||||
'gz', 'tgz', 'xz', 'txz', 'bz2' => $run("{$_7z} x -so {$archive_arg} | tar -f - -x -C {$target_arg} --strip-components 1"),
|
||||
default => $run("{$_7z} x {$archive_arg} -o{$target_arg} -y{$mute}"),
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,9 @@ abstract class Shell
|
||||
bool $console_output = false,
|
||||
?string $original_command = null,
|
||||
bool $capture_output = false,
|
||||
bool $throw_on_error = true
|
||||
bool $throw_on_error = true,
|
||||
?string $cwd = null,
|
||||
?array $env = null,
|
||||
): array {
|
||||
$file_res = null;
|
||||
if ($this->enable_log_file) {
|
||||
@@ -160,10 +162,16 @@ abstract class Shell
|
||||
}
|
||||
$descriptors = [
|
||||
0 => ['file', 'php://stdin', 'r'], // stdin
|
||||
1 => ['pipe', 'w'], // stdout
|
||||
2 => ['pipe', 'w'], // stderr
|
||||
1 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stdout
|
||||
2 => PHP_OS_FAMILY === 'Windows' ? ['socket'] : ['pipe', 'w'], // stderr
|
||||
];
|
||||
$process = proc_open($cmd, $descriptors, $pipes);
|
||||
if ($env !== null && $env !== []) {
|
||||
// merge current PHP envs
|
||||
$env = array_merge(getenv(), $env);
|
||||
} else {
|
||||
$env = null;
|
||||
}
|
||||
$process = proc_open($cmd, $descriptors, $pipes, $cwd, env_vars: $env, options: PHP_OS_FAMILY === 'Windows' ? ['create_process_group' => true] : null);
|
||||
|
||||
$output_value = '';
|
||||
try {
|
||||
|
||||
@@ -33,7 +33,7 @@ class UnixShell extends Shell
|
||||
$original_command = $cmd;
|
||||
$this->logCommandInfo($original_command);
|
||||
$this->last_cmd = $cmd = $this->getExecString($cmd);
|
||||
$this->passthru($cmd, $this->console_putput, $original_command, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class UnixShell extends Shell
|
||||
}
|
||||
$cmd = $this->getExecString($cmd);
|
||||
$this->logCommandInfo($cmd);
|
||||
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false);
|
||||
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd);
|
||||
$out = explode("\n", $result['output']);
|
||||
return [$result['code'], $out];
|
||||
}
|
||||
@@ -83,9 +83,6 @@ class UnixShell extends Shell
|
||||
if (!empty($env_str)) {
|
||||
$cmd = "{$env_str} {$cmd}";
|
||||
}
|
||||
if ($this->cd !== null) {
|
||||
$cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd;
|
||||
}
|
||||
return $cmd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Runtime\Shell;
|
||||
|
||||
use StaticPHP\Exception\ExecutionException;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
@@ -28,7 +27,7 @@ class WindowsCmd extends Shell
|
||||
$this->last_cmd = $cmd = $this->getExecString($cmd);
|
||||
// echo $cmd . PHP_EOL;
|
||||
|
||||
$this->passthru($cmd, $this->console_putput, $original_command, capture_output: false, throw_on_error: true);
|
||||
$this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd);
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -46,111 +45,18 @@ class WindowsCmd extends Shell
|
||||
logger()->debug('Running command with result: ' . $cmd);
|
||||
}
|
||||
$cmd = $this->getExecString($cmd);
|
||||
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false);
|
||||
$result = $this->passthru($cmd, $this->console_putput, $cmd, capture_output: true, throw_on_error: false, cwd: $this->cd, env: $this->env);
|
||||
$out = explode("\n", $result['output']);
|
||||
return [$result['code'], $out];
|
||||
}
|
||||
|
||||
public function setEnv(array $env): static
|
||||
{
|
||||
// windows currently does not support setting environment variables
|
||||
throw new SPCInternalException('Windows does not support setting environment variables in shell commands.');
|
||||
}
|
||||
|
||||
public function appendEnv(array $env): static
|
||||
{
|
||||
// windows currently does not support appending environment variables
|
||||
throw new SPCInternalException('Windows does not support appending environment variables in shell commands.');
|
||||
}
|
||||
|
||||
public function getLastCommand(): string
|
||||
{
|
||||
return $this->last_cmd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a command with console and log file output.
|
||||
*
|
||||
* @param string $cmd Full command to execute (including cd and env vars)
|
||||
* @param bool $console_output If true, output will be printed to console
|
||||
* @param null|string $original_command Original command string for logging
|
||||
* @param bool $capture_output If true, capture and return output
|
||||
* @param bool $throw_on_error If true, throw exception on non-zero exit code
|
||||
*
|
||||
* @return array{code: int, output: string} Returns exit code and captured output
|
||||
*/
|
||||
protected function passthru(
|
||||
string $cmd,
|
||||
bool $console_output = false,
|
||||
?string $original_command = null,
|
||||
bool $capture_output = false,
|
||||
bool $throw_on_error = true
|
||||
): array {
|
||||
$file_res = null;
|
||||
if ($this->enable_log_file) {
|
||||
$file_res = fopen(SPC_SHELL_LOG, 'a');
|
||||
}
|
||||
|
||||
$output_value = '';
|
||||
try {
|
||||
$process = popen($cmd . ' 2>&1', 'r');
|
||||
if (!$process) {
|
||||
throw new ExecutionException(
|
||||
cmd: $original_command ?? $cmd,
|
||||
message: 'Failed to open process for command, popen() failed.',
|
||||
code: -1,
|
||||
cd: $this->cd,
|
||||
env: $this->env
|
||||
);
|
||||
}
|
||||
|
||||
while (($line = fgets($process)) !== false) {
|
||||
if (static::$passthru_callback !== null) {
|
||||
$callback = static::$passthru_callback;
|
||||
$callback();
|
||||
}
|
||||
if ($console_output) {
|
||||
echo $line;
|
||||
}
|
||||
if ($file_res !== null) {
|
||||
fwrite($file_res, $line);
|
||||
}
|
||||
if ($capture_output) {
|
||||
$output_value .= $line;
|
||||
}
|
||||
}
|
||||
|
||||
$result_code = pclose($process);
|
||||
|
||||
if ($throw_on_error && $result_code !== 0) {
|
||||
if ($file_res !== null) {
|
||||
fwrite($file_res, "Command exited with non-zero code: {$result_code}\n");
|
||||
}
|
||||
throw new ExecutionException(
|
||||
cmd: $original_command ?? $cmd,
|
||||
message: "Command exited with non-zero code: {$result_code}",
|
||||
code: $result_code,
|
||||
cd: $this->cd,
|
||||
env: $this->env,
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $result_code,
|
||||
'output' => $output_value,
|
||||
];
|
||||
} finally {
|
||||
if ($file_res !== null) {
|
||||
fclose($file_res);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getExecString(string $cmd): string
|
||||
{
|
||||
if ($this->cd !== null) {
|
||||
$cmd = 'cd /d ' . escapeshellarg($this->cd) . ' && ' . $cmd;
|
||||
}
|
||||
return $cmd;
|
||||
}
|
||||
}
|
||||
|
||||
115
src/StaticPHP/Skeleton/ArtifactGenerator.php
Normal file
115
src/StaticPHP/Skeleton/ArtifactGenerator.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Skeleton;
|
||||
|
||||
use StaticPHP\Exception\FileSystemException;
|
||||
use StaticPHP\Exception\ValidationException;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
class ArtifactGenerator
|
||||
{
|
||||
protected ?array $source = null;
|
||||
|
||||
protected ?array $binary = null;
|
||||
|
||||
protected ?string $config_file = null;
|
||||
|
||||
protected bool $generate_class = false;
|
||||
|
||||
protected bool $generate_custom_source_func = false;
|
||||
|
||||
protected bool $generate_custom_binary_func_for_unix = false;
|
||||
|
||||
protected bool $generate_custom_binary_func_for_windows = false;
|
||||
|
||||
public function __construct(protected string $name) {}
|
||||
|
||||
/**
|
||||
* Get the artifact name.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setSource(array $source): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->source = $source;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function setCustomSource(): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->source = ['type' => 'custom'];
|
||||
$clone->generate_class = true;
|
||||
$clone->generate_custom_source_func = true;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function getSource(): ?array
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
public function setBinary(string $os, array $config): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
if ($clone->binary === null) {
|
||||
$clone->binary = [$os => $config];
|
||||
} else {
|
||||
$clone->binary[$os] = $config;
|
||||
}
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function generateConfigArray(): array
|
||||
{
|
||||
$config = [];
|
||||
|
||||
if ($this->source) {
|
||||
$config['source'] = $this->source;
|
||||
}
|
||||
if ($this->binary) {
|
||||
$config['binary'] = $this->binary;
|
||||
}
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function setConfigFile(string $file): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->config_file = $file;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the artifact configuration to the config file.
|
||||
*/
|
||||
public function writeConfigFile(): string
|
||||
{
|
||||
if ($this->config_file === null) {
|
||||
throw new ValidationException('Config file path is not set.');
|
||||
}
|
||||
$config_array = $this->generateConfigArray();
|
||||
$config_file_json = json_decode(FileSystem::readFile($this->config_file), true);
|
||||
if (!is_array($config_file_json)) {
|
||||
throw new ValidationException('Existing config file is not a valid JSON array.');
|
||||
}
|
||||
|
||||
$config_file_json[$this->name] = $config_array;
|
||||
// sort keys
|
||||
ksort($config_file_json);
|
||||
$json_content = json_encode($config_file_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
if ($json_content === false) {
|
||||
throw new ValidationException('Failed to encode config array to JSON.');
|
||||
}
|
||||
if (file_put_contents($this->config_file, $json_content) === false) {
|
||||
throw new FileSystemException("Failed to write config file: {$this->config_file}");
|
||||
}
|
||||
return $this->config_file;
|
||||
}
|
||||
}
|
||||
36
src/StaticPHP/Skeleton/ExecutorGenerator.php
Normal file
36
src/StaticPHP/Skeleton/ExecutorGenerator.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Skeleton;
|
||||
|
||||
use StaticPHP\Exception\ValidationException;
|
||||
use StaticPHP\Runtime\Executor\Executor;
|
||||
use StaticPHP\Runtime\Executor\UnixAutoconfExecutor;
|
||||
use StaticPHP\Runtime\Executor\UnixCMakeExecutor;
|
||||
use StaticPHP\Runtime\Executor\WindowsCMakeExecutor;
|
||||
|
||||
class ExecutorGenerator
|
||||
{
|
||||
public function __construct(protected string $class)
|
||||
{
|
||||
if (!is_a($class, Executor::class, true)) {
|
||||
throw new ValidationException('Executor class must extend ' . Executor::class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the code to create an instance of the executor.
|
||||
*
|
||||
* @return array{0: string, 1: string} an array containing the class name and the code string
|
||||
*/
|
||||
public function generateCode(): array
|
||||
{
|
||||
return match ($this->class) {
|
||||
UnixCMakeExecutor::class => [UnixCMakeExecutor::class, 'UnixCMakeExecutor::create($package)->build();'],
|
||||
UnixAutoconfExecutor::class => [UnixAutoconfExecutor::class, 'UnixAutoconfExecutor::create($package)->build();'],
|
||||
WindowsCMakeExecutor::class => [WindowsCMakeExecutor::class, 'WindowsCMakeExecutor::create($package)->build();'],
|
||||
default => throw new ValidationException("Unsupported executor class: {$this->class}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
412
src/StaticPHP/Skeleton/PackageGenerator.php
Normal file
412
src/StaticPHP/Skeleton/PackageGenerator.php
Normal file
@@ -0,0 +1,412 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Skeleton;
|
||||
|
||||
use Nette\PhpGenerator\PhpFile;
|
||||
use Nette\PhpGenerator\Printer;
|
||||
use StaticPHP\Attribute\Package\BuildFor;
|
||||
use StaticPHP\Attribute\Package\Extension;
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
use StaticPHP\Attribute\Package\Target;
|
||||
use StaticPHP\Exception\FileSystemException;
|
||||
use StaticPHP\Exception\ValidationException;
|
||||
use StaticPHP\Package\LibraryPackage;
|
||||
use StaticPHP\Package\PackageInstaller;
|
||||
use StaticPHP\Package\PhpExtensionPackage;
|
||||
use StaticPHP\Package\TargetPackage;
|
||||
use StaticPHP\Util\FileSystem;
|
||||
|
||||
/**
|
||||
* A skeleton class for generating package files and configs.
|
||||
*/
|
||||
class PackageGenerator
|
||||
{
|
||||
/** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $depends An array of dependencies required by the package, categorized by operating system. */
|
||||
protected array $depends = [];
|
||||
|
||||
/** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $suggests An array of suggested packages for the package, categorized by operating system. */
|
||||
protected array $suggests = [];
|
||||
|
||||
/** @var array<string> $frameworks An array of macOS frameworks for the package */
|
||||
protected array $frameworks = [];
|
||||
|
||||
/** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $static_libs An array of static libraries required by the package, categorized by operating system. */
|
||||
protected array $static_libs = [];
|
||||
|
||||
/** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $headers An array of header files required by the package, categorized by operating system. */
|
||||
protected array $headers = [];
|
||||
|
||||
/** @var array<''|'linux'|'macos'|'unix'|'windows', string[]> $static_bins An array of static binaries required by the package, categorized by operating system. */
|
||||
protected array $static_bins = [];
|
||||
|
||||
protected ?string $config_file = null;
|
||||
|
||||
/** @var null|ArtifactGenerator $artifact Artifact */
|
||||
protected ?ArtifactGenerator $artifact = null;
|
||||
|
||||
/** @var array $licenses Licenses */
|
||||
protected array $licenses = [];
|
||||
|
||||
/** @var array<'Darwin'|'Linux'|'Windows', null|string> $build_for_enables Enable build function generating */
|
||||
protected array $build_for_enables = [
|
||||
'Darwin' => null,
|
||||
'Linux' => null,
|
||||
'Windows' => null,
|
||||
];
|
||||
|
||||
/** @var array<string, ExecutorGenerator> */
|
||||
protected array $func_executor_binding = [];
|
||||
|
||||
/**
|
||||
* @param string $package_name Package name
|
||||
* @param 'library'|'php-extension'|'target'|'virtual-target' $type Package type ('library', 'target', 'virtual-target', etc.)
|
||||
*/
|
||||
public function __construct(protected string $package_name, protected string $type) {}
|
||||
|
||||
/**
|
||||
* Add package dependency.
|
||||
*
|
||||
* @param string $package Package name
|
||||
* @param string $os_category Operating system ('' for all OSes, 'unix', 'windows', 'macos')
|
||||
*/
|
||||
public function addDependency(string $package, string $os_category = ''): static
|
||||
{
|
||||
if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
|
||||
throw new ValidationException("Invalid OS suffix: {$os_category}");
|
||||
}
|
||||
$clone = clone $this;
|
||||
if (!isset($clone->depends[$os_category])) {
|
||||
$clone->depends[$os_category] = [];
|
||||
}
|
||||
if (!in_array($package, $clone->depends[$os_category], true)) {
|
||||
$clone->depends[$os_category][] = $package;
|
||||
}
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add package suggestion.
|
||||
*
|
||||
* @param string $package Package name
|
||||
* @param string $os_category Operating system ('' for all OSes)
|
||||
*/
|
||||
public function addSuggestion(string $package, string $os_category = ''): static
|
||||
{
|
||||
if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
|
||||
throw new ValidationException("Invalid OS suffix: {$os_category}");
|
||||
}
|
||||
$clone = clone $this;
|
||||
if (!isset($clone->suggests[$os_category])) {
|
||||
$clone->suggests[$os_category] = [];
|
||||
}
|
||||
if (!in_array($package, $clone->suggests[$os_category], true)) {
|
||||
$clone->suggests[$os_category][] = $package;
|
||||
}
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function addStaticLib(string $lib_a, string $os_category = ''): static
|
||||
{
|
||||
if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
|
||||
throw new ValidationException("Invalid OS suffix: {$os_category}");
|
||||
}
|
||||
if (!str_ends_with($lib_a, '.lib') && !str_ends_with($lib_a, '.a')) {
|
||||
throw new ValidationException("Static library must end with .lib or .a, got: {$lib_a}");
|
||||
}
|
||||
if (str_ends_with($lib_a, '.lib') && in_array($os_category, ['unix', 'linux', 'macos'], true)) {
|
||||
throw new ValidationException("Static library with .lib extension cannot be added for non-Windows OS: {$lib_a}");
|
||||
}
|
||||
if (str_ends_with($lib_a, '.a') && $os_category === 'windows') {
|
||||
throw new ValidationException("Static library with .a extension cannot be added for Windows OS: {$lib_a}");
|
||||
}
|
||||
if (isset($this->static_libs[$os_category]) && in_array($lib_a, $this->static_libs[$os_category], true)) {
|
||||
// already exists
|
||||
return $this;
|
||||
}
|
||||
$clone = clone $this;
|
||||
if (!isset($clone->static_libs[$os_category])) {
|
||||
$clone->static_libs[$os_category] = [];
|
||||
}
|
||||
if (!in_array($lib_a, $clone->static_libs[$os_category], true)) {
|
||||
$clone->static_libs[$os_category][] = $lib_a;
|
||||
}
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function addHeader(string $header_file, string $os_category = ''): static
|
||||
{
|
||||
if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
|
||||
throw new ValidationException("Invalid OS suffix: {$os_category}");
|
||||
}
|
||||
$clone = clone $this;
|
||||
if (!isset($clone->headers[$os_category])) {
|
||||
$clone->headers[$os_category] = [];
|
||||
}
|
||||
if (!in_array($header_file, $clone->headers[$os_category], true)) {
|
||||
$clone->headers[$os_category][] = $header_file;
|
||||
}
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function addStaticBin(string $bin_file, string $os_category = ''): static
|
||||
{
|
||||
if (!in_array($os_category, ['', ...SUPPORTED_OS_CATEGORY], true)) {
|
||||
throw new ValidationException("Invalid OS suffix: {$os_category}");
|
||||
}
|
||||
$clone = clone $this;
|
||||
if (!isset($clone->static_bins[$os_category])) {
|
||||
$clone->static_bins[$os_category] = [];
|
||||
}
|
||||
if (!in_array($bin_file, $clone->static_bins[$os_category], true)) {
|
||||
$clone->static_bins[$os_category][] = $bin_file;
|
||||
}
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add package artifact.
|
||||
*
|
||||
* @param ArtifactGenerator $artifactGenerator Artifact generator
|
||||
*/
|
||||
public function addArtifact(ArtifactGenerator $artifactGenerator): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->artifact = $artifactGenerator;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add license from string.
|
||||
*
|
||||
* @param string $text License content
|
||||
*/
|
||||
public function addLicenseFromString(string $text): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->licenses[] = [
|
||||
'type' => 'text',
|
||||
'text' => $text,
|
||||
];
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add license from file.
|
||||
*
|
||||
* @param string $file_path License file path
|
||||
*/
|
||||
public function addLicenseFromFile(string $file_path): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->licenses[] = [
|
||||
'type' => 'file',
|
||||
'path' => $file_path,
|
||||
];
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable build for specific OS.
|
||||
*
|
||||
* @param 'Darwin'|'Linux'|'Windows'|array<'Darwin'|'Linux'|'Windows'> $build_for Build for OS
|
||||
*/
|
||||
public function enableBuild(array|string $build_for, ?string $build_function_name = null): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
if (is_array($build_for)) {
|
||||
foreach ($build_for as $bf) {
|
||||
$clone = $clone->enableBuild($bf, $build_function_name ?? 'build');
|
||||
}
|
||||
return $clone;
|
||||
}
|
||||
if (!in_array($build_for, SUPPORTED_OS_FAMILY, true)) {
|
||||
throw new ValidationException("Unsupported build_for value: {$build_for}");
|
||||
}
|
||||
$clone->build_for_enables[$build_for] = $build_function_name ?? "buildFor{$build_for}";
|
||||
return $clone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind function executor.
|
||||
*
|
||||
* @param string $func_name Function name
|
||||
* @param ExecutorGenerator $executor Executor generator
|
||||
*/
|
||||
public function addFunctionExecutorBinding(string $func_name, ExecutorGenerator $executor): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->func_executor_binding[$func_name] = $executor;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function generatePackageClassFile(string $namespace, bool $uppercase = false): string
|
||||
{
|
||||
$printer = new class extends Printer {
|
||||
public string $indentation = ' ';
|
||||
};
|
||||
$file = new PhpFile();
|
||||
$namespace = $file->setStrictTypes()->addNamespace($namespace);
|
||||
|
||||
$uses = [];
|
||||
|
||||
// class name and package attribute
|
||||
$class_name = str_replace('-', '_', $uppercase ? ucwords($this->package_name, '-') : $this->package_name);
|
||||
$class_attribute = match ($this->type) {
|
||||
'library' => Library::class,
|
||||
'php-extension' => Extension::class,
|
||||
'target', 'virtual-target' => Target::class,
|
||||
};
|
||||
$package_class = match ($this->type) {
|
||||
'library' => LibraryPackage::class,
|
||||
'php-extension' => PhpExtensionPackage::class,
|
||||
'target', 'virtual-target' => TargetPackage::class,
|
||||
};
|
||||
$uses[] = $class_attribute;
|
||||
$uses[] = $package_class;
|
||||
$uses[] = BuildFor::class;
|
||||
$uses[] = PackageInstaller::class;
|
||||
|
||||
foreach ($uses as $use) {
|
||||
$namespace->addUse($use);
|
||||
}
|
||||
|
||||
// add class attribute
|
||||
$class = $namespace->addClass($class_name);
|
||||
$class->addAttribute($class_attribute, [$this->package_name]);
|
||||
|
||||
// add build functions if enabled
|
||||
$funcs = [];
|
||||
foreach ($this->build_for_enables as $os_family => $func_name) {
|
||||
if ($func_name !== null) {
|
||||
$funcs[$func_name][] = $os_family;
|
||||
}
|
||||
}
|
||||
foreach ($funcs as $name => $oss) {
|
||||
$method = $class->addMethod(name: $name ?: 'build')
|
||||
->setPublic()
|
||||
->setReturnType('void');
|
||||
// check if function executor is bound
|
||||
if (isset($this->func_executor_binding[$name])) {
|
||||
$executor = $this->func_executor_binding[$name];
|
||||
[$executor_use, $code] = $executor->generateCode();
|
||||
$namespace->addUse($executor_use);
|
||||
$method->setBody($code);
|
||||
}
|
||||
$method->addParameter('package')->setType($package_class);
|
||||
$method->addParameter('installer')->setType(PackageInstaller::class);
|
||||
foreach ($oss as $os) {
|
||||
$method->addAttribute(BuildFor::class, [$os]);
|
||||
}
|
||||
}
|
||||
|
||||
return $printer->printFile($file);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate package config
|
||||
*/
|
||||
public function generateConfigArray(): array
|
||||
{
|
||||
$config = ['type' => $this->type];
|
||||
|
||||
// Add dependencies
|
||||
foreach ($this->depends as $suffix => $depends) {
|
||||
$k = $suffix !== '' ? "depends@{$suffix}" : 'depends';
|
||||
$config[$k] = $depends;
|
||||
}
|
||||
|
||||
// add suggests
|
||||
foreach ($this->suggests as $suffix => $suggests) {
|
||||
$k = $suffix !== '' ? "suggests@{$suffix}" : 'suggests';
|
||||
$config[$k] = $suggests;
|
||||
}
|
||||
|
||||
// Add frameworks
|
||||
if (!empty($this->frameworks)) {
|
||||
$config['frameworks'] = $this->frameworks;
|
||||
}
|
||||
|
||||
// Add static libs
|
||||
foreach ($this->static_libs as $suffix => $libs) {
|
||||
$k = $suffix !== '' ? "static-libs@{$suffix}" : 'static-libs';
|
||||
$config[$k] = $libs;
|
||||
}
|
||||
|
||||
// Add headers
|
||||
foreach ($this->headers as $suffix => $headers) {
|
||||
$k = $suffix !== '' ? "headers@{$suffix}" : 'headers';
|
||||
$config[$k] = $headers;
|
||||
}
|
||||
|
||||
// Add static bins
|
||||
foreach ($this->static_bins as $suffix => $bins) {
|
||||
$k = $suffix !== '' ? "static-bins@{$suffix}" : 'static-bins';
|
||||
$config[$k] = $bins;
|
||||
}
|
||||
|
||||
// Add artifact
|
||||
if ($this->artifact !== null) {
|
||||
$config['artifact'] = $this->artifact->getName();
|
||||
}
|
||||
|
||||
// Add licenses
|
||||
if (!empty($this->licenses)) {
|
||||
if (count($this->licenses) === 1) {
|
||||
$config['license'] = $this->licenses[0];
|
||||
} else {
|
||||
$config['license'] = $this->licenses;
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
public function setConfigFile(string $config_file): static
|
||||
{
|
||||
$clone = clone $this;
|
||||
$clone->config_file = $config_file;
|
||||
return $clone;
|
||||
}
|
||||
|
||||
public function writeConfigFile(): string
|
||||
{
|
||||
if ($this->config_file === null) {
|
||||
throw new ValidationException('Config file path is not set.');
|
||||
}
|
||||
$config_array = $this->generateConfigArray();
|
||||
$config_file_json = json_decode(FileSystem::readFile($this->config_file), true);
|
||||
if (!is_array($config_file_json)) {
|
||||
throw new ValidationException('Existing config file is not a valid JSON array.');
|
||||
}
|
||||
$config_file_json[$this->package_name] = $config_array;
|
||||
ksort($config_file_json);
|
||||
$json_content = json_encode($config_file_json, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
if ($json_content === false) {
|
||||
throw new ValidationException('Failed to encode package config to JSON.');
|
||||
}
|
||||
if (file_put_contents($this->config_file, $json_content) === false) {
|
||||
throw new FileSystemException("Failed to write config file: {$this->config_file}");
|
||||
}
|
||||
return $this->config_file;
|
||||
}
|
||||
|
||||
public function writeAll(): array
|
||||
{
|
||||
// write config
|
||||
$package_config_file = $this->writeConfigFile();
|
||||
$artifact_config_file = $this->artifact->writeConfigFile();
|
||||
|
||||
// write class file
|
||||
$package_class_file_content = $this->generatePackageClassFile('StaticPHP\Packages');
|
||||
$package_class_file_path = str_replace('-', '_', $this->package_name) . '.php';
|
||||
// file_put_contents($package_class_file_path, $package_class_file_content); // Uncomment this line to actually write the file
|
||||
return [
|
||||
'package_config' => $package_config_file,
|
||||
'artifact_config' => $artifact_config_file,
|
||||
'package_class_file' => $package_class_file_path,
|
||||
'package_class_content' => $package_class_file_content,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,57 @@ declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\Toolchain;
|
||||
|
||||
use StaticPHP\Exception\EnvironmentException;
|
||||
use StaticPHP\Toolchain\Interface\ToolchainInterface;
|
||||
use StaticPHP\Util\GlobalEnvManager;
|
||||
use StaticPHP\Util\System\WindowsUtil;
|
||||
|
||||
class MSVCToolchain implements ToolchainInterface
|
||||
{
|
||||
public function initEnv(): void {}
|
||||
public function initEnv(): void
|
||||
{
|
||||
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\bin');
|
||||
$sdk = getenv('PHP_SDK_PATH');
|
||||
if ($sdk !== false) {
|
||||
GlobalEnvManager::addPathIfNotExists($sdk . '\bin');
|
||||
GlobalEnvManager::addPathIfNotExists($sdk . '\msys2\usr\bin');
|
||||
}
|
||||
// strawberry-perl
|
||||
if (is_dir(PKG_ROOT_PATH . '\strawberry-perl')) {
|
||||
GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl\perl\bin');
|
||||
}
|
||||
}
|
||||
|
||||
public function afterInit(): void {}
|
||||
public function afterInit(): void
|
||||
{
|
||||
$count = count(getenv());
|
||||
$vs = WindowsUtil::findVisualStudio();
|
||||
if ($vs === false || !file_exists($vcvarsall = "{$vs['dir']}\\VC\\Auxiliary\\Build\\vcvarsall.bat")) {
|
||||
throw new EnvironmentException(
|
||||
'Visual Studio with C++ tools not found',
|
||||
'Please install Visual Studio with C++ tools'
|
||||
);
|
||||
}
|
||||
if (getenv('VCINSTALLDIR') === false) {
|
||||
if (file_exists(DOWNLOAD_PATH . '/.vcenv-cache') && (time() - filemtime(DOWNLOAD_PATH . '/.vcenv-cache')) < 3600) {
|
||||
$output = file(DOWNLOAD_PATH . '/.vcenv-cache', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
} else {
|
||||
exec('call "' . $vcvarsall . '" x64 > NUL && set', $output);
|
||||
file_put_contents(DOWNLOAD_PATH . '/.vcenv-cache', implode("\n", $output));
|
||||
}
|
||||
array_map(fn ($x) => putenv($x), $output);
|
||||
}
|
||||
$after = count(getenv());
|
||||
if ($after > $count) {
|
||||
logger()->debug('Applied ' . ($after - $count) . ' environment variables from Visual Studio setup');
|
||||
}
|
||||
}
|
||||
|
||||
public function getCompilerInfo(): ?string
|
||||
{
|
||||
if ($vcver = getenv('VisualStudioVersion')) {
|
||||
return "Visual Studio {$vcver}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,7 @@ class FileSystem
|
||||
$src_path = FileSystem::convertPath($from);
|
||||
switch (PHP_OS_FAMILY) {
|
||||
case 'Windows':
|
||||
f_passthru('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i');
|
||||
cmd(false)->exec('xcopy "' . $src_path . '" "' . $dst_path . '" /s/e/v/y/i');
|
||||
break;
|
||||
case 'Linux':
|
||||
case 'Darwin':
|
||||
@@ -137,7 +137,7 @@ class FileSystem
|
||||
* @param string $from Source file path
|
||||
* @param string $to Destination file path
|
||||
*/
|
||||
public static function copy(string $from, string $to): void
|
||||
public static function copy(string $from, string $to): bool
|
||||
{
|
||||
logger()->debug("Copying file from {$from} to {$to}");
|
||||
$dst_path = FileSystem::convertPath($to);
|
||||
@@ -145,6 +145,7 @@ class FileSystem
|
||||
if (!copy($src_path, $dst_path)) {
|
||||
throw new FileSystemException('Cannot copy file from ' . $src_path . ' to ' . $dst_path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,6 +267,9 @@ class FileSystem
|
||||
if ($auto_require && !class_exists($class_name, false)) {
|
||||
require_once $file_path;
|
||||
}
|
||||
if (class_exists($class_name, false) === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_string($return_path_value)) {
|
||||
$classes[$class_name] = $return_path_value . '/' . $v;
|
||||
@@ -317,7 +321,12 @@ class FileSystem
|
||||
}
|
||||
} elseif (is_link($sub_file) || is_file($sub_file)) {
|
||||
if (!unlink($sub_file)) {
|
||||
return false;
|
||||
$cmd = PHP_OS_FAMILY === 'Windows' ? 'del /f /q' : 'rm -f';
|
||||
f_exec("{$cmd} " . escapeshellarg($sub_file), $out, $ret);
|
||||
if ($ret !== 0) {
|
||||
logger()->warning('Remove file failed: ' . $sub_file);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,6 +472,23 @@ class FileSystem
|
||||
return file_put_contents($file, implode('', $lines));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return full path, resolving relative paths against a base path.
|
||||
*
|
||||
* @param string $path Input path (relative or absolute)
|
||||
* @param string $relative_path_base Base path for relative paths
|
||||
*/
|
||||
public static function fullpath(string $path, string $relative_path_base): string
|
||||
{
|
||||
if (FileSystem::isRelativePath($path)) {
|
||||
$path = $relative_path_base . DIRECTORY_SEPARATOR . $path;
|
||||
}
|
||||
if (!file_exists($path)) {
|
||||
throw new FileSystemException("Path does not exist: {$path}");
|
||||
}
|
||||
return FileSystem::convertPath($path);
|
||||
}
|
||||
|
||||
private static function replaceFile(string $filename, int $replace_type = REPLACE_FILE_STR, mixed $callback_or_search = null, mixed $to_replace = null): false|int
|
||||
{
|
||||
logger()->debug('Replacing file with type[' . $replace_type . ']: ' . $filename);
|
||||
|
||||
@@ -107,6 +107,8 @@ class GlobalEnvManager
|
||||
{
|
||||
if (SystemTarget::isUnix() && !str_contains(getenv('PATH'), $path)) {
|
||||
self::putenv("PATH={$path}:" . getenv('PATH'));
|
||||
} elseif (SystemTarget::getTargetOS() === 'Windows' && !str_contains(getenv('PATH'), $path)) {
|
||||
self::putenv("PATH={$path};" . getenv('PATH'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace StaticPHP\Util;
|
||||
use StaticPHP\DI\ApplicationContext;
|
||||
use Symfony\Component\Console\Helper\ProgressIndicator;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use ZM\Logger\ConsoleColor;
|
||||
|
||||
@@ -16,41 +17,50 @@ class InteractiveTerm
|
||||
|
||||
public static function notice(string $message, bool $indent = false): void
|
||||
{
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class);
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput();
|
||||
if ($output->isVerbose()) {
|
||||
logger()->notice(strip_ansi_colors($message));
|
||||
} else {
|
||||
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::cyan(($indent ? ' ' : '') . '▶ ') . $message));
|
||||
logger()->debug(strip_ansi_colors($message));
|
||||
}
|
||||
}
|
||||
|
||||
public static function success(string $message, bool $indent = false): void
|
||||
{
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class);
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput();
|
||||
if ($output->isVerbose()) {
|
||||
logger()->info(strip_ansi_colors($message));
|
||||
} else {
|
||||
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message));
|
||||
logger()->debug(strip_ansi_colors($message));
|
||||
}
|
||||
}
|
||||
|
||||
public static function plain(string $message): void
|
||||
public static function plain(string $message, string $level = 'info'): void
|
||||
{
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class);
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput();
|
||||
if ($output->isVerbose()) {
|
||||
logger()->info(strip_ansi_colors($message));
|
||||
match ($level) {
|
||||
'debug' => logger()->debug(strip_ansi_colors($message)),
|
||||
'notice' => logger()->notice(strip_ansi_colors($message)),
|
||||
'warning' => logger()->warning(strip_ansi_colors($message)),
|
||||
'error' => logger()->error(strip_ansi_colors($message)),
|
||||
default => logger()->info(strip_ansi_colors($message)),
|
||||
};
|
||||
} else {
|
||||
$output = $level === 'error' && $output instanceof ConsoleOutput ? $output->getErrorOutput() : $output;
|
||||
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')($message));
|
||||
}
|
||||
}
|
||||
|
||||
public static function info(string $message): void
|
||||
{
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class);
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput();
|
||||
if (!$output->isVerbose()) {
|
||||
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green('▶ ') . $message));
|
||||
}
|
||||
@@ -59,12 +69,13 @@ class InteractiveTerm
|
||||
|
||||
public static function error(string $message, bool $indent = true): void
|
||||
{
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class);
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput();
|
||||
if ($output->isVerbose()) {
|
||||
logger()->error(strip_ansi_colors($message));
|
||||
} else {
|
||||
$output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message)));
|
||||
logger()->debug(strip_ansi_colors($message));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,15 +86,16 @@ class InteractiveTerm
|
||||
|
||||
public static function setMessage(string $message): void
|
||||
{
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
|
||||
self::$indicator?->setMessage(($no_ansi ? 'strip_ansi_colors' : 'strval')($message));
|
||||
logger()->debug(strip_ansi_colors($message));
|
||||
}
|
||||
|
||||
public static function finish(string $message, bool $status = true): void
|
||||
{
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
|
||||
$message = $no_ansi ? strip_ansi_colors($message) : $message;
|
||||
$output = ApplicationContext::get(OutputInterface::class);
|
||||
$output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput();
|
||||
if ($output->isVerbose()) {
|
||||
if ($status) {
|
||||
logger()->info($message);
|
||||
@@ -104,8 +116,8 @@ class InteractiveTerm
|
||||
|
||||
public static function indicateProgress(string $message): void
|
||||
{
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class);
|
||||
$no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false;
|
||||
$output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput();
|
||||
if ($output->isVerbose()) {
|
||||
logger()->info(strip_ansi_colors($message));
|
||||
return;
|
||||
@@ -116,6 +128,7 @@ class InteractiveTerm
|
||||
self::$indicator->advance();
|
||||
return;
|
||||
}
|
||||
logger()->debug(strip_ansi_colors($message));
|
||||
// if no ansi, use a dot instead of spinner
|
||||
if ($no_ansi) {
|
||||
self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' •', ' •']);
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace StaticPHP\Util;
|
||||
|
||||
use StaticPHP\Attribute\PatchDescription;
|
||||
use StaticPHP\Exception\PatchException;
|
||||
use StaticPHP\Registry\PackageLoader;
|
||||
|
||||
/**
|
||||
* SourcePatcher provides static utility methods for patching source files.
|
||||
@@ -194,4 +195,69 @@ class SourcePatcher
|
||||
{
|
||||
FileSystem::restoreBackupFile(SOURCE_PATH . '/php-src/ext/phar/phar.c');
|
||||
}
|
||||
|
||||
public static function patchPhpSrc(?array $items = null): bool
|
||||
{
|
||||
$patch_dir = ROOT_DIR . '/src/globals/patch/php-src-patches';
|
||||
// in phar mode, we need to extract all the patch files
|
||||
if (str_starts_with($patch_dir, 'phar://')) {
|
||||
$tmp_dir = sys_get_temp_dir() . '/php-src-patches';
|
||||
FileSystem::createDir($tmp_dir);
|
||||
foreach (FileSystem::scanDirFiles($patch_dir) as $file) {
|
||||
FileSystem::writeFile("{$tmp_dir}/" . basename($file), file_get_contents($file));
|
||||
}
|
||||
$patch_dir = $tmp_dir;
|
||||
}
|
||||
$php_package = PackageLoader::getTargetPackage('php');
|
||||
if (!file_exists("{$php_package->getSourceDir()}/sapi/micro/php_micro.c")) {
|
||||
return false;
|
||||
}
|
||||
$ver_file = "{$php_package->getSourceDir()}/main/php_version.h";
|
||||
if (!file_exists($ver_file)) {
|
||||
throw new PatchException('php-src patcher (original micro patches)', 'Patch failed, cannot find php source files');
|
||||
}
|
||||
$version_h = FileSystem::readFile("{$php_package->getSourceDir()}/main/php_version.h");
|
||||
preg_match('/#\s*define\s+PHP_MAJOR_VERSION\s+(\d+)\s+#\s*define\s+PHP_MINOR_VERSION\s+(\d+)\s+/m', $version_h, $match);
|
||||
// $ver = "{$match[1]}.{$match[2]}";
|
||||
|
||||
$major_ver = $match[1] . $match[2];
|
||||
if ($major_ver === '74') {
|
||||
return false;
|
||||
}
|
||||
// $check = !defined('DEBUG_MODE') ? ' -q' : '';
|
||||
// f_passthru('cd ' . SOURCE_PATH . '/php-src && git checkout' . $check . ' HEAD');
|
||||
|
||||
if ($items !== null) {
|
||||
$spc_micro_patches = $items;
|
||||
} else {
|
||||
$spc_micro_patches = getenv('SPC_MICRO_PATCHES');
|
||||
$spc_micro_patches = $spc_micro_patches === false ? [] : explode(',', $spc_micro_patches);
|
||||
}
|
||||
$spc_micro_patches = array_filter($spc_micro_patches, fn ($item) => trim((string) $item) !== '');
|
||||
$patch_list = $spc_micro_patches;
|
||||
$patches = [];
|
||||
$serial = ['80', '81', '82', '83', '84', '85'];
|
||||
foreach ($patch_list as $patchName) {
|
||||
if (file_exists("{$patch_dir}/{$patchName}.patch")) {
|
||||
$patches[] = "{$patch_dir}/{$patchName}.patch";
|
||||
continue;
|
||||
}
|
||||
for ($i = array_search($major_ver, $serial, true); $i >= 0; --$i) {
|
||||
$tryMajMin = $serial[$i];
|
||||
if (!file_exists("{$patch_dir}/{$patchName}_{$tryMajMin}.patch")) {
|
||||
continue;
|
||||
}
|
||||
$patches[] = "{$patch_dir}/{$patchName}_{$tryMajMin}.patch";
|
||||
continue 2;
|
||||
}
|
||||
throw new PatchException('phpmicro patches', "Failed finding patch file or versioned file {$patchName} !");
|
||||
}
|
||||
|
||||
foreach ($patches as $patch) {
|
||||
logger()->info("Patching micro with {$patch}");
|
||||
self::patchFile($patch, $php_package->getSourceDir());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,13 +15,10 @@ class WindowsUtil
|
||||
* @param array $paths search path (default use env path)
|
||||
* @return null|string null if not found, string is absolute path
|
||||
*/
|
||||
public static function findCommand(string $name, array $paths = [], bool $include_sdk_bin = false): ?string
|
||||
public static function findCommand(string $name, array $paths = []): ?string
|
||||
{
|
||||
if (!$paths) {
|
||||
$paths = explode(PATH_SEPARATOR, getenv('Path'));
|
||||
if ($include_sdk_bin) {
|
||||
$paths[] = getenv('PHP_SDK_PATH') . '\bin';
|
||||
}
|
||||
}
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path . DIRECTORY_SEPARATOR . $name)) {
|
||||
@@ -34,29 +31,35 @@ class WindowsUtil
|
||||
/**
|
||||
* Find Visual Studio installation.
|
||||
*
|
||||
* @return array<string, string>|false False if not installed, array contains 'version' and 'dir'
|
||||
* @return array{
|
||||
* version: string,
|
||||
* major_version: string,
|
||||
* dir: string
|
||||
* }|false False if not installed, array contains 'version' and 'dir'
|
||||
*/
|
||||
public static function findVisualStudio(): array|false
|
||||
{
|
||||
$check_path = [
|
||||
'C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
|
||||
'C:\Program Files\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
|
||||
'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs17',
|
||||
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
|
||||
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
|
||||
'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe' => 'vs16',
|
||||
// call vswhere (need VS and C++ tools installed), output is json
|
||||
$vswhere_exec = PKG_ROOT_PATH . DIRECTORY_SEPARATOR . 'bin' . DIRECTORY_SEPARATOR . 'vswhere.exe';
|
||||
$args = [
|
||||
'-latest',
|
||||
'-format', 'json',
|
||||
'-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64',
|
||||
];
|
||||
foreach ($check_path as $path => $vs_version) {
|
||||
if (file_exists($path)) {
|
||||
$vs_ver = $vs_version;
|
||||
$d_dir = dirname($path, 4);
|
||||
return [
|
||||
'version' => $vs_ver,
|
||||
'dir' => $d_dir,
|
||||
];
|
||||
}
|
||||
$cmd = escapeshellarg($vswhere_exec) . ' ' . implode(' ', $args);
|
||||
$result = f_exec($cmd, $out, $code);
|
||||
if ($code !== 0 || !$result) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
$json = json_decode(implode("\n", $out), true);
|
||||
if (!is_array($json) || count($json) === 0) {
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
'version' => $json[0]['installationVersion'],
|
||||
'major_version' => explode('.', $json[0]['installationVersion'])[0],
|
||||
'dir' => $json[0]['installationPath'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,13 +96,32 @@ const SPC_STATUS_ALREADY_BUILT = 1;
|
||||
|
||||
const SPC_DOWNLOAD_TYPE_DISPLAY_NAME = [
|
||||
'bitbuckettag' => 'BitBucket',
|
||||
'filelist' => 'website',
|
||||
'filelist' => 'File index website',
|
||||
'git' => 'git',
|
||||
'ghrel' => 'GitHub release',
|
||||
'ghtar', 'ghtagtar' => 'GitHub tarball',
|
||||
'ghtar' => 'GitHub release tarball',
|
||||
'ghtagtar' => 'GitHub tag tarball',
|
||||
'local' => 'local dir',
|
||||
'pie' => 'PHP Installer for Extensions',
|
||||
'pie' => 'PHP Installer for Extensions (PIE)',
|
||||
'url' => 'url',
|
||||
'php-release' => 'php.net',
|
||||
'custom' => 'custom downloader',
|
||||
];
|
||||
|
||||
const SUPPORTED_OS_CATEGORY = [
|
||||
'unix',
|
||||
'windows',
|
||||
'linux',
|
||||
'macos',
|
||||
];
|
||||
|
||||
const SUPPORTED_OS_FAMILY = [
|
||||
'Linux',
|
||||
'Darwin',
|
||||
'Windows',
|
||||
];
|
||||
|
||||
const SPC_MODE_SOURCE = 1;
|
||||
const SPC_MODE_VENDOR = 2;
|
||||
const SPC_MODE_PHAR = 4;
|
||||
const SPC_MODE_VENDOR_PHAR = SPC_MODE_VENDOR | SPC_MODE_PHAR;
|
||||
|
||||
@@ -10,6 +10,31 @@ use StaticPHP\Runtime\Shell\UnixShell;
|
||||
use StaticPHP\Runtime\Shell\WindowsCmd;
|
||||
use ZM\Logger\ConsoleLogger;
|
||||
|
||||
/**
|
||||
* Get the current SPC loading mode. If passed a mode to check, will return whether current mode matches the given mode.
|
||||
*/
|
||||
function spc_mode(?int $check_mode = null): bool|int
|
||||
{
|
||||
$mode = SPC_MODE_SOURCE;
|
||||
// if current file is in phar, then it's phar mode
|
||||
if (str_starts_with(__FILE__, 'phar://') && Phar::running()) {
|
||||
// judge whether it's vendor mode (inside vendor/) or source mode (inside src/)
|
||||
if (basename(dirname(__FILE__, 3)) === 'static-php-cli' && basename(dirname(__FILE__, 5)) === 'vendor') {
|
||||
$mode = SPC_MODE_VENDOR_PHAR;
|
||||
} else {
|
||||
$mode = SPC_MODE_PHAR;
|
||||
}
|
||||
} elseif (basename(dirname(__FILE__, 3)) === 'static-php-cli' && basename(dirname(__FILE__, 5)) === 'vendor') {
|
||||
$mode = SPC_MODE_VENDOR;
|
||||
}
|
||||
|
||||
if ($check_mode === null) {
|
||||
return $mode;
|
||||
}
|
||||
// use bitwise AND to check mode
|
||||
return ($mode & $check_mode) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Judge if an array is an associative array
|
||||
*/
|
||||
|
||||
@@ -26,7 +26,7 @@ class ArtifactConfigTest extends TestCase
|
||||
$reflection = new \ReflectionClass(ArtifactConfig::class);
|
||||
$property = $reflection->getProperty('artifact_configs');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, []);
|
||||
$property->setValue([]);
|
||||
}
|
||||
|
||||
/** @noinspection PhpExpressionResultUnusedInspection */
|
||||
@@ -42,7 +42,7 @@ class ArtifactConfigTest extends TestCase
|
||||
$reflection = new \ReflectionClass(ArtifactConfig::class);
|
||||
$property = $reflection->getProperty('artifact_configs');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, []);
|
||||
$property->setValue([]);
|
||||
}
|
||||
|
||||
public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void
|
||||
|
||||
@@ -26,7 +26,7 @@ class PackageConfigTest extends TestCase
|
||||
$reflection = new \ReflectionClass(PackageConfig::class);
|
||||
$property = $reflection->getProperty('package_configs');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, []);
|
||||
$property->setValue([]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
@@ -41,7 +41,7 @@ class PackageConfigTest extends TestCase
|
||||
$reflection = new \ReflectionClass(PackageConfig::class);
|
||||
$property = $reflection->getProperty('package_configs');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, []);
|
||||
$property->setValue([]);
|
||||
}
|
||||
|
||||
public function testLoadFromDirThrowsExceptionWhenDirectoryDoesNotExist(): void
|
||||
|
||||
@@ -7,15 +7,15 @@ namespace Tests\StaticPHP\DI;
|
||||
use DI\Container;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use StaticPHP\DI\CallbackInvoker;
|
||||
use StaticPHP\Exception\SPCInternalException;
|
||||
|
||||
/**
|
||||
* Helper class that requires constructor parameters for testing
|
||||
*/
|
||||
readonly class UnresolvableTestClass
|
||||
class UnresolvableTestClass
|
||||
{
|
||||
/** @noinspection PhpPropertyOnlyWrittenInspection */
|
||||
public function __construct(private string $requiredParam) {}
|
||||
public function __construct(
|
||||
private string $requiredParam
|
||||
) {}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,7 +92,7 @@ class CallbackInvokerTest extends TestCase
|
||||
|
||||
// Should not resolve from container as 'test.service' is not a type
|
||||
// Will try default value or null
|
||||
$this->expectException(SPCInternalException::class);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->invoker->invoke($callback);
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ class CallbackInvokerTest extends TestCase
|
||||
return $required;
|
||||
};
|
||||
|
||||
$this->expectException(SPCInternalException::class);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage("Cannot resolve parameter 'required' of type 'string'");
|
||||
$this->invoker->invoke($callback);
|
||||
}
|
||||
@@ -527,7 +527,7 @@ class CallbackInvokerTest extends TestCase
|
||||
$callback = eval('return function (string|int $param) { return $param; };');
|
||||
|
||||
// Union types are not ReflectionNamedType, should not be resolved from container
|
||||
$this->expectException(SPCInternalException::class);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->invoker->invoke($callback);
|
||||
}
|
||||
|
||||
@@ -594,7 +594,7 @@ class CallbackInvokerTest extends TestCase
|
||||
return $obj;
|
||||
};
|
||||
|
||||
$this->expectException(SPCInternalException::class);
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage("Cannot resolve parameter 'obj'");
|
||||
|
||||
$this->invoker->invoke($callback);
|
||||
|
||||
@@ -377,6 +377,10 @@ class PackageLoaderTest extends TestCase
|
||||
$this->createTestPackageConfig('test-lib', 'library');
|
||||
PackageLoader::initPackageInstances();
|
||||
|
||||
// Add a build function for current OS so the stage validation is triggered
|
||||
$package = PackageLoader::getPackage('test-lib');
|
||||
$package->addBuildFunction(PHP_OS_FAMILY, fn () => null);
|
||||
|
||||
// Manually add a before_stage for non-existent stage
|
||||
$reflection = new \ReflectionClass(PackageLoader::class);
|
||||
$property = $reflection->getProperty('before_stages');
|
||||
@@ -417,6 +421,33 @@ class PackageLoaderTest extends TestCase
|
||||
PackageLoader::checkLoadedStageEvents();
|
||||
}
|
||||
|
||||
public function testCheckLoadedStageEventsDoesNotThrowForNonCurrentOSPackage(): void
|
||||
{
|
||||
$this->createTestPackageConfig('test-lib', 'library');
|
||||
PackageLoader::initPackageInstances();
|
||||
|
||||
// Add a build function for a different OS (not current OS)
|
||||
$package = PackageLoader::getPackage('test-lib');
|
||||
$otherOS = PHP_OS_FAMILY === 'Windows' ? 'Linux' : 'Windows';
|
||||
$package->addBuildFunction($otherOS, fn () => null);
|
||||
|
||||
// Manually add a before_stage for 'build' stage
|
||||
// This should NOT throw an exception because the package has no build function for current OS
|
||||
$reflection = new \ReflectionClass(PackageLoader::class);
|
||||
$property = $reflection->getProperty('before_stages');
|
||||
$property->setAccessible(true);
|
||||
$property->setValue(null, [
|
||||
'test-lib' => [
|
||||
'build' => [[fn () => null, null]],
|
||||
],
|
||||
]);
|
||||
|
||||
// This should not throw an exception
|
||||
PackageLoader::checkLoadedStageEvents();
|
||||
|
||||
$this->assertTrue(true); // If we get here, the test passed
|
||||
}
|
||||
|
||||
public function testGetBeforeStageCallbacksReturnsCallbacks(): void
|
||||
{
|
||||
PackageLoader::initPackageInstances();
|
||||
@@ -502,13 +533,13 @@ class PackageLoaderTest extends TestCase
|
||||
mkdir($psr4Dir, 0755, true);
|
||||
|
||||
// Create test class file
|
||||
$classContent = '<?php
|
||||
namespace Test\Package;
|
||||
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
|
||||
#[Library("test-lib")]
|
||||
class TestPackage1 {
|
||||
$classContent = '<?php
|
||||
namespace Test\Package;
|
||||
|
||||
use StaticPHP\Attribute\Package\Library;
|
||||
|
||||
#[Library("test-lib")]
|
||||
class TestPackage1 {
|
||||
}';
|
||||
file_put_contents($psr4Dir . '/TestPackage1.php', $classContent);
|
||||
|
||||
|
||||
@@ -4,6 +4,6 @@ declare(strict_types=1);
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
require_once __DIR__ . '/../src/bootstrap.php';
|
||||
\StaticPHP\Registry\Registry::checkLoadedRegistries();
|
||||
\StaticPHP\Registry\Registry::resolve();
|
||||
|
||||
logger()->setLevel(LogLevel::ERROR);
|
||||
|
||||
Reference in New Issue
Block a user