Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config/pkg.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
23 changes: 23 additions & 0 deletions src/SPC/builder/BuilderBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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 (!$this->getLib('libxml2')) {
throw new WrongUsageException('FrankenPHP SAPI requires libxml2 library, please include `xml` extension in your build.');
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a requirement on macos? FrankenPHP itself doesn't need libxml2, it needs brotli and watcher for all the default functionality, but that's what -nowatcher and -nobrotli tags are for.

Copy link
Owner Author

@crazywhalecc crazywhalecc Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. When I build only one extension without dependencies, it fails to build, saying it can't find libxml-2.0. But this seems to be a problem with FrankenPHP itself. It will force add the system's homebrew and /usr/local directories as LDFLAGS, which will make the spc build unstable. I don't know much about FrankenPHP's build system, but it seems to hardcode the LDFLAGS in the go source.

EDIT: libxml2 missing seems not related to hardcoded LDFLAGS things. These are two problems.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems to be macos specific then? I'm able to build without libxml

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dunglas https://github.com/php/frankenphp/blob/main/frankenphp.go#L17

why are libxml2 and the brew paths required on MacOS? This may introduce problems with potentially linking to dynamic libraries or oldrr versions, depending on the order the linker iterates through the library paths

Copy link
Collaborator

@henderkes henderkes Jun 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may also introduce issues on linux when a user has the php-devel package installed

is there a way to make Go ignore these linker comments?

}
}

/**
* Generate micro extension test php code.
*/
Expand Down
83 changes: 0 additions & 83 deletions src/SPC/builder/traits/UnixGoCheckTrait.php

This file was deleted.

52 changes: 37 additions & 15 deletions src/SPC/builder/unix/UnixBuilderBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 . ' -v');
if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) {
throw new RuntimeException('FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']');
}
}
}

/**
Expand Down Expand Up @@ -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');
Expand All @@ -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' : '';
Expand All @@ -313,21 +329,27 @@ 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}",
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
];
shell()->cd(BUILD_BIN_PATH)
->setEnv($env)
->exec('xcaddy build --output frankenphp ' . $xcaddyModules);
->exec("{$xcaddy_exec} build --output frankenphp {$xcaddyModules}");
}
}
5 changes: 4 additions & 1 deletion src/SPC/command/BuildPHPCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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...');
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 0 additions & 6 deletions src/SPC/doctor/item/BSDToolCheckList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
8 changes: 0 additions & 8 deletions src/SPC/doctor/item/LinuxToolCheckList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,7 +14,6 @@
class LinuxToolCheckList
{
use UnixSystemUtilTrait;
use UnixGoCheckTrait;

public const TOOLS_ALPINE = [
'make', 'bison', 'flex',
Expand Down Expand Up @@ -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
{
Expand Down
8 changes: 0 additions & 8 deletions src/SPC/doctor/item/MacOSToolCheckList.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,7 +13,6 @@
class MacOSToolCheckList
{
use UnixSystemUtilTrait;
use UnixGoCheckTrait;

/** @var string[] MacOS 环境下编译依赖的命令 */
public const REQUIRED_COMMANDS = [
Expand All @@ -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
{
Expand Down
22 changes: 18 additions & 4 deletions src/SPC/store/Downloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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 (
Expand All @@ -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;
}
}
15 changes: 15 additions & 0 deletions src/SPC/store/PackageManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use SPC\exception\FileSystemException;
use SPC\exception\WrongUsageException;
use SPC\store\pkg\CustomPackage;

class PackageManager
{
Expand All @@ -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'];
Expand Down
Loading
Loading