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

This commit is contained in:
Jerry Ma
2026-07-10 21:02:19 +08:00
committed by GitHub
3 changed files with 87 additions and 15 deletions

View File

@@ -25,13 +25,13 @@ class imagemagick
* msbuild. The resulting CORE_RL_*.lib static libraries + MagickWand/MagickCore headers are * msbuild. The resulting CORE_RL_*.lib static libraries + MagickWand/MagickCore headers are
* installed into the build root for ext-imagick to link. * installed into the build root for ext-imagick to link.
* *
* A short working directory is used (VisualMagick's tree is deeply nested and otherwise exceeds * The VisualMagick tree lives under source/ so bin/spc reset cleans it up like everything
* MAX_PATH); override with SPC_IMAGEMAGICK_BUILD_DIR. * else; override the location with SPC_IMAGEMAGICK_BUILD_DIR.
*/ */
#[BuildFor('Windows')] #[BuildFor('Windows')]
public function buildWin(LibraryPackage $lib): void public function buildWin(LibraryPackage $lib): void
{ {
$work = getenv('SPC_IMAGEMAGICK_BUILD_DIR') ?: 'C:\im'; $work = getenv('SPC_IMAGEMAGICK_BUILD_DIR') ?: SOURCE_PATH . '\imagemagick-win';
$configure_release = '2026.05.30.2033'; $configure_release = '2026.05.30.2033';
$configure_url = "https://github.com/ImageMagick/Configure/releases/download/{$configure_release}/Configure.Release.x64.exe"; $configure_url = "https://github.com/ImageMagick/Configure/releases/download/{$configure_release}/Configure.Release.x64.exe";

View File

@@ -26,6 +26,55 @@ use ZM\Logger\ConsoleColor;
trait frankenphp 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] #[Stage]
public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void 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. * 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; * 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 * Clang >= 20 rejects it for the MSVC target. When it does, wrap Clang with a
* a tiny .bat that strips the flag before forwarding. * 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 * Workaround for https://github.com/golang/go/issues/80290, remove once Go
* stops passing the flag. * stops passing the flag.
@@ -403,14 +456,17 @@ trait frankenphp
logger()->info('Clang rejects -mthreads; wrapping it to strip the flag (golang/go#80290)'); logger()->info('Clang rejects -mthreads; wrapping it to strip the flag (golang/go#80290)');
$dir = dirname($clang); $dir = dirname($clang);
$cc = $package->getSourceDir() . '\cc-nothreads.bat'; $wrappers = [];
$cxx = $package->getSourceDir() . '\cxx-nothreads.bat'; foreach (['cc-nothreads' => 'clang.exe', 'cxx-nothreads' => 'clang++.exe'] as $name => $compiler) {
// %*: the whole command line; the substring replace drops the -mthreads token, $src = $package->getSourceDir() . "\\{$name}.c";
// preserving quoting of paths that a for-loop would mangle. $exe = $package->getSourceDir() . "\\{$name}.exe";
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($src, sprintf(self::MTHREADS_SHIM_C, str_replace('\\', '\\\\', "{$dir}\\{$compiler}")));
file_put_contents($cxx, "@echo off\r\nset \"a=%*\"\r\nset \"a=%a: -mthreads = %\"\r\n\"{$dir}\\clang++.exe\" %a%\r\n"); 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 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()) 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'); $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()) 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'); $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' : ''; $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()) 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'); $this->deployWindowsBinary($builder, $package, 'php-micro');
} }
@@ -884,4 +887,17 @@ C_CODE;
$package->setOutput('PHP headers path for embed SAPI', $php_include_dir); $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}";
}
} }