Merge pull request #772 from crazywhalecc/sapi/frankenphp

Sapi/frankenphp
This commit is contained in:
Marc 2025-06-19 09:30:16 +07:00 committed by GitHub
commit 3af40a66dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 344 additions and 49 deletions

View File

@ -7,7 +7,11 @@
> If your PR involves the changes mentioned below and completed the action, please tick the corresponding option.
> If a modification is not involved, please skip it directly.
- [ ] If you modified `*.php`, run `composer cs-fix` at local machine.
- [ ] If it's an extension or dependency update, make sure adding related extensions in `src/global/test-extensions.php`.
- [ ] If you changed the behavior of static-php-cli, update docs in `./docs/`.
- [ ] If you updated `config/xxx.json` content, run `bin/spc dev:sort-config xxx`.
- If you modified `*.php` or `*.json`, run them locally to ensure your changes are valid:
- [ ] `PHP_CS_FIXER_IGNORE_ENV=1 composer cs-fix`
- [ ] `composer analyse`
- [ ] `composer test`
- [ ] `bin/spc dev:sort-config`
- If it's an extension or dependency update, please ensure the following:
- [ ] Add your test combination to `src/globals/test-extensions.php`.
- [ ] If adding new or fixing bugs, add commit message containing `extension test` or `test extensions` to trigger full test suite.

View File

@ -1,16 +1,14 @@
name: "Extension matrix tests"
name: "Extension Matrix Tests"
on:
workflow_dispatch:
pull_request:
branches: [ "main" ]
paths:
- '.github/workflows/ext-matrix-tests.yml'
push:
jobs:
test:
name: "${{ matrix.extension }} (PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }})"
runs-on: ${{ matrix.operating-system }}
if: contains(github.event.head_commit.message, 'extension test') || contains(github.event.head_commit.message, 'test extensions')
strategy:
fail-fast: false
matrix:

View File

@ -94,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 && \

View File

@ -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
@ -68,8 +71,8 @@ CXX=${SPC_LINUX_DEFAULT_CXX}
AR=${SPC_LINUX_DEFAULT_AR}
LD=ld.gold
; default compiler flags, used in CMake toolchain file, openssl and pkg-config build
SPC_DEFAULT_C_FLAGS="-fpic -Os"
SPC_DEFAULT_CXX_FLAGS="-fpic -Os"
SPC_DEFAULT_C_FLAGS="-fPIC -Os"
SPC_DEFAULT_CXX_FLAGS="-fPIC -Os"
; extra libs for building php executable, used in `make` command for building php (this value may changed by extension build process, space separated)
SPC_EXTRA_LIBS=
; upx executable path
@ -89,7 +92,7 @@ SPC_CMD_VAR_PHP_EMBED_TYPE="static"
; *** default build vars for building php ***
; CFLAGS for configuring php
SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -fpie"
SPC_CMD_VAR_PHP_CONFIGURE_CFLAGS="${SPC_DEFAULT_C_FLAGS} -fPIE"
; CPPFLAGS for configuring php
SPC_CMD_VAR_PHP_CONFIGURE_CPPFLAGS="-I${BUILD_INCLUDE_PATH}"
; LDFLAGS for configuring php
@ -97,7 +100,7 @@ SPC_CMD_VAR_PHP_CONFIGURE_LDFLAGS="-L${BUILD_LIB_PATH}"
; LIBS for configuring php
SPC_CMD_VAR_PHP_CONFIGURE_LIBS="-ldl -lpthread -lm"
; EXTRA_CFLAGS for `make` php
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fpie ${SPC_DEFAULT_C_FLAGS}"
SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_C_FLAGS}"
; EXTRA_LIBS for `make` php
SPC_CMD_VAR_PHP_MAKE_EXTRA_LIBS="-ldl -lpthread -lm"
; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so

View File

