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/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index 7fc78e447..daa27765a 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -404,6 +404,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); } @@ -510,6 +513,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/traits/UnixGoCheckTrait.php b/src/SPC/builder/traits/UnixGoCheckTrait.php deleted file mode 100644 index 12e3d005f..000000000 --- a/src/SPC/builder/traits/UnixGoCheckTrait.php +++ /dev/null @@ -1,83 +0,0 @@ -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 9801d47bd..d424c5947 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -219,6 +219,19 @@ 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()->execWithResult("{$frankenphp} version"); + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { + throw new RuntimeException('FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']'); + } + } } /** @@ -285,16 +298,19 @@ protected function patchPhpScripts(): void */ 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}"); + $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 + $go_exec = PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin/go"; + $xcaddy_exec = PKG_ROOT_PATH . "/go-mod-frankenphp-{$arch}-{$os}/bin/xcaddy"; - $brotliLibs = $this->getLib('brotli') !== null ? '-lbrotlienc -lbrotlidec -lbrotlicommon' : ''; - $watcherLibs = $this->getLib('watcher') !== null ? '-lwatcher-c' : ''; $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; $xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES'); @@ -303,7 +319,7 @@ protected function buildFrankenphp(): void $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 broli library is not built. Disabling 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' : ''; @@ -313,14 +329,20 @@ protected function buildFrankenphp(): void if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { $libphpVersion = preg_replace('/\.\d$/', '', $libphpVersion); } - $debugFlags = $this->getOption('--with-debug') ? "'-w -s' " : ''; + $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' => '$(php-config --includes) -I$(php-config --include-dir)/..', - 'CGO_LDFLAGS' => '$(php-config --ldflags) -L' . BUILD_LIB_PATH . " $(php-config --libs) {$brotliLibs} {$watcherLibs} -lphp {$lrt}", + 'CGO_CFLAGS' => $config['cflags'], + 'CGO_LDFLAGS' => "{$config['ldflags']} {$config['libs']} {$lrt}", 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . - '-ldflags \\"-linkmode=external -extldflags \'-pie\' '. $debugFlags . + '-ldflags \"-linkmode=external -extldflags \'-pie\' ' . $debugFlags . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . "{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags=nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", @@ -328,6 +350,6 @@ protected function buildFrankenphp(): void ]; shell()->cd(BUILD_BIN_PATH) ->setEnv($env) - ->exec('xcaddy build --output frankenphp ' . $xcaddyModules); + ->exec("{$xcaddy_exec} build --output frankenphp {$xcaddyModules}"); } } diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 72d6a686f..29cedf8f3 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -196,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...'); @@ -316,7 +319,7 @@ private function parseRules(array $shared_extensions = []): int $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_NONE); + $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/doctor/item/BSDToolCheckList.php b/src/SPC/doctor/item/BSDToolCheckList.php index 97f0ccf90..2505227b0 100644 --- a/src/SPC/doctor/item/BSDToolCheckList.php +++ b/src/SPC/doctor/item/BSDToolCheckList.php @@ -47,12 +47,6 @@ public function checkCliTools(): ?CheckResult 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 07f6b5fb6..56235b0cf 100644 --- a/src/SPC/doctor/item/LinuxToolCheckList.php +++ b/src/SPC/doctor/item/LinuxToolCheckList.php @@ -5,7 +5,6 @@ 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; @@ -15,7 +14,6 @@ class LinuxToolCheckList { use UnixSystemUtilTrait; - use UnixGoCheckTrait; public const TOOLS_ALPINE = [ 'make', 'bison', 'flex', @@ -89,12 +87,6 @@ public function checkCliTools(): ?CheckResult 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 57ba81577..b4043a1d3 100644 --- a/src/SPC/doctor/item/MacOSToolCheckList.php +++ b/src/SPC/doctor/item/MacOSToolCheckList.php @@ -4,7 +4,6 @@ namespace SPC\doctor\item; -use SPC\builder\traits\UnixGoCheckTrait; use SPC\builder\traits\UnixSystemUtilTrait; use SPC\doctor\AsCheckItem; use SPC\doctor\AsFixItem; @@ -14,7 +13,6 @@ class MacOSToolCheckList { use UnixSystemUtilTrait; - use UnixGoCheckTrait; /** @var string[] MacOS 环境下编译依赖的命令 */ public const REQUIRED_COMMANDS = [ @@ -36,12 +34,6 @@ 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/SPC/store/Downloader.php b/src/SPC/store/Downloader.php index b0c663d36..e5cc6aae5 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; /** @@ -385,10 +386,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; } } @@ -708,7 +712,6 @@ private static function isAlreadyDownloaded(string $name, bool $force, int $down } } // 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 with env if ( @@ -719,6 +722,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 ca9302282..7e8ae3fd2 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 = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true); $source_type = $lock[$pkg_name]['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 ? (PKG_ROOT_PATH . "{$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"); + // TODO: Here to download dependencies for xcaddy and frankenphp first + } +} diff --git a/src/globals/defines.php b/src/globals/defines.php index aebd4d5f5..ab37ace9b 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -62,7 +62,7 @@ const BUILD_TARGET_MICRO = 2; // build micro const BUILD_TARGET_FPM = 4; // build fpm const BUILD_TARGET_EMBED = 8; // build embed -const BUILD_TARGET_FRANKENPHP = BUILD_TARGET_EMBED | 16; // build frankenphp +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 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 442431400..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; @@ -177,7 +180,7 @@ function quote2(string $param): string $build_cmd .= $no_strip ? '--no-strip ' : ''; $build_cmd .= $upx ? '--with-upx-pack ' : ''; $build_cmd .= $final_libs === '' ? '' : ('--with-libs=' . quote2($final_libs) . ' '); - $build_cmd .= str_starts_with($argv[2], 'windows-') ? '' : '--build-fpm --build-frankenphp'; + $build_cmd .= str_starts_with($argv[2], 'windows-') ? '' : '--build-fpm '; $build_cmd .= '--debug '; } @@ -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);