From 17204db6e1941e8ffa2e093eb53404414ddd8ef9 Mon Sep 17 00:00:00 2001 From: culix Date: Sat, 3 Jan 2026 12:40:20 -0700 Subject: [PATCH 01/13] Docs: Add notes on Windows build steps --- COMPILING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/COMPILING.md b/COMPILING.md index ec0a4b41a..1e14700ed 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -1,6 +1,8 @@ ## Windows 1) Open the solution in Visual Studio 2022 + * Mesen uses the "Desktop Development With C++" workload and the ".NET Desktop Development" workload + * You do **not** need to install them in advance. If you don't have them, Visual Studio will install them when you open the project. 2) Compile as `Release`/`x64` 3) Set the startup project to the `UI` project and run From 296a51009ecf5967a66ab93aa69c6c8f2bf397c6 Mon Sep 17 00:00:00 2001 From: culix Date: Sat, 3 Jan 2026 12:40:35 -0700 Subject: [PATCH 02/13] UI: move PreBuildWindows build steps to `.bat` file move build steps out of the `.csproj` file and into a separate `.bat` file. this should make it easier to read and edit. you can use newlines no behaviour changes --- UI/UI.csproj | 2 +- UI/prebuild_windows.bat | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 UI/prebuild_windows.bat diff --git a/UI/UI.csproj b/UI/UI.csproj index 9d5835a0f..57b902281 100644 --- a/UI/UI.csproj +++ b/UI/UI.csproj @@ -650,7 +650,7 @@ - + diff --git a/UI/prebuild_windows.bat b/UI/prebuild_windows.bat new file mode 100644 index 000000000..e5f4950ee --- /dev/null +++ b/UI/prebuild_windows.bat @@ -0,0 +1,31 @@ +:: find and copy the files needed for building + +:: called by the PreBuildWindows target in UI.csproj +:: steps stored as a batch file for easier reading and editing + +set OUTDIR=%~1 +set RUNTIME_IDENTIFIER=%~2 +set PROJECT_DIR=%~3 + +cd /d "%OUTDIR%" + +if exist Dependencies rd /s /q Dependencies +md Dependencies + +xcopy /s "%PROJECT_DIR%\Dependencies\*" Dependencies + +copy libHarfBuzzSharp.dll Dependencies +copy libSkiaSharp.dll Dependencies + +if exist "MesenCore.dll" ( + copy /y "MesenCore.dll" Dependencies\ >nul +) else ( + echo ERROR: MesenCore.dll missing from %OUTDIR%. Build Core project first. + exit /b 1 +) + +cd Dependencies +del ..\Dependencies.zip + +powershell -Command "Compress-Archive -Path * -DestinationPath '..\Dependencies.zip' -Force" +copy /y "..\Dependencies.zip" "%PROJECT_DIR%" From 17e664181aede9f22ec30b5b28041d7a1fa08287 Mon Sep 17 00:00:00 2001 From: culix Date: Sat, 3 Jan 2026 12:40:42 -0700 Subject: [PATCH 03/13] UI: use full path for prebuild step expand path for `Dependencies.zip` with `~f` batch syntax ensures that powershell can always find the file --- UI/prebuild_windows.bat | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/UI/prebuild_windows.bat b/UI/prebuild_windows.bat index e5f4950ee..bd4ea0f82 100644 --- a/UI/prebuild_windows.bat +++ b/UI/prebuild_windows.bat @@ -24,8 +24,11 @@ if exist "MesenCore.dll" ( exit /b 1 ) -cd Dependencies -del ..\Dependencies.zip +:: expand variable to use the full path with ~f. otherwise powershell has difficulty +set "FULL_OUTDIR=%~f1" +set "ZIP=%FULL_OUTDIR%\Dependencies.zip" +del "%ZIP%" + +powershell -Command "Compress-Archive -Path (Get-Item '%FULL_OUTDIR%\Dependencies') -DestinationPath '%ZIP%' -Force" +copy /y "%ZIP%" "%PROJECT_DIR%" -powershell -Command "Compress-Archive -Path * -DestinationPath '..\Dependencies.zip' -Force" -copy /y "..\Dependencies.zip" "%PROJECT_DIR%" From e0367320d1b2eec1217d5bcc37ecc67637edc18d Mon Sep 17 00:00:00 2001 From: culix Date: Tue, 13 Jan 2026 08:36:30 -0700 Subject: [PATCH 04/13] Build: Improve bat file * improve quoting and handle input vars * make sure cd and directory expansion happens correctly --- UI/prebuild_windows.bat | 56 ++++++++++++++++++++++++++++------------- 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/UI/prebuild_windows.bat b/UI/prebuild_windows.bat index bd4ea0f82..1a46d7776 100644 --- a/UI/prebuild_windows.bat +++ b/UI/prebuild_windows.bat @@ -3,32 +3,54 @@ :: called by the PreBuildWindows target in UI.csproj :: steps stored as a batch file for easier reading and editing -set OUTDIR=%~1 -set RUNTIME_IDENTIFIER=%~2 -set PROJECT_DIR=%~3 +set "PROJECT_DIR=%~2" -cd /d "%OUTDIR%" +:: Remove ending characters if MSBuild passed one +:strip +if "%PROJECT_DIR:~-1%"=="." (set "PROJECT_DIR=%PROJECT_DIR:~0,-1%" & goto strip) +if "%PROJECT_DIR:~-1%"=="\" (set "PROJECT_DIR=%PROJECT_DIR:~0,-1%" & goto strip) + +:: cd first before resolving outdir path +:: so outdir is expanded consistently every time +cd /d "%PROJECT_DIR%" +echo Setting up in '%CD%' +set "FULL_OUTDIR=%~f1" + +if not exist "%FULL_OUTDIR%" mkdir "%FULL_OUTDIR%" +cd /d "%FULL_OUTDIR%" +echo Running from %FULL_OUTDIR% if exist Dependencies rd /s /q Dependencies md Dependencies -xcopy /s "%PROJECT_DIR%\Dependencies\*" Dependencies +:: Search up one level from UI to find the root, then find MesenCore.dll +pushd ..\.. +echo Searching for MesenCore.dll in %CD%... +set "CORE_PATH=" +for /f "delims=" %%i in ('dir /s /b MesenCore.dll 2^>nul') do ( + set "CORE_PATH=%%i" + echo %%i | findstr /i "Release" | findstr /v /i "Dependencies" >nul + if not errorlevel 1 goto :found_it +) +:found_it +popd -copy libHarfBuzzSharp.dll Dependencies -copy libSkiaSharp.dll Dependencies +:: Get the directory and strip the trailing slash +for %%i in ("%CORE_PATH%") do set "BIN_SRC_DIR=%%~dpi" +if "%BIN_SRC_DIR:~-1%"=="\" set "BIN_SRC_DIR=%BIN_SRC_DIR:~0,-1%" -if exist "MesenCore.dll" ( - copy /y "MesenCore.dll" Dependencies\ >nul -) else ( - echo ERROR: MesenCore.dll missing from %OUTDIR%. Build Core project first. - exit /b 1 -) +echo Source Bin Dir: "%BIN_SRC_DIR%" + +echo Found Core at: "%CORE_PATH%" + +copy /y "%CORE_PATH%" Dependencies +copy /y "%BIN_SRC_DIR%libHarfBuzzSharp.dll" Dependencies +copy /y "%BIN_SRC_DIR%libSkiaSharp.dll" Dependencies + +xcopy /s /y "%PROJECT_DIR%Dependencies\*" Dependencies -:: expand variable to use the full path with ~f. otherwise powershell has difficulty -set "FULL_OUTDIR=%~f1" set "ZIP=%FULL_OUTDIR%\Dependencies.zip" -del "%ZIP%" +if exist "%ZIP%" del /f /q "%ZIP%" powershell -Command "Compress-Archive -Path (Get-Item '%FULL_OUTDIR%\Dependencies') -DestinationPath '%ZIP%' -Force" copy /y "%ZIP%" "%PROJECT_DIR%" - From b470f345f2bbf340e080dd8dedde8541c1d72aba Mon Sep 17 00:00:00 2001 From: culix Date: Tue, 13 Jan 2026 08:38:45 -0700 Subject: [PATCH 05/13] Build: use powershell instead of bat file for windows bat file has many problems being stable and consistent, and handliing paths and input. powershell should be more reliable --- UI/UI.csproj | 4 +- UI/prebuild_windows.bat | 56 -------------- UI/prebuild_windows.ps1 | 85 +++++++++++++++++++++ UI/tests/prebuild_windows.test.ps1 | 115 +++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 57 deletions(-) delete mode 100644 UI/prebuild_windows.bat create mode 100644 UI/prebuild_windows.ps1 create mode 100644 UI/tests/prebuild_windows.test.ps1 diff --git a/UI/UI.csproj b/UI/UI.csproj index 57b902281..9f00b5626 100644 --- a/UI/UI.csproj +++ b/UI/UI.csproj @@ -650,7 +650,9 @@ - + diff --git a/UI/prebuild_windows.bat b/UI/prebuild_windows.bat deleted file mode 100644 index 1a46d7776..000000000 --- a/UI/prebuild_windows.bat +++ /dev/null @@ -1,56 +0,0 @@ -:: find and copy the files needed for building - -:: called by the PreBuildWindows target in UI.csproj -:: steps stored as a batch file for easier reading and editing - -set "PROJECT_DIR=%~2" - -:: Remove ending characters if MSBuild passed one -:strip -if "%PROJECT_DIR:~-1%"=="." (set "PROJECT_DIR=%PROJECT_DIR:~0,-1%" & goto strip) -if "%PROJECT_DIR:~-1%"=="\" (set "PROJECT_DIR=%PROJECT_DIR:~0,-1%" & goto strip) - -:: cd first before resolving outdir path -:: so outdir is expanded consistently every time -cd /d "%PROJECT_DIR%" -echo Setting up in '%CD%' -set "FULL_OUTDIR=%~f1" - -if not exist "%FULL_OUTDIR%" mkdir "%FULL_OUTDIR%" -cd /d "%FULL_OUTDIR%" -echo Running from %FULL_OUTDIR% - -if exist Dependencies rd /s /q Dependencies -md Dependencies - -:: Search up one level from UI to find the root, then find MesenCore.dll -pushd ..\.. -echo Searching for MesenCore.dll in %CD%... -set "CORE_PATH=" -for /f "delims=" %%i in ('dir /s /b MesenCore.dll 2^>nul') do ( - set "CORE_PATH=%%i" - echo %%i | findstr /i "Release" | findstr /v /i "Dependencies" >nul - if not errorlevel 1 goto :found_it -) -:found_it -popd - -:: Get the directory and strip the trailing slash -for %%i in ("%CORE_PATH%") do set "BIN_SRC_DIR=%%~dpi" -if "%BIN_SRC_DIR:~-1%"=="\" set "BIN_SRC_DIR=%BIN_SRC_DIR:~0,-1%" - -echo Source Bin Dir: "%BIN_SRC_DIR%" - -echo Found Core at: "%CORE_PATH%" - -copy /y "%CORE_PATH%" Dependencies -copy /y "%BIN_SRC_DIR%libHarfBuzzSharp.dll" Dependencies -copy /y "%BIN_SRC_DIR%libSkiaSharp.dll" Dependencies - -xcopy /s /y "%PROJECT_DIR%Dependencies\*" Dependencies - -set "ZIP=%FULL_OUTDIR%\Dependencies.zip" - -if exist "%ZIP%" del /f /q "%ZIP%" -powershell -Command "Compress-Archive -Path (Get-Item '%FULL_OUTDIR%\Dependencies') -DestinationPath '%ZIP%' -Force" -copy /y "%ZIP%" "%PROJECT_DIR%" diff --git a/UI/prebuild_windows.ps1 b/UI/prebuild_windows.ps1 new file mode 100644 index 000000000..55142b0f7 --- /dev/null +++ b/UI/prebuild_windows.ps1 @@ -0,0 +1,85 @@ +# find and copy the files needed for building the UI project. +# called by the build target in UI.csproj. +# these steps are stored as a script for easier reading and editing. + +param ( + [string]$OutDir, # $(ProjectDir)..\bin\$(RuntimeIdentifier)\$(Configuration), like bin\win-x64\Release + [string]$ProjectDir, + [string]$RuntimeIdentifier # win-x64, win-arm64, etc. +) + +$ErrorActionPreference = "Stop" + + +# clean paths (PowerShell handles trailing slashes/dots automatically with Get-Item) +$ProjectDir = $ProjectDir.TrimEnd('\') +if ($OutDir -match '^[a-zA-Z]:') { + $FullOutDir = $OutDir +} else { + $FullOutDir = Join-Path $ProjectDir $OutDir +} + +Write-Host "[PREBUILD] Project Dir: $ProjectDir" +Write-Host "[PREBUILD] Target Dir : $FullOutDir" + +# ensure FullOutDir exists and move there +if (!(Test-Path $FullOutDir)) { New-Item -ItemType Directory -Path $FullOutDir | Out-Null } +Set-Location $FullOutDir + +# set up Dependencies folder +$DepsFolder = Join-Path $FullOutDir "Dependencies" +if (Test-Path $DepsFolder) { Remove-Item -Recurse -Force $DepsFolder } +New-Item -ItemType Directory -Path $DepsFolder | Out-Null + +# 1. copy external dlls managed by NuGet package manager. +# in web CI builds these are installed by 'dotnet restore'. +$Libs = @("libHarfBuzzSharp.dll", "libSkiaSharp.dll") + +foreach ($Lib in $Libs) { + # We MUST look specifically for the native folder matching our RID + $SpecificPathPart = "runtimes\$RuntimeIdentifier\native" + $LibSourcePath = (Get-ChildItem -Path "$env:USERPROFILE\.nuget\packages" -Filter $Lib -Recurse | + Where-Object { $_.FullName -like "*$SpecificPathPart*" } | + Select-Object -First 1).FullName + + if ($LibSourcePath -and (Test-Path $LibSourcePath)) { + Write-Host "[PREBUILD] Copying NuGet dll ($RuntimeIdentifier): $LibSourcePath" + Copy-Item $LibSourcePath -Destination $DepsFolder + } else { + throw "ERROR: Could not find $Lib for $RuntimeIdentifier in NuGet cache '$LibSourcePath' " + } +} + +# 2. copy MesenCore.dll output from building the Core c++ project +$Dll = "MesenCore.dll" +$DllSourcePath = Join-Path $FullOutDir $Dll + +if (!(Test-Path $DllSourcePath)) { + Write-Host "[PREBUILD] $Dll not found in $DllSourcePath . Checking fallback path" + # Fallback: Check if the file is one level up or if the OutDir had a double-slash issue + $ParentDir = Split-Path $FullOutDir -Parent + $DllSourcePath = (Get-ChildItem -Path $ParentDir -Filter $Dll -Recurse | Select-Object -First 1).FullName +} + +if ($DllSourcePath -and (Test-Path $DllSourcePath)) { + Write-Host "[PREBUILD] Copying $Dll from: $DllSourcePath" + Copy-Item $DllSourcePath -Destination $DepsFolder +} else { + Write-Host "DEBUG: Contents of $(Split-Path $FullOutDir -Parent):" + Get-ChildItem -Path (Split-Path $FullOutDir -Parent) -Recurse | Select-Object FullName + throw "ERROR: Required file $Dll not found in $DllSourcePath" +} + +# 3. copy files that exist in source control +Write-Host "[PREBUILD] Copying other dependencies from $ProjectDir\Dependencies" +Copy-Item -Path "$ProjectDir\Dependencies\*" -Destination $DepsFolder -Recurse -Force + +# 6. Zip and move back to Project Dir +$ZipPath = Join-Path $FullOutDir "Dependencies.zip" +if (Test-Path $ZipPath) { Remove-Item $ZipPath } + +Write-Host "[PREBUILD] Creating Zip $ZipPath..." +Compress-Archive -Path $DepsFolder -DestinationPath $ZipPath -Force + +Move-Item $ZipPath -Destination (Join-Path $ProjectDir "Dependencies.zip") -Force +Write-Host "[PREBUILD] Success." diff --git a/UI/tests/prebuild_windows.test.ps1 b/UI/tests/prebuild_windows.test.ps1 new file mode 100644 index 000000000..bc491db8c --- /dev/null +++ b/UI/tests/prebuild_windows.test.ps1 @@ -0,0 +1,115 @@ +Describe "Mesen Prebuild Logic Verification" { + BeforeAll { + # set up sandbox in Temp + $Sandbox = Join-Path $env:TEMP "Mesen_CI_Sandbox" + if (Test-Path $Sandbox) { Remove-Item $Sandbox -Recurse -Force -ErrorAction SilentlyContinue } + New-Item -Path $Sandbox -ItemType Directory -Force | Out-Null + + # map T: to this sandbox so Join-Path ..\.. works correctly + if (Get-PSDrive T -ErrorAction SilentlyContinue) { Remove-PSDrive T } + New-PSDrive -Name T -PSProvider FileSystem -Root $Sandbox | Out-Null + + $script:OldProfile = $env:USERPROFILE + $env:USERPROFILE = "T:\Users\testuser" + } + + AfterAll { + $env:USERPROFILE = $script:OldProfile + if (Get-PSDrive T -ErrorAction SilentlyContinue) { Remove-PSDrive T } + } + + Context "Full Mock Isolation" { + BeforeEach { + # mock all actions that might cause changes on disk + Mock Compress-Archive { } + Mock Copy-Item { } + Mock Get-Item { param($Path) return [PSCustomObject]@{ FullName = $Path } } + Mock Move-Item { } + Mock New-Item { return [PSCustomObject]@{ FullName = "MockedDir" } } + Mock Remove-Item { } + Mock Set-Location { } + Mock Test-Path { return $true } + + Mock Get-ChildItem { + param($Path, $Filter) + # This MUST contain 'runtimes\win-x64\native' or the script's filter fails + if ($Path -like "*\.nuget\*") { + return [PSCustomObject]@{ FullName = "T:\Users\testuser\.nuget\packages\runtimes\win-x64\native\libHarfBuzzSharp.dll" } + } + if ($Filter -eq "MesenCore.dll") { + return [PSCustomObject]@{ FullName = "T:\Users\testuser\code\mesen\bin\win-x64\Release\MesenCore.dll" } + } + } + } + + It "Should handle Relative OutDir (Local Style)" { + $params = @{ + ProjectDir = "T:\Users\testuser\code\UI" + OutDir = "..\bin\Release" + RuntimeIdentifier = "win-x64" + } + { & "$PSScriptRoot/../prebuild_windows.ps1" @params } | Should -Not -Throw + } + + It "Should handle Absolute OutDir (CI Style)" { + $params = @{ + ProjectDir = "T:\Users\testuser\code\UI" + OutDir = "T:\Users\testuser\code\bin\Release" + RuntimeIdentifier = "win-x64" + } + { & "$PSScriptRoot/../prebuild_windows.ps1" @params } | Should -Not -Throw + } + + It "Matches the vcxproj OutDir structure" { + $Repo = "T:\MesenRepo" + $ProjectDir = "$Repo\UI" + # This matches your vcxproj logic: $(SolutionDir)\bin\win-$(PlatformTarget)\$(Configuration)\ + $ActualDllLocation = "$Repo\bin\win-x64\Release\MesenCore.dll" + + # 1. Mock NuGet (The script hits this first!) + Mock Get-ChildItem { + param($Path, $Filter) + if ($Path -like "*\.nuget\*") { + return [PSCustomObject]@{ FullName = "T:\Users\testuser\.nuget\packages\runtimes\win-x64\native\$Filter" } + } + # 2. Mock MesenCore discovery + if ($Filter -eq "MesenCore.dll") { + return [PSCustomObject]@{ FullName = $ActualDllLocation } + } + return $null + } -Verifiable + + Mock Test-Path { return $true } + Mock Copy-Item { } + Mock Compress-Archive { } + Mock Move-Item { } + Mock New-Item { [PSCustomObject]@{ FullName = "MockedDir" } } + Mock Set-Location { } + + $params = @{ + ProjectDir = $ProjectDir + OutDir = "..\bin\win-x64\Release" + RuntimeIdentifier = "win-x64" + } + + { & "$PSScriptRoot/../prebuild_windows.ps1" @params } | Should -Not -Throw + } + + It "Should verify the zip ends up in the ProjectDir" { + $ProjectDir = "T:\Users\testuser\code\mesen\UI" + + $params = @{ + ProjectDir = $ProjectDir + OutDir = "T:\Some\Other\Path" + RuntimeIdentifier = "win-x64" + } + { & "$PSScriptRoot/../prebuild_windows.ps1" @params } | Should -Not -Throw + + $ExpectedDestination = Join-Path $ProjectDir "Dependencies.zip" + + Assert-MockCalled Move-Item -ParameterFilter { + $Destination -eq $ExpectedDestination + } + } + } +} From 668363f968d4aab7a3d5ad67863bb4ce59db02e9 Mon Sep 17 00:00:00 2001 From: culix Date: Tue, 13 Jan 2026 19:16:22 -0700 Subject: [PATCH 06/13] Build: Clean up powershell test use constants to remove repeated strings --- UI/tests/prebuild_windows.test.ps1 | 51 ++++++++++++------------------ 1 file changed, 20 insertions(+), 31 deletions(-) diff --git a/UI/tests/prebuild_windows.test.ps1 b/UI/tests/prebuild_windows.test.ps1 index bc491db8c..076a9a08c 100644 --- a/UI/tests/prebuild_windows.test.ps1 +++ b/UI/tests/prebuild_windows.test.ps1 @@ -1,3 +1,5 @@ +# test that all needed scenarios of the prebuild_windows script work. + Describe "Mesen Prebuild Logic Verification" { BeforeAll { # set up sandbox in Temp @@ -11,6 +13,9 @@ Describe "Mesen Prebuild Logic Verification" { $script:OldProfile = $env:USERPROFILE $env:USERPROFILE = "T:\Users\testuser" + + $PREBUILD_SCRIPT = "$PSScriptRoot/../prebuild_windows.ps1" + $RUNTIME_ID = "win-x64" } AfterAll { @@ -32,52 +37,36 @@ Describe "Mesen Prebuild Logic Verification" { Mock Get-ChildItem { param($Path, $Filter) - # This MUST contain 'runtimes\win-x64\native' or the script's filter fails if ($Path -like "*\.nuget\*") { - return [PSCustomObject]@{ FullName = "T:\Users\testuser\.nuget\packages\runtimes\win-x64\native\libHarfBuzzSharp.dll" } + return [PSCustomObject]@{ FullName = Join-Path $env:USERPROFILE ".nuget\packages\runtimes\$RUNTIME_ID\native\$Filter" } } if ($Filter -eq "MesenCore.dll") { - return [PSCustomObject]@{ FullName = "T:\Users\testuser\code\mesen\bin\win-x64\Release\MesenCore.dll" } + return [PSCustomObject]@{ FullName = Join-Path $env:USERPROFILE "code\mesen\bin\$RUNTIME_ID\Release\MesenCore.dll" } } } } It "Should handle Relative OutDir (Local Style)" { $params = @{ - ProjectDir = "T:\Users\testuser\code\UI" + ProjectDir = Join-Path $env:USERPROFILE "code\UI" OutDir = "..\bin\Release" - RuntimeIdentifier = "win-x64" + RuntimeIdentifier = $RUNTIME_ID } - { & "$PSScriptRoot/../prebuild_windows.ps1" @params } | Should -Not -Throw + { & $PREBUILD_SCRIPT @params } | Should -Not -Throw } It "Should handle Absolute OutDir (CI Style)" { $params = @{ - ProjectDir = "T:\Users\testuser\code\UI" - OutDir = "T:\Users\testuser\code\bin\Release" - RuntimeIdentifier = "win-x64" + ProjectDir = Join-Path $env:USERPROFILE "code\UI" + OutDir = Join-Path $env:USERPROFILE "code\bin\Release" + RuntimeIdentifier = $RUNTIME_ID } - { & "$PSScriptRoot/../prebuild_windows.ps1" @params } | Should -Not -Throw + { & $PREBUILD_SCRIPT @params } | Should -Not -Throw } It "Matches the vcxproj OutDir structure" { $Repo = "T:\MesenRepo" $ProjectDir = "$Repo\UI" - # This matches your vcxproj logic: $(SolutionDir)\bin\win-$(PlatformTarget)\$(Configuration)\ - $ActualDllLocation = "$Repo\bin\win-x64\Release\MesenCore.dll" - - # 1. Mock NuGet (The script hits this first!) - Mock Get-ChildItem { - param($Path, $Filter) - if ($Path -like "*\.nuget\*") { - return [PSCustomObject]@{ FullName = "T:\Users\testuser\.nuget\packages\runtimes\win-x64\native\$Filter" } - } - # 2. Mock MesenCore discovery - if ($Filter -eq "MesenCore.dll") { - return [PSCustomObject]@{ FullName = $ActualDllLocation } - } - return $null - } -Verifiable Mock Test-Path { return $true } Mock Copy-Item { } @@ -88,22 +77,22 @@ Describe "Mesen Prebuild Logic Verification" { $params = @{ ProjectDir = $ProjectDir - OutDir = "..\bin\win-x64\Release" - RuntimeIdentifier = "win-x64" + OutDir = "..\bin\$RUNTIME_ID\Release" + RuntimeIdentifier = $RUNTIME_ID } - { & "$PSScriptRoot/../prebuild_windows.ps1" @params } | Should -Not -Throw + { & $PREBUILD_SCRIPT @params } | Should -Not -Throw } It "Should verify the zip ends up in the ProjectDir" { - $ProjectDir = "T:\Users\testuser\code\mesen\UI" + $ProjectDir = Join-Path $env:USERPROFILE "code\mesen\UI" $params = @{ ProjectDir = $ProjectDir OutDir = "T:\Some\Other\Path" - RuntimeIdentifier = "win-x64" + RuntimeIdentifier = $RUNTIME_ID } - { & "$PSScriptRoot/../prebuild_windows.ps1" @params } | Should -Not -Throw + { & $PREBUILD_SCRIPT @params } | Should -Not -Throw $ExpectedDestination = Join-Path $ProjectDir "Dependencies.zip" From 144275ef11643b946bb43feeeb4f2ad43c667f47 Mon Sep 17 00:00:00 2001 From: culix Date: Mon, 12 Jan 2026 05:28:25 -0700 Subject: [PATCH 07/13] Build: Add step to verify prebuild script works --- .github/workflows/build.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fbc7fbf37..efc3ea358 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,11 @@ jobs: with: fetch-depth: 0 + - name: Verify Prebuild Script (Pester Tests) + shell: powershell + run: | + Invoke-Pester -Path "UI/tests/prebuild_windows.test.ps1" -Passthru | ForEach-Object { if ($_.FailedCount -gt 0) { exit 1 } } + - name: Install .NET Core uses: actions/setup-dotnet@v4 with: From b534e614e13e99bce10ed984324c485df5d53d78 Mon Sep 17 00:00:00 2001 From: culix Date: Tue, 13 Jan 2026 17:03:03 -0700 Subject: [PATCH 08/13] Build: turn off windows telemetry --- .github/workflows/build.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index efc3ea358..38032079d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,6 +5,8 @@ on: [push] env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + POWERSHELL_TELEMETRY_OPTOUT: 1 + POWERSHELL_UPDATECHECK: Off jobs: windows: From cf005d96d7744c1ade1958b178ad9b05e1e36b7f Mon Sep 17 00:00:00 2001 From: culix Date: Thu, 15 Jan 2026 19:07:57 -0700 Subject: [PATCH 09/13] Build: Improve windows prebuild script * Wrap the entire script in 'try { .. } catch {}' brackets. This allows us to capture anything that went wrong and report it for both tests and visual studio * Format a nice error message, and write that to stdout. If we format it in a specific way, Visual Studio will display it nicely right inside the IDE, in the Error List window. The user can click on that line to jump to the script. --- UI/prebuild_windows.ps1 | 138 +++++++++++++++++++++++----------------- 1 file changed, 80 insertions(+), 58 deletions(-) diff --git a/UI/prebuild_windows.ps1 b/UI/prebuild_windows.ps1 index 55142b0f7..c17b44349 100644 --- a/UI/prebuild_windows.ps1 +++ b/UI/prebuild_windows.ps1 @@ -10,76 +10,98 @@ param ( $ErrorActionPreference = "Stop" +try { -# clean paths (PowerShell handles trailing slashes/dots automatically with Get-Item) -$ProjectDir = $ProjectDir.TrimEnd('\') -if ($OutDir -match '^[a-zA-Z]:') { - $FullOutDir = $OutDir -} else { - $FullOutDir = Join-Path $ProjectDir $OutDir -} - -Write-Host "[PREBUILD] Project Dir: $ProjectDir" -Write-Host "[PREBUILD] Target Dir : $FullOutDir" - -# ensure FullOutDir exists and move there -if (!(Test-Path $FullOutDir)) { New-Item -ItemType Directory -Path $FullOutDir | Out-Null } -Set-Location $FullOutDir + # clean paths (PowerShell handles trailing slashes/dots automatically with Get-Item) + $ProjectDir = $ProjectDir.TrimEnd('\') + if ($OutDir -match '^[a-zA-Z]:') { + $FullOutDir = $OutDir + } else { + $FullOutDir = Join-Path $ProjectDir $OutDir + } -# set up Dependencies folder -$DepsFolder = Join-Path $FullOutDir "Dependencies" -if (Test-Path $DepsFolder) { Remove-Item -Recurse -Force $DepsFolder } -New-Item -ItemType Directory -Path $DepsFolder | Out-Null + Write-Host "[PREBUILD] Project Dir: $ProjectDir" + Write-Host "[PREBUILD] Target Dir : $FullOutDir" + + # ensure FullOutDir exists and move there + if (!(Test-Path $FullOutDir)) { New-Item -ItemType Directory -Path $FullOutDir | Out-Null } + Set-Location $FullOutDir + + # set up Dependencies folder + $DepsFolder = Join-Path $FullOutDir "Dependencies" + if (Test-Path $DepsFolder) { Remove-Item -Recurse -Force $DepsFolder } + New-Item -ItemType Directory -Path $DepsFolder | Out-Null + + # 1. copy external dlls managed by NuGet package manager. + # in web CI builds these are installed by 'dotnet restore'. + $Libs = @("libHarfBuzzSharp.dll", "libSkiaSharp.dll") + + foreach ($Lib in $Libs) { + # We MUST look specifically for the native folder matching our RID + $SpecificPathPart = "runtimes\$RuntimeIdentifier\native" + $LibSourcePath = (Get-ChildItem -Path "$env:USERPROFILE\.nuget\packages" -Filter $Lib -Recurse | + Where-Object { $_.FullName -like "*$SpecificPathPart*" } | + Select-Object -First 1).FullName + + if ($LibSourcePath -and (Test-Path $LibSourcePath)) { + Write-Host "[PREBUILD] Copying NuGet dll ($RuntimeIdentifier): $LibSourcePath" + Copy-Item $LibSourcePath -Destination $DepsFolder + } else { + throw "ERROR: Could not find $Lib for $RuntimeIdentifier in NuGet cache '$LibSourcePath' " + } + } -# 1. copy external dlls managed by NuGet package manager. -# in web CI builds these are installed by 'dotnet restore'. -$Libs = @("libHarfBuzzSharp.dll", "libSkiaSharp.dll") + # 2. copy MesenCore.dll output from building the Core c++ project + $Dll = "MesenCore.dll" + $DllSourcePath = Join-Path $FullOutDir $Dll -foreach ($Lib in $Libs) { - # We MUST look specifically for the native folder matching our RID - $SpecificPathPart = "runtimes\$RuntimeIdentifier\native" - $LibSourcePath = (Get-ChildItem -Path "$env:USERPROFILE\.nuget\packages" -Filter $Lib -Recurse | - Where-Object { $_.FullName -like "*$SpecificPathPart*" } | - Select-Object -First 1).FullName + if (!(Test-Path $DllSourcePath)) { + Write-Host "[PREBUILD] $Dll not found in $DllSourcePath . Checking fallback path" + # Fallback: Check if the file is one level up or if the OutDir had a double-slash issue + $ParentDir = Split-Path $FullOutDir -Parent + $DllSourcePath = (Get-ChildItem -Path $ParentDir -Filter $Dll -Recurse | Select-Object -First 1).FullName + } - if ($LibSourcePath -and (Test-Path $LibSourcePath)) { - Write-Host "[PREBUILD] Copying NuGet dll ($RuntimeIdentifier): $LibSourcePath" - Copy-Item $LibSourcePath -Destination $DepsFolder + if ($DllSourcePath -and (Test-Path $DllSourcePath)) { + Write-Host "[PREBUILD] Copying $Dll from: $DllSourcePath" + Copy-Item $DllSourcePath -Destination $DepsFolder } else { - throw "ERROR: Could not find $Lib for $RuntimeIdentifier in NuGet cache '$LibSourcePath' " + Write-Host "DEBUG: Contents of $(Split-Path $FullOutDir -Parent):" + Get-ChildItem -Path (Split-Path $FullOutDir -Parent) -Recurse | Select-Object FullName + throw "ERROR: Required file $Dll not found in $DllSourcePath" } -} -# 2. copy MesenCore.dll output from building the Core c++ project -$Dll = "MesenCore.dll" -$DllSourcePath = Join-Path $FullOutDir $Dll + # 3. copy files that exist in source control + Write-Host "[PREBUILD] Copying other dependencies from $ProjectDir\Dependencies" + Copy-Item -Path "$ProjectDir\Dependencies\*" -Destination $DepsFolder -Recurse -Force -if (!(Test-Path $DllSourcePath)) { - Write-Host "[PREBUILD] $Dll not found in $DllSourcePath . Checking fallback path" - # Fallback: Check if the file is one level up or if the OutDir had a double-slash issue - $ParentDir = Split-Path $FullOutDir -Parent - $DllSourcePath = (Get-ChildItem -Path $ParentDir -Filter $Dll -Recurse | Select-Object -First 1).FullName -} + # 6. Zip and move back to Project Dir + $ZipPath = Join-Path $FullOutDir "Dependencies.zip" + if (Test-Path $ZipPath) { Remove-Item $ZipPath } + + Write-Host "[PREBUILD] Creating Zip $ZipPath..." + Compress-Archive -Path $DepsFolder -DestinationPath $ZipPath -Force + + Move-Item $ZipPath -Destination (Join-Path $ProjectDir "Dependencies.zip") -Force + Write-Host "[PREBUILD] Success." -if ($DllSourcePath -and (Test-Path $DllSourcePath)) { - Write-Host "[PREBUILD] Copying $Dll from: $DllSourcePath" - Copy-Item $DllSourcePath -Destination $DepsFolder -} else { - Write-Host "DEBUG: Contents of $(Split-Path $FullOutDir -Parent):" - Get-ChildItem -Path (Split-Path $FullOutDir -Parent) -Recurse | Select-Object FullName - throw "ERROR: Required file $Dll not found in $DllSourcePath" } +catch { + + # format an error message that shows the error inside Visual Studio. + # using this specific format turns this into a clickable link in the Error List window or tab. + # Format: path\to\file.ps1(line): error: message + # https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasks -# 3. copy files that exist in source control -Write-Host "[PREBUILD] Copying other dependencies from $ProjectDir\Dependencies" -Copy-Item -Path "$ProjectDir\Dependencies\*" -Destination $DepsFolder -Recurse -Force + $file = $_.InvocationInfo.ScriptName + $line = $_.InvocationInfo.ScriptLineNumber + $errorMessage = "$file($line): error: $($_.Exception.Message)" -# 6. Zip and move back to Project Dir -$ZipPath = Join-Path $FullOutDir "Dependencies.zip" -if (Test-Path $ZipPath) { Remove-Item $ZipPath } + # write to regular output so Visual Studio picks up + parses the message + Write-Host $errorMessage -Write-Host "[PREBUILD] Creating Zip $ZipPath..." -Compress-Archive -Path $DepsFolder -DestinationPath $ZipPath -Force + # also write to stderr for pester tests and standard logging + Write-Error $_.Exception.Message -Move-Item $ZipPath -Destination (Join-Path $ProjectDir "Dependencies.zip") -Force -Write-Host "[PREBUILD] Success." + exit 1 +} From 1862cb3d73c9f3563221d17b751884ffc563f380 Mon Sep 17 00:00:00 2001 From: culix Date: Thu, 15 Jan 2026 19:10:58 -0700 Subject: [PATCH 10/13] Build: Further improve windows prebuild script * Use specific exception types when throwing exceptions, to better declare what is happening * Add input validation to check for empty args. This should never happen, but we might as well be robust to it. * Add checks for folders that don't exist * Extract some strings to constants * Add troubleshooting info on how to fix problems --- UI/prebuild_windows.ps1 | 77 ++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/UI/prebuild_windows.ps1 b/UI/prebuild_windows.ps1 index c17b44349..1678eb258 100644 --- a/UI/prebuild_windows.ps1 +++ b/UI/prebuild_windows.ps1 @@ -12,12 +12,23 @@ $ErrorActionPreference = "Stop" try { + $MissingParams = foreach ($Name in "ProjectDir", "OutDir", "RuntimeIdentifier") { + if ([string]::IsNullOrWhiteSpace((Get-Variable $Name -ValueOnly))) { + $Name + } + } + + if ($MissingParams) { + $List = $MissingParams -join ", " + throw [System.ArgumentException] "Required parameter(s) '[$List]' are missing or empty." + } + # clean paths (PowerShell handles trailing slashes/dots automatically with Get-Item) $ProjectDir = $ProjectDir.TrimEnd('\') if ($OutDir -match '^[a-zA-Z]:') { - $FullOutDir = $OutDir + $FullOutDir = $OutDir } else { - $FullOutDir = Join-Path $ProjectDir $OutDir + $FullOutDir = Join-Path "$ProjectDir" $OutDir } Write-Host "[PREBUILD] Project Dir: $ProjectDir" @@ -25,6 +36,9 @@ try { # ensure FullOutDir exists and move there if (!(Test-Path $FullOutDir)) { New-Item -ItemType Directory -Path $FullOutDir | Out-Null } + if (!(Test-Path $FullOutDir)) { + throw [System.IO.DirectoryNotFoundException] "$FullOutDir does not exist" + } Set-Location $FullOutDir # set up Dependencies folder @@ -35,20 +49,26 @@ try { # 1. copy external dlls managed by NuGet package manager. # in web CI builds these are installed by 'dotnet restore'. $Libs = @("libHarfBuzzSharp.dll", "libSkiaSharp.dll") + $NuGetBase = "$env:USERPROFILE\.nuget\packages" + $SpecificPathPart = "runtimes\$RuntimeIdentifier\native" foreach ($Lib in $Libs) { - # We MUST look specifically for the native folder matching our RID - $SpecificPathPart = "runtimes\$RuntimeIdentifier\native" - $LibSourcePath = (Get-ChildItem -Path "$env:USERPROFILE\.nuget\packages" -Filter $Lib -Recurse | - Where-Object { $_.FullName -like "*$SpecificPathPart*" } | - Select-Object -First 1).FullName - - if ($LibSourcePath -and (Test-Path $LibSourcePath)) { - Write-Host "[PREBUILD] Copying NuGet dll ($RuntimeIdentifier): $LibSourcePath" - Copy-Item $LibSourcePath -Destination $DepsFolder - } else { - throw "ERROR: Could not find $Lib for $RuntimeIdentifier in NuGet cache '$LibSourcePath' " - } + $FoundFile = Get-ChildItem -Path $NuGetBase -Filter $Lib -Recurse | + Where-Object { $_.FullName -like "*$SpecificPathPart*" } | + Select-Object -First 1 + + if ($FoundFile -and (Test-Path $FoundFile.FullName)) { + $LibSourcePath = $FoundFile.FullName + Write-Host "[PREBUILD] Copying NuGet dll $LibSourcePath" + Copy-Item $LibSourcePath -Destination $DepsFolder + } else { + $troubleshooting = "Please rebuild or restore packages. (Right-click solution in Visual Studio -> click 'Restore NuGet Packages', or run 'dotnet restore' in shell" + + $SearchPattern = Join-Path $NuGetBase "*\$SpecificPathPart" + $DllSourceFolder = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($SearchPattern) + + throw [System.IO.FileNotFoundException] "Required file '$Lib' for $RuntimeIdentifier not found in '$DllSourceFolder'. $troubleshooting" + } } # 2. copy MesenCore.dll output from building the Core c++ project @@ -56,35 +76,39 @@ try { $DllSourcePath = Join-Path $FullOutDir $Dll if (!(Test-Path $DllSourcePath)) { - Write-Host "[PREBUILD] $Dll not found in $DllSourcePath . Checking fallback path" - # Fallback: Check if the file is one level up or if the OutDir had a double-slash issue - $ParentDir = Split-Path $FullOutDir -Parent - $DllSourcePath = (Get-ChildItem -Path $ParentDir -Filter $Dll -Recurse | Select-Object -First 1).FullName + Write-Host "[PREBUILD] $Dll not found in $DllSourcePath . Checking fallback path" + # Fallback: Check if the file is one level up or if the OutDir had a double-slash issue + $ParentDir = Split-Path $FullOutDir -Parent + $FoundFile = Get-ChildItem -Path $ParentDir -Filter $Dll -Recurse | Select-Object -First 1 + + if ($FoundFile) { + $DllSourcePath = $FoundFile.FullName + } } if ($DllSourcePath -and (Test-Path $DllSourcePath)) { - Write-Host "[PREBUILD] Copying $Dll from: $DllSourcePath" - Copy-Item $DllSourcePath -Destination $DepsFolder + Write-Host "[PREBUILD] Copying $Dll from: $DllSourcePath" + Copy-Item $DllSourcePath -Destination $DepsFolder } else { - Write-Host "DEBUG: Contents of $(Split-Path $FullOutDir -Parent):" - Get-ChildItem -Path (Split-Path $FullOutDir -Parent) -Recurse | Select-Object FullName - throw "ERROR: Required file $Dll not found in $DllSourcePath" + $ParentPath = Split-Path $DllSourcePath -Parent + $DllSourceFolder = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ParentPath) + throw [System.IO.FileNotFoundException] "Required file '$Dll' not found in '$DllSourceFolder'. Please try rebuilding the solution." } # 3. copy files that exist in source control Write-Host "[PREBUILD] Copying other dependencies from $ProjectDir\Dependencies" Copy-Item -Path "$ProjectDir\Dependencies\*" -Destination $DepsFolder -Recurse -Force - # 6. Zip and move back to Project Dir + # zip files $ZipPath = Join-Path $FullOutDir "Dependencies.zip" if (Test-Path $ZipPath) { Remove-Item $ZipPath } Write-Host "[PREBUILD] Creating Zip $ZipPath..." Compress-Archive -Path $DepsFolder -DestinationPath $ZipPath -Force + # move instead of copy so we don't leave copies of the zip lying around Move-Item $ZipPath -Destination (Join-Path $ProjectDir "Dependencies.zip") -Force Write-Host "[PREBUILD] Success." - } catch { @@ -100,8 +124,5 @@ catch { # write to regular output so Visual Studio picks up + parses the message Write-Host $errorMessage - # also write to stderr for pester tests and standard logging - Write-Error $_.Exception.Message - exit 1 } From 372f81088ad30ab910d85d0e2f3064baad4aa0d2 Mon Sep 17 00:00:00 2001 From: culix Date: Thu, 15 Jan 2026 20:58:28 -0700 Subject: [PATCH 11/13] Tests: powershell tests - write the test number Apparently pester has no way to easily get the name of the current running test. This has been an open issue since at least June 2020 - https://github.com/pester/Pester/issues/1611 you can hack around it by setting an environment variable, but that makes the code pretty ugly. at least do this for now. --- UI/tests/prebuild_windows.test.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/UI/tests/prebuild_windows.test.ps1 b/UI/tests/prebuild_windows.test.ps1 index 076a9a08c..2f390e354 100644 --- a/UI/tests/prebuild_windows.test.ps1 +++ b/UI/tests/prebuild_windows.test.ps1 @@ -16,6 +16,8 @@ Describe "Mesen Prebuild Logic Verification" { $PREBUILD_SCRIPT = "$PSScriptRoot/../prebuild_windows.ps1" $RUNTIME_ID = "win-x64" + + $script:TestNumber = 0 } AfterAll { @@ -44,6 +46,11 @@ Describe "Mesen Prebuild Logic Verification" { return [PSCustomObject]@{ FullName = Join-Path $env:USERPROFILE "code\mesen\bin\$RUNTIME_ID\Release\MesenCore.dll" } } } + $error.Clear() + + $script:TestNumber += 1 + Write-Host "-------" + Write-Host "[Test] $TestNumber" -ForegroundColor Cyan } It "Should handle Relative OutDir (Local Style)" { From 8830bb554d338017cefc4272deef97049b26af0a Mon Sep 17 00:00:00 2001 From: culix Date: Thu, 15 Jan 2026 19:09:57 -0700 Subject: [PATCH 12/13] Build: Add powershell tests --- UI/tests/prebuild_windows.test.ps1 | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/UI/tests/prebuild_windows.test.ps1 b/UI/tests/prebuild_windows.test.ps1 index 2f390e354..f3f550be2 100644 --- a/UI/tests/prebuild_windows.test.ps1 +++ b/UI/tests/prebuild_windows.test.ps1 @@ -107,5 +107,52 @@ Describe "Mesen Prebuild Logic Verification" { $Destination -eq $ExpectedDestination } } + + It "Should raise exception on empty paths" { + $params = @{ ProjectDir = ""; OutDir = ""; RuntimeIdentifier = "win-x64" } + + & $PREBUILD_SCRIPT @params 2>&1 + + $error.Count | Should -BeGreaterThan 0 + $error[0].Exception.GetType().FullName | Should -Be "System.ArgumentException" + $LASTEXITCODE | Should -Be 1 + } + + It "Should raise exception when NuGet DLL is missing" { + Mock Get-ChildItem { return $null } -ParameterFilter { $Path -like "*\.nuget\*" } + + $params = @{ + ProjectDir = "T:\Mesen" + OutDir = "bin" + RuntimeIdentifier = $RUNTIME_ID + } + + & $PREBUILD_SCRIPT @params 2>&1 + + $error.Count | Should -BeGreaterThan 0 + $error[0].Exception.GetType().FullName | Should -Be "System.IO.FileNotFoundException" + $LASTEXITCODE | Should -Be 1 + } + + It "Should print error when MesenCore.dll is missing" { + Mock Test-Path { + param($Path) + if ($Path -like "*MesenCore.dll") { return $false } + return $true + } + Mock Get-ChildItem { return $null } -ParameterFilter { $Filter -eq "MesenCore.dll" } + + $params = @{ + ProjectDir = "T:\Mesen" + OutDir = "bin" + RuntimeIdentifier = $RUNTIME_ID + } + + & $PREBUILD_SCRIPT @params 2>&1 + + $error.Count | Should -BeGreaterThan 0 + $error[0].Exception.GetType().FullName | Should -Be "System.IO.FileNotFoundException" + $LASTEXITCODE | Should -Be 1 + } } } From 17be5f6f6fe9392b19c2e9c1fe6ca64d2096f45f Mon Sep 17 00:00:00 2001 From: culix Date: Thu, 15 Jan 2026 19:10:11 -0700 Subject: [PATCH 13/13] Docs: Add note on running windows powershell tests --- COMPILING.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/COMPILING.md b/COMPILING.md index 1e14700ed..2dd177c4f 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -6,6 +6,19 @@ 2) Compile as `Release`/`x64` 3) Set the startup project to the `UI` project and run +### Testing + +There is a [pester](https://pester.dev/) powershell test script in `UI/tests` that will verify the prebuild steps for the `UI` project. + + +1. [Install pester](https://pester.dev/docs/introduction/installation) if needed. +1. Run from any powershell console: + +```cmd +$ .\UI\tests\prebuild_windows.test.ps1 +``` + + ## Linux To build under Linux you need a version of Clang or GCC that supports C++17.