@ -42,5 +42,17 @@
"extract-files": {
"upx-*-win64/upx.exe": "{pkg_root_path}/bin/upx.exe"
}
},
"go-mod-frankenphp-x86_64-linux": {
"type": "custom"
},
"go-mod-frankenphp-aarch64-linux": {
"type": "custom"
},
"go-mod-frankenphp-x86_64-macos": {
"type": "custom"
},
"go-mod-frankenphp-aarch64-macos": {
"type": "custom"
}
}

View File

@ -42,6 +42,9 @@ build-options:
# Set micro SAPI as win32 mode, without this, micro SAPI will be compiled as a console application (only for Windows, default: false)
enable-micro-win32: false
# Build options for shared extensions (list or comma-separated are both accepted)
shared-extensions: [ ]
# Download options
download-options:
# Use custom url for specified sources, format: "{source-name}:{url}" (e.g. "php-src:https://example.com/php-8.4.0.tar.gz")

View File

@ -401,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);
}
@ -507,6 +510,26 @@ abstract class BuilderBase
}
}
public function checkBeforeBuildPHP(int $rule): void
{
if (($rule & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) {
// frankenphp only support linux and macOS
if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) {
throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!');
}
// frankenphp needs package go-mod-frankenphp installed
$pkg_dir = PKG_ROOT_PATH . '/go-mod-frankenphp-' . 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 go-mod-frankenphp package, please install it first: {$argv[0]} install-pkg go-mod-frankenphp");
}
// frankenphp needs libxml2 libs
if (PHP_OS_FAMILY === 'Darwin' && !$this->getLib('libxml2')) {
throw new WrongUsageException('FrankenPHP SAPI for macOS requires libxml2 library, please include `xml` extension in your build.');
}
}
}
/**
* Generate micro extension test php code.
*/

View File

@ -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)

View File

@ -110,10 +110,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 +126,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 +138,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 +157,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)

View File

@ -122,6 +122,7 @@ class MacOSBuilder extends UnixBuilderBase
$enableFpm = ($build_target & BUILD_TARGET_FPM) === BUILD_TARGET_FPM;
$enableMicro = ($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO;
$enableEmbed = ($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED;
$enableFrankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP;
// prepare build php envs
$mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : '';
@ -180,6 +181,10 @@ class MacOSBuilder extends UnixBuilderBase
}
$this->buildEmbed();
}
if ($enableFrankenphp) {
logger()->info('building frankenphp');
$this->buildFrankenphp();
}
}
public function testPHP(int $build_target = BUILD_TARGET_NONE)

View File

