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']);