diff --git a/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/AnotherClassLibrary.csproj b/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/AnotherClassLibrary.csproj
index 8bfbd192..6d46af7f 100644
--- a/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/AnotherClassLibrary.csproj
+++ b/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/AnotherClassLibrary.csproj
@@ -23,6 +23,9 @@
ADummyUserControl.xaml
+
+
+
diff --git a/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/ModuleInitializers/LibraryModuleInitializer.cs b/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/ModuleInitializers/LibraryModuleInitializer.cs
new file mode 100644
index 00000000..dc448624
--- /dev/null
+++ b/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/ModuleInitializers/LibraryModuleInitializer.cs
@@ -0,0 +1,35 @@
+//
+// Copyright (c) 2025 David Rettenbacher
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System;
+using System.Runtime.CompilerServices;
+
+namespace AnotherClassLibrary.ModuleInitializers
+{
+ public static class LibraryModuleInitializer
+ {
+ [ModuleInitializer]
+ public static void Initialize()
+ {
+ // some "complex" code to prove the ModuleInitializersRepackStep can handle it
+ var shouldInitialize = DateTime.Now > new DateTime(2000, 1, 1);
+ if (shouldInitialize)
+ {
+ MakeInitialized.Initialize();
+ }
+ }
+ }
+}
diff --git a/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/ModuleInitializers/MakeInitialized.cs b/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/ModuleInitializers/MakeInitialized.cs
new file mode 100644
index 00000000..e95ace63
--- /dev/null
+++ b/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/ModuleInitializers/MakeInitialized.cs
@@ -0,0 +1,35 @@
+//
+// Copyright (c) 2025 David Rettenbacher
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+namespace AnotherClassLibrary.ModuleInitializers
+{
+ public class MakeInitialized
+ {
+ public static bool IsInitialized { get; private set; }
+
+ public static int Counter { get;private set; }
+
+ public static void Initialize()
+ {
+ IsInitialized = true;
+
+ Counter = ClassLibrary.ModuleInitializers.MakeInitialized.Counter + 1;
+
+ // force an actual assembly dependency to ClassLibrary
+ ClassLibrary.DummyConverter dummy = new ClassLibrary.DummyConverter();
+ }
+ }
+}
diff --git a/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/ModuleInitializers/ModuleInitializerAttribute.cs b/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/ModuleInitializers/ModuleInitializerAttribute.cs
new file mode 100644
index 00000000..8164411b
--- /dev/null
+++ b/ILRepack.IntegrationTests/Scenarios/AnotherClassLibrary/ModuleInitializers/ModuleInitializerAttribute.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2025 David Rettenbacher
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+namespace System.Runtime.CompilerServices
+#pragma warning restore IDE0130 // Namespace does not match folder structure
+{
+ ///
+ /// Polyfill for compiler support
+ ///
+ [ExcludeFromCodeCoverage]
+ [DebuggerNonUserCode]
+ [AttributeUsage(AttributeTargets.Method, Inherited = false)]
+ internal sealed class ModuleInitializerAttribute : Attribute
+ {
+ }
+}
\ No newline at end of file
diff --git a/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ClassLibrary.csproj b/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ClassLibrary.csproj
index 1e72ad43..0a16da3d 100644
--- a/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ClassLibrary.csproj
+++ b/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ClassLibrary.csproj
@@ -42,6 +42,9 @@
DummyUserControl.xaml
+
+
+
True
True
diff --git a/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ModuleInitializers/LibraryModuleInitializer.cs b/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ModuleInitializers/LibraryModuleInitializer.cs
new file mode 100644
index 00000000..d650a4b4
--- /dev/null
+++ b/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ModuleInitializers/LibraryModuleInitializer.cs
@@ -0,0 +1,29 @@
+//
+// Copyright (c) 2025 David Rettenbacher
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Runtime.CompilerServices;
+
+namespace ClassLibrary.ModuleInitializers
+{
+ public static class LibraryModuleInitializer
+ {
+ [ModuleInitializer]
+ public static void Initialize()
+ {
+ MakeInitialized.Initialize();
+ }
+ }
+}
\ No newline at end of file
diff --git a/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ModuleInitializers/MakeInitialized.cs b/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ModuleInitializers/MakeInitialized.cs
new file mode 100644
index 00000000..47de4f4a
--- /dev/null
+++ b/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ModuleInitializers/MakeInitialized.cs
@@ -0,0 +1,32 @@
+//
+// Copyright (c) 2025 David Rettenbacher
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+namespace ClassLibrary.ModuleInitializers
+{
+ public class MakeInitialized
+ {
+ public static bool IsInitialized { get; private set; }
+
+ public static int Counter { get; private set; }
+
+ public static void Initialize()
+ {
+ Counter++;
+
+ IsInitialized = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ModuleInitializers/ModuleInitializerAttribute.cs b/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ModuleInitializers/ModuleInitializerAttribute.cs
new file mode 100644
index 00000000..8164411b
--- /dev/null
+++ b/ILRepack.IntegrationTests/Scenarios/ClassLibrary/ModuleInitializers/ModuleInitializerAttribute.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2025 David Rettenbacher
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+namespace System.Runtime.CompilerServices
+#pragma warning restore IDE0130 // Namespace does not match folder structure
+{
+ ///
+ /// Polyfill for compiler support
+ ///
+ [ExcludeFromCodeCoverage]
+ [DebuggerNonUserCode]
+ [AttributeUsage(AttributeTargets.Method, Inherited = false)]
+ internal sealed class ModuleInitializerAttribute : Attribute
+ {
+ }
+}
\ No newline at end of file
diff --git a/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/MainWindow.xaml.cs b/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/MainWindow.xaml.cs
index b9844514..14799738 100644
--- a/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/MainWindow.xaml.cs
+++ b/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/MainWindow.xaml.cs
@@ -1,4 +1,5 @@
-using System.Windows;
+using System;
+using System.Windows;
using AnotherClassLibrary;
namespace NestedLibraryUsageInXAML
@@ -13,6 +14,18 @@ public MainWindow()
private async void MainWindowLoaded(object sender, RoutedEventArgs e)
{
await new BclAsyncUsage().GetNumber();
+
+ if (!ClassLibrary.ModuleInitializers.MakeInitialized.IsInitialized &&
+ ClassLibrary.ModuleInitializers.MakeInitialized.Counter == 1)
+ throw new InvalidOperationException($"ModuleInitializer of '{nameof(ClassLibrary.ModuleInitializers.LibraryModuleInitializer)}' was not executed");
+
+ if (!AnotherClassLibrary.ModuleInitializers.MakeInitialized.IsInitialized &&
+ AnotherClassLibrary.ModuleInitializers.MakeInitialized.Counter == 2)
+ throw new InvalidOperationException($"ModuleInitializer of '{nameof(AnotherClassLibrary.ModuleInitializers.LibraryModuleInitializer)}' was not executed or not in right order");
+
+ if (Program.Counter != 3)
+ throw new InvalidOperationException($"ModuleInitializers of '{nameof(Program)}' were not executed or not in right order");
+
Close();
}
}
diff --git a/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/ModuleInitializers/ModuleInitializerAttribute.cs b/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/ModuleInitializers/ModuleInitializerAttribute.cs
new file mode 100644
index 00000000..8164411b
--- /dev/null
+++ b/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/ModuleInitializers/ModuleInitializerAttribute.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) 2025 David Rettenbacher
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+#pragma warning disable IDE0130 // Namespace does not match folder structure
+namespace System.Runtime.CompilerServices
+#pragma warning restore IDE0130 // Namespace does not match folder structure
+{
+ ///
+ /// Polyfill for compiler support
+ ///
+ [ExcludeFromCodeCoverage]
+ [DebuggerNonUserCode]
+ [AttributeUsage(AttributeTargets.Method, Inherited = false)]
+ internal sealed class ModuleInitializerAttribute : Attribute
+ {
+ }
+}
\ No newline at end of file
diff --git a/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/Program.cs b/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/Program.cs
index 558d42c2..ed4d667f 100644
--- a/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/Program.cs
+++ b/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/Program.cs
@@ -1,4 +1,5 @@
using System;
+using System.Runtime.CompilerServices;
using System.Windows;
namespace NestedLibraryUsageInXAML
@@ -19,5 +20,20 @@ public static int Main()
return 1;
}
}
+
+ internal static int Counter { get; private set; }
+
+ [ModuleInitializer]
+ internal static void TheInitializer()
+ {
+ Counter++;
+ Counter *= AnotherClassLibrary.ModuleInitializers.MakeInitialized.Counter;
+ }
+
+ [ModuleInitializer]
+ internal static void TheInitializer2()
+ {
+ Counter++;
+ }
}
}
diff --git a/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/Properties/launchSettings.json b/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/Properties/launchSettings.json
new file mode 100644
index 00000000..2320997d
--- /dev/null
+++ b/ILRepack.IntegrationTests/Scenarios/NestedLibraryUsageInXAML/Properties/launchSettings.json
@@ -0,0 +1,11 @@
+{
+ "profiles": {
+ "NestedLibraryUsageInXAML": {
+ "commandName": "Project"
+ },
+ "Merged": {
+ "commandName": "Executable",
+ "executablePath": "$(TargetDir)merged\\NestedLibraryUsageInXAML.exe"
+ }
+ }
+}
\ No newline at end of file
diff --git a/ILRepack/ILRepack.cs b/ILRepack/ILRepack.cs
index 12ad4283..3b768bc2 100644
--- a/ILRepack/ILRepack.cs
+++ b/ILRepack/ILRepack.cs
@@ -419,6 +419,7 @@ private void RepackCore(string tempOutputDirectory)
{
signingStep,
new ReferencesRepackStep(Logger, this, Options),
+ new ModuleInitializersRepackStep(Logger, this, Options),
new TypesRepackStep(Logger, this, _repackImporter, Options),
new ILLinkFileMergeStep(Logger, this, Options),
new ResourcesRepackStep(Logger, this, Options),
diff --git a/ILRepack/Steps/ModuleInitializersRepackStep.cs b/ILRepack/Steps/ModuleInitializersRepackStep.cs
new file mode 100644
index 00000000..385443e9
--- /dev/null
+++ b/ILRepack/Steps/ModuleInitializersRepackStep.cs
@@ -0,0 +1,205 @@
+//
+// Copyright (c) 2025 David Rettenbacher
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace ILRepacking.Steps
+{
+ internal class ModuleInitializersRepackStep : IRepackStep
+ {
+ private readonly ILogger _logger;
+ private readonly IRepackContext _repackContext;
+
+ public ModuleInitializersRepackStep(
+ ILogger logger,
+ IRepackContext repackContext,
+ RepackOptions repackOptions)
+ {
+ _logger = logger;
+ _repackContext = repackContext;
+ }
+
+ public void Perform()
+ {
+ RepackModuleInitializers();
+ }
+
+ private void RepackModuleInitializers()
+ {
+ _logger.Verbose("Processing module initializers");
+
+ var assemblies = _repackContext.OtherAssemblies
+ .Concat([_repackContext.PrimaryAssemblyDefinition])
+ .ToHashSet();
+
+ var orderedAssemblies = TopologicalSort(assemblies);
+
+ var modulesToMerge = orderedAssemblies // dependency-assemblies should be deep-first, so the call order is deep-first
+ .SelectMany(x => x.Modules);
+
+ MergeModuleInitializers(_repackContext.TargetAssemblyMainModule, modulesToMerge);
+ }
+
+ ///
+ /// Checks if there are other module initializers to call from the primary module initializer.
+ /// If that is the case, a new initializer is added which calls all found module initializers.
+ /// All found initializers are renamed to be unique while still conveying their origin.
+ ///
+ /// Target module which gets the new module initializer
+ /// Modules which should be scanned for module initializers.
+ private void MergeModuleInitializers(ModuleDefinition targetModule, IEnumerable modulesToMerge)
+ {
+ var anyModuleInitializersToMerge = modulesToMerge.Any(m =>
+ m.Types.Any(t =>
+ t.Name == "" &&
+ t.Methods.Any(m =>
+ m.IsStatic &&
+ m.Name == ".cctor")));
+ if (!anyModuleInitializersToMerge)
+ {
+ _logger.Verbose("- Found no module initializers to be merged - skip");
+ return;
+ }
+
+ var targetModuleType = targetModule.Types.FirstOrDefault(t => t.Name == "");
+ if (targetModuleType is null)
+ {
+ targetModuleType = new TypeDefinition(
+ "",
+ "",
+ TypeAttributes.NotPublic | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit,
+ targetModule.TypeSystem.Object
+ );
+ targetModule.Types.Add(targetModuleType);
+ }
+
+ var targetInitializer = new MethodDefinition(
+ ".cctor",
+ MethodAttributes.Private | MethodAttributes.Static | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
+ targetModule.TypeSystem.Void
+ );
+
+ var newIl = targetInitializer.Body.GetILProcessor();
+
+ foreach (var moduleToMerge in modulesToMerge)
+ {
+ // search inside the assembly the module initializer type ""
+ var type = moduleToMerge.Types.FirstOrDefault(t => t.Name == "");
+ if (type is null)
+ continue;
+
+ // search its static constructor (the module initializer method)
+ var subInitializer = type.Methods.FirstOrDefault(m => m.Name == ".cctor" && m.IsStatic);
+ if (subInitializer is null)
+ continue;
+
+ _logger.Verbose($"- Process module initializer of '{moduleToMerge.Assembly.Name.Name}'");
+
+ DemoteModuleInitializerMethodToNormalMethod(subInitializer);
+
+ var call = newIl.Create(OpCodes.Call, targetModule.ImportReference(subInitializer));
+ newIl.Append(call);
+ }
+
+ newIl.Append(newIl.Create(OpCodes.Ret));
+
+ targetModuleType.Methods.Add(targetInitializer);
+ }
+
+ private void DemoteModuleInitializerMethodToNormalMethod(MethodDefinition initializer)
+ {
+ var newName = $"{initializer.Module.Assembly.Name}_{Path.GetFileNameWithoutExtension(initializer.Module.Name)}_ModuleInitializer";
+ newName = newName.Replace(" ", "_").Replace("=", "_").Replace(",", "").Replace(".", "_");
+
+ _logger.Verbose($" - Rename module initializer of '{initializer.Module.Assembly.Name.Name}' to '{newName}'");
+
+ initializer.Name = newName;
+ initializer.IsSpecialName = false;
+ initializer.IsRuntimeSpecialName = false;
+ }
+
+ private List TopologicalSort(HashSet assemblies)
+ {
+ var loadedAssemblies = assemblies.ToDictionary(a => a.Name.Name); // Ensure quick lookup
+ var visited = new HashSet();
+ var deepFirstAssemblies = new List(assemblies.Count);
+
+ _logger.Verbose("- Sort dependencies");
+
+ foreach (var assembly in assemblies)
+ {
+ if (DepthFirstSearch(assembly))
+ {
+ break;
+ }
+ }
+
+ return deepFirstAssemblies;
+
+ bool DepthFirstSearch(AssemblyDefinition assembly)
+ {
+ if (!visited.Add(assembly)) // already visited
+ return false;
+
+ foreach (var reference in assembly.MainModule.AssemblyReferences)
+ {
+ if (!loadedAssemblies.TryGetValue(reference.Name, out var referencedAsm))
+ {
+ try
+ {
+ referencedAsm = _repackContext.GlobalAssemblyResolver.Resolve(reference);
+ loadedAssemblies[reference.Name] = referencedAsm;
+
+ _logger.Verbose($" - Loaded {reference.Name}");
+ }
+ catch
+ {
+ // noop
+ }
+
+ if (referencedAsm is null)
+ {
+ _logger.Verbose($"- Warning: Could not find {reference.Name}");
+ continue;
+ }
+ }
+
+ if (DepthFirstSearch(referencedAsm))
+ {
+ return true;
+ }
+ }
+
+ if (assemblies.Contains(assembly))
+ {
+ deepFirstAssemblies.Add(assembly);
+ }
+
+ // found all assemblies yet?
+ if (deepFirstAssemblies.Count == assemblies.Count)
+ {
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+}