@ -12,6 +12,7 @@ use SPC\exception\FileSystemException;
use SPC\exception\RuntimeException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\Downloader;
use SPC\store\FileSystem;
use SPC\util\DependencyUtil;
use SPC\util\SPCConfigUtil;
@ -218,6 +219,21 @@ abstract class UnixBuilderBase extends BuilderBase
throw new RuntimeException('embed failed sanity check: run failed. Error message: ' . implode("\n", $output));
}
}
// sanity check for frankenphp
if (($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) {
logger()->info('running frankenphp sanity check');
$frankenphp = BUILD_BIN_PATH . '/frankenphp';
if (!file_exists($frankenphp)) {
throw new RuntimeException('FrankenPHP binary not found: ' . $frankenphp);
}
[$ret, $output] = shell()
->setEnv(['LD_LIBRARY_PATH' => BUILD_LIB_PATH])
->execWithResult("{$frankenphp} version");
if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) {
throw new RuntimeException('FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']');
}
}
}
/**
@ -277,4 +293,64 @@ 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-mod-frankenphp-{$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'), 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('--with-debug') ? "'-w -s' " : '';
$config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list, with_dependencies: true);
$env = [
'PATH' => PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin:" . getenv('PATH'),
'GOROOT' => PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}",
'GOBIN' => PKG_ROOT_PATH . "/go-mod-frankenphp-{$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 \'-pie\' ' . $debugFlags .
'-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' .
"{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " .
"-tags=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}");
}
}

View File

@ -32,7 +32,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');
@ -83,7 +84,8 @@ class BuildPHPCommand extends BuildCommand
$this->output->writeln("<comment>\t--build-micro\tBuild phpmicro SAPI</comment>");
$this->output->writeln("<comment>\t--build-fpm\tBuild php-fpm SAPI</comment>");
$this->output->writeln("<comment>\t--build-embed\tBuild embed SAPI/libphp</comment>");
$this->output->writeln("<comment>\t--build-all\tBuild all SAPI: cli, micro, fpm, embed</comment>");
$this->output->writeln("<comment>\t--build-frankenphp\tBuild FrankenPHP SAPI/libphp</comment>");
$this->output->writeln("<comment>\t--build-all\tBuild all SAPI: cli, micro, fpm, embed, frankenphp</comment>");
return static::FAILURE;
}
if ($rule === BUILD_TARGET_ALL) {
@ -194,6 +196,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...');
@ -303,7 +308,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;
}

View File

@ -49,7 +49,7 @@ class CraftCommand extends BaseCommand
}
$static_extensions = implode(',', $craft['extensions']);
$shared_extensions = implode(',', $craft['shared-extensions']);
$shared_extensions = implode(',', $craft['shared-extensions'] ?? []);
$libs = implode(',', $craft['libs']);
// init log

View File

@ -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;
/**
@ -358,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;
}
}
@ -646,6 +650,17 @@ class Downloader
return true;
}
}
// If lock file exists, skip downloading for source mode
if (!$force && $download_as === SPC_DOWNLOAD_PACKAGE && 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("Package [{$name}] already downloaded: " . ($lock[$name]['filename'] ?? $lock[$name]['dirname']));
return true;
}
}
return false;
}
}

View File

@ -6,6 +6,7 @@ namespace SPC\store;
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\pkg\CustomPackage;
class PackageManager
{
@ -32,6 +33,20 @@ 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 = LockFile::get($pkg_name);
$source_type = $lock['source_type'];

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace SPC\store\pkg;
abstract class CustomPackage
{
abstract public function getSupportName(): array;
abstract public function fetch(string $name, bool $force = false, ?array $config = null): void;
public function extract(string $name): void
{
throw new \RuntimeException("Extract method not implemented for package: {$name}");
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace SPC\store\pkg;
use SPC\store\Downloader;
use SPC\store\FileSystem;
class GoModFrankenphp extends CustomPackage
{
public function getSupportName(): array
{
return [
'go-mod-frankenphp-x86_64-linux',
'go-mod-frankenphp-x86_64-macos',
'go-mod-frankenphp-aarch64-linux',
'go-mod-frankenphp-aarch64-macos',
];
}
public function fetch(string $name, bool $force = false, ?array $config = null): void
{
$arch = match (explode('-', $name)[3]) {
'x86_64' => 'amd64',
'aarch64' => 'arm64',
default => throw new \InvalidArgumentException('Unsupported architecture: ' . $name),
};
$os = match (explode('-', $name)[4]) {
'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;
$lock = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), 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);
// install xcaddy
$go_exec = PKG_ROOT_PATH . "{$pkgroot}/{$name}/bin/go";
// $xcaddy_exec = PKG_ROOT_PATH . "$pkgroot/$name/bin/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");
}
}

View File

@ -164,6 +164,12 @@ class ConfigValidator
if (is_string($craft['extensions'])) {
$craft['extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['extensions'])));
}
if (!isset($craft['shared-extensions'])) {
$craft['shared-extensions'] = [];
}
if (is_string($craft['shared-extensions'] ?? [])) {
$craft['shared-extensions'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['shared-extensions'])));
}
// check libs
if (isset($craft['libs']) && is_string($craft['libs'])) {
$craft['libs'] = array_filter(array_map(fn ($x) => trim($x), explode(',', $craft['libs'])));

View File

@ -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,21 @@ class UnixShell
}
return trim($str);
}
/**
* @param string $cmd
* @return string
*/
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;
}
}

View File

@ -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

View File

@ -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 */

View File

@ -40,6 +40,9 @@ $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;
@ -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-mod-frankenphp --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);