diff --git a/config/pkg/lib/pthreads4w.yml b/config/pkg/lib/pthreads4w.yml index 06a6245f..5c8eabac 100644 --- a/config/pkg/lib/pthreads4w.yml +++ b/config/pkg/lib/pthreads4w.yml @@ -10,3 +10,4 @@ pthreads4w: license: Apache-2.0 static-libs@windows: - libpthreadVC3.lib + - pthreadVC3.lib diff --git a/config/pkg/target/frankenphp.yml b/config/pkg/target/frankenphp.yml index 96f9c082..21d47448 100644 --- a/config/pkg/target/frankenphp.yml +++ b/config/pkg/target/frankenphp.yml @@ -11,8 +11,16 @@ frankenphp: depends: - php-embed - go-xcaddy + depends@windows: + - php-embed + - go-win + - pthreads4w suggests@unix: - brotli - watcher + suggests@windows: + - brotli static-bins@unix: - frankenphp + static-bins@windows: + - frankenphp.exe diff --git a/config/pkg/target/go-win.yml b/config/pkg/target/go-win.yml new file mode 100644 index 00000000..46c13f5f --- /dev/null +++ b/config/pkg/target/go-win.yml @@ -0,0 +1,10 @@ +go-win: + type: target + artifact: + binary: custom + env: + GOROOT: '{pkg_root_path}/go-win' + GOBIN: '{pkg_root_path}/go-win/bin' + GOPATH: '{pkg_root_path}/go-win/go' + path@windows: + - '{pkg_root_path}/go-win/bin' diff --git a/src/Package/Artifact/go_win.php b/src/Package/Artifact/go_win.php new file mode 100644 index 00000000..44cf61b9 --- /dev/null +++ b/src/Package/Artifact/go_win.php @@ -0,0 +1,85 @@ +executeCurl('https://go.dev/VERSION?m=text') ?: ''); + if ($version === '') { + throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + } + + // find SHA256 hash from download page + $page = default_shell()->executeCurl('https://go.dev/dl/'); + if ($page === '' || $page === false) { + throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); + } + + $version_regex = str_replace('.', '\.', $version); + $pattern = "/class=\"download\" href=\"\\/dl\\/{$version_regex}\\.windows-amd64\\.zip\">.*?([a-f0-9]{64})<\\/tt>/s"; + if (preg_match($pattern, $page, $matches)) { + $hash = $matches[1]; + } else { + throw new DownloaderException("Failed to find download hash for Go {$version} windows-amd64"); + } + + $url = "https://go.dev/dl/{$version}.windows-amd64.zip"; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . "{$version}.windows-amd64.zip"; + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + + // verify hash + $file_hash = hash_file('sha256', $path); + if ($file_hash !== $hash) { + throw new DownloaderException("Hash mismatch for downloaded go-win binary. Expected {$hash}, got {$file_hash}"); + } + + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-win", verified: true, version: $version); + } + + #[CustomBinaryCheckUpdate('go-win', ['windows-x86_64'])] + public function checkUpdateBinary(?string $old_version): CheckUpdateResult + { + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + if ($version === '') { + throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + } + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || $version !== $old_version, + ); + } + + #[AfterBinaryExtract('go-win', ['windows-x86_64'])] + public function afterExtract(string $target_path): void + { + if (!file_exists("{$target_path}\\bin\\go.exe")) { + throw new DownloaderException("Go installation appears incomplete: go.exe not found at {$target_path}\\bin\\go.exe"); + } + + GlobalEnvManager::putenv("GOROOT={$target_path}"); + GlobalEnvManager::putenv("GOPATH={$target_path}\\gopath"); + GlobalEnvManager::putenv("GOCACHE={$target_path}\\gocache"); + GlobalEnvManager::putenv("GOMODCACHE={$target_path}\\gopath\\pkg\\mod"); + GlobalEnvManager::addPathIfNotExists("{$target_path}\\bin"); + } +} diff --git a/src/Package/Library/pthreads4w.php b/src/Package/Library/pthreads4w.php index 69f181f0..6a263d4e 100644 --- a/src/Package/Library/pthreads4w.php +++ b/src/Package/Library/pthreads4w.php @@ -26,6 +26,8 @@ class pthreads4w FileSystem::createDir($lib->getLibDir()); FileSystem::createDir($lib->getIncludeDir()); FileSystem::copy("{$lib->getSourceDir()}\\libpthreadVC3.lib", "{$lib->getLibDir()}\\libpthreadVC3.lib"); + // FrankenPHP's cgo.go uses -lpthreadVC3, which lld-link maps to pthreadVC3.lib (no lib prefix) + FileSystem::copy("{$lib->getSourceDir()}\\libpthreadVC3.lib", "{$lib->getLibDir()}\\pthreadVC3.lib"); FileSystem::copy("{$lib->getSourceDir()}\\_ptw32.h", "{$lib->getIncludeDir()}\\_ptw32.h"); FileSystem::copy("{$lib->getSourceDir()}\\pthread.h", "{$lib->getIncludeDir()}\\pthread.h"); FileSystem::copy("{$lib->getSourceDir()}\\sched.h", "{$lib->getIncludeDir()}\\sched.h"); diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 29b6848a..e2513cb4 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -161,6 +161,7 @@ class php extends TargetPackage // embed build options if ($package->getName() === 'php' || $package->getName() === 'php-embed') { $package->addBuildOption('build-shared', 'D', InputOption::VALUE_REQUIRED, 'Shared extensions to build, comma separated', ''); + $package->addBuildOption('maintainer-skip-build', null, null, '(maintainer only) skip embed build if exists'); } // legacy php target build options @@ -265,10 +266,6 @@ class php extends TargetPackage if (!$package->getBuildOption('enable-zts')) { throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!'); } - // frankenphp doesn't support windows, BSD is currently not supported by StaticPHP - if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { - throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); - } } // linux does not support loading shared libraries when target is pure static $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; @@ -345,7 +342,11 @@ class php extends TargetPackage public function postInstall(TargetPackage $package, PackageInstaller $installer): void { if ($package->getName() === 'frankenphp') { - $package->runStage([$this, 'smokeTestFrankenphpForUnix']); + if (SystemTarget::getTargetOS() === 'Windows') { + $package->runStage([$this, 'smokeTestFrankenphpForWindows']); + } else { + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); + } return; } if ($package->getName() !== 'php') { diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index f513242b..4414af66 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -6,9 +6,12 @@ namespace Package\Target\php; use Package\Target\php; use StaticPHP\Attribute\Package\Stage; +use StaticPHP\Config\PackageConfig; +use StaticPHP\Exception\EnvironmentException; use StaticPHP\Exception\SPCInternalException; use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\TargetPackage; @@ -18,6 +21,7 @@ use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\SPCConfigUtil; use StaticPHP\Util\System\LinuxUtil; +use StaticPHP\Util\System\WindowsUtil; use ZM\Logger\ConsoleColor; trait frankenphp @@ -171,6 +175,197 @@ trait frankenphp } } + #[Stage] + public function buildFrankenphpForWindows(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + if (getenv('GOROOT') === false) { + throw new EnvironmentException('go-win is not initialized properly. GOROOT is not set.'); + } + + $clang_info = WindowsUtil::findClang(); + if ($clang_info === false) { + throw new EnvironmentException( + 'Clang not found. FrankenPHP Windows build requires the LLVM toolchain component of Visual Studio. ' . + 'Install it in Visual Studio Installer under "C++ Clang tools for Windows", or set the CC environment variable.' + ); + } + + $frankenphp_version = $this->getFrankenPHPVersion($package); + $libphp_version = php::getPHPVersion(); + $major = intdiv(PHP_VERSION_ID, 10000); + $source_dir = $package->getSourceDir(); + + // collect PHP include paths in clang -I format (not MSVC /I). + // Use forward slashes and NO quotes around paths: when Go passes CGO_CFLAGS tokens + // directly to clang via exec(), any embedded quotes become literal characters in + // the argument string and break include-path resolution. + $include = str_replace('\\', '/', BUILD_INCLUDE_PATH); + // The PHP source root is needed so that Windows-only headers installed only in + // the source tree (e.g. win32/ioutil.h, win32/winutil.h) can be found via their + // relative #include paths like `#include "win32/ioutil.h"`. + $php_src = str_replace('\\', '/', SOURCE_PATH . '/php-src'); + $cgo_cflags = implode(' ', [ + "-I{$include}", + "-I{$include}/php", + "-I{$include}/php/main", + "-I{$include}/php/Zend", + "-I{$include}/php/TSRM", + "-I{$include}/php/ext", + "-I{$php_src}", + "-I{$php_src}/main", + "-I{$php_src}/ext", + "-I{$php_src}/Zend", + "-I{$php_src}/TSRM", + "-DFRANKENPHP_VERSION={$frankenphp_version}", + '-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1', + ]); + + $dep_libs = []; + foreach ($installer->getResolvedPackages(LibraryPackage::class) as $lib) { + foreach (PackageConfig::get($lib->getName(), 'static-libs', []) as $lib_file) { + if (file_exists("{$package->getLibDir()}\\{$lib_file}")) { + $lib_name = preg_replace('/\.lib$/i', '', $lib_file); + $dep_libs[] = "-l{$lib_name}"; + } + } + } + + $dep_libs = array_unique($dep_libs); + $lib_dir = str_replace('\\', '/', BUILD_LIB_PATH); + $php_embed_lib = "-lphp{$major}embed"; + $win_sys_libs = '-lkernel32 -lole32 -luser32 -ladvapi32 -lshell32 -lws2_32 -ldnsapi -lpsapi -lbcrypt'; + $cgo_ldflags = clean_spaces(implode(' ', array_filter([ + "-L{$lib_dir}", + $php_embed_lib, + implode(' ', $dep_libs), + $win_sys_libs, + '-llibcmt', + '-Wl,/NODEFAULTLIB:msvcrt', + '-Wl,/NODEFAULTLIB:msvcrtd', + '-Wl,/FORCE:MULTIPLE', + ]))); + + // build tags: skip watcher (no inotify/kqueue on Windows) + $go_build_tags = 'nobadger,nomysql,nopgx,nowatcher'; + if (!$installer->isPackageResolved('brotli')) { + $go_build_tags .= ',nobrotli'; + } + + $go_ldflags = + '-extldflags=-fuse-ld=lld ' . + "-X 'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy' " . + "-X 'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp' " . + "-X 'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP v{$frankenphp_version} PHP {$libphp_version} Caddy'"; + + // CGO on Windows tokenizes CC/CXX like a shell command line, splitting on spaces. + // Paths like "C:\Program Files\..." break because only "C:\Program" is used. + // Fix: prepend clang's directory to PATH and use plain executable names instead, + // which matches FrankenPHP's official CI approach (CC=clang, CXX=clang++). + $clang_dir = dirname($clang_info['clang']); + $env = [ + 'CGO_ENABLED' => '1', + 'CC' => 'clang.exe', + 'CXX' => 'clang++.exe', + 'PATH' => $clang_dir . ';' . getenv('PATH'), + 'CGO_CFLAGS' => clean_spaces($cgo_cflags), + 'CGO_LDFLAGS' => $cgo_ldflags, + ]; + + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('embedding Windows metadata')); + $package->runStage([$this, 'embedFrankenphpWindowsMetadata']); + + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('building with go build')); + + cmd()->cd("{$source_dir}\\caddy\\frankenphp") + ->setEnv($env) + ->exec("go build -v -tags \"{$go_build_tags}\" -ldflags \"{$go_ldflags}\" -o frankenphp.exe ."); + + $builder->deployBinary("{$source_dir}\\caddy\\frankenphp\\frankenphp.exe", BUILD_BIN_PATH . '\frankenphp.exe'); + $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '\frankenphp.exe'); + } + + /** + * Embed Windows PE metadata (version info + icon) into resource.syso so that + * go build picks it up automatically. Mirrors the official FrankenPHP Windows CI. + */ + #[Stage] + public function embedFrankenphpWindowsMetadata(TargetPackage $package): void + { + $frankenphp_version = $this->getFrankenPHPVersion($package); + $source_dir = $package->getSourceDir(); + $build_dir = "{$source_dir}\\caddy\\frankenphp"; + + [$p1, $p2, $p3] = explode('.', $frankenphp_version); + $major = (int) $p1; + $minor = (int) $p2; + $patch = (int) $p3; + + $version_info = [ + 'FixedFileInfo' => [ + 'FileVersion' => ['Major' => $major, 'Minor' => $minor, 'Patch' => $patch, 'Build' => 0], + 'ProductVersion' => ['Major' => $major, 'Minor' => $minor, 'Patch' => $patch, 'Build' => 0], + ], + 'StringFileInfo' => [ + 'CompanyName' => 'FrankenPHP', + 'FileDescription' => 'The modern PHP app server', + 'FileVersion' => $frankenphp_version, + 'InternalName' => 'frankenphp', + 'OriginalFilename' => 'frankenphp.exe', + 'LegalCopyright' => '(c) 2022 Kévin Dunglas, MIT License', + 'ProductName' => 'FrankenPHP', + 'ProductVersion' => $frankenphp_version, + 'Comments' => 'https://frankenphp.dev/', + ], + 'VarFileInfo' => [ + 'Translation' => ['LangID' => 9, 'CharsetID' => 1200], + ], + ]; + + file_put_contents("{$build_dir}\\versioninfo.json", json_encode($version_info, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + // Install goversioninfo if not already installed. + // GOPATH is set by the go-win artifact initializer via GlobalEnvManager::putenv(). + $goversioninfo = getenv('GOROOT') . '\bin\goversioninfo.exe'; + if (!file_exists($goversioninfo)) { + cmd()->exec('go install github.com/josephspurrier/goversioninfo/cmd/goversioninfo@latest'); + } + + // -64: embed as 64-bit resource; -icon: relative path from the build dir to the repo root icon. + cmd()->cd($build_dir) + ->exec("\"{$goversioninfo}\" -64 -icon {$package->getSourceDir()}\\frankenphp.ico versioninfo.json -o resource.syso"); + } + + #[Stage] + public function smokeTestFrankenphpForWindows(PackageBuilder $builder): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test', false); + $option = match ($no_smoke_test) { + false => false, // default value, run all smoke tests + null => 'all', // --no-smoke-test without value, skip all smoke tests + default => parse_comma_list($no_smoke_test), // --no-smoke-test=frankenphp,... + }; + if ($option === 'all' || (is_array($option) && in_array('frankenphp', $option, true))) { + return; + } + + InteractiveTerm::setMessage('Running FrankenPHP smoke test'); + $frankenphp = BUILD_BIN_PATH . '\frankenphp.exe'; + if (!file_exists($frankenphp)) { + throw new ValidationException( + "FrankenPHP binary not found: {$frankenphp}", + validation_module: 'FrankenPHP smoke test' + ); + } + [$ret, $output] = cmd()->execWithResult("{$frankenphp} version"); + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { + throw new ValidationException( + 'FrankenPHP failed smoke test: ret[' . $ret . ']. out[' . implode('', $output) . ']', + validation_module: 'FrankenPHP smoke test' + ); + } + } + protected function getFrankenPHPVersion(TargetPackage $package): string { if ($version = getenv('FRANKENPHP_VERSION')) { diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index cffe04dc..8ab7b019 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -447,12 +447,26 @@ trait windows } #[BuildFor('Windows')] - public function buildWin(TargetPackage $package): void + public function buildWin(TargetPackage $package, PackageInstaller $installer): void { + if ($package->getName() === 'frankenphp') { + /* @var php $this */ + $package->runStage([$this, 'buildFrankenphpForWindows']); + return; + } if ($package->getName() !== 'php') { return; } + // maintainer can skip build though ... + if ( + $installer->isPackageResolved('php-embed') + && $installer->getTargetPackage('php-embed')->getBuildOption('maintainer-skip-build') + && file_exists(BUILD_LIB_PATH . '\php8embed.lib') + ) { + return; + } + $package->runStage([$this, 'buildconfForWindows']); $package->runStage([$this, 'configureForWindows']); $package->runStage([$this, 'makeForWindows']); @@ -467,6 +481,32 @@ trait windows // php-src patches from micro SourcePatcher::patchPhpSrc(); + /* wsyslog.h is generated by mc.exe from win32/build/wsyslog.mc but is absent in some + PHP tarballs (e.g. 8.4.x). wsyslog.c still #includes it for the PHP_SYSLOG_*_TYPE + event-ID constants. Recreate the missing header with the correct mc.exe-encoded values: + MessageId=N + Severity bits (Success=0x00, Info=0x40, Warning=0x80, Error=0xC0) + combined into a 32-bit DWORD (Facility=0, Customer=0). + */ + $wsyslog_h = "{$package->getSourceDir()}\\win32\\wsyslog.h"; + if (!file_exists($wsyslog_h)) { + $shim = <<<'HEADER' +/* Auto-generated compatibility shim: wsyslog.h (from win32/build/wsyslog.mc) */ +#ifndef WSYSLOG_H +#define WSYSLOG_H + +#include "syslog.h" + +/* Event IDs generated by mc.exe from wsyslog.mc (Facility=0, Customer=0) */ +#define PHP_SYSLOG_SUCCESS_TYPE ((DWORD)0x00000001L) +#define PHP_SYSLOG_INFO_TYPE ((DWORD)0x40000002L) +#define PHP_SYSLOG_WARNING_TYPE ((DWORD)0x80000003L) +#define PHP_SYSLOG_ERROR_TYPE ((DWORD)0xC0000004L) + +#endif /* WSYSLOG_H */ +HEADER; + FileSystem::writeFile($wsyslog_h, $shim); + } + // php 8.1 bug if ($this->getPHPVersionID() >= 80100 && $this->getPHPVersionID() < 80200) { logger()->info('Patching PHP 8.1 windows Fiber bug'); @@ -656,22 +696,24 @@ C_CODE; $ts = $package->getBuildOption('enable-zts', false) ? '_TS' : ''; $build_dir = "{$source_dir}\\x64\\{$rel_type}{$ts}"; - // Build include flags pointing to source dirs (like PHP Windows build does) // Note: embed.c uses #include , so we need $source_dir itself + $zts_define = $ts ? ' /D ZTS=1' : ''; $include_flags = sprintf( '/I"%s" /I"%s\main" /I"%s\Zend" /I"%s\TSRM" /I"%s" ' . - '/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES', + '/D ZEND_WIN32=1 /D PHP_WIN32=1 /D WIN32 /D _WINDOWS /D WINDOWS=1 /D _MBCS /D _USE_MATH_DEFINES%s', $build_dir, $source_dir, $source_dir, $source_dir, - $source_dir + $source_dir, + $zts_define ); // MSVC cl.exe format: compiler flags must come before /link, linker flags after // ldflags contains /LIBPATH which must be after /link + // /FORCE:MULTIPLE: in ZTS mode both zend.obj and php_embed.obj (both packed into the fat php8embed.lib) define _tsrm_ls_cache as a __declspec(thread) variable. $compile_cmd = sprintf( - 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /LIBPATH:"%s\lib" %s %s', + 'cl.exe /nologo /O2 /MT /Z7 %s embed.c /Fe:embed.exe /link /FORCE:MULTIPLE /LIBPATH:"%s\lib" %s %s', $include_flags, BUILD_ROOT_PATH, $config['libs'], diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 769612b9..fccb84ce 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -53,11 +53,12 @@ class LibraryPackage extends Package return false; } } - foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) { - $path = FileSystem::isRelativePath($bin) ? "{$this->getBinDir()}/{$bin}" : $bin; - if (!file_exists($path)) { - return false; - } + } + + foreach (PackageConfig::get($this->getName(), 'static-bins', []) as $bin) { + $path = FileSystem::isRelativePath($bin) ? "{$this->getBinDir()}/{$bin}" : $bin; + if (!file_exists($path)) { + return false; } } return true; diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 1f778a0d..fa049bbd 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -133,7 +133,8 @@ class PackageInstaller public function printBuildPackageOutputs(): void { - foreach ($this->build_packages as $package) { + /** @var Package $package */ + foreach ($this->getResolvedPackages() as $package) { if (($outputs = $package->getOutputs()) !== []) { InteractiveTerm::notice('Package ' . ConsoleColor::green($package->getName()) . ' outputs'); $this->printArrayInfo(info: $outputs); @@ -685,6 +686,7 @@ class PackageInstaller if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) { $frankenphp = PackageLoader::getPackage('frankenphp'); $this->install_packages[$frankenphp->getName()] = $frankenphp; + $this->build_packages[$package->getName()] = $package; $added = true; } $this->build_packages[$package->getName()] = $package; diff --git a/src/StaticPHP/Runtime/Shell/WindowsCmd.php b/src/StaticPHP/Runtime/Shell/WindowsCmd.php index ad07f93b..4c7af181 100644 --- a/src/StaticPHP/Runtime/Shell/WindowsCmd.php +++ b/src/StaticPHP/Runtime/Shell/WindowsCmd.php @@ -27,7 +27,7 @@ class WindowsCmd extends Shell $this->last_cmd = $cmd = $this->getExecString($cmd); // echo $cmd . PHP_EOL; - $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd); + $this->passthru($cmd, $this->console_putput, $original_command, cwd: $this->cd, env: $this->env); return $this; } diff --git a/src/StaticPHP/Util/System/WindowsUtil.php b/src/StaticPHP/Util/System/WindowsUtil.php index 150730c0..40637262 100644 --- a/src/StaticPHP/Util/System/WindowsUtil.php +++ b/src/StaticPHP/Util/System/WindowsUtil.php @@ -85,6 +85,44 @@ class WindowsUtil return intval($result); } + /** + * Find Clang compiler from the Visual Studio LLVM toolchain. + * + * Checks the CC environment variable first (user override), then searches + * the VS2022/VS2019 installation via vswhere. + * + * @return array{clang: string, clangpp: string}|false False if not found + */ + public static function findClang(): array|false + { + // Allow user to override via CC environment variable + if ($cc = getenv('CC')) { + if (file_exists($cc)) { + $clangpp = dirname($cc) . DIRECTORY_SEPARATOR . 'clang++.exe'; + return [ + 'clang' => $cc, + 'clangpp' => file_exists($clangpp) ? $clangpp : $cc, + ]; + } + } + + $vs = self::findVisualStudio(); + if ($vs === false) { + return false; + } + + $clang = $vs['dir'] . '\VC\Tools\Llvm\x64\bin\clang.exe'; + $clangpp = $vs['dir'] . '\VC\Tools\Llvm\x64\bin\clang++.exe'; + if (!file_exists($clang)) { + return false; + } + + return [ + 'clang' => $clang, + 'clangpp' => file_exists($clangpp) ? $clangpp : $clang, + ]; + } + /** * Create CMake toolchain file. *