|
6 | 6 |
|
7 | 7 | use Package\Target\php; |
8 | 8 | use StaticPHP\Attribute\Package\Stage; |
| 9 | +use StaticPHP\Config\PackageConfig; |
| 10 | +use StaticPHP\Exception\EnvironmentException; |
9 | 11 | use StaticPHP\Exception\SPCInternalException; |
10 | 12 | use StaticPHP\Exception\ValidationException; |
11 | 13 | use StaticPHP\Exception\WrongUsageException; |
| 14 | +use StaticPHP\Package\LibraryPackage; |
12 | 15 | use StaticPHP\Package\PackageBuilder; |
13 | 16 | use StaticPHP\Package\PackageInstaller; |
14 | 17 | use StaticPHP\Package\TargetPackage; |
|
18 | 21 | use StaticPHP\Util\InteractiveTerm; |
19 | 22 | use StaticPHP\Util\SPCConfigUtil; |
20 | 23 | use StaticPHP\Util\System\LinuxUtil; |
| 24 | +use StaticPHP\Util\System\WindowsUtil; |
21 | 25 | use ZM\Logger\ConsoleColor; |
22 | 26 |
|
23 | 27 | trait frankenphp |
@@ -171,6 +175,197 @@ public function processFrankenphpApp(TargetPackage $package): void |
171 | 175 | } |
172 | 176 | } |
173 | 177 |
|
| 178 | + #[Stage] |
| 179 | + public function buildFrankenphpForWindows(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void |
| 180 | + { |
| 181 | + if (getenv('GOROOT') === false) { |
| 182 | + throw new EnvironmentException('go-win is not initialized properly. GOROOT is not set.'); |
| 183 | + } |
| 184 | + |
| 185 | + $clang_info = WindowsUtil::findClang(); |
| 186 | + if ($clang_info === false) { |
| 187 | + throw new EnvironmentException( |
| 188 | + 'Clang not found. FrankenPHP Windows build requires the LLVM toolchain component of Visual Studio. ' . |
| 189 | + 'Install it in Visual Studio Installer under "C++ Clang tools for Windows", or set the CC environment variable.' |
| 190 | + ); |
| 191 | + } |
| 192 | + |
| 193 | + $frankenphp_version = $this->getFrankenPHPVersion($package); |
| 194 | + $libphp_version = php::getPHPVersion(); |
| 195 | + $major = intdiv(PHP_VERSION_ID, 10000); |
| 196 | + $source_dir = $package->getSourceDir(); |
| 197 | + |
| 198 | + // collect PHP include paths in clang -I format (not MSVC /I). |
| 199 | + // Use forward slashes and NO quotes around paths: when Go passes CGO_CFLAGS tokens |
| 200 | + // directly to clang via exec(), any embedded quotes become literal characters in |
| 201 | + // the argument string and break include-path resolution. |
| 202 | + $include = str_replace('\\', '/', BUILD_INCLUDE_PATH); |
| 203 | + // The PHP source root is needed so that Windows-only headers installed only in |
| 204 | + // the source tree (e.g. win32/ioutil.h, win32/winutil.h) can be found via their |
| 205 | + // relative #include paths like `#include "win32/ioutil.h"`. |
| 206 | + $php_src = str_replace('\\', '/', SOURCE_PATH . '/php-src'); |
| 207 | + $cgo_cflags = implode(' ', [ |
| 208 | + "-I{$include}", |
| 209 | + "-I{$include}/php", |
| 210 | + "-I{$include}/php/main", |
| 211 | + "-I{$include}/php/Zend", |
| 212 | + "-I{$include}/php/TSRM", |
| 213 | + "-I{$include}/php/ext", |
| 214 | + "-I{$php_src}", |
| 215 | + "-I{$php_src}/main", |
| 216 | + "-I{$php_src}/ext", |
| 217 | + "-I{$php_src}/Zend", |
| 218 | + "-I{$php_src}/TSRM", |
| 219 | + "-DFRANKENPHP_VERSION={$frankenphp_version}", |
| 220 | + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', |
| 221 | + ]); |
| 222 | + |
| 223 | + $dep_libs = []; |
| 224 | + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { |
| 225 | + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { |
| 226 | + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { |
| 227 | + $lib_name = preg_replace('/\.lib$/i', '', $lib_file); |
| 228 | + $dep_libs[] = "-l{$lib_name}"; |
| 229 | + } |
| 230 | + } |
| 231 | + } |
| 232 | + |
| 233 | + $dep_libs = array_unique($dep_libs); |
| 234 | + $lib_dir = str_replace('\\', '/', BUILD_LIB_PATH); |
| 235 | + $php_embed_lib = "-lphp{$major}embed"; |
| 236 | + $win_sys_libs = '-lkernel32 -lole32 -luser32 -ladvapi32 -lshell32 -lws2_32 -ldnsapi -lpsapi -lbcrypt'; |
| 237 | + $cgo_ldflags = clean_spaces(implode(' ', array_filter([ |
| 238 | + "-L{$lib_dir}", |
| 239 | + $php_embed_lib, |
| 240 | + implode(' ', $dep_libs), |
| 241 | + $win_sys_libs, |
| 242 | + '-llibcmt', |
| 243 | + '-Wl,/NODEFAULTLIB:msvcrt', |
| 244 | + '-Wl,/NODEFAULTLIB:msvcrtd', |
| 245 | + '-Wl,/FORCE:MULTIPLE', |
| 246 | + ]))); |
| 247 | + |
| 248 | + // build tags: skip watcher (no inotify/kqueue on Windows) |
| 249 | + $go_build_tags = 'nobadger,nomysql,nopgx,nowatcher'; |
| 250 | + if (!$installer->isPackageResolved('brotli')) { |
| 251 | + $go_build_tags .= ',nobrotli'; |
| 252 | + } |
| 253 | + |
| 254 | + $go_ldflags = |
| 255 | + '-extldflags=-fuse-ld=lld ' . |
| 256 | + "-X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy' " . |
| 257 | + "-X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' " . |
| 258 | + "-X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP v{$frankenphp_version} PHP {$libphp_version} Caddy'"; |
| 259 | + |
| 260 | + // CGO on Windows tokenizes CC/CXX like a shell command line, splitting on spaces. |
| 261 | + // Paths like "C:\Program Files\..." break because only "C:\Program" is used. |
| 262 | + // Fix: prepend clang's directory to PATH and use plain executable names instead, |
| 263 | + // which matches FrankenPHP's official CI approach (CC=clang, CXX=clang++). |
| 264 | + $clang_dir = dirname($clang_info['clang']); |
| 265 | + $env = [ |
| 266 | + 'CGO_ENABLED' => '1', |
| 267 | + 'CC' => 'clang.exe', |
| 268 | + 'CXX' => 'clang++.exe', |
| 269 | + 'PATH' => $clang_dir . ';' . getenv('PATH'), |
| 270 | + 'CGO_CFLAGS' => clean_spaces($cgo_cflags), |
| 271 | + 'CGO_LDFLAGS' => $cgo_ldflags, |
| 272 | + ]; |
| 273 | + |
| 274 | + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('embedding Windows metadata')); |
| 275 | + $package->runStage([$this, 'embedFrankenphpWindowsMetadata']); |
| 276 | + |
| 277 | + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('building with go build')); |
| 278 | + |
| 279 | + cmd()->cd("{$source_dir}\\caddy\\frankenphp") |
| 280 | + ->setEnv($env) |
| 281 | + ->exec("go build -v -tags \"{$go_build_tags}\" -ldflags \"{$go_ldflags}\" -o frankenphp.exe ."); |
| 282 | + |
| 283 | + $builder->deployBinary("{$source_dir}\\caddy\\frankenphp\\frankenphp.exe", BUILD_BIN_PATH . '\frankenphp.exe'); |
| 284 | + $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '\frankenphp.exe'); |
| 285 | + } |
| 286 | + |
| 287 | + /** |
| 288 | + * Embed Windows PE metadata (version info + icon) into resource.syso so that |
| 289 | + * go build picks it up automatically. Mirrors the official FrankenPHP Windows CI. |
| 290 | + */ |
| 291 | + #[Stage] |
| 292 | + public function embedFrankenphpWindowsMetadata(TargetPackage $package): void |
| 293 | + { |
| 294 | + $frankenphp_version = $this->getFrankenPHPVersion($package); |
| 295 | + $source_dir = $package->getSourceDir(); |
| 296 | + $build_dir = "{$source_dir}\\caddy\\frankenphp"; |
| 297 | + |
| 298 | + [$p1, $p2, $p3] = explode('.', $frankenphp_version); |
| 299 | + $major = (int) $p1; |
| 300 | + $minor = (int) $p2; |
| 301 | + $patch = (int) $p3; |
| 302 | + |
| 303 | + $version_info = [ |
| 304 | + 'FixedFileInfo' => [ |
| 305 | + 'FileVersion' => ['Major' => $major, 'Minor' => $minor, 'Patch' => $patch, 'Build' => 0], |
| 306 | + 'ProductVersion' => ['Major' => $major, 'Minor' => $minor, 'Patch' => $patch, 'Build' => 0], |
| 307 | + ], |
| 308 | + 'StringFileInfo' => [ |
| 309 | + 'CompanyName' => 'FrankenPHP', |
| 310 | + 'FileDescription' => 'The modern PHP app server', |
| 311 | + 'FileVersion' => $frankenphp_version, |
| 312 | + 'InternalName' => 'frankenphp', |
| 313 | + 'OriginalFilename' => 'frankenphp.exe', |
| 314 | + 'LegalCopyright' => '(c) 2022 Kévin Dunglas, MIT License', |
| 315 | + 'ProductName' => 'FrankenPHP', |
| 316 | + 'ProductVersion' => $frankenphp_version, |
| 317 | + 'Comments' => 'https://frankenphp.dev/', |
| 318 | + ], |
| 319 | + 'VarFileInfo' => [ |
| 320 | + 'Translation' => ['LangID' => 9, 'CharsetID' => 1200], |
| 321 | + ], |
| 322 | + ]; |
| 323 | + |
| 324 | + file_put_contents("{$build_dir}\\versioninfo.json", json_encode($version_info, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); |
| 325 | + |
| 326 | + // Install goversioninfo if not already installed. |
| 327 | + // GOPATH is set by the go-win artifact initializer via GlobalEnvManager::putenv(). |
| 328 | + $goversioninfo = getenv('GOROOT') . '\bin\goversioninfo.exe'; |
| 329 | + if (!file_exists($goversioninfo)) { |
| 330 | + cmd()->exec('go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest'); |
| 331 | + } |
| 332 | + |
| 333 | + // -64: embed as 64-bit resource; -icon: relative path from the build dir to the repo root icon. |
| 334 | + cmd()->cd($build_dir) |
| 335 | + ->exec("\"{$goversioninfo}\" -64 -icon {$package->getSourceDir()}\\frankenphp.ico versioninfo.json -o resource.syso"); |
| 336 | + } |
| 337 | + |
| 338 | + #[Stage] |
| 339 | + public function smokeTestFrankenphpForWindows(PackageBuilder $builder): void |
| 340 | + { |
| 341 | + // analyse --no-smoke-test option |
| 342 | + $no_smoke_test = $builder->getOption('no-smoke-test', false); |
| 343 | + $option = match ($no_smoke_test) { |
| 344 | + false => false, // default value, run all smoke tests |
| 345 | + null => 'all', // --no-smoke-test without value, skip all smoke tests |
| 346 | + default => parse_comma_list($no_smoke_test), // --no-smoke-test=frankenphp,... |
| 347 | + }; |
| 348 | + if ($option === 'all' || (is_array($option) && in_array('frankenphp', $option, true))) { |
| 349 | + return; |
| 350 | + } |
| 351 | + |
| 352 | + InteractiveTerm::setMessage('Running FrankenPHP smoke test'); |
| 353 | + $frankenphp = BUILD_BIN_PATH . '\frankenphp.exe'; |
| 354 | + if (!file_exists($frankenphp)) { |
| 355 | + throw new ValidationException( |
| 356 | + "FrankenPHP binary not found: {$frankenphp}", |
| 357 | + validation_module: 'FrankenPHP smoke test' |
| 358 | + ); |
| 359 | + } |
| 360 | + [$ret, $output] = cmd()->execWithResult("{$frankenphp} version"); |
| 361 | + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { |
| 362 | + throw new ValidationException( |
| 363 | + 'FrankenPHP failed smoke test: ret[' . $ret . ']. out[' . implode('', $output) . ']', |
| 364 | + validation_module: 'FrankenPHP smoke test' |
| 365 | + ); |
| 366 | + } |
| 367 | + } |
| 368 | + |
174 | 369 | protected function getFrankenPHPVersion(TargetPackage $package): string |
175 | 370 | { |
176 | 371 | if ($version = getenv('FRANKENPHP_VERSION')) { |
|
0 commit comments