diff --git a/src/SPC/builder/windows/WindowsBuilder.php b/src/SPC/builder/windows/WindowsBuilder.php index e4f1578c4..83997e792 100644 --- a/src/SPC/builder/windows/WindowsBuilder.php +++ b/src/SPC/builder/windows/WindowsBuilder.php @@ -124,8 +124,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void SourcePatcher::unpatchMicroWin32(); } if ($enableEmbed) { - logger()->warning('Windows does not currently support embed SAPI.'); - // logger()->info('building embed'); + logger()->info('building embed'); $this->buildEmbed(); } } @@ -191,13 +190,49 @@ public function buildCgi(): void public function buildEmbed(): void { - // TODO: add embed support for windows - /* - FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_embed_wrapper.bat', 'nmake /nologo %*'); + $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; + + // Add debug symbols for release build if --no-strip is specified + $debug_overrides = ''; + $makefile_content = file_get_contents(SOURCE_PATH . '\php-src\Makefile'); + if ($this->getOption('no-strip', false)) { + if (preg_match('/^CFLAGS=(.+?)$/m', $makefile_content, $matches)) { + $cflags = $matches[1]; + $cflags = str_replace('/Ox ', '/O2 /Zi ', $cflags); + $debug_overrides = '"CFLAGS=' . $cflags . '" "LDFLAGS=/DEBUG /LTCG /INCREMENTAL:NO secur32.lib" '; + } + } + // Fallback: if no debug overrides, still pass secur32.lib via LDFLAGS. + // Needed by curl's SSPI support (InitSecurityInterfaceA in libcurl_a.lib). + if ($debug_overrides === '') { + $debug_overrides = '"LDFLAGS=secur32.lib" '; + } + + // add nmake wrapper + FileSystem::writeFile(SOURCE_PATH . '\php-src\nmake_embed_wrapper.bat', "nmake /nologo {$debug_overrides}LIBS_EMBED=\"ws2_32.lib shell32.lib {$extra_libs}\" %*"); cmd()->cd(SOURCE_PATH . '\php-src') ->exec("{$this->sdk_prefix} nmake_embed_wrapper.bat --task-args php8embed.lib"); - */ + + $this->deploySAPIBinary(BUILD_TARGET_EMBED); + + // Install headers for embed SDK consumers + $include_dst = BUILD_ROOT_PATH . '\include\php'; + FileSystem::createDir($include_dst); + $php_src = SOURCE_PATH . '\php-src'; + foreach (['main', 'Zend', 'TSRM', 'sapi', 'ext'] as $dir) { + $src_dir = "{$php_src}\\{$dir}"; + if (is_dir($src_dir)) { + cmd()->exec('xcopy /E /I /Y ' . escapeshellarg($src_dir . '\*.h') . ' ' . escapeshellarg($include_dst . '\\' . $dir . '\\')); + } + } + // Copy generated config header + $rel_type = 'Release'; + $ts = $this->zts ? '_TS' : ''; + $config_h = "{$php_src}\\x64\\{$rel_type}{$ts}\\config.w32.h"; + if (file_exists($config_h)) { + cmd()->exec('copy ' . escapeshellarg($config_h) . ' ' . escapeshellarg($include_dst . '\main\config.w32.h')); + } } public function buildMicro(): void @@ -375,11 +410,14 @@ public function deploySAPIBinary(int $type): void BUILD_TARGET_CLI => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'php.exe', 'php.pdb'], BUILD_TARGET_MICRO => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'micro.sfx', 'micro.pdb'], BUILD_TARGET_CGI => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'php-cgi.exe', 'php-cgi.pdb'], + BUILD_TARGET_EMBED => [SOURCE_PATH . "\\php-src\\x64\\{$rel_type}{$ts}", 'php8embed.lib', null], default => throw new SPCInternalException("Deployment does not accept type {$type}"), }; - $src = "{$src[0]}\\{$src[1]}"; - $dst = BUILD_BIN_PATH . '\\' . basename($src); + $src_dir = $src[0]; + $src_pdb = $src[2]; + $src = "{$src_dir}\\{$src[1]}"; + $dst = ($type === BUILD_TARGET_EMBED ? BUILD_ROOT_PATH . '\lib' : BUILD_BIN_PATH) . '\\' . basename($src); // file must exists if (!file_exists($src)) { @@ -393,9 +431,17 @@ public function deploySAPIBinary(int $type): void cmd()->exec('copy ' . escapeshellarg($src) . ' ' . escapeshellarg($dst)); } + // Also copy php8embed.dll for embed SAPI + if ($type === BUILD_TARGET_EMBED) { + $dll_src = "{$src_dir}\\php8embed.dll"; + if (file_exists($dll_src)) { + cmd()->exec('copy ' . escapeshellarg($dll_src) . ' ' . escapeshellarg(BUILD_ROOT_PATH . '\lib\php8embed.dll')); + } + } + // extract debug info in buildroot/debug - if ($this->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) { - cmd()->exec('copy ' . escapeshellarg("{$src[0]}\\{$src[2]}") . ' ' . escapeshellarg($debug_dir)); + if ($this->getOption('no-strip', false) && $src_pdb !== null && file_exists("{$src_dir}\\{$src_pdb}")) { + cmd()->exec('copy ' . escapeshellarg("{$src_dir}\\{$src_pdb}") . ' ' . escapeshellarg($debug_dir)); } // with-upx-pack for cli and micro diff --git a/src/SPC/store/FileSystem.php b/src/SPC/store/FileSystem.php index d2d44b51f..073b37922 100644 --- a/src/SPC/store/FileSystem.php +++ b/src/SPC/store/FileSystem.php @@ -639,7 +639,17 @@ private static function extractArchive(string $filename, string $target): void // Yeah, I will be an MS HATER ! match (self::extname($filename)) { 'tar' => f_passthru("tar -xf {$filename} -C {$target} --strip-components 1"), - 'xz', 'txz', 'gz', 'tgz', 'bz2' => cmd()->execWithResult("\"{$_7z}\" x -so {$filename} | tar -f - -x -C \"{$target}\" --strip-components 1"), + 'xz', 'txz', 'gz', 'tgz', 'bz2' => (function () use ($_7z, $filename, $target) { + $dir = dirname($filename); + cmd()->execWithResult("\"{$_7z}\" x \"{$filename}\" -o\"{$dir}\" -y"); + $tarFile = preg_replace('/\.(xz|txz|gz|tgz|bz2)$/i', '', $filename); + if (!file_exists($tarFile)) { + $tarFile .= '.tar'; + } + $winTar = getenv('SystemRoot') . '\System32\tar.exe'; + f_passthru("\"{$winTar}\" -xf \"{$tarFile}\" -C \"{$target}\" --strip-components 1"); + @unlink($tarFile); + })(), 'zip' => self::unzipWithStrip($filename, $target), default => throw new FileSystemException("unknown archive format: {$filename}"), }; diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index 7ce2d2002..9b9e54731 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -283,6 +283,9 @@ public static function patchSwoole(): bool public static function patchBeforeMake(BuilderBase $builder): void { + if ($builder instanceof WindowsBuilder) { + self::patchLibxml2DefForWindows(); + } if ($builder instanceof UnixBuilderBase) { FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'install-micro', ''); } @@ -624,6 +627,113 @@ public static function patchPhpLibxml212(): bool return false; } + /** + * Strip symbols from php_libxml2.def that don't exist in libxml2_a.lib. + * + * libxml2 2.14+ removed many deprecated APIs (xmlUCSIs*, xmlNanoFTP*, + * xmlShell*, etc.) but PHP's static .def file still exports them, causing + * unresolved externals at link time. Instead of maintaining a blocklist, + * we scan the installed libxml2 headers for XMLPUBFUN/XMLPUBVAR + * declarations and strip any .def entry that doesn't match. + */ + public static function patchLibxml2DefForWindows(): void + { + $def_file = SOURCE_PATH . '/php-src/ext/libxml/php_libxml2.def'; + $include_dir = BUILD_ROOT_PATH . '/include/libxml2/libxml'; + if (!file_exists($def_file) || !is_dir($include_dir)) { + logger()->debug("patchLibxml2DefForWindows: def={$def_file} exists=" . (file_exists($def_file) ? 'yes' : 'no') . " include_dir={$include_dir} exists=" . (is_dir($include_dir) ? 'yes' : 'no')); + return; + } + + // Determine which libxml2 features are disabled so we can exclude + // symbols that are declared in headers but not compiled into the lib. + $disabled_features = []; + $version_h = "{$include_dir}/xmlversion.h"; + if (file_exists($version_h)) { + $version_content = file_get_contents($version_h); + // Disabled features have: #undef LIBXML__ENABLED + // or simply lack the #define. Check for explicit #undef. + if (preg_match_all('/#undef\s+(LIBXML_\w+_ENABLED)/', $version_content, $m)) { + foreach ($m[1] as $feat) { + $disabled_features[$feat] = true; + } + } + } + + // Scan all libxml2 headers for public API symbols. + // XMLPUBFUN marks functions, XMLPUBVAR marks variables. + // Respects #ifdef LIBXML_*_ENABLED guards — symbols inside + // disabled feature blocks are excluded. + $header_symbols = []; + foreach (glob("{$include_dir}/*.h") as $header) { + $content = file_get_contents($header); + $lines = explode("\n", $content); + $ifdef_depth = 0; + $disabled_depth = 0; // depth at which a disabled feature was entered + + foreach ($lines as $hline) { + $trimmed = ltrim($hline); + // Track #ifdef/#ifndef LIBXML_*_ENABLED blocks + if (preg_match('/^#\s*if(?:def|ndef)?\s+.*?(LIBXML_\w+_ENABLED)/', $trimmed, $im)) { + ++$ifdef_depth; + if (isset($disabled_features[$im[1]])) { + $disabled_depth = $ifdef_depth; + } + } elseif (preg_match('/^#\s*if\b/', $trimmed)) { + ++$ifdef_depth; + } elseif (preg_match('/^#\s*endif/', $trimmed)) { + if ($ifdef_depth === $disabled_depth) { + $disabled_depth = 0; + } + $ifdef_depth = max(0, $ifdef_depth - 1); + } + + // Skip symbols inside disabled feature blocks + if ($disabled_depth > 0) { + continue; + } + + // Match: XMLPUBFUN [call_conv] ( + if (preg_match('/XMLPUBFUN\s+\S+\s+(?:XMLCALL\s+|XMLCDECL\s+)?(\w+)\s*\(/', $trimmed, $fm)) { + $header_symbols[$fm[1]] = true; + } + // Match: XMLPUBVAR + if (preg_match('/XMLPUBVAR\s+\S+\s+(\w+)/', $trimmed, $vm)) { + $header_symbols[$vm[1]] = true; + } + } + } + + if (empty($header_symbols)) { + logger()->warning('Could not find any XMLPUBFUN/XMLPUBVAR symbols in libxml2 headers — skipping .def patch'); + return; + } + + logger()->debug('Found ' . count($header_symbols) . ' public symbols in libxml2 headers'); + + $lines = file($def_file, FILE_IGNORE_NEW_LINES); + $filtered = []; + $removed = 0; + foreach ($lines as $line) { + $sym = trim($line); + // Keep non-symbol lines (EXPORTS header, comments, blank lines) + // and any symbol that exists in the headers + if ($sym === '' || $sym === 'EXPORTS' || str_starts_with($sym, ';') || isset($header_symbols[$sym])) { + $filtered[] = $line; + } else { + ++$removed; + } + } + + if ($removed === 0) { + logger()->info('All php_libxml2.def symbols found in libxml2 headers — no patching needed'); + return; + } + + file_put_contents($def_file, implode("\n", $filtered) . "\n"); + logger()->info("Stripped {$removed} missing symbols from php_libxml2.def (libxml2 compat)"); + } + public static function patchGDWin32(): bool { $file = file_get_contents(SOURCE_PATH . '/php-src/main/php_version.h'); diff --git a/tests/SPC/store/SourcePatcherTest.php b/tests/SPC/store/SourcePatcherTest.php new file mode 100644 index 000000000..e5a51b019 --- /dev/null +++ b/tests/SPC/store/SourcePatcherTest.php @@ -0,0 +1,70 @@ +defDir = SOURCE_PATH . '/php-src/ext/libxml'; + if (!is_dir($this->defDir)) { + mkdir($this->defDir, 0755, true); + } + $this->defFile = $this->defDir . '/php_libxml2.def'; + + // Create fake buildroot/lib directory + $this->libDir = BUILD_ROOT_PATH . '/lib'; + if (!is_dir($this->libDir)) { + mkdir($this->libDir, 0755, true); + } + $this->libFile = $this->libDir . '/libxml2_a.lib'; + } + + protected function tearDown(): void + { + if (file_exists($this->defFile)) { + unlink($this->defFile); + } + if (file_exists($this->libFile)) { + unlink($this->libFile); + } + @rmdir(SOURCE_PATH . '/php-src/ext/libxml'); + @rmdir(SOURCE_PATH . '/php-src/ext'); + @rmdir(SOURCE_PATH . '/php-src'); + } + + public function testPatchLibxml2DefNoOpWhenDefFileMissing(): void + { + // No .def file — should return silently + SourcePatcher::patchLibxml2DefForWindows(); + $this->assertFileDoesNotExist($this->defFile); + } + + public function testPatchLibxml2DefNoOpWhenLibFileMissing(): void + { + // .def exists but no .lib — should return silently + file_put_contents($this->defFile, "EXPORTS\nxmlUCSIsArabic\n"); + $original = file_get_contents($this->defFile); + + SourcePatcher::patchLibxml2DefForWindows(); + + $this->assertEquals($original, file_get_contents($this->defFile)); + } +}