mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-19 06:45:35 +08:00
Compare commits
102 Commits
2.6.0
...
a2f0640b1b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2f0640b1b | ||
|
|
16a4245ad7 | ||
|
|
608a5559ac | ||
|
|
dfac385d21 | ||
|
|
40d602c82e | ||
|
|
fbd6360bda | ||
|
|
a0047e3ad7 | ||
|
|
437d6810b7 | ||
|
|
82ec7733ba | ||
|
|
1357990c4c | ||
|
|
61a9264802 | ||
|
|
4a70f260f3 | ||
|
|
bcea2007bd | ||
|
|
fcf2c967ab | ||
|
|
2f8e225abd | ||
|
|
5f3f999222 | ||
|
|
2bc9fef758 | ||
|
|
aec03b2f24 | ||
|
|
ba6ed137c6 | ||
|
|
32dc5d3cdb | ||
|
|
597db25178 | ||
|
|
c2d6b9ad2c | ||
|
|
4977286936 | ||
|
|
cc09184183 | ||
|
|
a9713c3bfa | ||
|
|
3af40a66dd | ||
|
|
6e70f16e1b | ||
|
|
16fccf8184 | ||
|
|
804468f7b9 | ||
|
|
7dc3b7c8ac | ||
|
|
b42409efd1 | ||
|
|
cb010d81ac | ||
|
|
15979d4636 | ||
|
|
1a164fa057 | ||
|
|
a76f49f927 | ||
|
|
becee5b426 | ||
|
|
4ecaffd908 | ||
|
|
74b1dda884 | ||
|
|
d6858e18df | ||
|
|
92284e92c9 | ||
|
|
f709f3bb18 | ||
|
|
8e2dffc3b5 | ||
|
|
ae569316ff | ||
|
|
eee2ff6d61 | ||
|
|
65b828c424 | ||
|
|
f10ba86218 | ||
|
|
24e19deb58 | ||
|
|
f7a3f80689 | ||
|
|
b4168d09b5 | ||
|
|
ba0ea5b40a | ||
|
|
04cefda66c | ||
|
|
8c6a708764 | ||
|
|
a1e76d9d02 | ||
|
|
82ee6f0dee | ||
|
|
d58534b07d | ||
|
|
f37c863092 | ||
|
|
e71f76288b | ||
|
|
d094824d76 | ||
|
|
d635b10e24 | ||
|
|
dca43d6d8d | ||
|
|
abf3bfb98e | ||
|
|
c46f8513dd | ||
|
|
92338d478e | ||
|
|
c1e68323c7 | ||
|
|
f64eb0dea5 | ||
|
|
c1870af1b1 | ||
|
|
71783088c0 | ||
|
|
7057a135cf | ||
|
|
5cb107b844 | ||
|
|
57b22782d3 | ||
|
|
cb0a90d1d9 | ||
|
|
1c439a01a1 | ||
|
|
2bfc8e92ef | ||
|
|
68548cf248 | ||
|
|
3a64feefd0 | ||
|
|
da75d2d707 | ||
|
|
3a85d96fa4 | ||
|
|
883cc4b6fd | ||
|
|
aa61a9e77b | ||
|
|
9de5c62136 | ||
|
|
7b3ea7e12e | ||
|
|
bafa67c8de | ||
|
|
0d3a80e582 | ||
|
|
5a401a5f92 | ||
|
|
0e88cdb258 | ||
|
|
e5cd3adf97 | ||
|
|
6253b7a912 | ||
|
|
fe455bf901 | ||
|
|
3a0d21eb44 | ||
|
|
45ec0cef24 | ||
|
|
1468bb99f0 | ||
|
|
3efabee153 | ||
|
|
962de5b25f | ||
|
|
b265d6dd56 | ||
|
|
302cf8345d | ||
|
|
d249391816 | ||
|
|
0a24a6af1f | ||
|
|
52f40b7f9f | ||
|
|
8fbe6ee8ff | ||
|
|
5a3a8db772 | ||
|
|
56cd6711ce | ||
|
|
29339b962c |
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -7,7 +7,11 @@
|
||||
> If your PR involves the changes mentioned below and completed the action, please tick the corresponding option.
|
||||
> If a modification is not involved, please skip it directly.
|
||||
|
||||
- [ ] If you modified `*.php`, run `composer cs-fix` at local machine.
|
||||
- [ ] If it's an extension or dependency update, make sure adding related extensions in `src/global/test-extensions.php`.
|
||||
- [ ] If you changed the behavior of static-php-cli, update docs in `./docs/`.
|
||||
- [ ] If you updated `config/xxx.json` content, run `bin/spc dev:sort-config xxx`.
|
||||
- If you modified `*.php` or `*.json`, run them locally to ensure your changes are valid:
|
||||
- [ ] `PHP_CS_FIXER_IGNORE_ENV=1 composer cs-fix`
|
||||
- [ ] `composer analyse`
|
||||
- [ ] `composer test`
|
||||
- [ ] `bin/spc dev:sort-config`
|
||||
- If it's an extension or dependency update, please ensure the following:
|
||||
- [ ] Add your test combination to `src/globals/test-extensions.php`.
|
||||
- [ ] If adding new or fixing bugs, add commit message containing `extension test` or `test extensions` to trigger full test suite.
|
||||
|
||||
8
.github/workflows/ext-matrix-tests.yml
vendored
8
.github/workflows/ext-matrix-tests.yml
vendored
@@ -1,16 +1,14 @@
|
||||
name: "Extension matrix tests"
|
||||
name: "Extension Matrix Tests"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- '.github/workflows/ext-matrix-tests.yml'
|
||||
push:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})"
|
||||
runs-on: ${{ matrix.operating-system }}
|
||||
if: contains(github.event.head_commit.message, 'extension test') || contains(github.event.head_commit.message, 'test extensions')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -278,7 +278,7 @@ bin/spc micro:combine my-app.phar -I "memory_limit=4G" -I "disable_functions=sys
|
||||
|
||||
如果你知道 [embed SAPI](https://github.com/php/php-src/tree/master/sapi/embed),你应该知道如何使用它。对于有可能编译用到引入其他库的问题,你可以使用 `buildroot/bin/php-config` 来获取编译时的配置。
|
||||
|
||||
另外,有关如何使用此功能的高级示例,请查看[如何使用它构建 FrankenPHP 的静态版本](https://github.com/dunglas/frankenphp/blob/main/docs/static.md)。
|
||||
另外,有关如何使用此功能的高级示例,请查看[如何使用它构建 FrankenPHP 的静态版本](https://github.com/php/frankenphp/blob/main/docs/static.md)。
|
||||
|
||||
## 贡献
|
||||
|
||||
|
||||
@@ -302,7 +302,7 @@ If you know [embed SAPI](https://github.com/php/php-src/tree/master/sapi/embed),
|
||||
You may require the introduction of other libraries during compilation,
|
||||
you can use `buildroot/bin/php-config` to obtain the compile-time configuration.
|
||||
|
||||
For an advanced example of how to use this feature, take a look at [how to use it to build a static version of FrankenPHP](https://github.com/dunglas/frankenphp/blob/main/docs/static.md).
|
||||
For an advanced example of how to use this feature, take a look at [how to use it to build a static version of FrankenPHP](https://github.com/php/frankenphp/blob/main/docs/static.md).
|
||||
|
||||
## Contribution
|
||||
|
||||
|
||||
@@ -84,7 +84,8 @@ RUN apk update; \
|
||||
wget \
|
||||
xz \
|
||||
gettext-dev \
|
||||
binutils-gold
|
||||
binutils-gold \
|
||||
patchelf
|
||||
|
||||
RUN curl -#fSL https://dl.static-php.dev/static-php-cli/bulk/php-8.4.4-cli-linux-\$(uname -m).tar.gz | tar -xz -C /usr/local/bin && \
|
||||
chmod +x /usr/local/bin/php
|
||||
|
||||
@@ -74,6 +74,11 @@ RUN echo "source scl_source enable devtoolset-10" >> /etc/bashrc
|
||||
RUN source /etc/bashrc
|
||||
RUN yum install -y which
|
||||
|
||||
RUN curl -fsSL -o patchelf.tgz https://github.com/NixOS/patchelf/releases/download/0.18.0/patchelf-0.18.0-$BASE_ARCH.tar.gz && \
|
||||
mkdir -p /patchelf && \
|
||||
tar -xzf patchelf.tgz -C /patchelf --strip-components=1 && \
|
||||
cp /patchelf/bin/patchelf /usr/bin/
|
||||
|
||||
RUN curl -o cmake.tgz -fsSL https://github.com/Kitware/CMake/releases/download/v3.31.4/cmake-3.31.4-linux-$BASE_ARCH.tar.gz && \
|
||||
mkdir /cmake && \
|
||||
tar -xzf cmake.tgz -C /cmake --strip-components 1
|
||||
@@ -89,7 +94,7 @@ ENV PATH="/app/bin:/cmake/bin:$PATH"
|
||||
ENV SPC_LIBC=glibc
|
||||
|
||||
ADD ./config/env.ini /app/config/env.ini
|
||||
RUN bin/spc doctor --auto-fix --debug
|
||||
RUN CC=gcc bin/spc doctor --auto-fix --debug
|
||||
|
||||
RUN curl -o make.tgz -fsSL https://ftp.gnu.org/gnu/make/make-4.4.tar.gz && \
|
||||
tar -zxvf make.tgz && \
|
||||
@@ -136,7 +141,7 @@ echo 'CC=/opt/rh/devtoolset-10/root/usr/bin/gcc' > /tmp/spc-gnu-docker.env
|
||||
echo 'CXX=/opt/rh/devtoolset-10/root/usr/bin/g++' >> /tmp/spc-gnu-docker.env
|
||||
echo 'AR=/opt/rh/devtoolset-10/root/usr/bin/ar' >> /tmp/spc-gnu-docker.env
|
||||
echo 'LD=/opt/rh/devtoolset-10/root/usr/bin/ld' >> /tmp/spc-gnu-docker.env
|
||||
echo 'SPC_DEFAULT_C_FLAGS=-fPIE -fPIC' >> /tmp/spc-gnu-docker.env
|
||||
echo 'SPC_DEFAULT_C_FLAGS=-fPIC' >> /tmp/spc-gnu-docker.env
|
||||
echo 'SPC_LIBC=glibc' >> /tmp/spc-gnu-docker.env
|
||||
echo 'SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM="-Wl,-O1 -pie"' >> /tmp/spc-gnu-docker.env
|
||||
echo 'SPC_CMD_VAR_PHP_MAKE_EXTRA_LIBS="-ldl -lpthread -lm -lresolv -lutil -lrt"' >> /tmp/spc-gnu-docker.env
|
||||
|
||||
@@ -42,6 +42,9 @@ SPC_CONCURRENCY=${CPU_COUNT}
|
||||
SPC_SKIP_PHP_VERSION_CHECK="no"
|
||||
; Ignore some check item for bin/spc doctor command, comma separated (e.g. SPC_SKIP_DOCTOR_CHECK_ITEMS="if homebrew has installed")
|
||||
SPC_SKIP_DOCTOR_CHECK_ITEMS=""
|
||||
; extra modules that xcaddy will include in the FrankenPHP build
|
||||
SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES="--with github.com/dunglas/frankenphp/caddy --with github.com/dunglas/mercure/caddy --with github.com/dunglas/vulcain/caddy --with github.com/dunglas/caddy-cbrotli"
|
||||
|
||||
; EXTENSION_DIR where the built php will look for extension when a .ini instructs to load them
|
||||
; only useful for builds targeting not pure-static linking
|
||||
; default paths
|
||||
@@ -66,10 +69,10 @@ SPC_LIBC=musl
|
||||
CC=${SPC_LINUX_DEFAULT_CC}
|
||||
CXX=${SPC_LINUX_DEFAULT_CXX}
|
||||
AR=${SPC_LINUX_DEFAULT_AR}
|
||||
LD=ld.gold
|
||||
LD=${SPC_LINUX_DEFAULT_LD}
|
||||
; default compiler flags, used in CMake toolchain file, openssl and pkg-config build
|
||||
SPC_DEFAULT_C_FLAGS="-fpic -Os"
|
||||
SPC_DEFAULT_CXX_FLAGS="-fpic -Os"
|
||||
SPC_DEFAULT_C_FLAGS="-fPIC -Os"
|
||||
SPC_DEFAULT_CXX_FLAGS="-fPIC -Os"
|
||||
; extra libs for building php executable, used in `make` command for building php (this value may changed by extension build process, space separated)
|
||||
SPC_EXTRA_LIBS=
|
||||
; upx executable path
|
||||
@@ -89,7 +92,7 @@ SPC_CMD_VAR_PHP_EMBED_TYPE="static"
|
||||
|
||||
; *** default build vars for building php ***
|
||||
; CFLAGS for configuring php
|
||||
SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -fpie"
|
||||
SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -fPIE"
|
||||
; CPPFLAGS for configuring php
|
||||
SPC_CMD_VAR_PHP_CONFIGURE_CPPFLAGS="-I${BUILD_INCLUDE_PATH}"
|
||||
; LDFLAGS for configuring php
|
||||
@@ -97,7 +100,7 @@ SPC_CMD_VAR_PHP_CONFIGURE_LDFLAGS="-L${BUILD_LIB_PATH}"
|
||||
; LIBS for configuring php
|
||||
SPC_CMD_VAR_PHP_CONFIGURE_LIBS="-ldl -lpthread -lm"
|
||||
; EXTRA_CFLAGS for `make` php
|
||||
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fpie ${SPC_DEFAULT_C_FLAGS}"
|
||||
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_C_FLAGS}"
|
||||
; EXTRA_LIBS for `make` php
|
||||
SPC_CMD_VAR_PHP_MAKE_EXTRA_LIBS="-ldl -lpthread -lm"
|
||||
; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so
|
||||
|
||||
@@ -854,5 +854,14 @@
|
||||
"zstd.h",
|
||||
"zstd_errors.h"
|
||||
]
|
||||
},
|
||||
"watcher": {
|
||||
"source": "watcher",
|
||||
"static-libs-unix": [
|
||||
"libwatcher-c.a"
|
||||
],
|
||||
"headers": [
|
||||
"wtr/watcher-c.h"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,5 +42,17 @@
|
||||
"extract-files": {
|
||||
"upx-*-win64/upx.exe": "{pkg_root_path}/bin/upx.exe"
|
||||
}
|
||||
},
|
||||
"go-xcaddy-x86_64-linux": {
|
||||
"type": "custom"
|
||||
},
|
||||
"go-xcaddy-aarch64-linux": {
|
||||
"type": "custom"
|
||||
},
|
||||
"go-xcaddy-x86_64-macos": {
|
||||
"type": "custom"
|
||||
},
|
||||
"go-xcaddy-aarch64-macos": {
|
||||
"type": "custom"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1070,5 +1070,14 @@
|
||||
"type": "file",
|
||||
"path": "LICENSE"
|
||||
}
|
||||
},
|
||||
"watcher": {
|
||||
"type": "ghtar",
|
||||
"repo": "e-dant/watcher",
|
||||
"prefer-stable": true,
|
||||
"license": {
|
||||
"type": "file",
|
||||
"path": "license"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ build-options:
|
||||
# Set micro SAPI as win32 mode, without this, micro SAPI will be compiled as a console application (only for Windows, default: false)
|
||||
enable-micro-win32: false
|
||||
|
||||
# Build options for shared extensions (list or comma-separated are both accepted)
|
||||
shared-extensions: [ ]
|
||||
|
||||
# Download options
|
||||
download-options:
|
||||
# Use custom url for specified sources, format: "{source-name}:{url}" (e.g. "php-src:https://example.com/php-8.4.0.tar.gz")
|
||||
|
||||
@@ -167,6 +167,7 @@ If the build is successful, you will see the `buildroot/bin` directory in the cu
|
||||
- fpm: The build result is `buildroot/bin/php-fpm`.
|
||||
- micro: The build result is `buildroot/bin/micro.sfx`. If you need to further package it with PHP code, please refer to [Packaging micro binary](./manual-build#command-micro-combine).
|
||||
- embed: See [Using embed](./manual-build#embed-usage).
|
||||
- frankenphp: The build result is `buildroot/bin/frankenphp`.
|
||||
|
||||
If the build fails, you can use the `--debug` parameter to view detailed error information,
|
||||
or use the `--with-clean` to clear the old compilation results and recompile.
|
||||
@@ -290,6 +291,7 @@ You need to specify a compilation target, choose from the following parameters:
|
||||
- `--build-fpm`: Build a fpm sapi (php-fpm, used in conjunction with other traditional fpm architecture software such as nginx)
|
||||
- `--build-micro`: Build a micro sapi (used to build a standalone executable binary containing PHP code)
|
||||
- `--build-embed`: Build an embed sapi (used to embed into other C language programs)
|
||||
- `--build-frankenphp`: Build a [FrankenPHP](https://github.com/php/frankenphp) executable
|
||||
- `--build-all`: build all above sapi
|
||||
|
||||
```bash
|
||||
@@ -509,6 +511,8 @@ When `bin/spc doctor` automatically repairs the Windows environment, tools such
|
||||
Here is an example of installing the tool:
|
||||
|
||||
- Download and install UPX (Linux and Windows only): `bin/spc install-pkg upx`
|
||||
- Download and install nasm (Windows only): `bin/spc install-pkg nasm`
|
||||
- Download and install go-xcaddy: `bin/spc install-pkg go-xcaddy`
|
||||
|
||||
## Command - del-download
|
||||
|
||||
|
||||
@@ -145,6 +145,7 @@ bin/spc craft --debug
|
||||
- fpm: 构建结果为 `buildroot/bin/php-fpm`。
|
||||
- micro: 构建结果为 `buildroot/bin/micro.sfx`,如需进一步与 PHP 代码打包,请查看 [打包 micro 二进制](./manual-build#命令-micro-combine-打包-micro-二进制)。
|
||||
- embed: 参见 [embed 使用](./manual-build#embed-使用)。
|
||||
- frankenphp: 构建结果为 `buildroot/bin/frankenphp`。
|
||||
|
||||
如果中途构建出错,你可以使用 `--debug` 参数查看详细的错误信息,或者使用 `--with-clean` 参数清除旧的编译结果,重新编译。
|
||||
|
||||
@@ -250,6 +251,7 @@ bin/spc doctor --auto-fix
|
||||
- `--build-fpm`: 构建一个 fpm sapi(php-fpm,用于和其他传统的 fpm 架构的软件如 nginx 配合使用)
|
||||
- `--build-micro`: 构建一个 micro sapi(用于构建一个包含 PHP 代码的独立可执行二进制)
|
||||
- `--build-embed`: 构建一个 embed sapi(用于嵌入到其他 C 语言程序中)
|
||||
- `--build-frankenphp`: 构建一个 [frankenphp](https://github.com/php/frankenphp) 二进制
|
||||
- `--build-all`: 构建以上所有 sapi
|
||||
|
||||
```bash
|
||||
@@ -457,6 +459,8 @@ bin/spc dev:sort-config ext
|
||||
下面是安装工具的示例:
|
||||
|
||||
- 下载安装 UPX(仅限 Linux 和 Windows): `bin/spc install-pkg upx`
|
||||
- 下载安装 nasm(仅限 Windows): `bin/spc install-pkg nasm`
|
||||
- 下载安装 go-xcaddy: `bin/spc install-pkg go-xcaddy`
|
||||
|
||||
## 命令 del-download - 删除已下载的资源
|
||||
|
||||
|
||||
@@ -2,4 +2,7 @@
|
||||
<phpunit
|
||||
bootstrap="tests/bootstrap.php"
|
||||
>
|
||||
</phpunit>
|
||||
<php>
|
||||
<env name="SPC_IGNORE_BAD_HASH" value="yes" force="true" />
|
||||
</php>
|
||||
</phpunit>
|
||||
|
||||
@@ -33,7 +33,7 @@ use Symfony\Component\Console\Application;
|
||||
*/
|
||||
final class ConsoleApplication extends Application
|
||||
{
|
||||
public const VERSION = '2.6.0';
|
||||
public const VERSION = '2.6.1';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@ use SPC\exception\RuntimeException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\Config;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\store\LockFile;
|
||||
use SPC\store\SourceManager;
|
||||
use SPC\util\CustomExt;
|
||||
|
||||
@@ -262,17 +263,6 @@ abstract class BuilderBase
|
||||
if (!$ext->isBuildShared()) {
|
||||
continue;
|
||||
}
|
||||
if (Config::getExt($ext->getName(), 'type') === 'builtin' || Config::getExt($ext->getName(), 'build-with-php') === true) {
|
||||
if (file_exists(BUILD_MODULES_PATH . '/' . $ext->getName() . '.so')) {
|
||||
logger()->info('Shared extension [' . $ext->getName() . '] was already built by php-src/configure (' . $ext->getName() . '.so)');
|
||||
continue;
|
||||
}
|
||||
if (Config::getExt($ext->getName(), 'build-with-php') === true) {
|
||||
logger()->warning('Shared extension [' . $ext->getName() . '] did not build with php-src/configure (' . $ext->getName() . '.so)');
|
||||
logger()->warning('Try deleting your build and source folders and running `spc build`` again.');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$ext->buildShared();
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
@@ -361,15 +351,11 @@ abstract class BuilderBase
|
||||
public function getPHPVersionFromArchive(?string $file = null): false|string
|
||||
{
|
||||
if ($file === null) {
|
||||
$lock = file_exists(DOWNLOAD_PATH . '/.lock.json') ? file_get_contents(DOWNLOAD_PATH . '/.lock.json') : false;
|
||||
if ($lock === false) {
|
||||
return false;
|
||||
}
|
||||
$lock = json_decode($lock, true);
|
||||
$file = $lock['php-src']['filename'] ?? null;
|
||||
if ($file === null) {
|
||||
$lock = LockFile::get('php-src');
|
||||
if ($lock === null) {
|
||||
return false;
|
||||
}
|
||||
$file = LockFile::getLockFullPath($lock);
|
||||
}
|
||||
if (preg_match('/php-(\d+\.\d+\.\d+(?:RC\d+)?)\.tar\.(?:gz|bz2|xz)/', $file, $match)) {
|
||||
return $match[1];
|
||||
@@ -415,6 +401,9 @@ abstract class BuilderBase
|
||||
if (($type & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) {
|
||||
$ls[] = 'embed';
|
||||
}
|
||||
if (($type & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) {
|
||||
$ls[] = 'frankenphp';
|
||||
}
|
||||
return implode(', ', $ls);
|
||||
}
|
||||
|
||||
@@ -521,6 +510,29 @@ abstract class BuilderBase
|
||||
}
|
||||
}
|
||||
|
||||
public function checkBeforeBuildPHP(int $rule): void
|
||||
{
|
||||
if (($rule & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) {
|
||||
if (!$this->getOption('enable-zts')) {
|
||||
throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!');
|
||||
}
|
||||
// frankenphp doesn't support windows, BSD is currently not supported by static-php-cli
|
||||
if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) {
|
||||
throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!');
|
||||
}
|
||||
// frankenphp needs package go-xcaddy installed
|
||||
$pkg_dir = PKG_ROOT_PATH . '/go-xcaddy-' . arch2gnu(php_uname('m')) . '-' . osfamily2shortname();
|
||||
if (!file_exists("{$pkg_dir}/bin/go") || !file_exists("{$pkg_dir}/bin/xcaddy")) {
|
||||
global $argv;
|
||||
throw new WrongUsageException("FrankenPHP SAPI requires the go-xcaddy package, please install it first: {$argv[0]} install-pkg go-xcaddy");
|
||||
}
|
||||
// frankenphp needs libxml2 lib on macos, see: https://github.com/php/frankenphp/blob/main/frankenphp.go#L17
|
||||
if (PHP_OS_FAMILY === 'Darwin' && !$this->getLib('libxml2')) {
|
||||
throw new WrongUsageException('FrankenPHP SAPI for macOS requires libxml2 library, please include the `xml` extension in your build.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate micro extension test php code.
|
||||
*/
|
||||
|
||||
@@ -264,9 +264,12 @@ class Extension
|
||||
// If you need to run some check, overwrite this or add your assert in src/globals/ext-tests/{extension_name}.php
|
||||
// If check failed, throw RuntimeException
|
||||
$sharedExtensions = $this->getSharedExtensionLoadString();
|
||||
[$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"');
|
||||
[$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $this->getDistName() . '"');
|
||||
if ($ret !== 0) {
|
||||
throw new RuntimeException('extension ' . $this->getName() . ' failed compile check: php-cli returned ' . $ret);
|
||||
throw new RuntimeException(
|
||||
'extension ' . $this->getName() . ' failed runtime check: php-cli returned ' . $ret . "\n" .
|
||||
join("\n", $out)
|
||||
);
|
||||
}
|
||||
|
||||
if (file_exists(ROOT_DIR . '/src/globals/ext-tests/' . $this->getName() . '.php')) {
|
||||
@@ -328,11 +331,18 @@ class Extension
|
||||
*/
|
||||
public function buildShared(): void
|
||||
{
|
||||
logger()->info('Building extension [' . $this->getName() . '] as shared extension (' . $this->getName() . '.so)');
|
||||
if (file_exists(BUILD_MODULES_PATH . '/' . $this->getName() . '.so')) {
|
||||
logger()->info('extension ' . $this->getName() . ' already built, skipping');
|
||||
return;
|
||||
if (Config::getExt($this->getName(), 'type') === 'builtin' || Config::getExt($this->getName(), 'build-with-php') === true) {
|
||||
if (file_exists(BUILD_MODULES_PATH . '/' . $this->getName() . '.so')) {
|
||||
logger()->info('Shared extension [' . $this->getName() . '] was already built by php-src/configure (' . $this->getName() . '.so)');
|
||||
return;
|
||||
}
|
||||
if (Config::getExt($this->getName(), 'build-with-php') === true) {
|
||||
logger()->warning('Shared extension [' . $this->getName() . '] did not build with php-src/configure (' . $this->getName() . '.so)');
|
||||
logger()->warning('Try deleting your build and source folders and running `spc build`` again.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
logger()->info('Building extension [' . $this->getName() . '] as shared extension (' . $this->getName() . '.so)');
|
||||
foreach ($this->dependencies as $dependency) {
|
||||
if (!$dependency instanceof Extension) {
|
||||
continue;
|
||||
@@ -361,11 +371,16 @@ class Extension
|
||||
{
|
||||
$config = (new SPCConfigUtil($this->builder))->config([$this->getName()], with_dependencies: true);
|
||||
[$staticLibString, $sharedLibString] = $this->getStaticAndSharedLibs();
|
||||
|
||||
// macOS ld64 doesn't understand these, while Linux and BSD do
|
||||
// use them to make sure that all symbols are picked up, even if a library has already been visited before
|
||||
$preStatic = PHP_OS_FAMILY !== 'Darwin' ? '-Wl,-Bstatic -Wl,--start-group ' : '';
|
||||
$postStatic = PHP_OS_FAMILY !== 'Darwin' ? ' -Wl,--end-group -Wl,-Bdynamic ' : ' ';
|
||||
$env = [
|
||||
'CFLAGS' => $config['cflags'],
|
||||
'CXXFLAGS' => $config['cflags'],
|
||||
'LDFLAGS' => $config['ldflags'],
|
||||
'LIBS' => '-Wl,-Bstatic -Wl,--start-group ' . $staticLibString . ' -Wl,--end-group -Wl,-Bdynamic ' . $sharedLibString,
|
||||
'LIBS' => $preStatic . $staticLibString . $postStatic . $sharedLibString,
|
||||
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
|
||||
];
|
||||
|
||||
@@ -386,6 +401,7 @@ class Extension
|
||||
'--enable-shared --disable-static'
|
||||
);
|
||||
|
||||
// some extensions don't define their dependencies well, this patch is only needed for a few
|
||||
FileSystem::replaceFileRegex(
|
||||
$this->source_dir . '/Makefile',
|
||||
'/^(.*_SHARED_LIBADD\s*=.*)$/m',
|
||||
@@ -472,7 +488,7 @@ class Extension
|
||||
*
|
||||
* @return array [staticLibString, sharedLibString]
|
||||
*/
|
||||
private function getStaticAndSharedLibs(): array
|
||||
protected function getStaticAndSharedLibs(): array
|
||||
{
|
||||
$config = (new SPCConfigUtil($this->builder))->config([$this->getName()], with_dependencies: true);
|
||||
$sharedLibString = '';
|
||||
@@ -487,7 +503,7 @@ class Extension
|
||||
continue;
|
||||
}
|
||||
$static_lib = 'lib' . $lib . '.a';
|
||||
if (file_exists(BUILD_LIB_PATH . '/' . $static_lib)) {
|
||||
if (file_exists(BUILD_LIB_PATH . '/' . $static_lib) && !str_contains($static_lib, 'libphp')) {
|
||||
if (!str_contains($staticLibString, '-l' . $lib . ' ')) {
|
||||
$staticLibString .= '-l' . $lib . ' ';
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use SPC\exception\WrongUsageException;
|
||||
use SPC\store\Config;
|
||||
use SPC\store\Downloader;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\store\LockFile;
|
||||
use SPC\store\SourceManager;
|
||||
use SPC\util\GlobalValueTrait;
|
||||
|
||||
@@ -46,12 +47,11 @@ abstract class LibraryBase
|
||||
*/
|
||||
public function setup(bool $force = false): int
|
||||
{
|
||||
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
|
||||
$source = Config::getLib(static::NAME, 'source');
|
||||
// if source is locked as pre-built, we just tryInstall it
|
||||
$pre_built_name = Downloader::getPreBuiltLockName($source);
|
||||
if (isset($lock[$pre_built_name]) && ($lock[$pre_built_name]['lock_as'] ?? SPC_DOWNLOAD_SOURCE) === SPC_DOWNLOAD_PRE_BUILT) {
|
||||
return $this->tryInstall($lock[$pre_built_name]['filename'], $force);
|
||||
if (($lock = LockFile::get($pre_built_name)) && $lock['lock_as'] === SPC_DOWNLOAD_PRE_BUILT) {
|
||||
return $this->tryInstall($lock, $force);
|
||||
}
|
||||
return $this->tryBuild($force);
|
||||
}
|
||||
@@ -166,14 +166,15 @@ abstract class LibraryBase
|
||||
* @throws WrongUsageException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public function tryInstall(string $install_file, bool $force_install = false): int
|
||||
public function tryInstall(array $lock, bool $force_install = false): int
|
||||
{
|
||||
$install_file = $lock['filename'];
|
||||
if ($force_install) {
|
||||
logger()->info('Installing required library [' . static::NAME . '] from pre-built binaries');
|
||||
|
||||
// Extract files
|
||||
try {
|
||||
FileSystem::extractPackage($install_file, DOWNLOAD_PATH . '/' . $install_file, BUILD_ROOT_PATH);
|
||||
FileSystem::extractPackage($install_file, $lock['source_type'], DOWNLOAD_PATH . '/' . $install_file, BUILD_ROOT_PATH);
|
||||
$this->install();
|
||||
return LIB_STATUS_OK;
|
||||
} catch (FileSystemException|RuntimeException $e) {
|
||||
@@ -183,19 +184,19 @@ abstract class LibraryBase
|
||||
}
|
||||
foreach ($this->getStaticLibs() as $name) {
|
||||
if (!file_exists(BUILD_LIB_PATH . "/{$name}")) {
|
||||
$this->tryInstall($install_file, true);
|
||||
$this->tryInstall($lock, true);
|
||||
return LIB_STATUS_OK;
|
||||
}
|
||||
}
|
||||
foreach ($this->getHeaders() as $name) {
|
||||
if (!file_exists(BUILD_INCLUDE_PATH . "/{$name}")) {
|
||||
$this->tryInstall($install_file, true);
|
||||
$this->tryInstall($lock, true);
|
||||
return LIB_STATUS_OK;
|
||||
}
|
||||
}
|
||||
// pkg-config is treated specially. If it is pkg-config, check if the pkg-config binary exists
|
||||
if (static::NAME === 'pkg-config' && !file_exists(BUILD_ROOT_PATH . '/bin/pkg-config')) {
|
||||
$this->tryInstall($install_file, true);
|
||||
$this->tryInstall($lock, true);
|
||||
return LIB_STATUS_OK;
|
||||
}
|
||||
return LIB_STATUS_ALREADY;
|
||||
@@ -214,6 +215,19 @@ abstract class LibraryBase
|
||||
*/
|
||||
public function tryBuild(bool $force_build = false): int
|
||||
{
|
||||
if (str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), '-release')) {
|
||||
FileSystem::replaceFileLineContainsString(
|
||||
SOURCE_PATH . '/php-src/ext/standard/info.c',
|
||||
'#ifdef CONFIGURE_COMMAND',
|
||||
'#ifdef NO_CONFIGURE_COMMAND',
|
||||
);
|
||||
} else {
|
||||
FileSystem::replaceFileLineContainsString(
|
||||
SOURCE_PATH . '/php-src/ext/standard/info.c',
|
||||
'#ifdef NO_CONFIGURE_COMMAND',
|
||||
'#ifdef CONFIGURE_COMMAND',
|
||||
);
|
||||
}
|
||||
if (file_exists($this->source_dir . '/.spc.patched')) {
|
||||
$this->patched = true;
|
||||
}
|
||||
|
||||
@@ -29,4 +29,15 @@ class imagick extends Extension
|
||||
$disable_omp = !(getenv('SPC_LIBC') === 'glibc' && str_contains(getenv('CC'), 'devtoolset-10')) ? '' : ' ac_cv_func_omp_pause_resource_all=no';
|
||||
return '--with-imagick=' . ($shared ? 'shared,' : '') . BUILD_ROOT_PATH . $disable_omp;
|
||||
}
|
||||
|
||||
protected function getStaticAndSharedLibs(): array
|
||||
{
|
||||
// on centos 7, it will use the symbol _ZTINSt6thread6_StateE, which is not defined in system libstdc++.so.6
|
||||
[$static, $shared] = parent::getStaticAndSharedLibs();
|
||||
if (getenv('SPC_LIBC') === 'glibc' && str_contains(getenv('CC'), 'devtoolset-10')) {
|
||||
$static .= ' -lstdc++';
|
||||
$shared = str_replace('-lstdc++', '', $shared);
|
||||
}
|
||||
return [$static, $shared];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@ class yac extends Extension
|
||||
|
||||
public function getUnixConfigureArg(bool $shared = false): string
|
||||
{
|
||||
return '--enable-yac ' . ($shared ? '=shared' : '') . ' --enable-igbinary --enable-json --with-system-fastlz';
|
||||
return '--enable-yac' . ($shared ? '=shared' : '') . ' --enable-igbinary --enable-json --with-system-fastlz';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ class BSDBuilder extends UnixBuilderBase
|
||||
$enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM;
|
||||
$enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
|
||||
$enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
|
||||
$enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP;
|
||||
|
||||
shell()->cd(SOURCE_PATH . '/php-src')
|
||||
->exec(
|
||||
@@ -143,6 +144,10 @@ class BSDBuilder extends UnixBuilderBase
|
||||
}
|
||||
$this->buildEmbed();
|
||||
}
|
||||
if ($enableFrankenphp) {
|
||||
logger()->info('building frankenphp');
|
||||
$this->buildFrankenphp();
|
||||
}
|
||||
}
|
||||
|
||||
public function testPHP(int $build_target = BUILD_TARGET_NONE)
|
||||
|
||||
12
src/SPC/builder/freebsd/library/watcher.php
Normal file
12
src/SPC/builder/freebsd/library/watcher.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\builder\freebsd\library;
|
||||
|
||||
class watcher extends BSDLibraryBase
|
||||
{
|
||||
use \SPC\builder\unix\library\watcher;
|
||||
|
||||
public const NAME = 'watcher';
|
||||
}
|
||||
@@ -33,7 +33,6 @@ class LinuxBuilder extends UnixBuilderBase
|
||||
if (getenv('SPC_LIBC') === 'musl' && !SystemUtil::isMuslDist()) {
|
||||
$this->setOptionIfNotExist('library_path', "LIBRARY_PATH=\"/usr/local/musl/{$arch}-linux-musl/lib\"");
|
||||
$this->setOptionIfNotExist('ld_library_path', "LD_LIBRARY_PATH=\"/usr/local/musl/{$arch}-linux-musl/lib\"");
|
||||
GlobalEnvManager::putenv("PATH=/usr/local/musl/bin:/usr/local/musl/{$arch}-linux-musl/bin:" . getenv('PATH'));
|
||||
$configure = getenv('SPC_CMD_PREFIX_PHP_CONFIGURE');
|
||||
$configure = "LD_LIBRARY_PATH=\"/usr/local/musl/{$arch}-linux-musl/lib\" " . $configure;
|
||||
GlobalEnvManager::putenv("SPC_CMD_PREFIX_PHP_CONFIGURE={$configure}");
|
||||
@@ -110,10 +109,11 @@ class LinuxBuilder extends UnixBuilderBase
|
||||
$config_file_scan_dir = $this->getOption('with-config-file-scan-dir', false) ?
|
||||
('--with-config-file-scan-dir=' . $this->getOption('with-config-file-scan-dir') . ' ') : '';
|
||||
|
||||
$enable_cli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI;
|
||||
$enable_fpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM;
|
||||
$enable_micro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
|
||||
$enable_embed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
|
||||
$enableCli = ($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI;
|
||||
$enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM;
|
||||
$enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
|
||||
$enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
|
||||
$enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP;
|
||||
|
||||
$mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : '';
|
||||
// prepare build php envs
|
||||
@@ -125,7 +125,7 @@ class LinuxBuilder extends UnixBuilderBase
|
||||
]);
|
||||
|
||||
// process micro upx patch if micro sapi enabled
|
||||
if ($enable_micro) {
|
||||
if ($enableMicro) {
|
||||
if (version_compare($this->getMicroVersion(), '0.2.0') < 0) {
|
||||
// for phpmicro 0.1.x
|
||||
$this->processMicroUPXLegacy();
|
||||
@@ -137,10 +137,10 @@ class LinuxBuilder extends UnixBuilderBase
|
||||
shell()->cd(SOURCE_PATH . '/php-src')
|
||||
->exec(
|
||||
getenv('SPC_CMD_PREFIX_PHP_CONFIGURE') . ' ' .
|
||||
($enable_cli ? '--enable-cli ' : '--disable-cli ') .
|
||||
($enable_fpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') .
|
||||
($enable_embed ? "--enable-embed={$embed_type} " : '--disable-embed ') .
|
||||
($enable_micro ? '--enable-micro=all-static ' : '--disable-micro ') .
|
||||
($enableCli ? '--enable-cli ' : '--disable-cli ') .
|
||||
($enableFpm ? '--enable-fpm ' . ($this->getLib('libacl') !== null ? '--with-fpm-acl ' : '') : '--disable-fpm ') .
|
||||
($enableEmbed ? "--enable-embed={$embed_type} " : '--disable-embed ') .
|
||||
($enableMicro ? '--enable-micro=all-static ' : '--disable-micro ') .
|
||||
$config_file_path .
|
||||
$config_file_scan_dir .
|
||||
$disable_jit .
|
||||
@@ -156,25 +156,29 @@ class LinuxBuilder extends UnixBuilderBase
|
||||
|
||||
$this->cleanMake();
|
||||
|
||||
if ($enable_cli) {
|
||||
if ($enableCli) {
|
||||
logger()->info('building cli');
|
||||
$this->buildCli();
|
||||
}
|
||||
if ($enable_fpm) {
|
||||
if ($enableFpm) {
|
||||
logger()->info('building fpm');
|
||||
$this->buildFpm();
|
||||
}
|
||||
if ($enable_micro) {
|
||||
if ($enableMicro) {
|
||||
logger()->info('building micro');
|
||||
$this->buildMicro();
|
||||
}
|
||||
if ($enable_embed) {
|
||||
if ($enableEmbed) {
|
||||
logger()->info('building embed');
|
||||
if ($enable_micro) {
|
||||
if ($enableMicro) {
|
||||
FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la');
|
||||
}
|
||||
$this->buildEmbed();
|
||||
}
|
||||
if ($enableFrankenphp) {
|
||||
logger()->info('building frankenphp');
|
||||
$this->buildFrankenphp();
|
||||
}
|
||||
}
|
||||
|
||||
public function testPHP(int $build_target = BUILD_TARGET_NONE)
|
||||
@@ -293,6 +297,24 @@ class LinuxBuilder extends UnixBuilderBase
|
||||
$cwd = getcwd();
|
||||
chdir(BUILD_LIB_PATH);
|
||||
symlink($realLibName, 'libphp.so');
|
||||
chdir(BUILD_MODULES_PATH);
|
||||
foreach ($this->getExts() as $ext) {
|
||||
if (!$ext->isBuildShared()) {
|
||||
continue;
|
||||
}
|
||||
$name = $ext->getName();
|
||||
$versioned = "{$name}-{$release}.so";
|
||||
$unversioned = "{$name}.so";
|
||||
if (is_file(BUILD_MODULES_PATH . "/{$versioned}")) {
|
||||
rename(BUILD_MODULES_PATH . "/{$versioned}", BUILD_MODULES_PATH . "/{$unversioned}");
|
||||
shell()->cd(BUILD_MODULES_PATH)
|
||||
->exec(sprintf(
|
||||
'patchelf --set-soname %s %s',
|
||||
escapeshellarg($unversioned),
|
||||
escapeshellarg($unversioned)
|
||||
));
|
||||
}
|
||||
}
|
||||
chdir($cwd);
|
||||
}
|
||||
$this->patchPhpScripts();
|
||||
|
||||
12
src/SPC/builder/linux/library/watcher.php
Normal file
12
src/SPC/builder/linux/library/watcher.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\builder\linux\library;
|
||||
|
||||
class watcher extends LinuxLibraryBase
|
||||
{
|
||||
use \SPC\builder\unix\library\watcher;
|
||||
|
||||
public const NAME = 'watcher';
|
||||
}
|
||||
@@ -122,6 +122,7 @@ class MacOSBuilder extends UnixBuilderBase
|
||||
$enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM;
|
||||
$enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
|
||||
$enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
|
||||
$enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP;
|
||||
|
||||
// prepare build php envs
|
||||
$mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : '';
|
||||
@@ -180,9 +181,10 @@ class MacOSBuilder extends UnixBuilderBase
|
||||
}
|
||||
$this->buildEmbed();
|
||||
}
|
||||
|
||||
$this->emitPatchPoint('before-sanity-check');
|
||||
$this->sanityCheck($build_target);
|
||||
if ($enableFrankenphp) {
|
||||
logger()->info('building frankenphp');
|
||||
$this->buildFrankenphp();
|
||||
}
|
||||
}
|
||||
|
||||
public function testPHP(int $build_target = BUILD_TARGET_NONE)
|
||||
|
||||
12
src/SPC/builder/macos/library/watcher.php
Normal file
12
src/SPC/builder/macos/library/watcher.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\builder\macos\library;
|
||||
|
||||
class watcher extends MacOSLibraryBase
|
||||
{
|
||||
use \SPC\builder\unix\library\watcher;
|
||||
|
||||
public const NAME = 'watcher';
|
||||
}
|
||||
@@ -12,6 +12,8 @@ use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\Config;
|
||||
use SPC\store\CurlHook;
|
||||
use SPC\store\Downloader;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\util\DependencyUtil;
|
||||
use SPC\util\SPCConfigUtil;
|
||||
@@ -218,6 +220,21 @@ abstract class UnixBuilderBase extends BuilderBase
|
||||
throw new RuntimeException('embed failed sanity check: run failed. Error message: ' . implode("\n", $output));
|
||||
}
|
||||
}
|
||||
|
||||
// sanity check for frankenphp
|
||||
if (($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) {
|
||||
logger()->info('running frankenphp sanity check');
|
||||
$frankenphp = BUILD_BIN_PATH . '/frankenphp';
|
||||
if (!file_exists($frankenphp)) {
|
||||
throw new RuntimeException('FrankenPHP binary not found: ' . $frankenphp);
|
||||
}
|
||||
[$ret, $output] = shell()
|
||||
->setEnv(['LD_LIBRARY_PATH' => BUILD_LIB_PATH])
|
||||
->execWithResult("{$frankenphp} version");
|
||||
if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) {
|
||||
throw new RuntimeException('FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -277,4 +294,70 @@ abstract class UnixBuilderBase extends BuilderBase
|
||||
FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WrongUsageException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function buildFrankenphp(): void
|
||||
{
|
||||
$os = match (PHP_OS_FAMILY) {
|
||||
'Linux' => 'linux',
|
||||
'Windows' => 'win',
|
||||
'Darwin' => 'macos',
|
||||
'BSD' => 'freebsd',
|
||||
default => throw new RuntimeException('Unsupported OS: ' . PHP_OS_FAMILY),
|
||||
};
|
||||
$arch = arch2gnu(php_uname('m'));
|
||||
|
||||
// define executables for go and xcaddy
|
||||
$xcaddy_exec = PKG_ROOT_PATH . "/go-xcaddy-{$arch}-{$os}/bin/xcaddy";
|
||||
|
||||
$nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : '';
|
||||
$nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : '';
|
||||
$xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES');
|
||||
// make it possible to build from a different frankenphp directory!
|
||||
if (!str_contains($xcaddyModules, '--with github.com/dunglas/frankenphp')) {
|
||||
$xcaddyModules = '--with github.com/dunglas/frankenphp ' . $xcaddyModules;
|
||||
}
|
||||
if ($this->getLib('brotli') === null && str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) {
|
||||
logger()->warning('caddy-cbrotli module is enabled, but brotli library is not built. Disabling caddy-cbrotli.');
|
||||
$xcaddyModules = str_replace('--with github.com/dunglas/caddy-cbrotli', '', $xcaddyModules);
|
||||
}
|
||||
$lrt = PHP_OS_FAMILY === 'Linux' ? '-lrt' : '';
|
||||
$releaseInfo = json_decode(Downloader::curlExec('https://api.github.com/repos/php/frankenphp/releases/latest', retries: 3, hooks: [[CurlHook::class, 'setupGithubToken']]), true);
|
||||
$frankenPhpVersion = $releaseInfo['tag_name'];
|
||||
$libphpVersion = $this->getPHPVersion();
|
||||
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') {
|
||||
$libphpVersion = preg_replace('/\.\d$/', '', $libphpVersion);
|
||||
}
|
||||
$debugFlags = $this->getOption('no-strip') ? "'-w -s' " : '';
|
||||
$extLdFlags = "-extldflags '-pie'";
|
||||
$muslTags = '';
|
||||
if (PHP_OS_FAMILY === 'Linux' && getenv('SPC_LIBC') === 'musl') {
|
||||
$extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000'";
|
||||
$muslTags = 'static_build,';
|
||||
}
|
||||
|
||||
$config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list, with_dependencies: true);
|
||||
|
||||
$env = [
|
||||
'PATH' => PKG_ROOT_PATH . "/go-xcaddy-{$arch}-{$os}/bin:" . getenv('PATH'),
|
||||
'GOROOT' => PKG_ROOT_PATH . "/go-xcaddy-{$arch}-{$os}",
|
||||
'GOBIN' => PKG_ROOT_PATH . "/go-xcaddy-{$arch}-{$os}/bin",
|
||||
'GOPATH' => PKG_ROOT_PATH . '/go',
|
||||
'CGO_ENABLED' => '1',
|
||||
'CGO_CFLAGS' => $config['cflags'],
|
||||
'CGO_LDFLAGS' => "{$config['ldflags']} {$config['libs']} {$lrt}",
|
||||
'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' .
|
||||
'-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . $debugFlags .
|
||||
'-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' .
|
||||
"{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " .
|
||||
"-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}",
|
||||
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
|
||||
];
|
||||
shell()->cd(BUILD_BIN_PATH)
|
||||
->setEnv($env)
|
||||
->exec("{$xcaddy_exec} build --output frankenphp {$xcaddyModules}");
|
||||
}
|
||||
}
|
||||
|
||||
28
src/SPC/builder/unix/library/watcher.php
Normal file
28
src/SPC/builder/unix/library/watcher.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\builder\unix\library;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\store\FileSystem;
|
||||
|
||||
trait watcher
|
||||
{
|
||||
/**
|
||||
* @throws RuntimeException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
protected function build(): void
|
||||
{
|
||||
shell()->cd($this->source_dir . '/watcher-c')
|
||||
->initializeEnv($this)
|
||||
->exec(getenv('CC') . ' -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra -fPIC')
|
||||
->exec(getenv('AR') . ' rcs libwatcher-c.a libwatcher-c.o');
|
||||
|
||||
copy($this->source_dir . '/watcher-c/libwatcher-c.a', BUILD_LIB_PATH . '/libwatcher-c.a');
|
||||
FileSystem::createDir(BUILD_INCLUDE_PATH . '/wtr');
|
||||
copy($this->source_dir . '/watcher-c/include/wtr/watcher-c.h', BUILD_INCLUDE_PATH . '/wtr/watcher-c.h');
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@ class BuildPHPCommand extends BuildCommand
|
||||
$this->addOption('build-micro', null, null, 'Build micro SAPI');
|
||||
$this->addOption('build-cli', null, null, 'Build cli SAPI');
|
||||
$this->addOption('build-fpm', null, null, 'Build fpm SAPI (not available on Windows)');
|
||||
$this->addOption('build-embed', null, null, 'Build embed SAPI (not available on Windows)');
|
||||
$this->addOption('build-embed', null, InputOption::VALUE_OPTIONAL, 'Build embed SAPI (not available on Windows)');
|
||||
$this->addOption('build-frankenphp', null, null, 'Build FrankenPHP SAPI (not available on Windows)');
|
||||
$this->addOption('build-all', null, null, 'Build all SAPI');
|
||||
$this->addOption('no-strip', null, null, 'build without strip, in order to debug and load external extensions');
|
||||
$this->addOption('disable-opcache-jit', null, null, 'disable opcache jit');
|
||||
@@ -82,7 +83,8 @@ class BuildPHPCommand extends BuildCommand
|
||||
$this->output->writeln("<comment>\t--build-micro\tBuild phpmicro SAPI</comment>");
|
||||
$this->output->writeln("<comment>\t--build-fpm\tBuild php-fpm SAPI</comment>");
|
||||
$this->output->writeln("<comment>\t--build-embed\tBuild embed SAPI/libphp</comment>");
|
||||
$this->output->writeln("<comment>\t--build-all\tBuild all SAPI: cli, micro, fpm, embed</comment>");
|
||||
$this->output->writeln("<comment>\t--build-frankenphp\tBuild FrankenPHP SAPI/libphp</comment>");
|
||||
$this->output->writeln("<comment>\t--build-all\tBuild all SAPI: cli, micro, fpm, embed, frankenphp</comment>");
|
||||
return static::FAILURE;
|
||||
}
|
||||
if ($rule === BUILD_TARGET_ALL) {
|
||||
@@ -158,14 +160,9 @@ class BuildPHPCommand extends BuildCommand
|
||||
if ($this->input->getOption('with-upx-pack') && in_array(PHP_OS_FAMILY, ['Linux', 'Windows'])) {
|
||||
$indent_texts['UPX Pack'] = 'enabled';
|
||||
}
|
||||
try {
|
||||
$ver = $builder->getPHPVersion();
|
||||
$indent_texts['PHP Version'] = $ver;
|
||||
} catch (\Throwable) {
|
||||
if (($ver = $builder->getPHPVersionFromArchive()) !== false) {
|
||||
$indent_texts['PHP Version'] = $ver;
|
||||
}
|
||||
}
|
||||
|
||||
$ver = $builder->getPHPVersionFromArchive() ?: $builder->getPHPVersion();
|
||||
$indent_texts['PHP Version'] = $ver;
|
||||
|
||||
if (!empty($not_included)) {
|
||||
$indent_texts['Extra Exts (' . count($not_included) . ')'] = implode(', ', $not_included);
|
||||
@@ -183,6 +180,9 @@ class BuildPHPCommand extends BuildCommand
|
||||
// validate libs and extensions
|
||||
$builder->validateLibsAndExts();
|
||||
|
||||
// check some things before building all the things
|
||||
$builder->checkBeforeBuildPHP($rule);
|
||||
|
||||
// clean builds and sources
|
||||
if ($this->input->getOption('with-clean')) {
|
||||
logger()->info('Cleaning source and previous build dir...');
|
||||
@@ -292,7 +292,18 @@ class BuildPHPCommand extends BuildCommand
|
||||
$rule |= ($this->getOption('build-cli') ? BUILD_TARGET_CLI : BUILD_TARGET_NONE);
|
||||
$rule |= ($this->getOption('build-micro') ? BUILD_TARGET_MICRO : BUILD_TARGET_NONE);
|
||||
$rule |= ($this->getOption('build-fpm') ? BUILD_TARGET_FPM : BUILD_TARGET_NONE);
|
||||
$rule |= ($this->getOption('build-embed') || !empty($shared_extensions) ? BUILD_TARGET_EMBED : BUILD_TARGET_NONE);
|
||||
$embed = $this->getOption('build-embed');
|
||||
if (!$embed && !empty($shared_extensions)) {
|
||||
$embed = true;
|
||||
}
|
||||
if ($embed) {
|
||||
if ($embed === true) {
|
||||
$embed = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static';
|
||||
}
|
||||
$rule |= BUILD_TARGET_EMBED;
|
||||
f_putenv('SPC_CMD_VAR_PHP_EMBED_TYPE=' . ($embed === 'static' ? 'static' : 'shared'));
|
||||
}
|
||||
$rule |= ($this->getOption('build-frankenphp') ? (BUILD_TARGET_FRANKENPHP | BUILD_TARGET_EMBED) : BUILD_TARGET_NONE);
|
||||
$rule |= ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE);
|
||||
return $rule;
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ class CraftCommand extends BaseCommand
|
||||
}
|
||||
|
||||
$static_extensions = implode(',', $craft['extensions']);
|
||||
$shared_extensions = implode(',', $craft['shared-extensions']);
|
||||
$shared_extensions = implode(',', $craft['shared-extensions'] ?? []);
|
||||
$libs = implode(',', $craft['libs']);
|
||||
|
||||
// init log
|
||||
@@ -66,6 +66,15 @@ class CraftCommand extends BaseCommand
|
||||
return static::FAILURE;
|
||||
}
|
||||
}
|
||||
// install go and xcaddy for frankenphp
|
||||
if (in_array('frankenphp', $craft['sapi'])) {
|
||||
$retcode = $this->runCommand('install-pkg', 'go-xcaddy');
|
||||
if ($retcode !== 0) {
|
||||
$this->output->writeln('<error>craft go-xcaddy failed</error>');
|
||||
$this->log("craft go-xcaddy failed with code: {$retcode}", true);
|
||||
return static::FAILURE;
|
||||
}
|
||||
}
|
||||
// craft download
|
||||
if ($craft['craft-options']['download']) {
|
||||
$sharedAppend = $shared_extensions ? ',' . $shared_extensions : '';
|
||||
|
||||
@@ -9,6 +9,7 @@ use SPC\exception\FileSystemException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\Downloader;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\store\LockFile;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -46,29 +47,29 @@ class DeleteDownloadCommand extends BaseCommand
|
||||
return static::SUCCESS;
|
||||
}
|
||||
$chosen_sources = $sources;
|
||||
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
|
||||
|
||||
$deleted_sources = [];
|
||||
foreach ($chosen_sources as $source) {
|
||||
$source = trim($source);
|
||||
foreach ([$source, Downloader::getPreBuiltLockName($source)] as $name) {
|
||||
if (isset($lock[$name])) {
|
||||
if (LockFile::get($name)) {
|
||||
$deleted_sources[] = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($deleted_sources as $lock_name) {
|
||||
$lock = LockFile::get($lock_name);
|
||||
// remove download file/dir if exists
|
||||
if ($lock[$lock_name]['source_type'] === 'archive') {
|
||||
if (file_exists($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock[$lock_name]['filename']))) {
|
||||
if ($lock['source_type'] === SPC_SOURCE_ARCHIVE) {
|
||||
if (file_exists($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock['filename']))) {
|
||||
logger()->info('Deleting file ' . $path);
|
||||
unlink($path);
|
||||
} else {
|
||||
logger()->warning("Source/Package [{$lock_name}] file not found, skip deleting file.");
|
||||
}
|
||||
} else {
|
||||
if (is_dir($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock[$lock_name]['dirname']))) {
|
||||
if (is_dir($path = FileSystem::convertPath(DOWNLOAD_PATH . '/' . $lock['dirname']))) {
|
||||
logger()->info('Deleting dir ' . $path);
|
||||
FileSystem::removeDir($path);
|
||||
} else {
|
||||
@@ -76,9 +77,8 @@ class DeleteDownloadCommand extends BaseCommand
|
||||
}
|
||||
}
|
||||
// remove locked sources
|
||||
unset($lock[$lock_name]);
|
||||
LockFile::put($lock_name, null);
|
||||
}
|
||||
FileSystem::writeFile(DOWNLOAD_PATH . '/.lock.json', json_encode($lock, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
logger()->info('Delete success!');
|
||||
return static::SUCCESS;
|
||||
} catch (DownloaderException $e) {
|
||||
|
||||
@@ -12,6 +12,7 @@ use SPC\exception\RuntimeException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\Config;
|
||||
use SPC\store\Downloader;
|
||||
use SPC\store\LockFile;
|
||||
use SPC\util\DependencyUtil;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -301,7 +302,7 @@ class DownloadCommand extends BaseCommand
|
||||
throw new WrongUsageException('Windows currently does not support --from-zip !');
|
||||
}
|
||||
|
||||
if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) {
|
||||
if (!file_exists(LockFile::LOCK_FILE)) {
|
||||
throw new RuntimeException('.lock.json not exist in "downloads/"');
|
||||
}
|
||||
} catch (RuntimeException $e) {
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace SPC\command;
|
||||
use SPC\store\Config;
|
||||
use SPC\store\Downloader;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\store\LockFile;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -40,16 +41,9 @@ class SwitchPhpVersionCommand extends BaseCommand
|
||||
}
|
||||
}
|
||||
|
||||
// detect if downloads/.lock.json exists
|
||||
$lock_file = DOWNLOAD_PATH . '/.lock.json';
|
||||
// parse php-src part of lock file
|
||||
$lock_data = json_decode(file_get_contents($lock_file), true);
|
||||
// get php-src downloaded file name
|
||||
$php_src = $lock_data['php-src'];
|
||||
$file = DOWNLOAD_PATH . '/' . ($php_src['filename'] ?? '.donot.delete.me');
|
||||
if (file_exists($file)) {
|
||||
if (LockFile::isLockFileExists('php-src')) {
|
||||
$this->output->writeln('<info>Removing old PHP source...</info>');
|
||||
unlink($file);
|
||||
LockFile::put('php-src', null);
|
||||
}
|
||||
|
||||
// Download new PHP source
|
||||
|
||||
@@ -14,6 +14,7 @@ use SPC\exception\RuntimeException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\Config;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\store\LockFile;
|
||||
use SPC\util\DependencyUtil;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -47,9 +48,8 @@ class PackLibCommand extends BuildCommand
|
||||
$lib->setup();
|
||||
} else {
|
||||
// Get lock info
|
||||
$lock = json_decode(file_get_contents(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
|
||||
$source = Config::getLib($lib->getName(), 'source');
|
||||
if (!isset($lock[$source]) || ($lock[$source]['lock_as'] ?? SPC_DOWNLOAD_SOURCE) === SPC_DOWNLOAD_PRE_BUILT) {
|
||||
if (($lock = LockFile::get($source)) === null || ($lock['lock_as'] === SPC_DOWNLOAD_PRE_BUILT)) {
|
||||
logger()->critical("The library {$lib->getName()} is downloaded as pre-built, we need to build it instead of installing pre-built.");
|
||||
return static::FAILURE;
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class LinuxMuslCheck
|
||||
];
|
||||
logger()->info('Downloading ' . $musl_source['url']);
|
||||
Downloader::downloadSource($musl_version_name, $musl_source);
|
||||
FileSystem::extractSource($musl_version_name, DOWNLOAD_PATH . "/{$musl_version_name}.tar.gz");
|
||||
FileSystem::extractSource($musl_version_name, SPC_SOURCE_ARCHIVE, DOWNLOAD_PATH . "/{$musl_version_name}.tar.gz");
|
||||
|
||||
// Apply CVE-2025-26519 patch
|
||||
SourcePatcher::patchFile('musl-1.2.5_CVE-2025-26519_0001.patch', SOURCE_PATH . "/{$musl_version_name}");
|
||||
|
||||
@@ -22,6 +22,7 @@ class LinuxToolCheckList
|
||||
'bzip2', 'cmake', 'gcc',
|
||||
'g++', 'patch', 'binutils-gold',
|
||||
'libtoolize', 'which',
|
||||
'patchelf',
|
||||
];
|
||||
|
||||
public const TOOLS_DEBIAN = [
|
||||
@@ -30,6 +31,7 @@ class LinuxToolCheckList
|
||||
'tar', 'unzip', 'gzip',
|
||||
'bzip2', 'cmake', 'patch',
|
||||
'xz', 'libtoolize', 'which',
|
||||
'patchelf',
|
||||
];
|
||||
|
||||
public const TOOLS_RHEL = [
|
||||
@@ -38,6 +40,7 @@ class LinuxToolCheckList
|
||||
'tar', 'unzip', 'gzip', 'gcc',
|
||||
'bzip2', 'cmake', 'patch', 'which',
|
||||
'xz', 'libtool', 'gettext-devel',
|
||||
'perl', 'patchelf',
|
||||
];
|
||||
|
||||
public const TOOLS_ARCH = [
|
||||
|
||||
@@ -9,6 +9,7 @@ use SPC\exception\DownloaderException;
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\pkg\CustomPackage;
|
||||
use SPC\store\source\CustomSourceBase;
|
||||
|
||||
/**
|
||||
@@ -208,31 +209,7 @@ class Downloader
|
||||
if ($download_as === SPC_DOWNLOAD_PRE_BUILT) {
|
||||
$name = self::getPreBuiltLockName($name);
|
||||
}
|
||||
self::lockSource($name, ['source_type' => 'archive', 'filename' => $filename, 'move_path' => $move_path, 'lock_as' => $download_as]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to lock source.
|
||||
*
|
||||
* @param string $name Source name
|
||||
* @param array{
|
||||
* source_type: string,
|
||||
* dirname: ?string,
|
||||
* filename: ?string,
|
||||
* move_path: ?string,
|
||||
* lock_as: int
|
||||
* } $data Source data
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function lockSource(string $name, array $data): void
|
||||
{
|
||||
if (!file_exists(FileSystem::convertPath(DOWNLOAD_PATH . '/.lock.json'))) {
|
||||
$lock = [];
|
||||
} else {
|
||||
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
|
||||
}
|
||||
$lock[$name] = $data;
|
||||
FileSystem::writeFile(DOWNLOAD_PATH . '/.lock.json', json_encode($lock, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
||||
LockFile::lockSource($name, ['source_type' => SPC_SOURCE_ARCHIVE, 'filename' => $filename, 'move_path' => $move_path, 'lock_as' => $download_as]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -278,7 +255,7 @@ class Downloader
|
||||
}
|
||||
// Lock
|
||||
logger()->debug("Locking git source {$name}");
|
||||
self::lockSource($name, ['source_type' => 'dir', 'dirname' => $name, 'move_path' => $move_path, 'lock_as' => $lock_as]);
|
||||
LockFile::lockSource($name, ['source_type' => SPC_SOURCE_GIT, 'dirname' => $name, 'move_path' => $move_path, 'lock_as' => $lock_as]);
|
||||
|
||||
/*
|
||||
// 复制目录过去
|
||||
@@ -371,11 +348,24 @@ class Downloader
|
||||
SPC_DOWNLOAD_PRE_BUILT
|
||||
);
|
||||
break;
|
||||
case 'local':
|
||||
// Local directory, do nothing, just lock it
|
||||
logger()->debug("Locking local source {$name}");
|
||||
LockFile::lockSource($name, [
|
||||
'source_type' => SPC_SOURCE_LOCAL,
|
||||
'dirname' => $pkg['dirname'],
|
||||
'move_path' => $pkg['extract'] ?? null,
|
||||
'lock_as' => SPC_DOWNLOAD_PACKAGE,
|
||||
]);
|
||||
break;
|
||||
case 'custom': // Custom download method, like API-based download or other
|
||||
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/source', 'SPC\store\source');
|
||||
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg');
|
||||
foreach ($classes as $class) {
|
||||
if (is_a($class, CustomSourceBase::class, true) && $class::NAME === $name) {
|
||||
(new $class())->fetch($force);
|
||||
if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) {
|
||||
$cls = new $class();
|
||||
if (in_array($name, $cls->getSupportName())) {
|
||||
(new $class())->fetch($name, $force, $pkg);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -477,6 +467,16 @@ class Downloader
|
||||
$download_as
|
||||
);
|
||||
break;
|
||||
case 'local':
|
||||
// Local directory, do nothing, just lock it
|
||||
logger()->debug("Locking local source {$name}");
|
||||
LockFile::lockSource($name, [
|
||||
'source_type' => SPC_SOURCE_LOCAL,
|
||||
'dirname' => $source['dirname'],
|
||||
'move_path' => $source['extract'] ?? null,
|
||||
'lock_as' => $download_as,
|
||||
]);
|
||||
break;
|
||||
case 'custom': // Custom download method, like API-based download or other
|
||||
if (isset($source['func']) && is_callable($source['func'])) {
|
||||
$source['name'] = $name;
|
||||
@@ -629,33 +629,30 @@ class Downloader
|
||||
|
||||
/**
|
||||
* @throws FileSystemException
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
private static function isAlreadyDownloaded(string $name, bool $force, int $download_as = SPC_DOWNLOAD_SOURCE): bool
|
||||
{
|
||||
if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) {
|
||||
$lock = [];
|
||||
} else {
|
||||
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true) ?? [];
|
||||
}
|
||||
// If lock file exists, skip downloading for source mode
|
||||
if (!$force && $download_as === SPC_DOWNLOAD_SOURCE && isset($lock[$name])) {
|
||||
if (
|
||||
$lock[$name]['source_type'] === 'archive' && file_exists(DOWNLOAD_PATH . '/' . $lock[$name]['filename']) ||
|
||||
$lock[$name]['source_type'] === 'dir' && is_dir(DOWNLOAD_PATH . '/' . $lock[$name]['dirname'])
|
||||
) {
|
||||
logger()->notice("Source [{$name}] already downloaded: " . ($lock[$name]['filename'] ?? $lock[$name]['dirname']));
|
||||
// If the lock file exists, skip downloading for source mode
|
||||
$lock_item = LockFile::get($name);
|
||||
if (!$force && $download_as === SPC_DOWNLOAD_SOURCE && $lock_item !== null) {
|
||||
if (file_exists($path = LockFile::getLockFullPath($lock_item))) {
|
||||
logger()->notice("Source [{$name}] already downloaded: {$path}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// If lock file exists for current arch and glibc target, skip downloading
|
||||
|
||||
if (!$force && $download_as === SPC_DOWNLOAD_PRE_BUILT && isset($lock[$lock_name = self::getPreBuiltLockName($name)])) {
|
||||
$lock_name = self::getPreBuiltLockName($name);
|
||||
$lock_item = LockFile::get($lock_name);
|
||||
if (!$force && $download_as === SPC_DOWNLOAD_PRE_BUILT && $lock_item !== null) {
|
||||
// lock name with env
|
||||
if (
|
||||
$lock[$lock_name]['source_type'] === 'archive' && file_exists(DOWNLOAD_PATH . '/' . $lock[$lock_name]['filename']) ||
|
||||
$lock[$lock_name]['source_type'] === 'dir' && is_dir(DOWNLOAD_PATH . '/' . $lock[$lock_name]['dirname'])
|
||||
) {
|
||||
logger()->notice("Pre-built content [{$name}] already downloaded: " . ($lock[$lock_name]['filename'] ?? $lock[$lock_name]['dirname']));
|
||||
if (file_exists($path = LockFile::getLockFullPath($lock_item))) {
|
||||
logger()->notice("Pre-built content [{$name}] already downloaded: {$path}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (!$force && $download_as === SPC_DOWNLOAD_PACKAGE && $lock_item !== null) {
|
||||
if (file_exists($path = LockFile::getLockFullPath($lock_item))) {
|
||||
logger()->notice("Source [{$name}] already downloaded: {$path}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ class FileSystem
|
||||
* @throws RuntimeException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
public static function extractPackage(string $name, string $filename, ?string $extract_path = null): void
|
||||
public static function extractPackage(string $name, string $source_type, string $filename, ?string $extract_path = null): void
|
||||
{
|
||||
if ($extract_path !== null) {
|
||||
// replace
|
||||
@@ -151,14 +151,15 @@ class FileSystem
|
||||
} else {
|
||||
$extract_path = PKG_ROOT_PATH . '/' . $name;
|
||||
}
|
||||
logger()->info("extracting {$name} package to {$extract_path} ...");
|
||||
logger()->info("Extracting {$name} package to {$extract_path} ...");
|
||||
$target = self::convertPath($extract_path);
|
||||
|
||||
if (!is_dir($dir = dirname($target))) {
|
||||
self::createDir($dir);
|
||||
}
|
||||
try {
|
||||
self::extractArchive($filename, $target);
|
||||
// extract wrapper command
|
||||
self::extractWithType($source_type, $filename, $extract_path);
|
||||
} catch (RuntimeException $e) {
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
f_passthru('rmdir /s /q ' . $target);
|
||||
@@ -177,24 +178,23 @@ class FileSystem
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function extractSource(string $name, string $filename, ?string $move_path = null): void
|
||||
public static function extractSource(string $name, string $source_type, string $filename, ?string $move_path = null): void
|
||||
{
|
||||
// if source hook is empty, load it
|
||||
if (self::$_extract_hook === []) {
|
||||
SourcePatcher::init();
|
||||
}
|
||||
if ($move_path !== null) {
|
||||
$move_path = SOURCE_PATH . '/' . $move_path;
|
||||
} else {
|
||||
$move_path = SOURCE_PATH . "/{$name}";
|
||||
}
|
||||
$move_path = match ($move_path) {
|
||||
null => SOURCE_PATH . '/' . $name,
|
||||
default => self::isRelativePath($move_path) ? (SOURCE_PATH . '/' . $move_path) : $move_path,
|
||||
};
|
||||
$target = self::convertPath($move_path);
|
||||
logger()->info("extracting {$name} source to {$target}" . ' ...');
|
||||
logger()->info("Extracting {$name} source to {$target}" . ' ...');
|
||||
if (!is_dir($dir = dirname($target))) {
|
||||
self::createDir($dir);
|
||||
}
|
||||
try {
|
||||
self::extractArchive($filename, $target);
|
||||
self::extractWithType($source_type, $filename, $move_path);
|
||||
self::emitSourceExtractHook($name, $target);
|
||||
} catch (RuntimeException $e) {
|
||||
if (PHP_OS_FAMILY === 'Windows') {
|
||||
@@ -484,11 +484,6 @@ class FileSystem
|
||||
*/
|
||||
private static function extractArchive(string $filename, string $target): void
|
||||
{
|
||||
// Git source, just move
|
||||
if (is_dir(self::convertPath($filename))) {
|
||||
self::copyDir(self::convertPath($filename), $target);
|
||||
return;
|
||||
}
|
||||
// Create base dir
|
||||
if (f_mkdir(directory: $target, recursive: true) !== true) {
|
||||
throw new FileSystemException('create ' . $target . ' dir failed');
|
||||
@@ -553,4 +548,16 @@ class FileSystem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function extractWithType(string $source_type, string $filename, string $extract_path): void
|
||||
{
|
||||
logger()->debug('Extracting source [' . $source_type . ']: ' . $filename);
|
||||
/* @phpstan-ignore-next-line */
|
||||
match ($source_type) {
|
||||
SPC_SOURCE_ARCHIVE => self::extractArchive($filename, $extract_path),
|
||||
SPC_SOURCE_GIT => self::copyDir(self::convertPath($filename), $extract_path),
|
||||
// soft link to the local source
|
||||
SPC_SOURCE_LOCAL => symlink(self::convertPath($filename), $extract_path),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
227
src/SPC/store/LockFile.php
Normal file
227
src/SPC/store/LockFile.php
Normal file
@@ -0,0 +1,227 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\store;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
|
||||
class LockFile
|
||||
{
|
||||
public const string LOCK_FILE = DOWNLOAD_PATH . '/.lock.json';
|
||||
|
||||
private static ?array $lock_file_content = null;
|
||||
|
||||
/**
|
||||
* Get a lock entry by its name.
|
||||
*
|
||||
* @param string $lock_name Lock name to retrieve
|
||||
* @return null|array{
|
||||
* source_type: string,
|
||||
* filename: ?string,
|
||||
* dirname: ?string,
|
||||
* move_path: ?string,
|
||||
* lock_as: int,
|
||||
* hash: string
|
||||
* } Returns the lock entry as an associative array if it exists, or null if it does not
|
||||
*/
|
||||
public static function get(string $lock_name): ?array
|
||||
{
|
||||
self::init();
|
||||
|
||||
// Return the specific lock entry if it exists, otherwise return an empty array
|
||||
$result = self::$lock_file_content[$lock_name] ?? null;
|
||||
|
||||
// Add old `dir` compatibility
|
||||
if (($result['source_type'] ?? null) === 'dir') {
|
||||
logger()->warning("Lock entry for '{$lock_name}' has 'source_type' set to 'dir', which is deprecated. Please re-download your dependencies.");
|
||||
$result['source_type'] = SPC_SOURCE_GIT;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a lock file exists for a given lock name.
|
||||
*
|
||||
* @param string $lock_name Lock name to check
|
||||
*/
|
||||
public static function isLockFileExists(string $lock_name): bool
|
||||
{
|
||||
return match (self::get($lock_name)['source_type'] ?? null) {
|
||||
SPC_SOURCE_ARCHIVE => file_exists(DOWNLOAD_PATH . '/' . (self::get($lock_name)['filename'] ?? '.never-exist-file')),
|
||||
SPC_SOURCE_GIT, SPC_SOURCE_LOCAL => is_dir(DOWNLOAD_PATH . '/' . (self::get($lock_name)['dirname'] ?? '.never-exist-dir')),
|
||||
default => false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a lock entry into the lock file.
|
||||
*
|
||||
* @param string $lock_name Lock name to set or remove
|
||||
* @param null|array $lock_content lock content to set, or null to remove the lock entry
|
||||
* @throws FileSystemException
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
public static function put(string $lock_name, ?array $lock_content): void
|
||||
{
|
||||
self::init();
|
||||
|
||||
if ($lock_content === null && isset(self::$lock_file_content[$lock_name])) {
|
||||
self::removeLockFileIfExists(self::$lock_file_content[$lock_name]);
|
||||
unset(self::$lock_file_content[$lock_name]);
|
||||
} else {
|
||||
self::$lock_file_content[$lock_name] = $lock_content;
|
||||
}
|
||||
|
||||
// Write the updated lock data back to the file
|
||||
FileSystem::createDir(dirname(self::LOCK_FILE));
|
||||
file_put_contents(self::LOCK_FILE, json_encode(self::$lock_file_content, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full path of a lock file or directory based on the lock options.
|
||||
*
|
||||
* @param array $lock_options lock item options, must contain 'source_type', 'filename' or 'dirname'
|
||||
* @return string the absolute path to the lock file or directory
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
public static function getLockFullPath(array $lock_options): string
|
||||
{
|
||||
return match ($lock_options['source_type']) {
|
||||
SPC_SOURCE_ARCHIVE => FileSystem::isRelativePath($lock_options['filename']) ? (DOWNLOAD_PATH . '/' . $lock_options['filename']) : $lock_options['filename'],
|
||||
SPC_SOURCE_GIT, SPC_SOURCE_LOCAL => FileSystem::isRelativePath($lock_options['dirname']) ? (DOWNLOAD_PATH . '/' . $lock_options['dirname']) : $lock_options['dirname'],
|
||||
default => throw new WrongUsageException("Unknown source type: {$lock_options['source_type']}"),
|
||||
};
|
||||
}
|
||||
|
||||
public static function getExtractPath(string $lock_name, string $default_path): ?string
|
||||
{
|
||||
$lock = self::get($lock_name);
|
||||
if ($lock === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If move_path is set, use it; otherwise, use the default extract directory
|
||||
if (isset($lock['move_path'])) {
|
||||
if (FileSystem::isRelativePath($lock['move_path'])) {
|
||||
// If move_path is relative, prepend the default extract directory
|
||||
return match ($lock['lock_as']) {
|
||||
SPC_DOWNLOAD_SOURCE, SPC_DOWNLOAD_PRE_BUILT => FileSystem::convertPath(SOURCE_PATH . '/' . $lock['move_path']),
|
||||
SPC_DOWNLOAD_PACKAGE => FileSystem::convertPath(PKG_ROOT_PATH . '/' . $lock['move_path']),
|
||||
default => throw new WrongUsageException("Unknown lock type: {$lock['lock_as']}"),
|
||||
};
|
||||
}
|
||||
return FileSystem::convertPath($lock['move_path']);
|
||||
}
|
||||
return FileSystem::convertPath($default_path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hash of the lock source based on the lock options.
|
||||
*
|
||||
* @param array $lock_options Lock options
|
||||
* @return string Hash of the lock source
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function getLockSourceHash(array $lock_options): string
|
||||
{
|
||||
$result = match ($lock_options['source_type']) {
|
||||
SPC_SOURCE_ARCHIVE => sha1_file(DOWNLOAD_PATH . '/' . $lock_options['filename']),
|
||||
SPC_SOURCE_GIT => exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $lock_options['dirname']) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD'),
|
||||
SPC_SOURCE_LOCAL => 'LOCAL HASH IS ALWAYS DIFFERENT',
|
||||
default => filter_var(getenv('SPC_IGNORE_BAD_HASH'), FILTER_VALIDATE_BOOLEAN) ? '' : throw new RuntimeException("Unknown source type: {$lock_options['source_type']}"),
|
||||
};
|
||||
if ($result === false && !filter_var(getenv('SPC_IGNORE_BAD_HASH'), FILTER_VALIDATE_BOOLEAN)) {
|
||||
throw new RuntimeException("Failed to get hash for source: {$lock_options['source_type']}");
|
||||
}
|
||||
return $result ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $lock_options Lock options
|
||||
* @param string $destination Target directory
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public static function putLockSourceHash(array $lock_options, string $destination): void
|
||||
{
|
||||
$hash = LockFile::getLockSourceHash($lock_options);
|
||||
if ($lock_options['source_type'] === SPC_SOURCE_LOCAL) {
|
||||
logger()->debug("Source [{$lock_options['dirname']}] is local, no hash will be written.");
|
||||
return;
|
||||
}
|
||||
FileSystem::writeFile("{$destination}/.spc-hash", $hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to lock source with hash.
|
||||
*
|
||||
* @param string $name Source name
|
||||
* @param array{
|
||||
* source_type: string,
|
||||
* dirname: ?string,
|
||||
* filename: ?string,
|
||||
* move_path: ?string,
|
||||
* lock_as: int
|
||||
* } $data Source data
|
||||
* @throws FileSystemException
|
||||
* @throws RuntimeException
|
||||
* @throws WrongUsageException
|
||||
*/
|
||||
public static function lockSource(string $name, array $data): void
|
||||
{
|
||||
// calculate hash
|
||||
$hash = LockFile::getLockSourceHash($data);
|
||||
$data['hash'] = $hash;
|
||||
self::put($name, $data);
|
||||
}
|
||||
|
||||
private static function init(): void
|
||||
{
|
||||
if (self::$lock_file_content === null) {
|
||||
// Initialize the lock file content if it hasn't been loaded yet
|
||||
if (!file_exists(self::LOCK_FILE)) {
|
||||
logger()->debug('Lock file does not exist: ' . self::LOCK_FILE . ', initializing empty lock file.');
|
||||
self::$lock_file_content = [];
|
||||
file_put_contents(self::LOCK_FILE, json_encode(self::$lock_file_content, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
} else {
|
||||
$file_content = file_get_contents(self::LOCK_FILE);
|
||||
self::$lock_file_content = json_decode($file_content, true);
|
||||
if (self::$lock_file_content === null) {
|
||||
throw new \RuntimeException('Failed to decode lock file: ' . self::LOCK_FILE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the lock file or directory if it exists.
|
||||
*
|
||||
* @param array $lock_options lock item options, must contain 'source_type', 'filename' or 'dirname'
|
||||
* @throws WrongUsageException
|
||||
* @throws FileSystemException
|
||||
*/
|
||||
private static function removeLockFileIfExists(array $lock_options): void
|
||||
{
|
||||
if ($lock_options['source_type'] === SPC_SOURCE_ARCHIVE) {
|
||||
$path = self::getLockFullPath($lock_options);
|
||||
if (file_exists($path)) {
|
||||
logger()->info('Removing file ' . $path);
|
||||
unlink($path);
|
||||
} else {
|
||||
logger()->debug("Lock file [{$lock_options['filename']}] not found, skip removing file.");
|
||||
}
|
||||
} else {
|
||||
$path = self::getLockFullPath($lock_options);
|
||||
if (is_dir($path)) {
|
||||
logger()->info('Removing directory ' . $path);
|
||||
FileSystem::removeDir($path);
|
||||
} else {
|
||||
logger()->debug("Lock directory [{$lock_options['dirname']}] not found, skip removing directory.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace SPC\store;
|
||||
|
||||
use SPC\exception\FileSystemException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\pkg\CustomPackage;
|
||||
|
||||
class PackageManager
|
||||
{
|
||||
@@ -32,11 +33,27 @@ class PackageManager
|
||||
|
||||
// Download package
|
||||
Downloader::downloadPackage($pkg_name, $config, $force);
|
||||
if (Config::getPkg($pkg_name)['type'] === 'custom') {
|
||||
// Custom extract function
|
||||
$classes = FileSystem::getClassesPsr4(ROOT_DIR . '/src/SPC/store/pkg', 'SPC\store\pkg');
|
||||
foreach ($classes as $class) {
|
||||
if (is_a($class, CustomPackage::class, true) && $class !== CustomPackage::class) {
|
||||
$cls = new $class();
|
||||
if (in_array($pkg_name, $cls->getSupportName())) {
|
||||
(new $class())->extract($pkg_name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// After download, read lock file name
|
||||
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true);
|
||||
$filename = DOWNLOAD_PATH . '/' . ($lock[$pkg_name]['filename'] ?? $lock[$pkg_name]['dirname']);
|
||||
$extract = $lock[$pkg_name]['move_path'] === null ? (PKG_ROOT_PATH . '/' . $pkg_name) : $lock[$pkg_name]['move_path'];
|
||||
FileSystem::extractPackage($pkg_name, $filename, $extract);
|
||||
$lock = LockFile::get($pkg_name);
|
||||
$source_type = $lock['source_type'];
|
||||
$filename = LockFile::getLockFullPath($lock);
|
||||
$extract = LockFile::getExtractPath($pkg_name, PKG_ROOT_PATH . '/' . $pkg_name);
|
||||
|
||||
FileSystem::extractPackage($pkg_name, $source_type, $filename, $extract);
|
||||
|
||||
// if contains extract-files, we just move this file to destination, and remove extract dir
|
||||
if (is_array($config['extract-files'] ?? null) && is_assoc_array($config['extract-files'])) {
|
||||
|
||||
@@ -17,11 +17,6 @@ class SourceManager
|
||||
*/
|
||||
public static function initSource(?array $sources = null, ?array $libs = null, ?array $exts = null, bool $source_only = false): void
|
||||
{
|
||||
if (!file_exists(DOWNLOAD_PATH . '/.lock.json')) {
|
||||
throw new WrongUsageException('Download lock file "downloads/.lock.json" not found, maybe you need to download sources first ?');
|
||||
}
|
||||
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true);
|
||||
|
||||
$sources_extracted = [];
|
||||
// source check exist
|
||||
if (is_array($sources)) {
|
||||
@@ -56,8 +51,8 @@ class SourceManager
|
||||
}
|
||||
// check source downloaded
|
||||
$pre_built_name = Downloader::getPreBuiltLockName($source);
|
||||
if ($source_only || !isset($lock[$pre_built_name])) {
|
||||
if (!isset($lock[$source])) {
|
||||
if ($source_only || LockFile::get($pre_built_name) === null) {
|
||||
if (LockFile::get($source) === null) {
|
||||
throw new WrongUsageException("Source [{$source}] not downloaded or not locked, you should download it first !");
|
||||
}
|
||||
$lock_name = $source;
|
||||
@@ -65,14 +60,38 @@ class SourceManager
|
||||
$lock_name = $pre_built_name;
|
||||
}
|
||||
|
||||
$lock_content = LockFile::get($lock_name);
|
||||
|
||||
// check source dir exist
|
||||
$check = $lock[$lock_name]['move_path'] === null ? (SOURCE_PATH . '/' . $source) : (SOURCE_PATH . '/' . $lock[$lock_name]['move_path']);
|
||||
$check = LockFile::getExtractPath($lock_name, SOURCE_PATH . '/' . $source);
|
||||
// $check = $lock[$lock_name]['move_path'] === null ? (SOURCE_PATH . '/' . $source) : (SOURCE_PATH . '/' . $lock[$lock_name]['move_path']);
|
||||
if (!is_dir($check)) {
|
||||
logger()->debug('Extracting source [' . $source . '] to ' . $check . ' ...');
|
||||
FileSystem::extractSource($source, DOWNLOAD_PATH . '/' . ($lock[$lock_name]['filename'] ?? $lock[$lock_name]['dirname']), $lock[$lock_name]['move_path']);
|
||||
} else {
|
||||
logger()->debug('Source [' . $source . '] already extracted in ' . $check . ', skip !');
|
||||
$filename = LockFile::getLockFullPath($lock_content);
|
||||
FileSystem::extractSource($source, $lock_content['source_type'], $filename, $check);
|
||||
LockFile::putLockSourceHash($lock_content, $check);
|
||||
continue;
|
||||
}
|
||||
// if a lock file does not have hash, calculate with the current source (backward compatibility)
|
||||
if (!isset($lock_content['hash'])) {
|
||||
$hash = LockFile::getLockSourceHash($lock_content);
|
||||
} else {
|
||||
$hash = $lock_content['hash'];
|
||||
}
|
||||
|
||||
// when source already extracted, detect if the extracted source hash is the same as the lock file one
|
||||
if (file_exists("{$check}/.spc-hash") && FileSystem::readFile("{$check}/.spc-hash") === $hash) {
|
||||
logger()->debug('Source [' . $source . '] already extracted in ' . $check . ', skip !');
|
||||
continue;
|
||||
}
|
||||
|
||||
// if not, remove the source dir and extract again
|
||||
logger()->notice("Source [{$source}] hash mismatch, removing old source dir and extracting again ...");
|
||||
FileSystem::removeDir($check);
|
||||
$filename = LockFile::getLockFullPath($lock_content);
|
||||
$move_path = LockFile::getExtractPath($lock_name, SOURCE_PATH . '/' . $source);
|
||||
FileSystem::extractSource($source, $lock_content['source_type'], $filename, $move_path);
|
||||
LockFile::putLockSourceHash($lock_content, $check);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +454,7 @@ class SourcePatcher
|
||||
|
||||
public static function patchFfiCentos7FixO3strncmp(): bool
|
||||
{
|
||||
if (PHP_OS_FAMILY !== 'Linux' || SystemUtil::getLibcVersionIfExists() >= '2.17') {
|
||||
if (PHP_OS_FAMILY !== 'Linux' || SystemUtil::getLibcVersionIfExists() > '2.17') {
|
||||
return false;
|
||||
}
|
||||
if (!file_exists(SOURCE_PATH . '/php-src/main/php_version.h')) {
|
||||
|
||||
17
src/SPC/store/pkg/CustomPackage.php
Normal file
17
src/SPC/store/pkg/CustomPackage.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\store\pkg;
|
||||
|
||||
abstract class CustomPackage
|
||||
{
|
||||
abstract public function getSupportName(): array;
|
||||
|
||||
abstract public function fetch(string $name, bool $force = false, ?array $config = null): void;
|
||||
|
||||
public function extract(string $name): void
|
||||
{
|
||||
throw new \RuntimeException("Extract method not implemented for package: {$name}");
|
||||
}
|
||||
}
|
||||
76
src/SPC/store/pkg/GoXcaddy.php
Normal file
76
src/SPC/store/pkg/GoXcaddy.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace SPC\store\pkg;
|
||||
|
||||
use SPC\store\Downloader;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\store\LockFile;
|
||||
use SPC\util\GlobalEnvManager;
|
||||
|
||||
class GoXcaddy extends CustomPackage
|
||||
{
|
||||
public function getSupportName(): array
|
||||
{
|
||||
return [
|
||||
'go-xcaddy-x86_64-linux',
|
||||
'go-xcaddy-x86_64-macos',
|
||||
'go-xcaddy-aarch64-linux',
|
||||
'go-xcaddy-aarch64-macos',
|
||||
];
|
||||
}
|
||||
|
||||
public function fetch(string $name, bool $force = false, ?array $config = null): void
|
||||
{
|
||||
$pkgroot = PKG_ROOT_PATH;
|
||||
$go_exec = "{$pkgroot}/{$name}/bin/go";
|
||||
$xcaddy_exec = "{$pkgroot}/{$name}/bin/xcaddy";
|
||||
if (file_exists($go_exec) && file_exists($xcaddy_exec)) {
|
||||
return;
|
||||
}
|
||||
$arch = match (explode('-', $name)[2]) {
|
||||
'x86_64' => 'amd64',
|
||||
'aarch64' => 'arm64',
|
||||
default => throw new \InvalidArgumentException('Unsupported architecture: ' . $name),
|
||||
};
|
||||
$os = match (explode('-', $name)[3]) {
|
||||
'linux' => 'linux',
|
||||
'macos' => 'darwin',
|
||||
default => throw new \InvalidArgumentException('Unsupported OS: ' . $name),
|
||||
};
|
||||
$go_version = '1.24.4';
|
||||
$config = [
|
||||
'type' => 'url',
|
||||
'url' => "https://go.dev/dl/go{$go_version}.{$os}-{$arch}.tar.gz",
|
||||
];
|
||||
Downloader::downloadPackage($name, $config, $force);
|
||||
}
|
||||
|
||||
public function extract(string $name): void
|
||||
{
|
||||
$pkgroot = PKG_ROOT_PATH;
|
||||
$go_exec = "{$pkgroot}/{$name}/bin/go";
|
||||
$xcaddy_exec = "{$pkgroot}/{$name}/bin/xcaddy";
|
||||
if (file_exists($go_exec) && file_exists($xcaddy_exec)) {
|
||||
return;
|
||||
}
|
||||
$lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true);
|
||||
$source_type = $lock[$name]['source_type'];
|
||||
$filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']);
|
||||
$extract = $lock[$name]['move_path'] === null ? "{$pkgroot}/{$name}" : $lock[$name]['move_path'];
|
||||
|
||||
FileSystem::extractPackage($name, $source_type, $filename, $extract);
|
||||
|
||||
GlobalEnvManager::init();
|
||||
// install xcaddy
|
||||
shell()
|
||||
->appendEnv([
|
||||
'PATH' => "{$pkgroot}/{$name}/bin:" . getenv('PATH'),
|
||||
'GOROOT' => "{$pkgroot}/{$name}",
|
||||
'GOBIN' => "{$pkgroot}/{$name}/bin",
|
||||
'GOPATH' => "{$pkgroot}/go",
|
||||
])
|
||||
->exec("{$go_exec} install github.com/caddyserver/xcaddy/cmd/xcaddy@latest");
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,12 @@ class ConfigValidator
|
||||
if (is_string($craft['extensions'])) {
|
||||
$craft['extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['extensions'])));
|
||||
}
|
||||
if (!isset($craft['shared-extensions'])) {
|
||||
$craft['shared-extensions'] = [];
|
||||
}
|
||||
if (is_string($craft['shared-extensions'] ?? [])) {
|
||||
$craft['shared-extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['shared-extensions'])));
|
||||
}
|
||||
// check libs
|
||||
if (isset($craft['libs']) && is_string($craft['libs'])) {
|
||||
$craft['libs'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['libs'])));
|
||||
|
||||
@@ -45,14 +45,17 @@ class GlobalEnvManager
|
||||
// Define env vars for linux
|
||||
if (PHP_OS_FAMILY === 'Linux') {
|
||||
$arch = getenv('GNU_ARCH');
|
||||
if (SystemUtil::isMuslDist()) {
|
||||
if (SystemUtil::isMuslDist() || getenv('SPC_LIBC') === 'glibc') {
|
||||
self::putenv('SPC_LINUX_DEFAULT_CC=gcc');
|
||||
self::putenv('SPC_LINUX_DEFAULT_CXX=g++');
|
||||
self::putenv('SPC_LINUX_DEFAULT_AR=ar');
|
||||
self::putenv('SPC_LINUX_DEFAULT_LD=ld.gold');
|
||||
} else {
|
||||
self::putenv("SPC_LINUX_DEFAULT_CC={$arch}-linux-musl-gcc");
|
||||
self::putenv("SPC_LINUX_DEFAULT_CXX={$arch}-linux-musl-g++");
|
||||
self::putenv("SPC_LINUX_DEFAULT_AR={$arch}-linux-musl-ar");
|
||||
self::putenv("SPC_LINUX_DEFAULT_LD={$arch}-linux-musl-ld");
|
||||
GlobalEnvManager::putenv("PATH=/usr/local/musl/bin:/usr/local/musl/{$arch}-linux-musl/bin:" . getenv('PATH'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,14 +44,7 @@ class UnixShell
|
||||
{
|
||||
/* @phpstan-ignore-next-line */
|
||||
logger()->info(ConsoleColor::yellow('[EXEC] ') . ConsoleColor::green($cmd));
|
||||
logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']);
|
||||
$env_str = $this->getEnvString();
|
||||
if (!empty($env_str)) {
|
||||
$cmd = "{$env_str} {$cmd}";
|
||||
}
|
||||
if ($this->cd !== null) {
|
||||
$cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd;
|
||||
}
|
||||
$cmd = $this->getExecString($cmd);
|
||||
if (!$this->debug) {
|
||||
$cmd .= ' 1>/dev/null 2>&1';
|
||||
}
|
||||
@@ -99,10 +92,7 @@ class UnixShell
|
||||
/* @phpstan-ignore-next-line */
|
||||
logger()->debug(ConsoleColor::blue('[EXEC] ') . ConsoleColor::gray($cmd));
|
||||
}
|
||||
logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']);
|
||||
if ($this->cd !== null) {
|
||||
$cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd;
|
||||
}
|
||||
$cmd = $this->getExecString($cmd);
|
||||
exec($cmd, $out, $code);
|
||||
return [$code, $out];
|
||||
}
|
||||
@@ -126,4 +116,17 @@ class UnixShell
|
||||
}
|
||||
return trim($str);
|
||||
}
|
||||
|
||||
private function getExecString(string $cmd): string
|
||||
{
|
||||
logger()->debug('Executed at: ' . debug_backtrace()[0]['file'] . ':' . debug_backtrace()[0]['line']);
|
||||
$env_str = $this->getEnvString();
|
||||
if (!empty($env_str)) {
|
||||
$cmd = "{$env_str} {$cmd}";
|
||||
}
|
||||
if ($this->cd !== null) {
|
||||
$cmd = 'cd ' . escapeshellarg($this->cd) . ' && ' . $cmd;
|
||||
}
|
||||
return $cmd;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const SPC_EXTENSION_ALIAS = [
|
||||
'zendopcache' => 'opcache',
|
||||
];
|
||||
|
||||
// spc lock type
|
||||
// spc download lock type
|
||||
const SPC_DOWNLOAD_SOURCE = 1; // lock source
|
||||
const SPC_DOWNLOAD_PRE_BUILT = 2; // lock pre-built
|
||||
const SPC_DOWNLOAD_PACKAGE = 3; // lock as package
|
||||
@@ -62,7 +62,8 @@ const BUILD_TARGET_CLI = 1; // build cli
|
||||
const BUILD_TARGET_MICRO = 2; // build micro
|
||||
const BUILD_TARGET_FPM = 4; // build fpm
|
||||
const BUILD_TARGET_EMBED = 8; // build embed
|
||||
const BUILD_TARGET_ALL = 15; // build all
|
||||
const BUILD_TARGET_FRANKENPHP = 16; // build frankenphp
|
||||
const BUILD_TARGET_ALL = BUILD_TARGET_CLI | BUILD_TARGET_MICRO | BUILD_TARGET_FPM | BUILD_TARGET_EMBED | BUILD_TARGET_FRANKENPHP; // build all
|
||||
|
||||
// doctor error fix policy
|
||||
const FIX_POLICY_DIE = 1; // die directly
|
||||
@@ -84,5 +85,10 @@ const AUTOCONF_CPPFLAGS = 4;
|
||||
const AUTOCONF_LDFLAGS = 8;
|
||||
const AUTOCONF_ALL = 15;
|
||||
|
||||
// spc download source type
|
||||
const SPC_SOURCE_ARCHIVE = 'archive'; // download as archive
|
||||
const SPC_SOURCE_GIT = 'git'; // download as git repository
|
||||
const SPC_SOURCE_LOCAL = 'local'; // download as local directory
|
||||
|
||||
ConsoleLogger::$date_format = 'H:i:s';
|
||||
ConsoleLogger::$format = '[%date%] [%level_short%] %body%';
|
||||
|
||||
@@ -102,6 +102,17 @@ function osfamily2dir(): string
|
||||
};
|
||||
}
|
||||
|
||||
function osfamily2shortname(): string
|
||||
{
|
||||
return match (PHP_OS_FAMILY) {
|
||||
'Windows' => 'win',
|
||||
'Darwin' => 'macos',
|
||||
'Linux' => 'linux',
|
||||
'BSD' => 'bsd',
|
||||
default => throw new WrongUsageException('Not support os: ' . PHP_OS_FAMILY),
|
||||
};
|
||||
}
|
||||
|
||||
function shell(?bool $debug = null): UnixShell
|
||||
{
|
||||
/* @noinspection PhpUnhandledExceptionInspection */
|
||||
|
||||
@@ -24,7 +24,7 @@ $test_os = [
|
||||
'macos-13',
|
||||
// 'macos-14',
|
||||
'macos-15',
|
||||
// 'ubuntu-latest',
|
||||
'ubuntu-latest',
|
||||
'ubuntu-22.04',
|
||||
'ubuntu-24.04',
|
||||
'ubuntu-22.04-arm',
|
||||
@@ -40,12 +40,15 @@ $no_strip = false;
|
||||
// compress with upx
|
||||
$upx = false;
|
||||
|
||||
// whether to test frankenphp build, only available for macos and linux
|
||||
$frankenphp = true;
|
||||
|
||||
// prefer downloading pre-built packages to speed up the build process
|
||||
$prefer_pre_built = false;
|
||||
|
||||
// If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`).
|
||||
$extensions = match (PHP_OS_FAMILY) {
|
||||
'Linux', 'Darwin' => 'dom,mongodb',
|
||||
'Linux', 'Darwin' => 'apcu,ast,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,iconv,libxml,mbregex,mbstring,opcache,openssl,pcntl,phar,posix,readline,session,simplexml,sockets,sodium,tokenizer,xml,xmlreader,xmlwriter,zip,zlib',
|
||||
'Windows' => 'xlswriter,openssl',
|
||||
};
|
||||
|
||||
@@ -208,7 +211,13 @@ switch ($argv[1] ?? null) {
|
||||
passthru($prefix . $build_cmd . ' --build-cli --build-micro', $retcode);
|
||||
break;
|
||||
case 'build_embed_cmd':
|
||||
passthru($prefix . $build_cmd . (str_starts_with($argv[2], 'windows-') ? ' --build-cli' : ' --build-embed'), $retcode);
|
||||
if ($frankenphp) {
|
||||
passthru("{$prefix}install-pkg go-xcaddy --debug", $retcode);
|
||||
if ($retcode !== 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
passthru($prefix . $build_cmd . (str_starts_with($argv[2], 'windows-') ? ' --build-cli' : (' --build-embed' . ($frankenphp ? ' --build-frankenphp' : ''))), $retcode);
|
||||
break;
|
||||
case 'doctor_cmd':
|
||||
passthru($prefix . $doctor_cmd, $retcode);
|
||||
|
||||
@@ -13,6 +13,7 @@ use SPC\builder\LibraryBase;
|
||||
use SPC\exception\RuntimeException;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\FileSystem;
|
||||
use SPC\store\LockFile;
|
||||
use SPC\util\CustomExt;
|
||||
use SPC\util\DependencyUtil;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
@@ -117,7 +118,7 @@ class BuilderTest extends TestCase
|
||||
|
||||
public function testGetPHPVersionFromArchive()
|
||||
{
|
||||
$lock = file_exists(DOWNLOAD_PATH . '/.lock.json') ? file_get_contents(DOWNLOAD_PATH . '/.lock.json') : false;
|
||||
$lock = file_exists(LockFile::LOCK_FILE) ? file_get_contents(LockFile::LOCK_FILE) : false;
|
||||
if ($lock === false) {
|
||||
$this->assertFalse($this->builder->getPHPVersionFromArchive());
|
||||
} else {
|
||||
@@ -161,7 +162,8 @@ class BuilderTest extends TestCase
|
||||
[BUILD_TARGET_FPM, 'fpm'],
|
||||
[BUILD_TARGET_MICRO, 'micro'],
|
||||
[BUILD_TARGET_EMBED, 'embed'],
|
||||
[BUILD_TARGET_ALL, 'cli, micro, fpm, embed'],
|
||||
[BUILD_TARGET_FRANKENPHP, 'frankenphp'],
|
||||
[BUILD_TARGET_ALL, 'cli, micro, fpm, embed, frankenphp'],
|
||||
[BUILD_TARGET_CLI | BUILD_TARGET_EMBED, 'cli, embed'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace SPC\Tests\store;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use SPC\exception\WrongUsageException;
|
||||
use SPC\store\Downloader;
|
||||
use SPC\store\LockFile;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -57,16 +58,16 @@ class DownloaderTest extends TestCase
|
||||
|
||||
public function testLockSource()
|
||||
{
|
||||
Downloader::lockSource('fake-file', ['source_type' => 'archive', 'filename' => 'fake-file-name', 'move_path' => 'fake-path', 'lock_as' => 'fake-lock-as']);
|
||||
$this->assertFileExists(DOWNLOAD_PATH . '/.lock.json');
|
||||
$json = json_decode(file_get_contents(DOWNLOAD_PATH . '/.lock.json'), true);
|
||||
LockFile::lockSource('fake-file', ['source_type' => SPC_SOURCE_ARCHIVE, 'filename' => 'fake-file-name', 'move_path' => 'fake-path', 'lock_as' => 'fake-lock-as']);
|
||||
$this->assertFileExists(LockFile::LOCK_FILE);
|
||||
$json = json_decode(file_get_contents(LockFile::LOCK_FILE), true);
|
||||
$this->assertIsArray($json);
|
||||
$this->assertArrayHasKey('fake-file', $json);
|
||||
$this->assertArrayHasKey('source_type', $json['fake-file']);
|
||||
$this->assertArrayHasKey('filename', $json['fake-file']);
|
||||
$this->assertArrayHasKey('move_path', $json['fake-file']);
|
||||
$this->assertArrayHasKey('lock_as', $json['fake-file']);
|
||||
$this->assertEquals('archive', $json['fake-file']['source_type']);
|
||||
$this->assertEquals(SPC_SOURCE_ARCHIVE, $json['fake-file']['source_type']);
|
||||
$this->assertEquals('fake-file-name', $json['fake-file']['filename']);
|
||||
$this->assertEquals('fake-path', $json['fake-file']['move_path']);
|
||||
$this->assertEquals('fake-lock-as', $json['fake-file']['lock_as']);
|
||||
|
||||
Reference in New Issue
Block a user