diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index a7166ec2e..13441a81f 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -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.
diff --git a/.github/workflows/ext-matrix-tests.yml b/.github/workflows/ext-matrix-tests.yml
index da30d0086..1c04f5c93 100644
--- a/.github/workflows/ext-matrix-tests.yml
+++ b/.github/workflows/ext-matrix-tests.yml
@@ -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:
diff --git a/bin/spc-gnu-docker b/bin/spc-gnu-docker
index b69e07101..9583b4dd9 100755
--- a/bin/spc-gnu-docker
+++ b/bin/spc-gnu-docker
@@ -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 && \
diff --git a/config/env.ini b/config/env.ini
index ba8652d5f..895a08e9b 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
@@ -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
diff --git a/config/pkg.json b/config/pkg.json
index 5760c0b18..a2ec8a146 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-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"
}
}
diff --git a/docs/deps-craft-yml.md b/docs/deps-craft-yml.md
index 9e660be88..0ee0e11d0 100644
--- a/docs/deps-craft-yml.md
+++ b/docs/deps-craft-yml.md
@@ -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")
diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php
index e9caabfce..6303ffec5 100644
--- a/src/SPC/builder/BuilderBase.php
+++ b/src/SPC/builder/BuilderBase.php
@@ -401,6 +401,9 @@ public function getBuildTypeName(int $type): string
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 @@ public function emitPatchPoint(string $point_name): void
}
}
+ 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.
*/
diff --git a/src/SPC/builder/freebsd/BSDBuilder.php b/src/SPC/builder/freebsd/BSDBuilder.php
index 04fd43d30..65ebea571 100644
--- a/src/SPC/builder/freebsd/BSDBuilder.php
+++ b/src/SPC/builder/freebsd/BSDBuilder.php
@@ -96,6 +96,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void
$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 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void
}
$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/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php
index 315d7df99..addd2f575 100644
--- a/src/SPC/builder/linux/LinuxBuilder.php
+++ b/src/SPC/builder/linux/LinuxBuilder.php
@@ -110,10 +110,11 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void
$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 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void
]);
// 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 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void
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 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void
$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)
diff --git a/src/SPC/builder/macos/MacOSBuilder.php b/src/SPC/builder/macos/MacOSBuilder.php
index a05221201..153da08c3 100644
--- a/src/SPC/builder/macos/MacOSBuilder.php
+++ b/src/SPC/builder/macos/MacOSBuilder.php
@@ -122,6 +122,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void
$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 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void
}
$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/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php
index 0d367de19..adde474a7 100644
--- a/src/SPC/builder/unix/UnixBuilderBase.php
+++ b/src/SPC/builder/unix/UnixBuilderBase.php
@@ -12,6 +12,7 @@
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 @@ protected function sanityCheck(int $build_target): void
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 @@ protected function patchPhpScripts(): void
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}");
+ }
}
diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php
index 353605b12..29cedf8f3 100644
--- a/src/SPC/command/BuildPHPCommand.php
+++ b/src/SPC/command/BuildPHPCommand.php
@@ -32,7 +32,8 @@ public function configure(): void
$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 @@ public function handle(): int
$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) {
@@ -194,6 +196,9 @@ public function handle(): int
// 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 @@ private function parseRules(array $shared_extensions = []): int
$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 8d3cc2bee..9a2ac4419 100644
--- a/src/SPC/command/CraftCommand.php
+++ b/src/SPC/command/CraftCommand.php
@@ -49,7 +49,7 @@ public function handle(): int
}
$static_extensions = implode(',', $craft['extensions']);
- $shared_extensions = implode(',', $craft['shared-extensions']);
+ $shared_extensions = implode(',', $craft['shared-extensions'] ?? []);
$libs = implode(',', $craft['libs']);
// init log
diff --git a/src/SPC/store/Downloader.php b/src/SPC/store/Downloader.php
index 919d1f15d..9435202fa 100644
--- a/src/SPC/store/Downloader.php
+++ b/src/SPC/store/Downloader.php
@@ -9,6 +9,7 @@
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 @@ public static function downloadPackage(string $name, ?array $pkg = null, bool $f
]);
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 @@ private static function isAlreadyDownloaded(string $name, bool $force, int $down
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;
}
}
diff --git a/src/SPC/store/PackageManager.php b/src/SPC/store/PackageManager.php
index ae2c93800..d5c1043d3 100644
--- a/src/SPC/store/PackageManager.php
+++ b/src/SPC/store/PackageManager.php
@@ -6,6 +6,7 @@
use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
+use SPC\store\pkg\CustomPackage;
class PackageManager
{
@@ -32,6 +33,20 @@ public static function installPackage(string $pkg_name, ?array $config = null, b
// 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'];
diff --git a/src/SPC/store/pkg/CustomPackage.php b/src/SPC/store/pkg/CustomPackage.php
new file mode 100644
index 000000000..89edb17e3
--- /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)[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");
+ }
+}
diff --git a/src/SPC/util/ConfigValidator.php b/src/SPC/util/ConfigValidator.php
index eae2ef2bd..dbc8ce236 100644
--- a/src/SPC/util/ConfigValidator.php
+++ b/src/SPC/util/ConfigValidator.php
@@ -164,6 +164,12 @@ public static function validateAndParseCraftFile(mixed $craft_file, Command $com
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'])));
diff --git a/src/SPC/util/UnixShell.php b/src/SPC/util/UnixShell.php
index 0f320d50c..bb4e8d5eb 100644
--- a/src/SPC/util/UnixShell.php
+++ b/src/SPC/util/UnixShell.php
@@ -44,14 +44,7 @@ public function exec(string $cmd): 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 @@ public function execWithResult(string $cmd, bool $with_log = true): array
/* @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 @@ public function getEnvString(): string
}
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;
+ }
}
diff --git a/src/globals/defines.php b/src/globals/defines.php
index eab2fcc4e..ab37ace9b 100644
--- a/src/globals/defines.php
+++ b/src/globals/defines.php
@@ -62,7 +62,8 @@
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 8718f4abf..998b2d1cc 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 cfde83f6c..79acc9849 100644
--- a/src/globals/test-extensions.php
+++ b/src/globals/test-extensions.php
@@ -40,6 +40,9 @@
// 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 @@ function quote2(string $param): string
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);