Skip to content
This repository was archived by the owner on Jun 4, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -24,6 +26,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:
Expand Down
15 changes: 15 additions & 0 deletions COMPILING.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,24 @@
## 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

### 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.
Expand Down
4 changes: 3 additions & 1 deletion UI/UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,9 @@
</ItemGroup>

<Target Name="PreBuildWindows" BeforeTargets="PreBuildEvent" Condition="'$(RuntimeIdentifier)'=='win-x64'">
<Exec Command="cd $(OutDir)&#xD;&#xA;rd Dependencies /s /q&#xD;&#xA;md Dependencies&#xD;&#xA;xcopy /s $(ProjectDir)Dependencies\* Dependencies&#xD;&#xA;copy libHarfBuzzSharp.dll Dependencies&#xD;&#xA;copy libSkiaSharp.dll Dependencies&#xD;&#xA;copy MesenCore.dll Dependencies&#xD;&#xA;cd Dependencies&#xD;&#xA;del ..\Dependencies.zip&#xD;&#xA;powershell Compress-Archive -Path * -DestinationPath '..\Dependencies.zip' -Force&#xD;&#xA;copy ..\Dependencies.zip $(ProjectDir)" />
<Exec
Command="powershell -ExecutionPolicy Bypass -File &quot;$(ProjectDir)prebuild_windows.ps1&quot; -OutDir &quot;$(ProjectDir)..\bin\$(RuntimeIdentifier)\$(Configuration)&quot; -ProjectDir &quot;$(ProjectDir.TrimEnd('\'))&quot; -RuntimeIdentifier &quot;$(RuntimeIdentifier)&quot;"
WorkingDirectory="$(ProjectDir)" />
</Target>

<Target Name="PreBuildLinux" BeforeTargets="PreBuildEvent" Condition="'$(RuntimeIdentifier)'=='linux-x64' Or '$(RuntimeIdentifier)'=='linux-arm64'">
Expand Down
128 changes: 128 additions & 0 deletions UI/prebuild_windows.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# 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"

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
} 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 }
if (!(Test-Path $FullOutDir)) {
throw [System.IO.DirectoryNotFoundException] "$FullOutDir does not exist"
}
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")
$NuGetBase = "$env:USERPROFILE\.nuget\packages"
$SpecificPathPart = "runtimes\$RuntimeIdentifier\native"

foreach ($Lib in $Libs) {
$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
$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
$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
} else {
$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

# 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 {

# 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

$file = $_.InvocationInfo.ScriptName
$line = $_.InvocationInfo.ScriptLineNumber
$errorMessage = "$file($line): error: $($_.Exception.Message)"

# write to regular output so Visual Studio picks up + parses the message
Write-Host $errorMessage

exit 1
}
158 changes: 158 additions & 0 deletions UI/tests/prebuild_windows.test.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# test that all needed scenarios of the prebuild_windows script work.

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"

$PREBUILD_SCRIPT = "$PSScriptRoot/../prebuild_windows.ps1"
$RUNTIME_ID = "win-x64"

$script:TestNumber = 0
}

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)
if ($Path -like "*\.nuget\*") {
return [PSCustomObject]@{ FullName = Join-Path $env:USERPROFILE ".nuget\packages\runtimes\$RUNTIME_ID\native\$Filter" }
}
if ($Filter -eq "MesenCore.dll") {
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)" {
$params = @{
ProjectDir = Join-Path $env:USERPROFILE "code\UI"
OutDir = "..\bin\Release"
RuntimeIdentifier = $RUNTIME_ID
}
{ & $PREBUILD_SCRIPT @params } | Should -Not -Throw
}

It "Should handle Absolute OutDir (CI Style)" {
$params = @{
ProjectDir = Join-Path $env:USERPROFILE "code\UI"
OutDir = Join-Path $env:USERPROFILE "code\bin\Release"
RuntimeIdentifier = $RUNTIME_ID
}
{ & $PREBUILD_SCRIPT @params } | Should -Not -Throw
}

It "Matches the vcxproj OutDir structure" {
$Repo = "T:\MesenRepo"
$ProjectDir = "$Repo\UI"

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\$RUNTIME_ID\Release"
RuntimeIdentifier = $RUNTIME_ID
}

{ & $PREBUILD_SCRIPT @params } | Should -Not -Throw
}

It "Should verify the zip ends up in the ProjectDir" {
$ProjectDir = Join-Path $env:USERPROFILE "code\mesen\UI"

$params = @{
ProjectDir = $ProjectDir
OutDir = "T:\Some\Other\Path"
RuntimeIdentifier = $RUNTIME_ID
}
{ & $PREBUILD_SCRIPT @params } | Should -Not -Throw

$ExpectedDestination = Join-Path $ProjectDir "Dependencies.zip"

Assert-MockCalled Move-Item -ParameterFilter {
$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
}
}
}