Add frankenphp build

This commit is contained in:
crazywhalecc
2026-04-12 23:17:50 +08:00
parent 4ddc137eae
commit 8cc5c82595
4 changed files with 256 additions and 6 deletions

View File

@@ -11,8 +11,16 @@ frankenphp:
depends:
- php-embed
- go-xcaddy
depends@windows:
- php-embed
- go-win
- pthreads4w
suggests@unix:
- brotli
- watcher
suggests@windows:
- brotli
static-bins@unix:
- frankenphp
static-bins@windows:
- frankenphp.exe

View File

@@ -342,7 +342,11 @@ class php extends TargetPackage
public function postInstall(TargetPackage $package, PackageInstaller $installer): void
{
if ($package->getName() === 'frankenphp') {
$package->runStage([$this, 'smokeTestFrankenphpForUnix']);
if (SystemTarget::getTargetOS() === 'Windows') {
$package->runStage([$this, 'smokeTestFrankenphpForWindows']);
} else {
$package->runStage([$this, 'smokeTestFrankenphpForUnix']);
}
return;
}
if ($package->getName() !== 'php') {

View File

@@ -6,9 +6,12 @@ namespace Package\Target\php;
use Package\Target\php;
use StaticPHP\Attribute\Package\Stage;
use StaticPHP\Config\PackageConfig;
use StaticPHP\Exception\EnvironmentException;
use StaticPHP\Exception\SPCInternalException;
use StaticPHP\Exception\ValidationException;
use StaticPHP\Exception\WrongUsageException;
use StaticPHP\Package\LibraryPackage;
use StaticPHP\Package\PackageBuilder;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Package\TargetPackage;
@@ -18,6 +21,7 @@ use StaticPHP\Util\FileSystem;
use StaticPHP\Util\InteractiveTerm;
use StaticPHP\Util\SPCConfigUtil;
use StaticPHP\Util\System\LinuxUtil;
use StaticPHP\Util\System\WindowsUtil;
use ZM\Logger\ConsoleColor;
trait frankenphp
@@ -171,6 +175,198 @@ trait frankenphp
}
}
#[Stage]
public function buildFrankenphpForWindows(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
if (getenv('GOROOT') === false) {
throw new EnvironmentException('go-win is not initialized properly. GOROOT is not set.');
}
$clang_info = WindowsUtil::findClang();
if ($clang_info === false) {
throw new EnvironmentException(
'Clang not found. FrankenPHP Windows build requires the LLVM toolchain component of Visual Studio. ' .
'Install it in Visual Studio Installer under "C++ Clang tools for Windows", or set the CC environment variable.'
);
}
$frankenphp_version = $this->getFrankenPHPVersion($package);
$libphp_version = php::getPHPVersion();
$major = intdiv(PHP_VERSION_ID, 10000);
$source_dir = $package->getSourceDir();
// collect PHP include paths in clang -I format (not MSVC /I).
// Use forward slashes and NO quotes around paths: when Go passes CGO_CFLAGS tokens
// directly to clang via exec(), any embedded quotes become literal characters in
// the argument string and break include-path resolution.
$include = str_replace('\\', '/', BUILD_INCLUDE_PATH);
// The PHP source root is needed so that Windows-only headers installed only in
// the source tree (e.g. win32/ioutil.h, win32/winutil.h) can be found via their
// relative #include paths like `#include "win32/ioutil.h"`.
$php_src = str_replace('\\', '/', SOURCE_PATH . '/php-src');
$cgo_cflags = implode(' ', [
"-I{$include}",
"-I{$include}/php",
"-I{$include}/php/main",
"-I{$include}/php/Zend",
"-I{$include}/php/TSRM",
"-I{$include}/php/ext",
"-I{$php_src}",
"-I{$php_src}/main",
"-I{$php_src}/ext",
"-I{$php_src}/Zend",
"-I{$php_src}/TSRM",
"-DFRANKENPHP_VERSION={$frankenphp_version}",
'-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1',
]);
$dep_libs = [];
foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) {
foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) {
if (file_exists("{$package->getLibDir()}\\{$lib_file}")) {
$lib_name = preg_replace('/\.lib$/i', '', $lib_file);
$dep_libs[] = "-l{$lib_name}";
}
}
}
$dep_libs = array_unique($dep_libs);
$lib_dir = str_replace('\\', '/', BUILD_LIB_PATH);
$php_embed_lib = "-lphp{$major}embed";
$win_sys_libs = '-lkernel32 -lole32 -luser32 -ladvapi32 -lshell32 -lws2_32 -ldnsapi -lpsapi -lbcrypt';
$cgo_ldflags = clean_spaces(implode(' ', array_filter([
"-L{$lib_dir}",
$php_embed_lib,
implode(' ', $dep_libs),
$win_sys_libs,
'-llibcmt',
'-Wl,/NODEFAULTLIB:msvcrt',
'-Wl,/NODEFAULTLIB:msvcrtd',
'-Wl,/FORCE:MULTIPLE',
])));
// build tags: skip watcher (no inotify/kqueue on Windows)
$go_build_tags = 'nobadger,nomysql,nopgx,nowatcher';
if (!$installer->isPackageResolved('brotli')) {
$go_build_tags .= ',nobrotli';
}
$go_ldflags =
'-extldflags=-fuse-ld=lld ' .
"-X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy' " .
"-X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' " .
"-X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP v{$frankenphp_version} PHP {$libphp_version} Caddy'";
// CGO on Windows tokenizes CC/CXX like a shell command line, splitting on spaces.
// Paths like "C:\Program Files\..." break because only "C:\Program" is used.
// Fix: prepend clang's directory to PATH and use plain executable names instead,
// which matches FrankenPHP's official CI approach (CC=clang, CXX=clang++).
$clang_dir = dirname($clang_info['clang']);
$env = [
'CGO_ENABLED' => '1',
'CC' => 'clang.exe',
'CXX' => 'clang++.exe',
'PATH' => $clang_dir . ';' . getenv('PATH'),
'CGO_CFLAGS' => clean_spaces($cgo_cflags),
'CGO_LDFLAGS' => $cgo_ldflags,
];
InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('embedding Windows metadata'));
$package->runStage([$this, 'embedFrankenphpWindowsMetadata']);
InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('building with go build'));
cmd()->cd("{$source_dir}\\caddy\\frankenphp")
->setEnv($env)
->exec("go build -v -tags \"{$go_build_tags}\" -ldflags \"{$go_ldflags}\" -o frankenphp.exe .");
$builder->deployBinary("{$source_dir}\\caddy\\frankenphp\\frankenphp.exe", BUILD_BIN_PATH . '\frankenphp.exe');
$package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '\frankenphp.exe');
}
/**
* Embed Windows PE metadata (version info + icon) into resource.syso so that
* go build picks it up automatically. Mirrors the official FrankenPHP Windows CI.
*/
#[Stage]
public function embedFrankenphpWindowsMetadata(TargetPackage $package): void
{
$frankenphp_version = $this->getFrankenPHPVersion($package);
$source_dir = $package->getSourceDir();
$build_dir = "{$source_dir}\\caddy\\frankenphp";
// Parse version components for the FixedFileInfo block.
$parts = explode('.', $frankenphp_version);
$major = (int) ($parts[0] ?? 0);
$minor = (int) ($parts[1] ?? 0);
$patch = (int) ($parts[2] ?? 0);
$version_info = [
'FixedFileInfo' => [
'FileVersion' => ['Major' => $major, 'Minor' => $minor, 'Patch' => $patch, 'Build' => 0],
'ProductVersion' => ['Major' => $major, 'Minor' => $minor, 'Patch' => $patch, 'Build' => 0],
],
'StringFileInfo' => [
'CompanyName' => 'FrankenPHP',
'FileDescription' => 'The modern PHP app server',
'FileVersion' => $frankenphp_version,
'InternalName' => 'frankenphp',
'OriginalFilename' => 'frankenphp.exe',
'LegalCopyright' => '(c) 2022 Kévin Dunglas, MIT License',
'ProductName' => 'FrankenPHP',
'ProductVersion' => $frankenphp_version,
'Comments' => 'https://frankenphp.dev/',
],
'VarFileInfo' => [
'Translation' => ['LangID' => 9, 'CharsetID' => 1200],
],
];
file_put_contents("{$build_dir}\\versioninfo.json", json_encode($version_info, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
// Install goversioninfo if not already installed.
// GOPATH is set by the go-win artifact initializer via GlobalEnvManager::putenv().
$goversioninfo = getenv('GOROOT') . '\bin\goversioninfo.exe';
if (!file_exists($goversioninfo)) {
cmd()->exec('go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest');
}
// -64: embed as 64-bit resource; -icon: relative path from the build dir to the repo root icon.
cmd()->cd($build_dir)
->exec("\"{$goversioninfo}\" -64 -icon {$package->getSourceDir()}\\frankenphp.ico versioninfo.json -o resource.syso");
}
#[Stage]
public function smokeTestFrankenphpForWindows(PackageBuilder $builder): void
{
// analyse --no-smoke-test option
$no_smoke_test = $builder->getOption('no-smoke-test', false);
$option = match ($no_smoke_test) {
false => false, // default value, run all smoke tests
null => 'all', // --no-smoke-test without value, skip all smoke tests
default => parse_comma_list($no_smoke_test), // --no-smoke-test=frankenphp,...
};
if ($option === 'all' || (is_array($option) && in_array('frankenphp', $option, true))) {
return;
}
InteractiveTerm::setMessage('Running FrankenPHP smoke test');
$frankenphp = BUILD_BIN_PATH . '\frankenphp.exe';
if (!file_exists($frankenphp)) {
throw new ValidationException(
"FrankenPHP binary not found: {$frankenphp}",
validation_module: 'FrankenPHP smoke test'
);
}
[$ret, $output] = cmd()->execWithResult("{$frankenphp} version");
if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) {
throw new ValidationException(
'FrankenPHP failed smoke test: ret[' . $ret . ']. out[' . implode('', $output) . ']',
validation_module: 'FrankenPHP smoke test'
);
}
}
protected function getFrankenPHPVersion(TargetPackage $package): string
{
if ($version = getenv('FRANKENPHP_VERSION')) {

View File

@@ -447,12 +447,26 @@ trait windows
}
#[BuildFor('Windows')]
public function buildWin(TargetPackage $package): void
public function buildWin(TargetPackage $package, PackageInstaller $installer): void
{
if ($package->getName() === 'frankenphp') {
/* @var php $this */
$package->runStage([$this, 'buildFrankenphpForWindows']);
return;
}
if ($package->getName() !== 'php') {
return;
}
// maintainer can skip build though ...
if (
$installer->isPackageResolved('php-embed')
&& $installer->getTargetPackage('php-embed')->getBuildOption('maintainer-skip-build')
&& file_exists(BUILD_LIB_PATH . '\php8embed.lib')
) {
return;
}
$package->runStage([$this, 'buildconfForWindows']);
$package->runStage([$this, 'configureForWindows']);
$package->runStage([$this, 'makeForWindows']);
@@ -467,6 +481,32 @@ trait windows
// php-src patches from micro
SourcePatcher::patchPhpSrc();
/* wsyslog.h is generated by mc.exe from win32/build/wsyslog.mc but is absent in some
PHP tarballs (e.g. 8.4.x). wsyslog.c still #includes it for the PHP_SYSLOG_*_TYPE
event-ID constants. Recreate the missing header with the correct mc.exe-encoded values:
MessageId=N + Severity bits (Success=0x00, Info=0x40, Warning=0x80, Error=0xC0)
combined into a 32-bit DWORD (Facility=0, Customer=0).
*/
$wsyslog_h = "{$package->getSourceDir()}\\win32\\wsyslog.h";
if (!file_exists($wsyslog_h)) {
$shim = <<<'HEADER'
/* Auto-generated compatibility shim: wsyslog.h (from win32/build/wsyslog.mc) */
#ifndef WSYSLOG_H
#define WSYSLOG_H
#include "syslog.h"
/* Event IDs generated by mc.exe from wsyslog.mc (Facility=0, Customer=0) */
#define PHP_SYSLOG_SUCCESS_TYPE ((DWORD)0x00000001L)
#define PHP_SYSLOG_INFO_TYPE ((DWORD)0x40000002L)
#define PHP_SYSLOG_WARNING_TYPE ((DWORD)0x80000003L)
#define PHP_SYSLOG_ERROR_TYPE ((DWORD)0xC0000004L)
#endif /* WSYSLOG_H */
HEADER;
FileSystem::writeFile($wsyslog_h, $shim);
}
// php 8.1 bug
if ($this->getPHPVersionID() >= 80100 && $this->getPHPVersionID() < 80200) {
logger()->info('Patching PHP 8.1 windows Fiber bug');
@@ -656,22 +696,24 @@ C_CODE;
$ts = $package->getBuildOption('enable-zts', false) ? '_TS' : '';
$build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}";
// Build include flags pointing to source dirs (like PHP Windows build does)
// Note: embed.c uses #include <sapi/embed/php_embed.h>, so we need $source_dir itself
$zts_define = $ts ? ' /D ZTS=1' : '';
$include_flags = sprintf(
'/I"%s" /I"%s\main" /I"%s\Zend" /I"%s\TSRM" /I"%s" ' .
'/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES',
'/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES%s',
$build_dir,
$source_dir,
$source_dir,
$source_dir,
$source_dir
$source_dir,
$zts_define
);
// MSVC cl.exe format: compiler flags must come before /link, linker flags after
// ldflags contains /LIBPATH which must be after /link
// /FORCE:MULTIPLE: in ZTS mode both zend.obj and php_embed.obj (both packed into the fat php8embed.lib) define _tsrm_ls_cache as a __declspec(thread) variable.
$compile_cmd = sprintf(
'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /LIBPATH:"%s\lib" %s %s',
'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /FORCE:MULTIPLE /LIBPATH:"%s\lib" %s %s',
$include_flags,
BUILD_ROOT_PATH,
$config['libs'],