diff --git a/.github/patches/phpx-windows-mpdecimal-target.patch b/.github/patches/phpx-windows-mpdecimal-target.patch deleted file mode 100644 index 853cfe23..00000000 --- a/.github/patches/phpx-windows-mpdecimal-target.patch +++ /dev/null @@ -1,36 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 897706d..b4d85ec 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -355,14 +355,6 @@ if (IS_WINDOWS) - INTERFACE_INCLUDE_DIRECTORIES "${PHP_SDK_DIR}/include;${MPDECIMAL_DIR}/libmpdec++" - ) - -- # 复制 DLL 到输出目录 -- file(GLOB MPDEC_DLLS "${PHP_LIBRARY_DIR}/libmpdec-4.0.1.dll" "${PHP_LIBRARY_DIR}/libmpdec++-4.0.1.dll") -- foreach(_dll ${MPDEC_DLLS}) -- add_custom_command(TARGET phpx POST_BUILD -- COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dll}" $ -- COMMENT "Copying ${_dll} to output" -- ) -- endforeach() - else() - # Linux/macOS: 内置编译 mpdecimal + libmpdec++ - message(STATUS "Using bundled mpdecimal with uint128") -@@ -419,6 +411,16 @@ set_target_properties(phpx PROPERTIES - CLEAN_DIRECT_OUTPUT 1 - ) - -+if (IS_WINDOWS) -+ # Register post-build commands only after the phpx target exists. -+ file(GLOB MPDEC_DLLS "${PHP_LIBRARY_DIR}/libmpdec-4.0.1.dll" "${PHP_LIBRARY_DIR}/libmpdec++-4.0.1.dll") -+ foreach(_dll ${MPDEC_DLLS}) -+ add_custom_command(TARGET phpx POST_BUILD -+ COMMAND ${CMAKE_COMMAND} -E copy_if_different "${_dll}" $ -+ COMMENT "Copying ${_dll} to output") -+ endforeach() -+endif() -+ - if (IS_WINDOWS) - target_link_libraries(phpx PRIVATE - ${PHP_LIBRARY} diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 490306fa..f15ef9ba 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -18,13 +18,14 @@ env: jobs: build-tpc: - name: tpc.exe (PHP ${{ matrix.php }}) + name: tpc.exe (PHP ${{ matrix.php }} ${{ matrix.thread_safety }}) runs-on: windows-2022 timeout-minutes: 90 strategy: fail-fast: false matrix: php: ["8.4", "8.5"] + thread_safety: [nts, zts] steps: - name: Checkout TypePHP @@ -38,36 +39,72 @@ jobs: tools: composer:v2 env: fail-fast: true - phpts: nts + phpts: ${{ matrix.thread_safety == 'zts' && 'ts' || 'nts' }} update: true - - name: Install matching PHP development SDK + - name: Resolve PHP build environment + id: php-build-env shell: pwsh run: | $ErrorActionPreference = 'Stop' $phpVersion = php -r 'echo PHP_VERSION;' + $threadSafety = php -r 'echo PHP_ZTS ? "zts" : "nts";' + if ($threadSafety -ne '${{ matrix.thread_safety }}') { + throw "setup-php installed $threadSafety PHP, expected ${{ matrix.thread_safety }}" + } $phpExe = (Get-Command php).Source $phpHome = Split-Path -Parent $phpExe - $archiveName = "php-devel-pack-$phpVersion-nts-Win32-vs17-x64.zip" + $threadSuffix = if ($threadSafety -eq 'nts') { '-nts' } else { '' } + $archiveName = "php-devel-pack-$phpVersion$threadSuffix-Win32-vs17-x64.zip" $archive = Join-Path $env:RUNNER_TEMP $archiveName + + "PHP_VERSION=$phpVersion" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 + "PHP_THREAD_SAFETY=$threadSafety" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 + "PHP_HOME=$phpHome" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 + "PHPX_HOME=${{ github.workspace }}\vendor\swoole\phpx" | + Out-File $env:GITHUB_ENV -Append -Encoding utf8 + "PHP_DEVEL_ARCHIVE=$archive" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 + "version=$phpVersion" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + "thread_safety=$threadSafety" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + "archive=$archive" | Out-File $env:GITHUB_OUTPUT -Append -Encoding utf8 + + - name: Cache PHP development package + id: cache-php-devel + uses: actions/cache/restore@v4 + with: + path: ${{ steps.php-build-env.outputs.archive }} + key: windows-2022-php-devel-${{ steps.php-build-env.outputs.version }}-${{ steps.php-build-env.outputs.thread_safety }}-vs17-x64 + + - name: Install matching PHP development SDK + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + $phpVersion = $env:PHP_VERSION + $phpHome = $env:PHP_HOME + $threadSafety = $env:PHP_THREAD_SAFETY + $archive = $env:PHP_DEVEL_ARCHIVE + $archiveName = Split-Path -Leaf $archive $extractDir = Join-Path $env:RUNNER_TEMP "php-devel-$phpVersion" - $downloaded = $false - foreach ($baseUrl in @( - 'https://downloads.php.net/~windows/releases', - 'https://downloads.php.net/~windows/releases/archives' - )) { - try { - Invoke-WebRequest -Uri "$baseUrl/$archiveName" -OutFile $archive - $downloaded = $true - break - } catch { - Remove-Item $archive -Force -ErrorAction SilentlyContinue + if (-not (Test-Path $archive)) { + $downloaded = $false + foreach ($baseUrl in @( + 'https://downloads.php.net/~windows/releases', + 'https://downloads.php.net/~windows/releases/archives' + )) { + try { + Invoke-WebRequest -Uri "$baseUrl/$archiveName" -OutFile $archive + $downloaded = $true + break + } catch { + Remove-Item $archive -Force -ErrorAction SilentlyContinue + } + } + if (-not $downloaded) { + throw "Unable to download the PHP $phpVersion NTS development pack" } - } - if (-not $downloaded) { - throw "Unable to download the PHP $phpVersion NTS development pack" } Expand-Archive -Path $archive -DestinationPath $extractDir -Force @@ -91,6 +128,9 @@ jobs: } Copy-Item $embedLibrary $sdkLib -Force + $coreLibrary = if ($threadSafety -eq 'zts') { 'php8ts.lib' } else { 'php8.lib' } + $runtimeLibrary = if ($threadSafety -eq 'zts') { 'php8ts.dll' } else { 'php8.dll' } + # Temporary compatibility fix for PHP packages predating php/php-src#22940. $hashHeader = Join-Path $sdkInclude 'ext\hash\php_hash.h' $hashSource = [IO.File]::ReadAllText($hashHeader) @@ -109,33 +149,72 @@ jobs: foreach ($required in @( (Join-Path $sdkInclude 'main\php.h'), - (Join-Path $sdkLib 'php8.lib'), + (Join-Path $sdkLib $coreLibrary), (Join-Path $sdkLib 'php8embed.lib'), - (Join-Path $phpHome 'php8.dll') + (Join-Path $phpHome $runtimeLibrary) )) { if (-not (Test-Path $required)) { throw "Required PHP SDK file is missing: $required" } } - "PHP_HOME=$phpHome" | Out-File $env:GITHUB_ENV -Append -Encoding utf8 - "PHPX_HOME=${{ github.workspace }}\vendor\swoole\phpx" | - Out-File $env:GITHUB_ENV -Append -Encoding utf8 Write-Host "Using PHP $phpVersion from $phpHome" + - name: Save PHP development package + if: steps.cache-php-devel.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ steps.php-build-env.outputs.archive }} + key: ${{ steps.cache-php-devel.outputs.cache-primary-key }} + - name: Install Composer dependencies run: composer install --prefer-dist --no-progress - name: Patch PHPX Windows CMake target ordering shell: pwsh - run: git apply --directory=vendor/swoole/phpx .github/patches/phpx-windows-mpdecimal-target.patch + run: | + $ErrorActionPreference = 'Stop' + + $cmakeFile = Join-Path $env:PHPX_HOME 'CMakeLists.txt' + $source = [IO.File]::ReadAllText($cmakeFile).Replace("`r`n", "`n") + $copyBlockPattern = '(?ms)^ # 复制 DLL 到输出目录\n file\(GLOB MPDEC_DLLS .*?^ endforeach\(\)\n' + $copyBlock = [regex]::Match($source, $copyBlockPattern) + if (-not $copyBlock.Success) { + throw 'Unable to locate the pre-target PHPX mpdecimal copy block' + } + $source = $source.Remove($copyBlock.Index, $copyBlock.Length) + + $targetPattern = '(?ms)(add_library\(phpx SHARED \$\{SRC_FILES\}\)\nset_target_properties\(phpx PROPERTIES\n CLEAN_DIRECT_OUTPUT 1\n\)\n)' + $target = [regex]::Match($source, $targetPattern) + if (-not $target.Success) { + throw 'Unable to locate the PHPX target declaration' + } + + $copyBlockText = $copyBlock.Value.Replace(' # 复制 DLL 到输出目录', ' # Copy mpdecimal DLLs after the phpx target exists.') + $guardedCopyBlock = "`nif (IS_WINDOWS)`n$copyBlockText" + "endif()`n" + $source = $source.Insert($target.Index + $target.Length, $guardedCopyBlock) + [IO.File]::WriteAllText($cmakeFile, $source, [Text.UTF8Encoding]::new($false)) + + $targetOffset = $source.IndexOf('add_library(phpx SHARED') + $copyOffset = $source.IndexOf('add_custom_command(TARGET phpx POST_BUILD') + if ($targetOffset -lt 0 -or $copyOffset -le $targetOffset) { + throw 'PHPX post-build command still precedes its target declaration' + } - name: Configure MSVC uses: ilammy/msvc-dev-cmd@v1 with: arch: x64 + - name: Cache GMP and MPFR + id: cache-gmp-mpfr + uses: actions/cache/restore@v4 + with: + path: ${{ runner.temp }}\typephp-cache\vcpkg-x64-windows + key: windows-2022-msvc-vcpkg-gmp-mpfr-x64-v1 + - name: Install GMP and MPFR + if: steps.cache-gmp-mpfr.outputs.cache-hit != 'true' shell: pwsh run: | $ErrorActionPreference = 'Stop' @@ -148,30 +227,83 @@ jobs: } $installed = Join-Path $env:VCPKG_INSTALLATION_ROOT "installed\$triplet" - $sdkInclude = Join-Path $env:PHP_HOME 'SDK\include' - $sdkLib = Join-Path $env:PHP_HOME 'SDK\lib' - Copy-Item (Join-Path $installed 'include\*') $sdkInclude -Recurse -Force + $cache = Join-Path $env:RUNNER_TEMP 'typephp-cache\vcpkg-x64-windows' + New-Item "$cache\include", "$cache\lib", "$cache\bin" -ItemType Directory -Force | + Out-Null + Copy-Item (Join-Path $installed 'include\*') "$cache\include" -Recurse -Force foreach ($library in @('gmp.lib', 'gmpxx.lib', 'mpfr.lib')) { $source = Join-Path $installed "lib\$library" if (-not (Test-Path $source)) { throw "vcpkg did not install $library" } + Copy-Item $source "$cache\lib" -Force + } + Copy-Item (Join-Path $installed 'bin\*.dll') "$cache\bin" -Force + + - name: Save GMP and MPFR + if: steps.cache-gmp-mpfr.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ runner.temp }}\typephp-cache\vcpkg-x64-windows + key: ${{ steps.cache-gmp-mpfr.outputs.cache-primary-key }} + + - name: Stage GMP and MPFR + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + $cache = Join-Path $env:RUNNER_TEMP 'typephp-cache\vcpkg-x64-windows' + $sdkInclude = Join-Path $env:PHP_HOME 'SDK\include' + $sdkLib = Join-Path $env:PHP_HOME 'SDK\lib' + Copy-Item "$cache\include\*" $sdkInclude -Recurse -Force + + foreach ($library in @('gmp.lib', 'gmpxx.lib', 'mpfr.lib')) { + $source = Join-Path $cache "lib\$library" + if (-not (Test-Path $source)) { + throw "The dependency cache does not contain $library" + } Copy-Item $source $sdkLib -Force } - Copy-Item (Join-Path $installed 'bin\*.dll') $env:PHP_HOME -Force + Copy-Item "$cache\bin\*.dll" $env:PHP_HOME -Force + + - name: Cache mpdecimal + id: cache-mpdecimal + uses: actions/cache/restore@v4 + with: + path: ${{ env.PHPX_HOME }}\thirdparty\mpdecimal\vcbuild\dist64 + key: windows-2022-msvc-mpdecimal-x64-${{ hashFiles('vendor/swoole/phpx/thirdparty/mpdecimal/**') }} - name: Build mpdecimal + if: steps.cache-mpdecimal.outputs.cache-hit != 'true' shell: pwsh run: | $ErrorActionPreference = 'Stop' $buildScript = Join-Path $env:PHPX_HOME 'thirdparty\mpdecimal\vcbuild\vcbuild64.bat' - & cmd.exe /d /s /c "`"$buildScript`"" - if ($LASTEXITCODE -ne 0) { - throw "mpdecimal build failed with exit code $LASTEXITCODE" + $buildDirectory = Split-Path -Parent $buildScript + Push-Location $buildDirectory + try { + & cmd.exe /d /s /c vcbuild64.bat + if ($LASTEXITCODE -ne 0) { + throw "mpdecimal build failed with exit code $LASTEXITCODE" + } + } finally { + Pop-Location } + - name: Save mpdecimal + if: steps.cache-mpdecimal.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: ${{ env.PHPX_HOME }}\thirdparty\mpdecimal\vcbuild\dist64 + key: ${{ steps.cache-mpdecimal.outputs.cache-primary-key }} + + - name: Stage mpdecimal + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $dist = Join-Path $env:PHPX_HOME 'thirdparty\mpdecimal\vcbuild\dist64' $sdkInclude = Join-Path $env:PHP_HOME 'SDK\include' $sdkLib = Join-Path $env:PHP_HOME 'SDK\lib' @@ -224,10 +356,58 @@ jobs: throw 'The compiler did not produce tpc.exe' } - - name: Upload tpc.exe + - name: Run Windows smoke tests + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + $env:PATH = "$env:PHPX_HOME\build;$env:PATH" + & .\tpc.exe tests\windows\smoke\project.yml --job 1 --no-progress + if ($LASTEXITCODE -ne 0) { + throw "Windows smoke project compilation failed with exit code $LASTEXITCODE" + } + + $smokeExe = Join-Path '${{ github.workspace }}' 'tests\windows\smoke\windows_smoke.exe' + if (-not (Test-Path $smokeExe)) { + throw "Windows smoke executable was not generated: $smokeExe" + } + + $processInfo = [Diagnostics.ProcessStartInfo]::new() + $processInfo.FileName = $smokeExe + $processInfo.ArgumentList.Add('${{ matrix.thread_safety }}') + $processInfo.UseShellExecute = $false + $processInfo.RedirectStandardOutput = $true + $processInfo.RedirectStandardError = $true + + $process = [Diagnostics.Process]::new() + $process.StartInfo = $processInfo + if (-not $process.Start()) { + throw 'Unable to start the Windows smoke executable' + } + $stdout = $process.StandardOutput.ReadToEnd() + $stderr = $process.StandardError.ReadToEnd() + $process.WaitForExit() + + Write-Host "Windows smoke stdout: $stdout" + Write-Host "Windows smoke stderr: $stderr" + Write-Host "Windows smoke exit code: $($process.ExitCode)" + if ($process.ExitCode -ne 0) { + throw "Windows smoke executable failed with exit code $($process.ExitCode)" + } + if ($stdout.Trim() -ne 'windows-smoke-ok:${{ matrix.thread_safety }}') { + throw "Unexpected Windows smoke output: $stdout" + } + + - name: Upload Windows build outputs + if: always() uses: actions/upload-artifact@v4 with: - name: tpc-windows-x64-php-${{ matrix.php }} - if-no-files-found: error + name: tpc-windows-x64-php-${{ matrix.php }}-${{ matrix.thread_safety }} + if-no-files-found: warn retention-days: 7 - path: tpc.exe + path: | + tpc.exe + tests/windows/smoke/windows_smoke.exe + tests/windows/smoke/build/**/*.cc + tests/windows/smoke/build/**/*.h + tests/windows/smoke/build/**/*.rsp diff --git a/tests/windows/smoke/cpp-src/winapi.cc b/tests/windows/smoke/cpp-src/winapi.cc new file mode 100644 index 00000000..a922dd05 --- /dev/null +++ b/tests/windows/smoke/cpp-src/winapi.cc @@ -0,0 +1,30 @@ +#include +#include + +using namespace php; + +Int php_windows_current_process_id() +{ + return static_cast(GetCurrentProcessId()); +} + +Bool php_windows_has_module_handle() +{ + return GetModuleHandleW(nullptr) != nullptr; +} + +Int php_windows_logical_processor_count() +{ + SYSTEM_INFO info{}; + GetNativeSystemInfo(&info); + return static_cast(info.dwNumberOfProcessors); +} + +Bool php_windows_php_is_zts() +{ +#ifdef ZTS + return true; +#else + return false; +#endif +} diff --git a/tests/windows/smoke/cpp-src/winapi.stub.php b/tests/windows/smoke/cpp-src/winapi.stub.php new file mode 100644 index 00000000..fe203479 --- /dev/null +++ b/tests/windows/smoke/cpp-src/winapi.stub.php @@ -0,0 +1,10 @@ + 0, 'GetCurrentProcessId() failed'); + requireWindowsCondition(windows_has_module_handle(), 'GetModuleHandleW() failed'); + requireWindowsCondition(windows_logical_processor_count() > 0, 'GetNativeSystemInfo() returned no processors'); + + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR + . 'typephp-windows-smoke-' . windows_current_process_id() . '.txt'; + requireWindowsCondition(file_put_contents($path, 'windows-file-api') === 16, 'Windows file write failed'); + requireWindowsCondition(file_get_contents($path) === 'windows-file-api', 'Windows file read failed'); + requireWindowsCondition(unlink($path), 'Windows file cleanup failed'); + + echo 'windows-smoke-ok:', $expectedZts ? 'zts' : 'nts'; +} diff --git a/tests/windows/smoke/project.yml b/tests/windows/smoke/project.yml new file mode 100644 index 00000000..a22a6bcc --- /dev/null +++ b/tests/windows/smoke/project.yml @@ -0,0 +1,9 @@ +name: windows-smoke +mode: bin +build-dir: build +output: windows_smoke +cxx-std: c++17 + +sources: + - main.php + - cpp-src