diff --git a/.github/workflows/test-build.yml b/.github/workflows/test-build.yml index 5113bce4..0c6062ff 100644 --- a/.github/workflows/test-build.yml +++ b/.github/workflows/test-build.yml @@ -59,6 +59,25 @@ jobs: dotnet build -c ${{inputs.build_configuration}} Harmony.sln fi + - name: Validate Metadata (Unity Burst Compatibility Check) + if: ${{ inputs.build_configuration == 'Release' }} + continue-on-error: true + shell: pwsh + run: | + if (Test-Path "Lib.Harmony/bin/Release") { + Write-Host "Validating build metadata for Unity Burst compatibility..." -ForegroundColor Cyan + try { + & ./scripts/validate-build.ps1 -BuildPath "Lib.Harmony/bin/Release" + if ($LASTEXITCODE -ne 0) { + Write-Host "::warning::Lib.Harmony fat builds have known metadata issues with Unity Burst compiler. Use Lib.Harmony.Thin for Unity projects." + } + } catch { + Write-Host "::warning::Validation encountered an error: $_" + } + } else { + Write-Host "Skipping validation - Lib.Harmony/bin/Release not found" -ForegroundColor Yellow + } + - name: Upload Test Build Output Cache uses: actions/upload-artifact@v4 with: diff --git a/README.md b/README.md index 9a92f438..69291b67 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ If you want a single file, dependency-merged assembly, you should use the [Lib.H If you instead want to supply the dependencies yourself, you should use the [Lib.Harmony.Thin](https://www.nuget.org/packages/Lib.Harmony.Thin) nuget package. You get more control but you are responsible to make all references available at runtime. +> **Note for Unity/Burst Users**: If you're using Unity with the Burst compiler, you should use [Lib.Harmony.Thin](https://www.nuget.org/packages/Lib.Harmony.Thin) instead of Lib.Harmony. The fat build (Lib.Harmony) uses ILRepack to merge dependencies, which can produce metadata that's incompatible with Unity's Burst IL analyzer. The thin build ships dependencies separately and doesn't have this issue. + ### Documentation Please check out the [documentation](https://harmony.pardeike.net) and join the official [discord server](https://discord.gg/xXgghXR). diff --git a/scripts/pack.ps1 b/scripts/pack.ps1 index b0246458..864baede 100644 --- a/scripts/pack.ps1 +++ b/scripts/pack.ps1 @@ -29,3 +29,13 @@ dotnet clean --nologo --verbosity minimal # Build Solution dotnet build --nologo --configuration Release --verbosity minimal dotnet pack --nologo --configuration Release --verbosity minimal --no-restore --no-build + +# Validate build outputs for metadata integrity +Write-Host "`nValidating build outputs for Unity Burst compatibility..." -ForegroundColor Cyan +& "$PSScriptRoot/validate-build.ps1" -BuildPath "Lib.Harmony/bin/Release" +if ($LASTEXITCODE -ne 0) { + Write-Host "`nWARNING: .NET Framework builds have metadata issues that cause problems with Unity Burst compiler." -ForegroundColor Yellow + Write-Host "This is a known limitation of the ILRepack merge process used in Lib.Harmony fat builds." -ForegroundColor Yellow + Write-Host "For Unity/Burst projects, use Lib.Harmony.Thin instead, which ships dependencies separately." -ForegroundColor Yellow + Write-Host "(.NET Core/.NET 5+ builds pass validation and don't have this issue.)`n" -ForegroundColor Yellow +} diff --git a/scripts/validate-build.ps1 b/scripts/validate-build.ps1 new file mode 100644 index 00000000..6e09501f --- /dev/null +++ b/scripts/validate-build.ps1 @@ -0,0 +1,79 @@ +#!/usr/bin/env pwsh +# Script to validate Harmony build outputs for metadata integrity +# This checks for "coded rid out of range" errors that can cause issues with Unity Burst compiler + +param( + [Parameter(Mandatory=$false)] + [string]$BuildPath = "Lib.Harmony/bin/Release" +) + +$ErrorActionPreference = "Stop" + +Write-Host "==================================" -ForegroundColor Cyan +Write-Host "Harmony Build Metadata Validator" -ForegroundColor Cyan +Write-Host "==================================" -ForegroundColor Cyan +Write-Host "" + +# Build the validator if needed +$validatorPath = "tools/MetadataValidator/bin/Release/net8.0/MetadataValidator.dll" +if (-not (Test-Path $validatorPath)) { + Write-Host "Building MetadataValidator..." -ForegroundColor Yellow + dotnet build tools/MetadataValidator/MetadataValidator.csproj -c Release --verbosity quiet + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to build MetadataValidator" + exit 1 + } +} + +# Find all 0Harmony.dll files +$harmonyDlls = Get-ChildItem -Path $BuildPath -Filter "0Harmony.dll" -Recurse -ErrorAction SilentlyContinue + +if ($harmonyDlls.Count -eq 0) { + Write-Host "No 0Harmony.dll files found in $BuildPath" -ForegroundColor Yellow + Write-Host "Skipping validation (no builds to validate)" -ForegroundColor Yellow + exit 0 +} + +Write-Host "Found $($harmonyDlls.Count) 0Harmony.dll file(s) to validate" -ForegroundColor Green +Write-Host "" + +$failedValidations = @() +$succeededValidations = @() + +foreach ($dll in $harmonyDlls) { + $relativePath = $dll.FullName.Replace((Get-Location).Path, "").TrimStart("/\") + Write-Host "Validating: $relativePath" -ForegroundColor Cyan + + $result = & dotnet $validatorPath $dll.FullName + $exitCode = $LASTEXITCODE + + if ($exitCode -eq 0) { + Write-Host " ✓ PASSED" -ForegroundColor Green + $succeededValidations += $relativePath + } else { + Write-Host " ✗ FAILED" -ForegroundColor Red + $failedValidations += $relativePath + Write-Host " Output:" -ForegroundColor Red + $result | ForEach-Object { Write-Host " $_" -ForegroundColor Red } + } + Write-Host "" +} + +Write-Host "==================================" -ForegroundColor Cyan +Write-Host "Validation Summary" -ForegroundColor Cyan +Write-Host "==================================" -ForegroundColor Cyan +Write-Host "Passed: $($succeededValidations.Count)" -ForegroundColor Green +Write-Host "Failed: $($failedValidations.Count)" -ForegroundColor Red +Write-Host "" + +if ($failedValidations.Count -gt 0) { + Write-Host "Failed validations:" -ForegroundColor Red + $failedValidations | ForEach-Object { Write-Host " - $_" -ForegroundColor Red } + Write-Host "" + Write-Host "These builds have metadata corruption that will cause issues with Unity Burst compiler." -ForegroundColor Red + Write-Host "For Unity/Burst projects, use Lib.Harmony.Thin instead." -ForegroundColor Yellow + exit 1 +} + +Write-Host "All validations passed! ✓" -ForegroundColor Green +exit 0 diff --git a/tools/Directory.Build.props b/tools/Directory.Build.props new file mode 100644 index 00000000..beb6f388 --- /dev/null +++ b/tools/Directory.Build.props @@ -0,0 +1,3 @@ + + + diff --git a/tools/Directory.Build.targets b/tools/Directory.Build.targets new file mode 100644 index 00000000..b5015998 --- /dev/null +++ b/tools/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/tools/MetadataValidator/MetadataValidator.csproj b/tools/MetadataValidator/MetadataValidator.csproj new file mode 100644 index 00000000..729e465f --- /dev/null +++ b/tools/MetadataValidator/MetadataValidator.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + preview + false + + + + + + + diff --git a/tools/MetadataValidator/Program.cs b/tools/MetadataValidator/Program.cs new file mode 100644 index 00000000..305af007 --- /dev/null +++ b/tools/MetadataValidator/Program.cs @@ -0,0 +1,174 @@ +using System; +using System.IO; +using System.Reflection.Metadata; +using System.Reflection.Metadata.Ecma335; +using System.Reflection.PortableExecutable; + +namespace MetadataValidator; + +class Program +{ + static int Main(string[] args) + { + if (args.Length == 0) + { + Console.WriteLine("Usage: MetadataValidator "); + return 1; + } + + var dllPath = args[0]; + if (!File.Exists(dllPath)) + { + Console.WriteLine($"Error: File not found: {dllPath}"); + return 1; + } + + Console.WriteLine($"Validating metadata for: {dllPath}"); + + try + { + using var stream = File.OpenRead(dllPath); + using var peReader = new PEReader(stream); + + if (!peReader.HasMetadata) + { + Console.WriteLine("Error: PE file does not contain metadata"); + return 1; + } + + var metadataReader = peReader.GetMetadataReader(); + + var errors = 0; + + // Validate TypeRefs + Console.WriteLine($"Validating {metadataReader.TypeReferences.Count} type references..."); + foreach (var typeRefHandle in metadataReader.TypeReferences) + { + try + { + var typeRef = metadataReader.GetTypeReference(typeRefHandle); + + // Try to get the name - this is where "coded rid out of range" errors occur + _ = metadataReader.GetString(typeRef.Name); + _ = metadataReader.GetString(typeRef.Namespace); + + // Validate the resolution scope + var resolutionScope = typeRef.ResolutionScope; + if (!resolutionScope.IsNil) + { + switch (resolutionScope.Kind) + { + case HandleKind.AssemblyReference: + var asmRef = metadataReader.GetAssemblyReference((AssemblyReferenceHandle)resolutionScope); + _ = metadataReader.GetString(asmRef.Name); + break; + case HandleKind.ModuleReference: + var modRef = metadataReader.GetModuleReference((ModuleReferenceHandle)resolutionScope); + _ = metadataReader.GetString(modRef.Name); + break; + case HandleKind.TypeReference: + // Nested type - recursively validate + break; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Error validating TypeRef 0x{MetadataTokens.GetToken(typeRefHandle):X8}: {ex.Message}"); + errors++; + } + } + + // Validate AssemblyRefs + Console.WriteLine($"Validating {metadataReader.AssemblyReferences.Count} assembly references..."); + foreach (var asmRefHandle in metadataReader.AssemblyReferences) + { + try + { + var asmRef = metadataReader.GetAssemblyReference(asmRefHandle); + _ = metadataReader.GetString(asmRef.Name); + + if (!asmRef.Culture.IsNil) + { + _ = metadataReader.GetString(asmRef.Culture); + } + + if (!asmRef.PublicKeyOrToken.IsNil) + { + _ = metadataReader.GetBlobBytes(asmRef.PublicKeyOrToken); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error validating AssemblyRef 0x{MetadataTokens.GetToken(asmRefHandle):X8}: {ex.Message}"); + errors++; + } + } + + // Validate MemberRefs + Console.WriteLine($"Validating {metadataReader.MemberReferences.Count} member references..."); + foreach (var memberRefHandle in metadataReader.MemberReferences) + { + try + { + var memberRef = metadataReader.GetMemberReference(memberRefHandle); + _ = metadataReader.GetString(memberRef.Name); + + // Validate parent + var parent = memberRef.Parent; + if (!parent.IsNil) + { + switch (parent.Kind) + { + case HandleKind.TypeReference: + var typeRef = metadataReader.GetTypeReference((TypeReferenceHandle)parent); + _ = metadataReader.GetString(typeRef.Name); + break; + case HandleKind.TypeDefinition: + var typeDef = metadataReader.GetTypeDefinition((TypeDefinitionHandle)parent); + _ = metadataReader.GetString(typeDef.Name); + break; + case HandleKind.ModuleReference: + var modRef = metadataReader.GetModuleReference((ModuleReferenceHandle)parent); + _ = metadataReader.GetString(modRef.Name); + break; + case HandleKind.MethodDefinition: + var methodDef = metadataReader.GetMethodDefinition((MethodDefinitionHandle)parent); + _ = metadataReader.GetString(methodDef.Name); + break; + case HandleKind.TypeSpecification: + // TypeSpec - skip for now + break; + } + } + + // Validate signature + if (!memberRef.Signature.IsNil) + { + _ = metadataReader.GetBlobBytes(memberRef.Signature); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error validating MemberRef 0x{MetadataTokens.GetToken(memberRefHandle):X8}: {ex.Message}"); + errors++; + } + } + + if (errors > 0) + { + Console.WriteLine($"\nValidation FAILED with {errors} error(s)"); + return 1; + } + + Console.WriteLine("\nValidation PASSED - No metadata errors detected"); + return 0; + } + catch (Exception ex) + { + Console.WriteLine($"Fatal error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); + return 1; + } + } +}