fix(windows): survive cmd.exe's 8191 character command line limit

- with big extension stacks (imagick) the link command lines exceed what
  cmd.exe allows and fail with 'The command line is too long'
- pass the collected LIBS_CLI/LIBS_CGI/LIBS_MICRO lib lists to link.exe
  through @response files, like php-src already does for its object lists
- compile the frankenphp -mthreads shim instead of generating a .bat:
  batch files run through cmd.exe and cap cgo link lines at 8191 chars,
  a real executable gets the full 32K and past that go writes a response
  file by itself; still probe-gated, still a stopgap for golang/go#80290
This commit is contained in:
m-this
2026-07-09 13:31:54 +02:00
parent 4d6f334cbb
commit c54fa78ad1
2 changed files with 84 additions and 12 deletions

View File

@@ -26,6 +26,55 @@ use ZM\Logger\ConsoleColor;
trait frankenphp
{
/**
* Source of the -mthreads stripping shim, see windowsCgoCompilers(). The %s
* placeholder receives the wrapped compiler path as a C string literal.
*/
private const string MTHREADS_SHIM_C = <<<'C'
// Generated by static-php-cli. Drops the MinGW-only -mthreads flag Go passes to
// cgo (golang/go#80290) and forwards everything else to clang.
#include <windows.h>
#include <wchar.h>
#include <stdlib.h>
static const wchar_t compiler[] = L"%s";
int main(void)
{
const wchar_t *args = GetCommandLineW();
// skip our own (possibly quoted) argv[0]
if (*args == L'"') {
args++;
while (*args && *args != L'"') args++;
if (*args) args++;
} else {
while (*args && *args != L' ' && *args != L'\t') args++;
}
// "<compiler>" + args + trailing space so a flag at the very end still matches
size_t len = wcslen(compiler) + wcslen(args) + 8;
wchar_t *cmd = calloc(len, sizeof(wchar_t));
if (cmd == NULL) return 1;
swprintf(cmd, len, L"\"%%ls\"%%ls ", compiler, args);
// remove every space-delimited -mthreads token
wchar_t *p;
while ((p = wcsstr(cmd, L" -mthreads ")) != NULL) {
wmemmove(p, p + 10, wcslen(p + 10) + 1);
}
STARTUPINFOW si = {sizeof(si)};
PROCESS_INFORMATION pi;
if (!CreateProcessW(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
return 127;
}
WaitForSingleObject(pi.hProcess, INFINITE);
DWORD code = 1;
GetExitCodeProcess(pi.hProcess, &code);
return (int) code;
}
C;
#[Stage]
public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void
{
@@ -382,8 +431,12 @@ trait frankenphp
* Return the [CC, CXX] cgo should use to build FrankenPHP on Windows.
*
* Go passes the MinGW-only `-mthreads` flag to the C compiler for cgo builds;
* Clang >= 20 rejects it for the MSVC target. When it does, wrap Clang with
* a tiny .bat that strips the flag before forwarding.
* Clang >= 20 rejects it for the MSVC target. When it does, wrap Clang with a
* small compiled shim that strips the flag before forwarding. An executable and
* not a .bat on purpose: batch files run through cmd.exe, which caps the command
* line at 8191 characters, and cgo link steps with big extension stacks (imagick)
* blow past that. A real process gets the full ~32K limit, and beyond that Go
* switches to a response file by itself.
*
* Workaround for https://github.com/golang/go/issues/80290, remove once Go
* stops passing the flag.
@@ -403,14 +456,17 @@ trait frankenphp
logger()->info('Clang rejects -mthreads; wrapping it to strip the flag (golang/go#80290)');
$dir = dirname($clang);
$cc = $package->getSourceDir() . '\cc-nothreads.bat';
$cxx = $package->getSourceDir() . '\cxx-nothreads.bat';
// %*: the whole command line; the substring replace drops the -mthreads token,
// preserving quoting of paths that a for-loop would mangle.
file_put_contents($cc, "@echo off\r\nset \"a=%*\"\r\nset \"a=%a: -mthreads = %\"\r\n\"{$dir}\\clang.exe\" %a%\r\n");
file_put_contents($cxx, "@echo off\r\nset \"a=%*\"\r\nset \"a=%a: -mthreads = %\"\r\n\"{$dir}\\clang++.exe\" %a%\r\n");
$wrappers = [];
foreach (['cc-nothreads' => 'clang.exe', 'cxx-nothreads' => 'clang++.exe'] as $name => $compiler) {
$src = $package->getSourceDir() . "\\{$name}.c";
$exe = $package->getSourceDir() . "\\{$name}.exe";
file_put_contents($src, sprintf(self::MTHREADS_SHIM_C, str_replace('\\', '\\\\', "{$dir}\\{$compiler}")));
cmd()->cd($package->getSourceDir())
->exec('"' . $clang . '" -O2 ' . escapeshellarg($src) . ' -o ' . escapeshellarg($exe));
$wrappers[] = $exe;
}
return [$cc, $cxx];
return $wrappers;
}
protected function getFrankenPHPVersion(TargetPackage $package): string

View File

@@ -175,8 +175,9 @@ trait windows
}
}
$libs_cli = $this->makeLibsResponseFile($package, 'cli', "ws2_32.lib shell32.lib {$extra_libs}");
cmd()->cd($package->getSourceDir())
->exec("nmake /nologo {$debug_overrides}LIBS_CLI=\"ws2_32.lib shell32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php.exe");
->exec("nmake /nologo {$debug_overrides}LIBS_CLI=\"{$libs_cli}\" EXTRA_LD_FLAGS_PROGRAM= php.exe");
$this->deployWindowsBinary($builder, $package, 'php-cli');
}
@@ -245,8 +246,9 @@ trait windows
}
}
$libs_cgi = $this->makeLibsResponseFile($package, 'cgi', "ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}");
cmd()->cd($package->getSourceDir())
->exec("nmake /nologo {$debug_overrides}LIBS_CGI=\"ws2_32.lib kernel32.lib advapi32.lib {$extra_libs}\" EXTRA_LD_FLAGS_PROGRAM= php-cgi.exe");
->exec("nmake /nologo {$debug_overrides}LIBS_CGI=\"{$libs_cgi}\" EXTRA_LD_FLAGS_PROGRAM= php-cgi.exe");
$this->deployWindowsBinary($builder, $package, 'php-cgi');
}
@@ -331,8 +333,9 @@ trait windows
$fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : '';
$libs_micro = $this->makeLibsResponseFile($package, 'micro', "ws2_32.lib shell32.lib {$extra_libs}");
cmd()->cd($package->getSourceDir())
->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro");
->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"{$libs_micro}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro");
$this->deployWindowsBinary($builder, $package, 'php-micro');
}
@@ -884,4 +887,17 @@ C_CODE;
$package->setOutput('PHP headers path for embed SAPI', $php_include_dir);
}
/**
* Write the collected lib list into a linker response file and return the @file
* token for the nmake command line. With big extension stacks (imagick alone pulls
* in ~60 static libs) the inline list can push the whole command past cmd.exe's
* 8191 character limit; link.exe reads @file without any length limit.
*/
private function makeLibsResponseFile(TargetPackage $package, string $sapi, string $libs): string
{
$file = "{$package->getSourceDir()}\\spc-libs-{$sapi}.rsp";
FileSystem::writeFile($file, $libs);
return "@{$file}";
}
}