Skip to content
Draft
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
19 changes: 19 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
10 changes: 10 additions & 0 deletions scripts/pack.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
79 changes: 79 additions & 0 deletions scripts/validate-build.ps1
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions tools/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<!-- Empty file to prevent parent Directory.Build.props from being imported -->
</Project>
3 changes: 3 additions & 0 deletions tools/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Project>
<!-- Empty file to prevent parent Directory.Build.targets from being imported -->
</Project>
14 changes: 14 additions & 0 deletions tools/MetadataValidator/MetadataValidator.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<LangVersion>preview</LangVersion>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Reflection.Metadata" Version="8.0.1" />
</ItemGroup>

</Project>
174 changes: 174 additions & 0 deletions tools/MetadataValidator/Program.cs
Original file line number Diff line number Diff line change
@@ -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 <path-to-dll>");
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;
}
}
}
Loading