Merge pull request #775 from crazywhalecc/sapi/frankenphp-prerequisites

Sapi/frankenphp prerequisites
This commit is contained in:
Marc 2025-06-19 08:54:42 +07:00 committed by GitHub
commit 1a164fa057
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 213 additions and 128 deletions

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

@ -404,6 +404,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);
}
@ -510,6 +513,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

@ -1,83 +0,0 @@
<?php
declare(strict_types=1);
namespace SPC\builder\traits;
use SPC\doctor\CheckResult;
use SPC\exception\RuntimeException;
use SPC\store\Downloader;
use SPC\store\FileSystem;
trait UnixGoCheckTrait
{
private function checkGoAndXcaddy(): ?CheckResult
{
$paths = explode(PATH_SEPARATOR, getenv('PATH'));
$goroot = getenv('GOROOT') ?: '/usr/local/go';
$goBin = "{$goroot}/bin";
$paths[] = $goBin;
if ($this->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;
}
}
}

View File

@ -219,6 +219,19 @@ 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()->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 @@ abstract class UnixBuilderBase extends BuilderBase
*/
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 @@ abstract class UnixBuilderBase extends BuilderBase
$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 @@ abstract class UnixBuilderBase extends BuilderBase
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 @@ abstract class UnixBuilderBase extends BuilderBase
];
shell()->cd(BUILD_BIN_PATH)
->setEnv($env)
->exec('xcaddy build --output frankenphp ' . $xcaddyModules);
->exec("{$xcaddy_exec} build --output frankenphp {$xcaddyModules}");
}
}

View File

@ -196,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...');
@ -316,7 +319,7 @@ class BuildPHPCommand extends BuildCommand
$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;
}

View File

@ -47,12 +47,6 @@ 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
{

View File

@ -5,7 +5,6 @@ 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;
@ -15,7 +14,6 @@ use SPC\exception\RuntimeException;
class LinuxToolCheckList
{
use UnixSystemUtilTrait;
use UnixGoCheckTrait;
public const TOOLS_ALPINE = [
'make', 'bison', 'flex',
@ -89,12 +87,6 @@ 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
{

View File

@ -4,7 +4,6 @@ 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;
@ -14,7 +13,6 @@ use SPC\exception\RuntimeException;
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
{

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;
/**
@ -385,10 +386,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;
}
}
@ -708,7 +712,6 @@ class Downloader
}
}
// 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 @@ 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 = json_decode(FileSystem::readFile(DOWNLOAD_PATH . '/.lock.json'), true);
$source_type = $lock[$pkg_name]['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 ? (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
}
}

View File

@ -62,7 +62,7 @@ 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_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

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;
@ -177,7 +180,7 @@ if ($argv[1] === 'build_cmd' || $argv[1] === 'build_embed_cmd') {
$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 @@ 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);