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; + } + } + } +}