diff --git a/README-zh.md b/README-zh.md
index 0cbc4780..03fa7160 100755
--- a/README-zh.md
+++ b/README-zh.md
@@ -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)。
## 贡献
diff --git a/README.md b/README.md
index 301d76fd..94c10d58 100755
--- a/README.md
+++ b/README.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
diff --git a/bin/spc-alpine-docker b/bin/spc-alpine-docker
index 0733b232..cb7d7b4c 100755
--- a/bin/spc-alpine-docker
+++ b/bin/spc-alpine-docker
@@ -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
diff --git a/bin/spc-gnu-docker b/bin/spc-gnu-docker
index 36e0420e..92496e03 100755
--- a/bin/spc-gnu-docker
+++ b/bin/spc-gnu-docker
@@ -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
diff --git a/config/env.ini b/config/env.ini
index d6217f5f..d80bc3e7 100644
--- a/config/env.ini
+++ b/config/env.ini
@@ -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,7 +69,7 @@ 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"
diff --git a/config/lib.json b/config/lib.json
index 160d43f2..dc792c3b 100644
--- a/config/lib.json
+++ b/config/lib.json
@@ -854,5 +854,14 @@
"zstd.h",
"zstd_errors.h"
]
+ },
+ "watcher": {
+ "source": "watcher",
+ "static-libs-unix": [
+ "libwatcher-c.a"
+ ],
+ "headers": [
+ "wtr/watcher-c.h"
+ ]
}
}
diff --git a/config/pkg.json b/config/pkg.json
index 5760c0b1..e0762cac 100644
--- a/config/pkg.json
+++ b/config/pkg.json
@@ -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"
}
}
diff --git a/config/source.json b/config/source.json
index 2ad8ec87..57a10a4e 100644
--- a/config/source.json
+++ b/config/source.json
@@ -1070,5 +1070,14 @@
"type": "file",
"path": "LICENSE"
}
+ },
+ "watcher": {
+ "type": "ghtar",
+ "repo": "e-dant/watcher",
+ "prefer-stable": true,
+ "license": {
+ "type": "file",
+ "path": "license"
+ }
}
}
diff --git a/docs/en/guide/manual-build.md b/docs/en/guide/manual-build.md
index 3c40d5eb..13038360 100644
--- a/docs/en/guide/manual-build.md
+++ b/docs/en/guide/manual-build.md
@@ -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
diff --git a/docs/zh/guide/manual-build.md b/docs/zh/guide/manual-build.md
index ca0395c8..0db0bc86 100644
--- a/docs/zh/guide/manual-build.md
+++ b/docs/zh/guide/manual-build.md
@@ -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 - 删除已下载的资源
diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php
index c74c9bdc..b69dcf08 100644
--- a/src/SPC/ConsoleApplication.php
+++ b/src/SPC/ConsoleApplication.php
@@ -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()
{
diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php
index 433d0b97..88e827c5 100644
--- a/src/SPC/builder/BuilderBase.php
+++ b/src/SPC/builder/BuilderBase.php
@@ -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.
*/
diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php
index 74be112c..a457d82e 100644
--- a/src/SPC/builder/Extension.php
+++ b/src/SPC/builder/Extension.php
@@ -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,6 +331,17 @@ class Extension
*/
public function buildShared(): void
{
+ 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) {
@@ -357,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,
];
@@ -382,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',
@@ -468,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 = '';
@@ -483,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 . ' ';
}
diff --git a/src/SPC/builder/LibraryBase.php b/src/SPC/builder/LibraryBase.php
index f62e297b..a130e8a8 100644
--- a/src/SPC/builder/LibraryBase.php
+++ b/src/SPC/builder/LibraryBase.php
@@ -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], $force);
+ if (($lock = LockFile::get($pre_built_name)) && $lock['lock_as'] === SPC_DOWNLOAD_PRE_BUILT) {
+ return $this->tryInstall($lock, $force);
}
return $this->tryBuild($force);
}
@@ -215,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;
}
diff --git a/src/SPC/builder/extension/imagick.php b/src/SPC/builder/extension/imagick.php
index d78627ef..7951ea69 100644
--- a/src/SPC/builder/extension/imagick.php
+++ b/src/SPC/builder/extension/imagick.php
@@ -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];
+ }
}
diff --git a/src/SPC/builder/freebsd/BSDBuilder.php b/src/SPC/builder/freebsd/BSDBuilder.php
index 04fd43d3..65ebea57 100644
--- a/src/SPC/builder/freebsd/BSDBuilder.php
+++ b/src/SPC/builder/freebsd/BSDBuilder.php
@@ -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)
diff --git a/src/SPC/builder/freebsd/library/watcher.php b/src/SPC/builder/freebsd/library/watcher.php
new file mode 100644
index 00000000..1a08dd24
--- /dev/null
+++ b/src/SPC/builder/freebsd/library/watcher.php
@@ -0,0 +1,12 @@
+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();
diff --git a/src/SPC/builder/linux/library/watcher.php b/src/SPC/builder/linux/library/watcher.php
new file mode 100644
index 00000000..01bc114d
--- /dev/null
+++ b/src/SPC/builder/linux/library/watcher.php
@@ -0,0 +1,12 @@
+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)
diff --git a/src/SPC/builder/macos/library/watcher.php b/src/SPC/builder/macos/library/watcher.php
new file mode 100644
index 00000000..ef88c0ed
--- /dev/null
+++ b/src/SPC/builder/macos/library/watcher.php
@@ -0,0 +1,12 @@
+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}");
+ }
}
diff --git a/src/SPC/builder/unix/library/watcher.php b/src/SPC/builder/unix/library/watcher.php
new file mode 100644
index 00000000..2463cc4f
--- /dev/null
+++ b/src/SPC/builder/unix/library/watcher.php
@@ -0,0 +1,28 @@
+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');
+ }
+}
diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php
index 61781d2c..e40a7d83 100644
--- a/src/SPC/command/BuildPHPCommand.php
+++ b/src/SPC/command/BuildPHPCommand.php
@@ -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("\t--build-micro\tBuild phpmicro SAPI");
$this->output->writeln("\t--build-fpm\tBuild php-fpm SAPI");
$this->output->writeln("\t--build-embed\tBuild embed SAPI/libphp");
- $this->output->writeln("\t--build-all\tBuild all SAPI: cli, micro, fpm, embed");
+ $this->output->writeln("\t--build-frankenphp\tBuild FrankenPHP SAPI/libphp");
+ $this->output->writeln("\t--build-all\tBuild all SAPI: cli, micro, fpm, embed, frankenphp");
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;
}
diff --git a/src/SPC/command/CraftCommand.php b/src/SPC/command/CraftCommand.php
index 9a2ac441..c8003a3e 100644
--- a/src/SPC/command/CraftCommand.php
+++ b/src/SPC/command/CraftCommand.php
@@ -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('craft go-xcaddy failed');
+ $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 : '';
diff --git a/src/SPC/command/DeleteDownloadCommand.php b/src/SPC/command/DeleteDownloadCommand.php
index 12b0b420..0306de4c 100644
--- a/src/SPC/command/DeleteDownloadCommand.php
+++ b/src/SPC/command/DeleteDownloadCommand.php
@@ -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'] === SPC_SOURCE_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) {
diff --git a/src/SPC/command/DownloadCommand.php b/src/SPC/command/DownloadCommand.php
index 57e83cd0..d5054aa9 100644
--- a/src/SPC/command/DownloadCommand.php
+++ b/src/SPC/command/DownloadCommand.php
@@ -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) {
diff --git a/src/SPC/command/SwitchPhpVersionCommand.php b/src/SPC/command/SwitchPhpVersionCommand.php
index ef463ef4..30ee0c79 100644
--- a/src/SPC/command/SwitchPhpVersionCommand.php
+++ b/src/SPC/command/SwitchPhpVersionCommand.php
@@ -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('Removing old PHP source...');
- unlink($file);
+ LockFile::put('php-src', null);
}
// Download new PHP source
diff --git a/src/SPC/command/dev/PackLibCommand.php b/src/SPC/command/dev/PackLibCommand.php
index 924c3171..d0d9797e 100644
--- a/src/SPC/command/dev/PackLibCommand.php
+++ b/src/SPC/command/dev/PackLibCommand.php
@@ -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;
}
diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php
index 2522eb58..56235b0c 100644
--- a/src/SPC/doctor/item/LinuxToolCheckList.php
+++ b/src/SPC/doctor/item/LinuxToolCheckList.php
@@ -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 = [
diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php
index b0c663d3..ba0cd124 100644
--- a/src/SPC/store/Downloader.php
+++ b/src/SPC/store/Downloader.php
@@ -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,34 +209,7 @@ class Downloader
if ($download_as === SPC_DOWNLOAD_PRE_BUILT) {
$name = self::getPreBuiltLockName($name);
}
- self::lockSource($name, ['source_type' => SPC_SOURCE_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) ?? [];
- }
- // calculate hash
- $hash = self::getLockSourceHash($data);
- $data['hash'] = $hash;
- $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]);
}
/**
@@ -281,7 +255,7 @@ class Downloader
}
// Lock
logger()->debug("Locking git source {$name}");
- self::lockSource($name, ['source_type' => SPC_SOURCE_GIT, '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]);
/*
// 复制目录过去
@@ -377,7 +351,7 @@ class Downloader
case 'local':
// Local directory, do nothing, just lock it
logger()->debug("Locking local source {$name}");
- self::lockSource($name, [
+ LockFile::lockSource($name, [
'source_type' => SPC_SOURCE_LOCAL,
'dirname' => $pkg['dirname'],
'move_path' => $pkg['extract'] ?? null,
@@ -385,10 +359,13 @@ class Downloader
]);
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;
}
}
@@ -493,7 +470,7 @@ class Downloader
case 'local':
// Local directory, do nothing, just lock it
logger()->debug("Locking local source {$name}");
- self::lockSource($name, [
+ LockFile::lockSource($name, [
'source_type' => SPC_SOURCE_LOCAL,
'dirname' => $source['dirname'],
'move_path' => $source['extract'] ?? null,
@@ -617,43 +594,6 @@ class Downloader
return "{$source}-" . PHP_OS_FAMILY . '-' . getenv('GNU_ARCH') . '-' . (getenv('SPC_LIBC') ?: 'default') . '-' . (SystemUtil::getLibcVersionIfExists() ?? 'default');
}
- /**
- * 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 = self::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);
- }
-
/**
* Register CTRL+C event for different OS.
*
@@ -689,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'] === SPC_SOURCE_ARCHIVE && file_exists(DOWNLOAD_PATH . '/' . $lock[$name]['filename']) ||
- $lock[$name]['source_type'] === SPC_SOURCE_GIT && 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'] === SPC_SOURCE_ARCHIVE && file_exists(DOWNLOAD_PATH . '/' . $lock[$lock_name]['filename']) ||
- $lock[$lock_name]['source_type'] === SPC_SOURCE_GIT && 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;
}
}
diff --git a/src/SPC/store/LockFile.php b/src/SPC/store/LockFile.php
new file mode 100644
index 00000000..999f91b4
--- /dev/null
+++ b/src/SPC/store/LockFile.php
@@ -0,0 +1,227 @@
+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.");
+ }
+ }
+ }
+}
diff --git a/src/SPC/store/PackageManager.php b/src/SPC/store/PackageManager.php
index ca930228..d5c1043d 100644
--- a/src/SPC/store/PackageManager.php
+++ b/src/SPC/store/PackageManager.php
@@ -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,26 @@ 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);
- $source_type = $lock[$pkg_name]['source_type'];
- $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'];
+ $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
diff --git a/src/SPC/store/SourceManager.php b/src/SPC/store/SourceManager.php
index dd419e35..02d07263 100644
--- a/src/SPC/store/SourceManager.php
+++ b/src/SPC/store/SourceManager.php
@@ -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,20 +60,23 @@ 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 . ' ...');
- $filename = self::getSourceFullPath($lock[$lock_name]);
- FileSystem::extractSource($source, $lock[$lock_name]['source_type'], $filename, $lock[$lock_name]['move_path']);
- Downloader::putLockSourceHash($lock[$lock_name], $check);
+ $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[$lock_name]['hash'])) {
- $hash = Downloader::getLockSourceHash($lock[$lock_name]);
+ if (!isset($lock_content['hash'])) {
+ $hash = LockFile::getLockSourceHash($lock_content);
} else {
- $hash = $lock[$lock_name]['hash'];
+ $hash = $lock_content['hash'];
}
// when source already extracted, detect if the extracted source hash is the same as the lock file one
@@ -90,18 +88,10 @@ class SourceManager
// 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 = self::getSourceFullPath($lock[$lock_name]);
- FileSystem::extractSource($source, $lock[$lock_name]['source_type'], $filename, $lock[$lock_name]['move_path']);
- Downloader::putLockSourceHash($lock[$lock_name], $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);
}
}
-
- private static function getSourceFullPath(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']}"),
- };
- }
}
diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php
index d61020b8..11317314 100644
--- a/src/SPC/store/SourcePatcher.php
+++ b/src/SPC/store/SourcePatcher.php
@@ -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')) {
diff --git a/src/SPC/store/pkg/CustomPackage.php b/src/SPC/store/pkg/CustomPackage.php
new file mode 100644
index 00000000..89edb17e
--- /dev/null
+++ b/src/SPC/store/pkg/CustomPackage.php
@@ -0,0 +1,17 @@
+ '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");
+ }
+}
diff --git a/src/SPC/util/GlobalEnvManager.php b/src/SPC/util/GlobalEnvManager.php
index 8ffd5d05..17785205 100644
--- a/src/SPC/util/GlobalEnvManager.php
+++ b/src/SPC/util/GlobalEnvManager.php
@@ -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'));
}
}
diff --git a/src/SPC/util/UnixShell.php b/src/SPC/util/UnixShell.php
index 0f320d50..ffe18910 100644
--- a/src/SPC/util/UnixShell.php
+++ b/src/SPC/util/UnixShell.php
@@ -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;
+ }
}
diff --git a/src/globals/defines.php b/src/globals/defines.php
index eab2fcc4..ab37ace9 100644
--- a/src/globals/defines.php
+++ b/src/globals/defines.php
@@ -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
diff --git a/src/globals/functions.php b/src/globals/functions.php
index 8718f4ab..998b2d1c 100644
--- a/src/globals/functions.php
+++ b/src/globals/functions.php
@@ -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 */
diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php
index cfde83f6..229da487 100644
--- a/src/globals/test-extensions.php
+++ b/src/globals/test-extensions.php
@@ -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);
diff --git a/tests/SPC/builder/BuilderTest.php b/tests/SPC/builder/BuilderTest.php
index 17f1e799..b80f4a0d 100644
--- a/tests/SPC/builder/BuilderTest.php
+++ b/tests/SPC/builder/BuilderTest.php
@@ -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'],
];
}
diff --git a/tests/SPC/store/DownloaderTest.php b/tests/SPC/store/DownloaderTest.php
index 5c15d42e..91865bfb 100644
--- a/tests/SPC/store/DownloaderTest.php
+++ b/tests/SPC/store/DownloaderTest.php
@@ -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,9 +58,9 @@ class DownloaderTest extends TestCase
public function testLockSource()
{
- Downloader::lockSource('fake-file', ['source_type' => SPC_SOURCE_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']);