diff --git a/config/env.ini b/config/env.ini index ba8652d5..112d0187 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/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 diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 315d7df9..fc131214 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -114,6 +114,7 @@ class LinuxBuilder extends UnixBuilderBase $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; + $enable_frankenphp = ($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP; $mimallocLibs = $this->getLib('mimalloc') !== null ? BUILD_LIB_PATH . '/mimalloc.o ' : ''; // prepare build php envs @@ -175,6 +176,10 @@ class LinuxBuilder extends UnixBuilderBase } $this->buildEmbed(); } + if ($enable_frankenphp) { + logger()->info('building frankenphp'); + $this->buildFrankenphp(); + } } public function testPHP(int $build_target = BUILD_TARGET_NONE) diff --git a/src/SPC/builder/traits/UnixGoCheckTrait.php b/src/SPC/builder/traits/UnixGoCheckTrait.php new file mode 100644 index 00000000..12e3d005 --- /dev/null +++ b/src/SPC/builder/traits/UnixGoCheckTrait.php @@ -0,0 +1,83 @@ +findCommand('go', $paths) === null) { + $this->installGo(); + } + + $gobin = getenv('GOBIN') ?: (getenv('HOME') . '/go/bin'); + putenv("GOBIN={$gobin}"); + + $paths[] = $gobin; + + if ($this->findCommand('xcaddy', $paths) === null) { + shell(true)->exec('go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest'); + } + + return CheckResult::ok(); + } + + private function installGo(): bool + { + $prefix = ''; + if (get_current_user() !== 'root') { + $prefix = 'sudo '; + logger()->warning('Current user is not root, using sudo for running command'); + } + + $arch = php_uname('m'); + $go_arch = match ($arch) { + 'x86_64' => 'amd64', + 'aarch64' => 'arm64', + default => $arch + }; + $os = strtolower(PHP_OS_FAMILY); + + $go_version = '1.24.4'; + $go_filename = "go{$go_version}.{$os}-{$go_arch}.tar.gz"; + $go_url = "https://go.dev/dl/{$go_filename}"; + + logger()->info("Downloading Go {$go_version} for {$go_arch}"); + + try { + // Download Go binary + Downloader::downloadFile('go', $go_url, $go_filename); + + // Extract the tarball + FileSystem::extractSource('go', SPC_SOURCE_ARCHIVE, DOWNLOAD_PATH . "/{$go_filename}"); + + // Move to /usr/local/go + logger()->info('Installing Go to /usr/local/go'); + shell()->exec("{$prefix}rm -rf /usr/local/go"); + shell()->exec("{$prefix}mv " . SOURCE_PATH . '/go /usr/local/'); + + if (!str_contains(getenv('PATH'), '/usr/local/go/bin')) { + logger()->info('Adding Go to PATH'); + shell()->exec("{$prefix}echo 'export PATH=\$PATH:/usr/local/go/bin' >> /etc/profile"); + putenv('PATH=' . getenv('PATH') . ':/usr/local/go/bin'); + } + + logger()->info('Go has been installed successfully'); + return true; + } catch (RuntimeException $e) { + logger()->error('Failed to install Go: ' . $e->getMessage()); + return false; + } + } +} diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 0d367de1..19880bf1 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -277,4 +277,34 @@ abstract class UnixBuilderBase extends BuilderBase FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str); } } + + protected function buildFrankenphp(): void + { + $path = getenv('PATH'); + $xcaddyPath = getenv('GOBIN') ?: (getenv('HOME') . '/go/bin'); + if (!str_contains($path, $xcaddyPath)) { + $path = $path . ':' . $xcaddyPath; + } + $path = BUILD_BIN_PATH . ':' . $path; + f_putenv("PATH={$path}"); + + $brotliLibs = $this->getLib('brotli') !== null ? '-lbrotlienc -lbrotlidec -lbrotlicommon' : ''; + $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; + $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; + + $env = [ + 'CGO_ENABLED' => '1', + 'CGO_CFLAGS' => '$(php-config --includes) -I$(php-config --include-dir)/..', + 'CGO_LDFLAGS' => "$(php-config --ldflags) $(php-config --libs) {$brotliLibs} -lwatcher-c -lphp -Wl,-rpath=" . BUILD_LIB_PATH, + 'XCADDY_GO_BUILD_FLAGS' => "-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" . $nobrotli . $nowatcher, + ]; + shell()->cd(BUILD_BIN_PATH) + ->setEnv($env) + ->exec( + 'xcaddy build ' . + '--output frankenphp ' . + '--with github.com/dunglas/frankenphp/caddy ' . + getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES') + ); + } } diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 353605b1..0ef9dab3 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -33,6 +33,7 @@ class BuildPHPCommand extends BuildCommand $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-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("\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) { @@ -304,6 +306,7 @@ class BuildPHPCommand extends BuildCommand $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); + $rule |= ($this->getOption('build-frankenphp') || !empty($shared_extensions) ? BUILD_TARGET_FRANKENPHP : BUILD_TARGET_NONE); $rule |= ($this->getOption('build-all') ? BUILD_TARGET_ALL : BUILD_TARGET_NONE); return $rule; } diff --git a/src/SPC/doctor/AsCheckItem.php b/src/SPC/doctor/AsCheckItem.php index f64d914b..0417bcfa 100644 --- a/src/SPC/doctor/AsCheckItem.php +++ b/src/SPC/doctor/AsCheckItem.php @@ -14,5 +14,6 @@ class AsCheckItem public ?string $limit_os = null, public int $level = 100, public bool $manual = false, - ) {} + ) { + } } diff --git a/src/SPC/doctor/item/BSDToolCheckList.php b/src/SPC/doctor/item/BSDToolCheckList.php index 2505227b..97f0ccf9 100644 --- a/src/SPC/doctor/item/BSDToolCheckList.php +++ b/src/SPC/doctor/item/BSDToolCheckList.php @@ -47,6 +47,12 @@ class BSDToolCheckList return CheckResult::ok(); } + #[AsCheckItem('if xcaddy is installed', limit_os: 'BSD')] + public function checkXcaddy(): ?CheckResult + { + return $this->checkGoAndXcaddy(); + } + #[AsFixItem('build-tools-bsd')] public function fixBuildTools(array $missing): bool { diff --git a/src/SPC/doctor/item/LinuxToolCheckList.php b/src/SPC/doctor/item/LinuxToolCheckList.php index 56235b0c..07f6b5fb 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace SPC\doctor\item; use SPC\builder\linux\SystemUtil; +use SPC\builder\traits\UnixGoCheckTrait; use SPC\builder\traits\UnixSystemUtilTrait; use SPC\doctor\AsCheckItem; use SPC\doctor\AsFixItem; @@ -14,6 +15,7 @@ use SPC\exception\RuntimeException; class LinuxToolCheckList { use UnixSystemUtilTrait; + use UnixGoCheckTrait; public const TOOLS_ALPINE = [ 'make', 'bison', 'flex', @@ -87,6 +89,12 @@ class LinuxToolCheckList return CheckResult::ok(); } + #[AsCheckItem('if xcaddy is installed', limit_os: 'Linux')] + public function checkXcaddy(): ?CheckResult + { + return $this->checkGoAndXcaddy(); + } + #[AsCheckItem('if cmake version >= 3.18', limit_os: 'Linux')] public function checkCMakeVersion(): ?CheckResult { diff --git a/src/SPC/doctor/item/MacOSToolCheckList.php b/src/SPC/doctor/item/MacOSToolCheckList.php index b4043a1d..57ba8157 100644 --- a/src/SPC/doctor/item/MacOSToolCheckList.php +++ b/src/SPC/doctor/item/MacOSToolCheckList.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace SPC\doctor\item; +use SPC\builder\traits\UnixGoCheckTrait; use SPC\builder\traits\UnixSystemUtilTrait; use SPC\doctor\AsCheckItem; use SPC\doctor\AsFixItem; @@ -13,6 +14,7 @@ use SPC\exception\RuntimeException; class MacOSToolCheckList { use UnixSystemUtilTrait; + use UnixGoCheckTrait; /** @var string[] MacOS 环境下编译依赖的命令 */ public const REQUIRED_COMMANDS = [ @@ -34,6 +36,12 @@ class MacOSToolCheckList 'glibtoolize', ]; + #[AsCheckItem('if xcaddy is installed', limit_os: 'Darwin')] + public function checkXcaddy(): ?CheckResult + { + return $this->checkGoAndXcaddy(); + } + #[AsCheckItem('if homebrew has installed', limit_os: 'Darwin', level: 998)] public function checkBrew(): ?CheckResult { diff --git a/src/globals/defines.php b/src/globals/defines.php index eab2fcc4..aebd4d5f 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 = BUILD_TARGET_EMBED | 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