ci: add Windows PHP ABI build matrix

master
韩天峰 1 day ago
parent 063476360b
commit 87c328b3db
  1. 36
      .github/patches/phpx-windows-mpdecimal-target.patch
  2. 250
      .github/workflows/windows-build.yml
  3. 30
      tests/windows/smoke/cpp-src/winapi.cc
  4. 10
      tests/windows/smoke/cpp-src/winapi.stub.php
  5. 34
      tests/windows/smoke/main.php
  6. 9
      tests/windows/smoke/project.yml

@ -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}" $<TARGET_FILE_DIR:phpx>
- 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}" $<TARGET_FILE_DIR:phpx>
+ COMMENT "Copying ${_dll} to output")
+ endforeach()
+endif()
+
if (IS_WINDOWS)
target_link_libraries(phpx PRIVATE
${PHP_LIBRARY}

@ -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

@ -0,0 +1,30 @@
#include <phpx.h>
#include <windows.h>
using namespace php;
Int php_windows_current_process_id()
{
return static_cast<Int>(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<Int>(info.dwNumberOfProcessors);
}
Bool php_windows_php_is_zts()
{
#ifdef ZTS
return true;
#else
return false;
#endif
}

@ -0,0 +1,10 @@
<?php
/** Windows API smoke-test declarations implemented by winapi.cc. */
function windows_current_process_id(): int {}
function windows_has_module_handle(): bool {}
function windows_logical_processor_count(): int {}
function windows_php_is_zts(): bool {}

@ -0,0 +1,34 @@
<?php
function requireWindowsCondition(bool $condition, string $message): void
{
if (!$condition) {
throw new RuntimeException($message);
}
}
function main(int $argc, array $argv): void
{
requireWindowsCondition($argc === 2, 'Expected the thread-safety mode argument');
$expectedZts = $argv[1] === 'zts';
requireWindowsCondition(PHP_OS_FAMILY === 'Windows', 'Expected PHP_OS_FAMILY=Windows');
requireWindowsCondition(DIRECTORY_SEPARATOR === '\\', 'Expected the Windows directory separator');
// PHP exposes PHP_ZTS as either an integer-like or boolean-like constant,
// depending on how the runtime constants were imported. Test its meaning,
// rather than its representation.
$runtimeZts = PHP_ZTS !== 0 && PHP_ZTS !== false;
requireWindowsCondition($runtimeZts === $expectedZts, 'PHP_ZTS does not match the CI matrix');
requireWindowsCondition(windows_php_is_zts() === $expectedZts, 'The native ZTS macro does not match PHP_ZTS');
requireWindowsCondition(windows_current_process_id() > 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';
}

@ -0,0 +1,9 @@
name: windows-smoke
mode: bin
build-dir: build
output: windows_smoke
cxx-std: c++17
sources:
- main.php
- cpp-src
Loading…
Cancel
Save