Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
66 changes: 56 additions & 10 deletions src/SPC/builder/windows/WindowsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)) {
Expand All @@ -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
Expand Down
12 changes: 11 additions & 1 deletion src/SPC/store/FileSystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}"),
};
Expand Down
110 changes: 110 additions & 0 deletions src/SPC/store/SourcePatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -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', '');
}
Expand Down Expand Up @@ -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_<FEATURE>_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 <return_type> [call_conv] <name>(
if (preg_match('/XMLPUBFUN\s+\S+\s+(?:XMLCALL\s+|XMLCDECL\s+)?(\w+)\s*\(/', $trimmed, $fm)) {
$header_symbols[$fm[1]] = true;
}
// Match: XMLPUBVAR <type> <name>
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');
Expand Down
70 changes: 70 additions & 0 deletions tests/SPC/store/SourcePatcherTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace SPC\Tests\store;

use PHPUnit\Framework\TestCase;
use SPC\store\SourcePatcher;

/**
* @internal
*/
class SourcePatcherTest extends TestCase
{
private string $defDir;

private string $defFile;

private string $libDir;

private string $libFile;

protected function setUp(): void
{
// Create fake php-src/ext/libxml directory under SOURCE_PATH
$this->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));
}
}
Loading