diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 3cafe502..854dc8cf 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -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 + #include + #include + + 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++; + } + + // "" + 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 diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index dfc09882..915b1821 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -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}"; + } }