From d4892f7183d7a91e87408e3225c4bebc1da0670f Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 6 Oct 2026 15:57:05 +0200 Subject: [PATCH 1/7] Add XML output for retained type maps in ILC and ILLink Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/tools/illink/data-formats.md | 50 ++++ docs/tools/illink/illink-options.md | 23 ++ .../Microsoft.NETCore.Native.targets | 4 +- .../Compiler/Compilation.cs | 10 + .../AnalyzedExternalTypeMapNode.cs | 2 + .../AnalyzedProxyTypeMapNode.cs | 3 + .../Compiler/TypeMapOutput.cs | 58 ++++ .../ILCompiler.Compiler.csproj | 1 + .../TestCases/TypeMapArtifactTests.cs | 89 ++++++ .../aot/ILCompiler/ILCompilerRootCommand.cs | 3 + src/coreclr/tools/aot/ILCompiler/Program.cs | 16 ++ .../src/ILLink.Shared/SharedStrings.resx | 6 + .../src/ILLink.Shared/TypeMapXmlWriter.cs | 115 ++++++++ .../build/Microsoft.NET.ILLink.targets | 7 +- .../src/linker/Linker.Steps/MarkStep.cs | 1 + .../src/linker/Linker.Steps/OutputStep.cs | 18 ++ .../Linker.Steps/RootAssemblyInputStep.cs | 5 + src/tools/illink/src/linker/Linker/Driver.cs | 11 + .../illink/src/linker/Linker/LinkContext.cs | 6 + .../src/linker/Linker/TypeMapHandler.cs | 5 + .../illink/src/linker/Linker/TypeMapOutput.cs | 127 +++++++++ .../Reflection.IndividualTests.g.cs | 19 ++ .../ILLink.Tasks.Tests.csproj | 7 + .../ILLink.Tasks.Tests/TypeMapFileTests.cs | 263 +++++++++++++++++ .../Dependencies/TypeMapOutputLibrary.cs | 12 + .../Individual/CanOutputTypeMaps.cs | 76 +++++ .../TestCases/IndividualTests.cs | 67 +++++ .../TypeMapOutputTests.cs | 265 ++++++++++++++++++ 28 files changed, 1267 insertions(+), 2 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapOutput.cs create mode 100644 src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TypeMapArtifactTests.cs create mode 100644 src/tools/illink/src/ILLink.Shared/TypeMapXmlWriter.cs create mode 100644 src/tools/illink/src/linker/Linker/TypeMapOutput.cs create mode 100644 src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Reflection.IndividualTests.g.cs create mode 100644 src/tools/illink/test/ILLink.Tasks.Tests/TypeMapFileTests.cs create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapOutputLibrary.cs create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Individual/CanOutputTypeMaps.cs create mode 100644 src/tools/illink/test/Trimming.Tests.Shared/TypeMapOutputTests.cs diff --git a/docs/tools/illink/data-formats.md b/docs/tools/illink/data-formats.md index 06490e3e3308d9..e654eeecc856e9 100644 --- a/docs/tools/illink/data-formats.md +++ b/docs/tools/illink/data-formats.md @@ -11,6 +11,56 @@ ILLink uses several data formats to control or influence the trimming process. T ## Output Data Formats - [Dependencies Trace](#dependencies-trace-format) +- [Type Maps](#type-map-output-format) + +## Type Map Output Format + +ILLink and NativeAOT ILC accept `--output-typemaps` to write a UTF-8 XML +sidecar describing their retained type maps. This is an opt-in intermediate build +artifact, independent of dependency tracing. It does not root any additional +types or maps. +The filename and location are derived from the producer output. In SDK builds, +the sidecar stays in the intermediate output directory; it is not published or +bundled with the application. + +```xml + + + + + + + + + + + + + +``` + +The root `typemaps` element declares format version `1`. +Each `group` identifies a universe by its type. Its optional `external` and +`proxy` sections contain the retained mappings of the corresponding kind. +An absent section means that map kind was not requested/retained. An empty +section represents a retained map with no reported entries. + +Every `entry` has the same two required attributes, `key` and `value`: + +| Map kind | `key` | `value` | +| --- | --- | --- | +| `external` | The original external string, such as a Java/JNI name. | The mapped managed type name. | +| `proxy` | The managed source type name. | The mapped proxy type name. | + +Managed type names (`group.type`, every `entry.value`, and proxy `entry.key`) +use reflection type-name syntax with simple assembly qualification. Unusual +metadata names are not guaranteed to round-trip through a reflection type-name +parser. + +External keys are preserved verbatim, without normalization. Keys containing +characters that XML 1.0 cannot represent cause artifact generation to fail. +Consumers should check the format version and only use artifacts from successful +builds. # Format Details diff --git a/docs/tools/illink/illink-options.md b/docs/tools/illink/illink-options.md index 1ed9928c08fc7f..8e1152e2b6e0f8 100644 --- a/docs/tools/illink/illink-options.md +++ b/docs/tools/illink/illink-options.md @@ -99,6 +99,29 @@ Example: `illink -d ../../libs -a Program.dll` +### Writing retained type maps + +`--output-typemaps` enables an XML sidecar containing the retained external +(string-to-type) and proxy (type-to-type) maps, grouped by universe. No report is +generated by default, and there is no separate report-path option. + +ILLink writes `.typemaps.xml` beside the trimmed assembly in its +output directory, including when the application is copied rather than trimmed. +NativeAOT ILC writes `.typemaps.xml` beside its native object. +For SDK builds, set `TypeMapGenerateXmlFile=true`. The XML remains an intermediate +artifact under `obj`: in the native intermediate directory for ILC, or the linked +assembly directory for ILLink. It is not copied to the publish directory or +included in a single-file bundle. + +The sidecar is an expected output of the trimming/compilation step. Deleting it +causes that step to run again. An unchanged build skips the producer, while an +actual producer invocation replaces the sidecar even when its contents match. +Consumers generating R8 rules should write those rules only when their contents +change, so unrelated managed-code changes do not force R8 work just because the +sidecar was rewritten. + +See the [type map output format](data-formats.md#type-map-output-format). + ### Excluding framework features One of the ways to reduce core assembly sizes is by removing framework capabilities. This diff --git a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets index 3124302b5dfe5e..2eb4a3b4f8c3e6 100644 --- a/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets +++ b/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets @@ -91,6 +91,7 @@ The .NET Foundation licenses this file to you under the MIT license. $(NativeIntermediateOutputPath)$(TargetName)$(ExportsFileExt) $(NativeObject) + <_IlcTypeMapXmlFile Condition="'$(TypeMapGenerateXmlFile)' == 'true' and '$(BuildingFrameworkLibrary)' != 'true'">$(NativeIntermediateOutputPath)$(TargetName).typemaps.xml IlcCompile @@ -270,6 +271,7 @@ The .NET Foundation licenses this file to you under the MIT license. + @@ -346,7 +348,7 @@ The .NET Foundation licenses this file to you under the MIT license. diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Compilation.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Compilation.cs index 9a2ffae5c102e3..7110f412608cfd 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Compilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Compilation.cs @@ -650,6 +650,16 @@ public void WriteDependencyLog(string fileName) } } + public void WriteTypeMapArtifact(string fileName) + { + using (FileStream output = new FileStream(fileName, FileMode.Create)) + { + var adapter = new TypeMapOutput(); + ILLink.Shared.TypeMapXmlWriter.WriteTypeMapsToStream(output, adapter.GetTypeMapGroups(_factory)); + output.Flush(); + } + } + public IEnumerable CompiledMethodBodies { get diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedExternalTypeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedExternalTypeMapNode.cs index 531d6a9713d893..6c28c00ca89d61 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedExternalTypeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedExternalTypeMapNode.cs @@ -14,6 +14,8 @@ internal sealed class AnalyzedExternalTypeMapNode(TypeDesc typeMapGroup, IReadOn { public TypeDesc TypeMapGroup => typeMapGroup; + public IReadOnlyDictionary Entries => entries; + public Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section section, INativeFormatTypeReferenceProvider externalReferences) { VertexHashtable typeMapHashTable = new(); diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedProxyTypeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedProxyTypeMapNode.cs index 77d4c629d0b032..66a3842dbdd3cc 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedProxyTypeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedProxyTypeMapNode.cs @@ -14,6 +14,9 @@ namespace ILCompiler.DependencyAnalysis internal sealed class AnalyzedProxyTypeMapNode(TypeDesc typeMapGroup, IReadOnlyDictionary entries) : SortableDependencyNode, IProxyTypeMapNode { public TypeDesc TypeMapGroup => typeMapGroup; + + public IReadOnlyDictionary Entries => entries; + public Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section section, INativeFormatTypeReferenceProvider externalReferences) { VertexHashtable typeMapHashTable = new(); diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapOutput.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapOutput.cs new file mode 100644 index 00000000000000..dd4353ae02035a --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapOutput.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +using ILCompiler.DependencyAnalysis; +using Internal.TypeSystem; + +using Map = ILLink.Shared.TypeMapXmlWriter.Map; + +namespace ILCompiler; + +internal sealed class TypeMapOutput +{ + private readonly CustomAttributeTypeNameFormatter _typeNameFormatter = new CustomAttributeTypeNameFormatter(); + + public SortedDictionary GetTypeMapGroups(NodeFactory factory) + { + var groups = new SortedDictionary(StringComparer.Ordinal); + + foreach (IExternalTypeMapNode original in factory.TypeMapManager.GetExternalTypeMaps()) + { + IExternalTypeMapNode node = original.ToAnalysisBasedNode(factory); + string name = GetName(node.TypeMapGroup); + var map = new Map(); + + if (node is AnalyzedExternalTypeMapNode analyzed) + { + foreach (KeyValuePair entry in analyzed.Entries) + map.Entries.Add((entry.Key, GetName(entry.Value))); + } + + groups[name] = (map, null); + } + + foreach (IProxyTypeMapNode original in factory.TypeMapManager.GetProxyTypeMaps()) + { + IProxyTypeMapNode node = original.ToAnalysisBasedNode(factory); + string name = GetName(node.TypeMapGroup); + var map = new Map(); + + if (node is AnalyzedProxyTypeMapNode analyzed) + { + foreach (KeyValuePair entry in analyzed.Entries) + map.Entries.Add((GetName(entry.Key), GetName(entry.Value))); + } + + groups.TryGetValue(name, out var maps); + maps.Proxy = map; + groups[name] = maps; + } + + return groups; + } + + private string GetName(TypeDesc type) => _typeNameFormatter.FormatName(type, options: true); +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index 292557eb3f62df..d6a0db15368c21 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -726,6 +726,7 @@ + diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TypeMapArtifactTests.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TypeMapArtifactTests.cs new file mode 100644 index 00000000000000..e8a9974e388b35 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TypeMapArtifactTests.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Mono.Linker.Tests.TestCasesRunner; +using Xunit; + +namespace Mono.Linker.Tests.TestCases; + +public class TypeMapArtifactTests +{ + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task CompilerProducesBuildArtifact(bool useScanner) + { + TrimmedTestCaseResult fixture = TypeMapOutputTests.CreateFixture(new TestRunner(new ObjectFactory())); + string outputDirectory = Path.Combine(fixture.Sandbox.OutputDirectory.ToString(), "native output"); + Directory.CreateDirectory(outputDirectory); + string objectPath = Path.Combine(outputDirectory, "test.o"); + string reportPath = Path.ChangeExtension(objectPath, ".typemaps.xml"); + + await RunCompiler(generateArtifact: false, expectSuccess: true); + Assert.Empty(Directory.EnumerateFiles(outputDirectory, "*typemap*.xml", SearchOption.AllDirectories)); + + await RunCompiler(generateArtifact: true, expectSuccess: true); + TypeMapOutputTests.CheckArtifact(reportPath, nativeAot: true); + + byte[] firstReport = File.ReadAllBytes(reportPath); + File.WriteAllText(reportPath, "stale output"); + await RunCompiler(generateArtifact: true, expectSuccess: true); + Assert.Equal(firstReport, File.ReadAllBytes(reportPath)); + + File.Delete(reportPath); + await RunCompiler(generateArtifact: true, expectSuccess: true); + Assert.Equal(firstReport, File.ReadAllBytes(reportPath)); + + TypeMapOutputTests.UseUnrepresentableKey(fixture); + await RunCompiler(generateArtifact: true, expectSuccess: false); + Assert.False(File.Exists(reportPath)); + + async Task RunCompiler(bool generateArtifact, bool expectSuccess) + { + string runtimeDirectory = (string)AppContext.GetData("Mono.Linker.Tests.RuntimeBinDirectory")!; + string compiler = Path.Combine(runtimeDirectory, "ilc", "ilc.dll"); + Assert.True(File.Exists(compiler), $"The built compiler was not found at '{compiler}'."); + var arguments = new List + { + fixture.InputAssemblyPath.ToString(), + $"-r:{Path.Combine(runtimeDirectory, "aotsdk", "*.dll")}", + $"-r:{Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll")}", + $"-r:{Path.Combine(fixture.Sandbox.InputDirectory.ToString(), "*.dll")}", + "--initassembly:System.Private.CoreLib", + "--initassembly:System.Private.StackTraceMetadata", + "--initassembly:System.Private.TypeLoader", + "--initassembly:System.Private.Reflection.Execution", + "--feature:System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization=false", + "--feature:System.Resources.ResourceManager.AllowCustomResourceTypes=false", + "--feature:System.Linq.Expressions.CanEmitObjectArrayDelegate=false", + "--feature:System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported=false", + "--feature:System.Diagnostics.Debugger.IsSupported=false", + "--feature:System.Text.Encoding.EnableUnsafeUTF7Encoding=false", + "--feature:System.Diagnostics.Tracing.EventSource.IsSupported=false", + "--feature:System.Globalization.Invariant=true", + "--feature:System.Resources.UseSystemResourceKeys=true", + "--scanreflection", + useScanner ? "-O" : "--noscan", + $"-o:{objectPath}" + }; + if (generateArtifact) + arguments.Add("--output-typemaps"); + (int exitCode, string diagnostics) = await TypeMapOutputTests.RunTool(compiler, outputDirectory, arguments); + if (expectSuccess) + { + Assert.True(exitCode == 0, $"Compiler failed with exit code {exitCode}.\n{diagnostics}"); + Assert.True(File.Exists(objectPath), "Compilation succeeded without producing its native object."); + } + else + { + Assert.True(exitCode != 0, "The compiler reported success despite failing to write the build artifact."); + Assert.Contains(reportPath, diagnostics, StringComparison.Ordinal); + } + } + } +} diff --git a/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs b/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs index 875bbeaf73b9e7..1bdfb9df110c48 100644 --- a/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs +++ b/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs @@ -186,6 +186,8 @@ internal sealed class ILCompilerRootCommand : RootCommand new("--disable-generated-code-heuristics") { Description = "Disable heuristics for detecting compiler-generated code" }; public Option TypeMapEntryAssembly { get; } = new("--typemap-entry-assembly") { Description = "Assembly name to use as entry point for TypeMap generation" }; + public Option TypeMapGenerateXmlFile { get; } = + new("--output-typemaps") { Description = "Write retained type maps to a .typemaps.xml sidecar beside the native object" }; public OptimizationMode OptimizationMode { get; private set; } public ParseResult Result; @@ -277,6 +279,7 @@ public ILCompilerRootCommand(string[] args) : base(".NET Native IL Compiler") Options.Add(UnmanagedEntryPointsAssemblies); Options.Add(DisableGeneratedCodeHeuristics); Options.Add(TypeMapEntryAssembly); + Options.Add(TypeMapGenerateXmlFile); this.SetAction(result => { diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index 34305bc9ebf504..fcf5fda1f270c2 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -746,6 +746,22 @@ static bool IsRelatedToInvalidInput(MethodDesc method) // Rename the temporary file to the final output file File.Move(tempOutputFilePath, outputFilePath, overwrite: true); + if (Get(_command.TypeMapGenerateXmlFile)) + { + string typeMapOutputFile = Path.ChangeExtension(outputFilePath, ".typemaps.xml"); + try + { + compilationResults.WriteTypeMapArtifact(typeMapOutputFile); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or InvalidOperationException or XmlException) + { + logger.LogError(new DiagnosticString(DiagnosticId.FailedToWriteOutput).GetMessage(typeMapOutputFile) + " " + e.Message, (int)DiagnosticId.FailedToWriteOutput); + if (File.Exists(typeMapOutputFile)) + File.Delete(typeMapOutputFile); + return 1; + } + } + return 0; } diff --git a/src/tools/illink/src/ILLink.Shared/SharedStrings.resx b/src/tools/illink/src/ILLink.Shared/SharedStrings.resx index 7f46e89eea556c..81602304012266 100644 --- a/src/tools/illink/src/ILLink.Shared/SharedStrings.resx +++ b/src/tools/illink/src/ILLink.Shared/SharedStrings.resx @@ -1119,6 +1119,12 @@ Type '{0}' must not contain signature variables to be used as a type map group. + + Cannot write type map output because attribute '{0}' has invalid constructor arguments. + + + Cannot serialize type '{0}' in type map output using the reflection type name format. + Avoid accessing Assembly file path when publishing as a single file diff --git a/src/tools/illink/src/ILLink.Shared/TypeMapXmlWriter.cs b/src/tools/illink/src/ILLink.Shared/TypeMapXmlWriter.cs new file mode 100644 index 00000000000000..87e558f92879b9 --- /dev/null +++ b/src/tools/illink/src/ILLink.Shared/TypeMapXmlWriter.cs @@ -0,0 +1,115 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Xml; + +#nullable enable + +namespace ILLink.Shared; + +internal sealed class TypeMapXmlWriter : IDisposable +{ + private readonly XmlWriter _writer; + private bool _done; + + public sealed class Map + { + public List<(string Key, string Value)> Entries { get; } = []; + } + + private TypeMapXmlWriter(XmlWriter writer) + { + _writer = writer; + + _writer.WriteStartDocument(); + _writer.WriteStartElement("typemaps"); + _writer.WriteAttributeString("version", "1"); + } + + // Reflection type identifiers escape a different set of characters than assembly display names. + public static string EscapeTypeName(string name) + { + var builder = new StringBuilder(); + foreach (char character in name) + { + if (character is '[' or ']' or '&' or '*' or ',' or '+' or '\\') + builder.Append('\\'); + builder.Append(character); + } + + return builder.ToString(); + } + + public static void WriteTypeMapsToStream(Stream output, SortedDictionary groups) + { + using XmlWriter xmlWriter = XmlWriter.Create(output, new XmlWriterSettings + { + Encoding = new UTF8Encoding(false), + Indent = true, + NewLineChars = "\n", + NewLineHandling = NewLineHandling.Entitize + }); + + using var writer = new TypeMapXmlWriter(xmlWriter); + writer.WriteTypeMaps(groups); + } + + private void WriteTypeMaps(SortedDictionary groups) + { + foreach (KeyValuePair group in groups) + { + _writer.WriteStartElement("group"); + _writer.WriteAttributeString("type", group.Key); + + if (group.Value.External is not null) + WriteMap(group.Value.External, "external"); + + if (group.Value.Proxy is not null) + WriteMap(group.Value.Proxy, "proxy"); + + _writer.WriteEndElement(); + } + } + + private void WriteMap(Map map, string mapKind) + { + _writer.WriteStartElement(mapKind); + + map.Entries.Sort(static (left, right) => + { + int result = StringComparer.Ordinal.Compare(left.Key, right.Key); + + return result != 0 ? result : StringComparer.Ordinal.Compare(left.Value, right.Value); + }); + + foreach (var (key, value) in map.Entries) + { + _writer.WriteStartElement("entry"); + _writer.WriteAttributeString("key", key); + _writer.WriteAttributeString("value", value); + _writer.WriteEndElement(); + } + + _writer.WriteEndElement(); + } + + public void Close() + { + if (!_done) + { + _done = true; + + _writer.WriteEndElement(); + _writer.WriteEndDocument(); + } + } + + void IDisposable.Dispose() + { + Close(); + } +} diff --git a/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets index fd8868d1967b61..502be9fd4f01e1 100644 --- a/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets +++ b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets @@ -85,6 +85,11 @@ Copyright (c) .NET Foundation. All rights reserved. <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --typemap-entry-assembly "$(TypeMapEntryAssembly)" + + <_TrimmerTypeMapXmlFile>$(IntermediateLinkDir)$(TargetName).typemaps.xml + <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --output-typemaps + + $(NoWarn);IL2121 @@ -141,7 +146,7 @@ Copyright (c) .NET Foundation. All rights reserved. + Outputs="$(_LinkSemaphore);$(_TrimmerTypeMapXmlFile)"> diff --git a/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs b/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs index f4ed5179199350..3db4fcefbd8a32 100644 --- a/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs +++ b/src/tools/illink/src/linker/Linker.Steps/MarkStep.cs @@ -269,6 +269,7 @@ protected virtual void Initialize() startingAssembly ??= Annotations.GetEntryPointAssembly(); _typeMapHandler.Initialize(Context, this, startingAssembly); + Context.TypeMapHandler = _typeMapHandler; ProcessMarkedPending(); } diff --git a/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs b/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs index 779fbcdc3ec355..a3f9c65d4becb7 100644 --- a/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs +++ b/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs @@ -63,6 +63,24 @@ protected override void Process() protected override void EndProcess() { + if (Context.TypeMapGenerateXmlFile) + { + AssemblyDefinition? entryAssembly = Annotations.GetEntryPointAssembly() ?? Context.TypeMapOutputAssembly ?? Context.TypeMapHandler?.EntryPointAssembly; + string fileName = entryAssembly is null ? "typemaps.xml" + : Path.ChangeExtension(GetOriginalAssemblyFileInfo(entryAssembly).Name, ".typemaps.xml"); + string outputPath = Path.GetFullPath(Path.Combine(Context.OutputDirectory, fileName)); + try + { + TypeMapOutput.Write(Context, outputPath); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or InvalidOperationException or BadImageFormatException or System.Xml.XmlException) + { + if (File.Exists(outputPath)) + File.Delete(outputPath); + throw new LinkerFatalErrorException(MessageContainer.CreateErrorMessage(null, DiagnosticId.FailedToWriteOutput, outputPath), e); + } + } + if (Context.AssemblyListFile != null) { using (var w = File.CreateText(Context.AssemblyListFile)) diff --git a/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs b/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs index 1275e90939695c..03a2e88c345928 100644 --- a/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs +++ b/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs @@ -23,6 +23,11 @@ protected override void Process() if (assembly == null) return; +#if !ILTRIM + if (Context.TypeMapGenerateXmlFile && (rootMode == AssemblyRootMode.EntryPoint || Context.TypeMapOutputAssembly is null)) + Context.TypeMapOutputAssembly = assembly; +#endif + var di = new DependencyInfo(DependencyKind.RootAssembly, assembly); var origin = new MessageOrigin(assembly); diff --git a/src/tools/illink/src/linker/Linker/Driver.cs b/src/tools/illink/src/linker/Linker/Driver.cs index c4284a1ef8a3b1..a7b2d2c6060517 100644 --- a/src/tools/illink/src/linker/Linker/Driver.cs +++ b/src/tools/illink/src/linker/Linker/Driver.cs @@ -595,6 +595,14 @@ protected int SetupContext(ILogger? customLogger = null) continue; +#if !ILTRIM + case "--output-typemaps": + if (!GetBoolParam(token, value => context.TypeMapGenerateXmlFile = value)) + return -1; + + continue; +#endif + case "--link-attributes": if (arguments.Count < 1) { @@ -1546,6 +1554,9 @@ static void Usage() Console.WriteLine(" --ignore-descriptors Skips reading embedded descriptors (short -z). Defaults to false"); Console.WriteLine(" --skip-unresolved Ignore unresolved types, methods, and assemblies. Defaults to true"); Console.WriteLine(" --output-pinvokes PATH Output a JSON file with all modules and entry points of the P/Invokes found"); +#if !ILTRIM + Console.WriteLine(" --output-typemaps Write retained type maps to a .typemaps.xml sidecar in the output directory"); +#endif Console.WriteLine(" --verbose Log messages indicating progress and warnings"); Console.WriteLine(" --nowarn WARN Disable specific warning messages"); Console.WriteLine(" --warn VERSION Only print out warnings with version <= VERSION. Defaults to '9999'"); diff --git a/src/tools/illink/src/linker/Linker/LinkContext.cs b/src/tools/illink/src/linker/Linker/LinkContext.cs index 376d9fcf7e2b31..85fc3a84bbd10a 100644 --- a/src/tools/illink/src/linker/Linker/LinkContext.cs +++ b/src/tools/illink/src/linker/Linker/LinkContext.cs @@ -141,6 +141,12 @@ public Pipeline Pipeline public string? TypeMapEntryAssembly { get; set; } +#if !ILTRIM + internal bool TypeMapGenerateXmlFile { get; set; } + internal AssemblyDefinition? TypeMapOutputAssembly { get; set; } + internal TypeMapHandler? TypeMapHandler { get; set; } +#endif + /// /// Option to not special case EventSource. /// Currently, values are hard-coded and does not have a command line option to control diff --git a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs index 7a7c3afd94025a..5431c25b84998f 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapHandler.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapHandler.cs @@ -42,6 +42,10 @@ sealed class TypeMapHandler HashSet _referencedExternalTypeMaps = null!; HashSet _referencedProxyTypeMaps = null!; + internal AssemblyDefinition? EntryPointAssembly { get; private set; } + internal IEnumerable ExternalTypeMapGroups => _referencedExternalTypeMaps; + internal IEnumerable ProxyTypeMapGroups => _referencedProxyTypeMaps; + public TypeMapHandler() { } @@ -57,6 +61,7 @@ public void Initialize(LinkContext context, MarkStep markStep, AssemblyDefinitio { _context = context; _markStep = markStep; + EntryPointAssembly = entryPointAssembly; var typeReferenceEqualityComparer = new TypeReferenceEqualityComparer(context); _pendingExternalTypeMapEntries = new(typeReferenceEqualityComparer); _pendingProxyTypeMapEntries = new(typeReferenceEqualityComparer); diff --git a/src/tools/illink/src/linker/Linker/TypeMapOutput.cs b/src/tools/illink/src/linker/Linker/TypeMapOutput.cs new file mode 100644 index 00000000000000..a6669116ae4b88 --- /dev/null +++ b/src/tools/illink/src/linker/Linker/TypeMapOutput.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using ILLink.Shared; +using Mono.Cecil; +using AssemblyNameInfo = System.Reflection.Metadata.AssemblyNameInfo; +using TypeName = System.Reflection.Metadata.TypeName; +using Map = ILLink.Shared.TypeMapXmlWriter.Map; + +namespace Mono.Linker; + +internal static class TypeMapOutput +{ + public static void Write(LinkContext context, string fileName) + { + Debug.Assert(context.TypeMapHandler is not null); + TypeMapHandler handler = context.TypeMapHandler; + var groups = new SortedDictionary(StringComparer.Ordinal); + + foreach (TypeReference group in handler.ExternalTypeMapGroups) + groups[GetTypeName(group).AssemblyQualifiedName] = (GetMap(group, proxy: false), null); + + foreach (TypeReference group in handler.ProxyTypeMapGroups) + { + string name = GetTypeName(group).AssemblyQualifiedName; + groups.TryGetValue(name, out var maps); + maps.Proxy = GetMap(group, proxy: true); + groups[name] = maps; + } + + using (FileStream output = new FileStream(fileName, FileMode.Create)) + { + TypeMapXmlWriter.WriteTypeMapsToStream(output, groups); + output.Flush(); + } + + Map GetMap(TypeReference groupType, bool proxy) + { + var map = new Map(); + + if ((handler.EntryPointAssembly ?? context.TypeMapOutputAssembly) is not AssemblyDefinition entryPoint) + return map; + + var visited = new HashSet(); + var pending = new Queue(); + pending.Enqueue(entryPoint); + + while (pending.Count > 0) + { + AssemblyDefinition assembly = pending.Dequeue(); + if (!visited.Add(assembly) || + context.Annotations.GetAction(assembly) is not (AssemblyAction.Link or AssemblyAction.Save or AssemblyAction.Copy or AssemblyAction.AddBypassNGen)) + continue; + + foreach (CustomAttribute attribute in assembly.CustomAttributes) + { + if (attribute.AttributeType is not GenericInstanceType + { + Namespace: "System.Runtime.InteropServices", + GenericArguments: [TypeReference attributeGroup] + } || + !TypeReferenceEqualityComparer.AreEqual(groupType, attributeGroup, context)) + continue; + + switch (attribute.AttributeType.Name) + { + case "TypeMapAssemblyTargetAttribute`1": + if (attribute.ConstructorArguments is not [{ Value: string targetAssemblyName }]) + throw new BadImageFormatException(string.Format(SharedStrings.TypeMapOutputInvalidAttribute, attribute)); + if (context.TryResolve(AssemblyNameReference.Parse(targetAssemblyName)) is AssemblyDefinition targetAssembly) + pending.Enqueue(targetAssembly); + break; + + case "TypeMapAttribute`1" when !proxy: + if (attribute.ConstructorArguments.Count is not (2 or 3) || + attribute.ConstructorArguments is not [{ Value: string key }, { Value: TypeReference type }, ..]) + throw new BadImageFormatException(string.Format(SharedStrings.TypeMapOutputInvalidAttribute, attribute)); + map.Entries.Add((key, GetTypeName(type).AssemblyQualifiedName)); + break; + + case "TypeMapAssociationAttribute`1" when proxy: + if (attribute.ConstructorArguments is not [{ Value: TypeReference source }, { Value: TypeReference target }]) + throw new BadImageFormatException(string.Format(SharedStrings.TypeMapOutputInvalidAttribute, attribute)); + map.Entries.Add((GetTypeName(source).AssemblyQualifiedName, GetTypeName(target).AssemblyQualifiedName)); + break; + } + } + } + + return map; + } + + TypeName GetTypeName(TypeReference type) => type switch + { + ArrayType array => array.IsVector + ? GetTypeName(array.ElementType).MakeSZArrayTypeName() + : GetTypeName(array.ElementType).MakeArrayTypeName(array.Rank), + PointerType pointer => GetTypeName(pointer.ElementType).MakePointerTypeName(), + ByReferenceType byRef => GetTypeName(byRef.ElementType).MakeByRefTypeName(), + GenericInstanceType instance => GetTypeName(instance.ElementType) + .MakeGenericTypeName(instance.GenericArguments.Select(GetTypeName).ToImmutableArray()), + TypeSpecification or GenericParameter => throw new NotSupportedException(string.Format(SharedStrings.TypeMapOutputUnsupportedType, type.FullName)), + _ => GetSimpleTypeName(context.TryResolve(type) ?? type) + }; + + TypeName GetSimpleTypeName(TypeReference type) + { + var name = new StringBuilder(); + if (type.DeclaringType is TypeReference declaringType) + name.Append(GetTypeName(declaringType).FullName).Append('+'); + else if (type.Namespace.Length > 0) + name.Append(TypeMapXmlWriter.EscapeTypeName(type.Namespace)).Append('.'); + name.Append(TypeMapXmlWriter.EscapeTypeName(type.Name)); + string assemblyName = type.Scope is AssemblyNameReference assembly + ? assembly.Name : type.Module.Assembly.Name.Name; + + return TypeName.Parse(name.ToString()).WithAssemblyName(new AssemblyNameInfo(assemblyName)); + } + } +} diff --git a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Reflection.IndividualTests.g.cs b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Reflection.IndividualTests.g.cs new file mode 100644 index 00000000000000..c59d81c3eeb03c --- /dev/null +++ b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Reflection.IndividualTests.g.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using Xunit; + +namespace ILLink.RoslynAnalyzer.Tests.Reflection +{ + public sealed partial class IndividualTests : LinkerTestBase + { + + protected override string TestSuiteName => "Reflection.Individual"; + + [Fact] + public Task CanOutputTypeMaps() + { + return RunTest(allowMissingWarnings: true); + } + + } +} diff --git a/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.csproj b/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.csproj index 5f3daab5629a06..5d43a05601fbf8 100644 --- a/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.csproj +++ b/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.csproj @@ -9,4 +9,11 @@ + + + + + + + diff --git a/src/tools/illink/test/ILLink.Tasks.Tests/TypeMapFileTests.cs b/src/tools/illink/test/ILLink.Tasks.Tests/TypeMapFileTests.cs new file mode 100644 index 00000000000000..ccc1f63dd2fd84 --- /dev/null +++ b/src/tools/illink/test/ILLink.Tasks.Tests/TypeMapFileTests.cs @@ -0,0 +1,263 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace ILLink.Tasks.Tests; + +public class TypeMapFileTests +{ + [Theory] + [InlineData("full", true)] + [InlineData("partial", true)] + [InlineData("partial", false)] + public async Task SDKProducesIncrementalTypeMapSidecar(string trimMode, bool isTrimmable) + { + string directory = Path.Combine(Path.GetTempPath(), nameof(TypeMapFileTests), Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(directory); + try + { + string input = Path.Combine(directory, "App.dll"); + string linkedDirectory = Path.Combine(directory, "linked"); + string sidecar = Path.Combine(linkedDirectory, "App.typemaps.xml"); + string publishedSidecar = Path.Combine(directory, "publish", "App.typemaps.xml"); + string project = Path.Combine(directory, "App.proj"); + string tasksAssembly = (string)AppContext.GetData("ILLink.Tasks.Tests.ILLinkTasksAssembly"); + string msbuild = (string)AppContext.GetData("ILLink.Tasks.Tests.MSBuildPath"); + string sdkTasksAssembly = (string)AppContext.GetData("ILLink.Tasks.Tests.SDKTasksAssembly"); + string corelib = typeof(object).Assembly.Location; + await CreateApplication(input, corelib, "kept"); + new XElement("Project", + new XElement("UsingTask", new XAttribute("TaskName", "NETSdkInformation"), new XAttribute("AssemblyFile", sdkTasksAssembly)), + new XElement("UsingTask", new XAttribute("TaskName", "NETSdkError"), new XAttribute("AssemblyFile", sdkTasksAssembly)), + new XElement("PropertyGroup", + new XElement("PublishTrimmed", "true"), + new XElement("TypeMapGenerateXmlFile", "true"), + new XElement("TargetFrameworkIdentifier", ".NETCoreApp"), + new XElement("_TargetFrameworkVersionWithoutV", Environment.Version.ToString(2)), + new XElement("AssemblyName", "App"), + new XElement("TargetName", "App"), + new XElement("IntermediateOutputPath", Path.Combine(directory, "obj") + Path.DirectorySeparatorChar), + new XElement("IntermediateLinkDir", linkedDirectory + Path.DirectorySeparatorChar), + new XElement("ILLinkTasksAssembly", tasksAssembly), + new XElement("MicrosoftNETBuildTasksAssembly", sdkTasksAssembly), + new XElement("TrimMode", trimMode), + new XElement("_ExtraTrimmerArgs", "--ignore-descriptors true --ignore-substitutions true --ignore-link-attributes false --deterministic true")), + new XElement("ItemGroup", + new XElement("ManagedAssemblyToLink", new XAttribute("Include", input), new XElement("IsTrimmable", isTrimmable)), + new XElement("ManagedAssemblyToLink", new XAttribute("Include", corelib), new XElement("IsTrimmable", true)), + new XElement("ReferencePath", new XAttribute("Include", Path.Combine(Path.GetDirectoryName(corelib), "*.dll"))), + new XElement("TrimmerRootAssembly", new XAttribute("Include", "App"), new XElement("RootMode", "EntryPoint"))), + new XElement("Import", new XAttribute("Project", Path.Combine(Path.GetDirectoryName(msbuild), "Sdks", "Microsoft.NET.Sdk", "targets", "Microsoft.NET.Publish.targets"))), + new XElement("Import", new XAttribute("Project", Path.Combine(Path.GetDirectoryName(tasksAssembly), "build", "Microsoft.NET.ILLink.targets"))), + new XElement("Target", new XAttribute("Name", "_ComputeManagedAssemblyToLink")), + new XElement("Target", new XAttribute("Name", "PrepareForILLink")), + new XElement("Target", new XAttribute("Name", "_HandleFileConflictsForPublish")), + new XElement("Target", new XAttribute("Name", "Publish"), new XAttribute("DependsOnTargets", "ILLink;PrepareForBundle"), + new XElement("ItemGroup", + new XElement("_PublishedTypeMap", new XAttribute("Include", "@(ResolvedFileToPublish);@(FilesToBundle)"), + new XAttribute("Condition", "'%(Filename)%(Extension)' == 'App.typemaps.xml'"))), + new XElement("Error", new XAttribute("Condition", "'@(_PublishedTypeMap)' != ''"), + new XAttribute("Text", "The intermediate type map artifact must not be published or bundled.")), + new XElement("Copy", + new XAttribute("SourceFiles", "@(ResolvedFileToPublish)"), + new XAttribute("DestinationFiles", "@(ResolvedFileToPublish->'$(MSBuildProjectDirectory)/publish/%(RelativePath)')"), + new XAttribute("SkipUnchangedFiles", "true")))).Save(project); + + await Publish(); + Assert.True(File.Exists(Path.Combine(linkedDirectory, "App.dll"))); + Assert.False(File.Exists(Path.Combine(linkedDirectory, "typemaps.xml"))); + CheckArtifact("kept"); + byte[] original = File.ReadAllBytes(sidecar); + DateTime written = File.GetLastWriteTimeUtc(sidecar); + + await Publish(); + Assert.Equal(written, File.GetLastWriteTimeUtc(sidecar)); + Assert.Equal(original, File.ReadAllBytes(sidecar)); + + await Task.Delay(TimeSpan.FromSeconds(1)); + File.SetLastWriteTimeUtc(input, DateTime.UtcNow); + await Publish(); + Assert.True(File.GetLastWriteTimeUtc(sidecar) > written); + Assert.Equal(original, File.ReadAllBytes(sidecar)); + written = File.GetLastWriteTimeUtc(sidecar); + await Publish(); + Assert.Equal(written, File.GetLastWriteTimeUtc(sidecar)); + + File.Delete(sidecar); + await Publish(); + Assert.Equal(original, File.ReadAllBytes(sidecar)); + CheckArtifact("kept"); + written = File.GetLastWriteTimeUtc(sidecar); + await Publish(); + Assert.Equal(written, File.GetLastWriteTimeUtc(sidecar)); + + await Task.Delay(TimeSpan.FromSeconds(1)); + await CreateApplication(input, corelib, "changed"); + await Publish(); + CheckArtifact("changed"); + Assert.NotEqual(original, File.ReadAllBytes(sidecar)); + written = File.GetLastWriteTimeUtc(sidecar); + await Publish(); + Assert.Equal(written, File.GetLastWriteTimeUtc(sidecar)); + + foreach (bool includeAllContent in new[] { false, true }) + { + await Publish(publishSingleFile: true, includeAllContent: includeAllContent); + CheckArtifact("changed"); + } + + await CreateApplication(input, corelib, "invalid\0key"); + File.SetLastWriteTimeUtc(input, written.AddSeconds(-1)); + File.Delete(sidecar); + + await Publish(expectSuccess: false); + Assert.False(File.Exists(sidecar)); + await Publish(expectSuccess: false); + Assert.False(File.Exists(sidecar)); + + await CreateApplication(input, corelib, "changed"); + await Publish(); + CheckArtifact("changed"); + + await Publish(typeMapGenerateXmlFile: false, output: Path.Combine(directory, "disabled")); + Assert.Empty(Directory.EnumerateFiles(Path.Combine(directory, "disabled"), "*.typemaps.xml")); + + void CheckArtifact(string expectedKey) + { + Assert.False(File.Exists(publishedSidecar)); + XElement root = XElement.Load(sidecar); + Assert.Equal("typemaps", root.Name); + Assert.Equal("1", (string)root.Attribute("version")); + XElement group = Assert.Single(root.Elements("group")); + Assert.Equal("Example.Universe, App", (string)group.Attribute("type")); + Assert.Equal(new[] { "external", "proxy" }, group.Elements().Select(map => map.Name.LocalName)); + XElement external = Assert.Single(group.Elements("external")); + Assert.Equal(isTrimmable + ? new[] { (expectedKey, "Example.Target, App") } + : new[] { (expectedKey, "Example.Target, App"), ("trimmed", "Example.Unused, App") }, + external.Elements("entry").Select(entry => ((string)entry.Attribute("key"), (string)entry.Attribute("value")))); + XElement proxy = Assert.Single(group.Elements("proxy")); + Assert.Equal(isTrimmable + ? new[] { ("Example.Source, App", "Example.Target, App") } + : new[] { ("Example.Source, App", "Example.Target, App"), ("Example.Unused, App", "Example.Unused, App") }, + proxy.Elements("entry").Select(entry => ((string)entry.Attribute("key"), (string)entry.Attribute("value")))); + if (!isTrimmable) + Assert.Equal(File.ReadAllBytes(input), File.ReadAllBytes(Path.Combine(linkedDirectory, "App.dll"))); + } + + async Task Publish(bool typeMapGenerateXmlFile = true, string output = null, bool publishSingleFile = false, bool includeAllContent = false, bool expectSuccess = true) + { + var arguments = new List + { + msbuild, + project, + "-t:Publish", + "-nologo", + "-v:minimal", + $"-p:TypeMapGenerateXmlFile={typeMapGenerateXmlFile}", + $"-p:PublishSingleFile={publishSingleFile}", + $"-p:IncludeAllContentForSelfExtract={includeAllContent}" + }; + + if (output is not null) + arguments.Add($"-p:IntermediateLinkDir={output}{Path.DirectorySeparatorChar}"); + + (int exitCode, string diagnostics) = await RunTool(directory, arguments); + if (expectSuccess) + { + Assert.True(exitCode == 0, $"MSBuild failed with exit code {exitCode}.\n{diagnostics}"); + } + else + { + Assert.True(exitCode != 0, "MSBuild skipped the failed artifact generation and reported success."); + Assert.Contains(sidecar, diagnostics, StringComparison.Ordinal); + } + } + } + finally + { + Directory.Delete(directory, recursive: true); + } + } + + private static async Task CreateApplication(string path, string corelibPath, string key) + { + string source = Path.ChangeExtension(path, ".cs"); + await File.WriteAllTextAsync(source, $$""" + using System.Runtime.InteropServices; + + [assembly: TypeMap({{JsonSerializer.Serialize(key)}}, typeof(Example.Target))] + [assembly: TypeMap("trimmed", typeof(Example.Unused), typeof(Example.Unused))] + [assembly: TypeMapAssociation(typeof(Example.Source), typeof(Example.Target))] + [assembly: TypeMapAssociation(typeof(Example.Unused), typeof(Example.Unused))] + + namespace Example; + + public class Program + { + public static void Main() + { + _ = new Source(); + _ = TypeMapping.GetOrCreateExternalTypeMapping(); + _ = TypeMapping.GetOrCreateProxyTypeMapping(); + } + } + + public class Universe; + public class Target; + public class Source; + public class Unused; + """); + + string msbuild = (string)AppContext.GetData("ILLink.Tasks.Tests.MSBuildPath"); + string compiler = Path.Combine(Path.GetDirectoryName(msbuild), "Roslyn", "bincore", "csc.dll"); + (int exitCode, string diagnostics) = await RunTool(Path.GetDirectoryName(path), new[] + { + compiler, "-nologo", "-noconfig", "-nostdlib", "-target:exe", + $"-out:{path}", $"-reference:{corelibPath}", source + }); + Assert.True(exitCode == 0, $"Fixture compilation failed with exit code {exitCode}.\n{diagnostics}"); + } + + private static async Task<(int ExitCode, string Diagnostics)> RunTool(string workingDirectory, IEnumerable arguments) + { + string dotnet = (string)AppContext.GetData("ILLink.Tasks.Tests.DotNetHostPath"); + var startInfo = new ProcessStartInfo(dotnet) + { + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + startInfo.Environment["DOTNET_HOST_PATH"] = dotnet; + foreach (string argument in arguments) + startInfo.ArgumentList.Add(argument); + + using var process = Process.Start(startInfo); + Task stdout = process.StandardOutput.ReadToEndAsync(); + Task stderr = process.StandardError.ReadToEndAsync(); + using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + try + { + await process.WaitForExitAsync(timeout.Token); + } + catch (OperationCanceledException) when (timeout.IsCancellationRequested) + { + process.Kill(entireProcessTree: true); + await process.WaitForExitAsync(); + Assert.Fail($"Tool timed out.\n{await stdout}\n{await stderr}"); + } + + return (process.ExitCode, await stdout + await stderr); + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapOutputLibrary.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapOutputLibrary.cs new file mode 100644 index 00000000000000..553a55a02a05d3 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapOutputLibrary.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; + +[assembly: TypeMap("copied", typeof(TypeMapOutputTarget))] + +namespace Mono.Linker.Tests.Cases.Reflection.Dependencies; + +public class TypeMapOutputGroup; +public class TypeMapOutputTarget; diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Individual/CanOutputTypeMaps.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Individual/CanOutputTypeMaps.cs new file mode 100644 index 00000000000000..bd752627ad5b24 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Individual/CanOutputTypeMaps.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Mono.Linker.Tests.Cases.Expectations.Metadata; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; +using Mono.Linker.Tests.Cases.Reflection.Individual; + +[assembly: TypeMap("keep<&\"\t\r\n", typeof(CanOutputTypeMaps.Nested.Target))] +[assembly: TypeMap("conditional", typeof(CanOutputTypeMaps.Nested.Target), typeof(CanOutputTypeMaps.Source))] +[assembly: TypeMap("trimmed", typeof(CanOutputTypeMaps.Unused), typeof(CanOutputTypeMaps.Unused))] +[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(CanOutputTypeMaps.Nested.Generic))] +[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Unused), typeof(CanOutputTypeMaps.Unused))] +[assembly: TypeMap("array", typeof(int[,]))] +[assembly: TypeMap("byref", typeof(int))] +[assembly: TypeMap("example/Outer$Inner[0]", typeof(CanOutputTypeMaps.Nested.Target))] +[assembly: TypeMap("[Lexample/Outer$Inner;", typeof(CanOutputTypeMaps.Nested.Target[]))] +[assembly: TypeMap("", typeof(CanOutputTypeMaps.Nested.Generic))] +[assembly: TypeMap("pointer", typeof(int*))] +[assembly: TypeMap("unicode/\u0130\u4e2d\U0001f600", typeof(CanOutputTypeMaps.Nested.Target))] +[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(object))] +[assembly: TypeMap("unrequested", typeof(object))] +[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(object))] +[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Nested.Generic), typeof(CanOutputTypeMaps.Nested.Target))] +[assembly: TypeMap>("generic-universe", typeof(CanOutputTypeMaps.Nested.Generic))] +[assembly: TypeMap("trimmed", typeof(CanOutputTypeMaps.Unused), typeof(CanOutputTypeMaps.Unused))] +[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Unused), typeof(object))] +[assembly: TypeMap("unrequested", typeof(object))] +[assembly: TypeMap("duplicate", typeof(object))] +[assembly: TypeMap("duplicate", typeof(string))] +[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(object))] +[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(string))] +[assembly: TypeMapAssemblyTarget("Maps+Library")] + +namespace Mono.Linker.Tests.Cases.Reflection.Individual; + +[SetupCompileArgument("/unsafe")] +[SetupLinkerAction("link", "System.Private.CoreLib")] +[SetupLinkerArgument("--ignore-link-attributes", "false")] +[SetupCompileBefore("Maps+Library.dll", new[] { "../Dependencies/TypeMapOutputLibrary.cs" })] +[SetupLinkerAction("copy", "Maps+Library")] +public class CanOutputTypeMaps +{ + public static void Main() + { + Console.WriteLine(new Source()); + Console.WriteLine(new Nested.Generic()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping>()); + } + + public class Group; + public class ExternalOnly; + public class ProxyOnly; + public class Empty; + public class Unrequested; + public class Invalid; + public class GenericUniverse; + public class Source; + public class Unused; + + public class Nested + { + public class Target; + public class Generic; + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCases/IndividualTests.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCases/IndividualTests.cs index e77533cec7d8d7..260f0049a48932 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCases/IndividualTests.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCases/IndividualTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Runtime.Serialization.Json; +using System.Threading.Tasks; using System.Xml; using Mono.Cecil; using Mono.Linker.Tests.Cases.CommandLine.Mvid; @@ -37,6 +38,72 @@ public void CanSkipUnresolved() Assert.Fail($"The linked assembly is missing. Should have existed at {result.OutputAssemblyPath}"); } + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task CanOutputTypeMaps(bool absoluteOutputDirectory) + { + var runner = new TypeMapArtifactTestRunner(); + TrimmedTestCaseResult result = TypeMapOutputTests.CreateFixture(runner); + string outputDirectory = result.Sandbox.OutputDirectory.ToString(); + string reportPath = Path.Combine(outputDirectory, "test.typemaps.xml"); + await RunLinker(generateArtifact: false, expectSuccess: true); + byte[] assemblyWithoutReport = File.ReadAllBytes(result.OutputAssemblyPath.ToString()); + Assert.Empty(Directory.EnumerateFiles(result.Sandbox.OutputDirectory.ToString(), "*typemap*.xml", SearchOption.AllDirectories)); + + await RunLinker(generateArtifact: true, expectSuccess: true); + TypeMapOutputTests.CheckArtifact(reportPath, nativeAot: false); + Assert.Equal(assemblyWithoutReport, File.ReadAllBytes(result.OutputAssemblyPath.ToString())); + + byte[] firstReport = File.ReadAllBytes(reportPath); + File.WriteAllText(reportPath, "stale output"); + await RunLinker(generateArtifact: true, expectSuccess: true); + Assert.Equal(firstReport, File.ReadAllBytes(reportPath)); + + File.Delete(reportPath); + await RunLinker(generateArtifact: true, expectSuccess: true); + Assert.Equal(firstReport, File.ReadAllBytes(reportPath)); + + TypeMapOutputTests.UseUnrepresentableKey(result); + await RunLinker(generateArtifact: true, expectSuccess: false); + Assert.False(File.Exists(reportPath)); + + async Task RunLinker(bool generateArtifact, bool expectSuccess) + { + string[] arguments = (string[])runner.Arguments.Clone(); + int outputOption = Array.IndexOf(arguments, "-o"); + Assert.True(outputOption >= 0); + arguments[outputOption + 1] = absoluteOutputDirectory ? outputDirectory + : Path.GetRelativePath(result.Sandbox.InputDirectory.ToString(), outputDirectory); + IEnumerable invocation = generateArtifact ? arguments.Concat(new[] { "--output-typemaps" }) : arguments; + (int exitCode, string diagnostics) = await TypeMapOutputTests.RunTool(typeof(Driver).Assembly.Location, + result.Sandbox.InputDirectory.ToString(), invocation); + if (expectSuccess) + { + Assert.True(exitCode == 0, $"ILLink failed with exit code {exitCode}.\n{diagnostics}"); + Assert.True(File.Exists(result.OutputAssemblyPath.ToString())); + } + else + { + Assert.True(exitCode != 0, "ILLink reported success despite failing to write the build artifact."); + Assert.Contains(reportPath, diagnostics, StringComparison.Ordinal); + } + } + } + + private sealed class TypeMapArtifactTestRunner() : TestRunner(new ObjectFactory()) + { + public string[] Arguments { get; private set; } = []; + + protected override void AddTrimmingOptions(TestCaseSandbox sandbox, ManagedCompilationResult compilationResult, + TrimmingArgumentBuilder builder, TestCaseMetadataProvider metadataProvider) + { + base.AddTrimmingOptions(sandbox, compilationResult, builder, metadataProvider); + builder.AddAdditionalArgument("--deterministic", ["true"]); + Arguments = builder.Build(); + } + } + [Fact] public void CanOutputPInvokes() { diff --git a/src/tools/illink/test/Trimming.Tests.Shared/TypeMapOutputTests.cs b/src/tools/illink/test/Trimming.Tests.Shared/TypeMapOutputTests.cs new file mode 100644 index 00000000000000..d8c986e73536ef --- /dev/null +++ b/src/tools/illink/test/Trimming.Tests.Shared/TypeMapOutputTests.cs @@ -0,0 +1,265 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Mono.Cecil; +using Mono.Linker.Tests.Cases.Reflection.Individual; +using Mono.Linker.Tests.Extensions; +using Mono.Linker.Tests.TestCasesRunner; +using Xunit; +using TypeName = System.Reflection.Metadata.TypeName; + +#nullable enable + +namespace Mono.Linker.Tests.TestCases; + +internal static class TypeMapOutputTests +{ + private const string Prefix = "Mono.Linker.Tests.Cases.Reflection.Individual.CanOutputTypeMaps+"; + private const string LibraryPrefix = "Mono.Linker.Tests.Cases.Reflection.Dependencies."; + private const string Target = Prefix + @"Nested+Target\+With\,Reserved=""value"""; + + private sealed class Map + { + public List<(string Key, string Target)> Entries { get; } = []; + } + + public static async Task<(int ExitCode, string Diagnostics)> RunTool(string tool, string workingDirectory, IEnumerable arguments) + { + string dotnet = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") + ?? Path.GetFullPath(Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "..", "..", "..", + OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet")); + Assert.True(File.Exists(dotnet), $"The dotnet host was not found at '{dotnet}'."); + var startInfo = new ProcessStartInfo(dotnet) + { + WorkingDirectory = workingDirectory, + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + startInfo.ArgumentList.Add("exec"); + startInfo.ArgumentList.Add("--runtimeconfig"); + startInfo.ArgumentList.Add(Path.ChangeExtension(typeof(TypeMapOutputTests).Assembly.Location, ".runtimeconfig.json")); + startInfo.ArgumentList.Add(tool); + foreach (string argument in arguments) + startInfo.ArgumentList.Add(argument); + + using var process = new Process { StartInfo = startInfo }; + Assert.True(process.Start()); + Task stdout = process.StandardOutput.ReadToEndAsync(); + Task stderr = process.StandardError.ReadToEndAsync(); + using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(5)); + try + { + await process.WaitForExitAsync(timeout.Token); + } + catch (OperationCanceledException) when (timeout.IsCancellationRequested) + { + process.Kill(entireProcessTree: true); + await process.WaitForExitAsync(); + Assert.Fail($"Tool timed out.\n{await stdout}\n{await stderr}"); + } + + return (process.ExitCode, await stdout + await stderr); + } + + public static TrimmedTestCaseResult CreateFixture(TestRunner runner) + { + NPath casesRoot = TestDatabase.TestCasesRootDirectory; + var testCase = new TestCase(casesRoot.Combine("Reflection/Individual/CanOutputTypeMaps.cs"), casesRoot, + typeof(CanOutputTypeMaps).Assembly.Location.ToNPath()); + TrimmedTestCaseResult? result = runner.Run(testCase); + Assert.NotNull(result); + + // C# cannot declare identifiers containing these legal metadata characters. + using var assembly = AssemblyDefinition.ReadAssembly(result.InputAssemblyPath.ToString(), new ReaderParameters { InMemory = true }); + TypeDefinition declaringType = assembly.MainModule.GetType(typeof(CanOutputTypeMaps).FullName); + TypeDefinition nestedType = Assert.Single(declaringType.NestedTypes, type => type.Name == "Nested"); + TypeDefinition target = Assert.Single(nestedType.NestedTypes, type => type.Name == "Target"); + string originalName = target.FullName; + target.Name = "Target+With,Reserved=\"value\""; + foreach (CustomAttribute attribute in assembly.CustomAttributes) + { + // A byref typeof expression also requires constructing metadata rather than C# syntax. + if (attribute.AttributeType.Name == "TypeMapAttribute`1" && + attribute.ConstructorArguments[0].Value is string key && key == "byref") + { + CustomAttributeArgument argument = attribute.ConstructorArguments[1]; + attribute.ConstructorArguments[1] = new CustomAttributeArgument(argument.Type, new ByReferenceType((TypeReference)argument.Value)); + } + for (int i = 0; i < attribute.ConstructorArguments.Count; i++) + { + CustomAttributeArgument argument = attribute.ConstructorArguments[i]; + if (argument.Value is TypeReference reference && reference.FullName == originalName) + attribute.ConstructorArguments[i] = new CustomAttributeArgument(argument.Type, target); + else if (argument.Value is ArrayType array && array.ElementType.FullName == originalName) + attribute.ConstructorArguments[i] = new CustomAttributeArgument(argument.Type, new ArrayType(target)); + } + } + assembly.Write(result.InputAssemblyPath.ToString()); + + return result; + } + + public static void UseUnrepresentableKey(TrimmedTestCaseResult result) + { + using var assembly = AssemblyDefinition.ReadAssembly(result.InputAssemblyPath.ToString(), new ReaderParameters { InMemory = true }); + CustomAttribute attribute = Assert.Single(assembly.CustomAttributes, attribute => + attribute.AttributeType.Name == "TypeMapAttribute`1" && + attribute.ConstructorArguments[0].Value is string key && key == "conditional"); + CustomAttributeArgument keyArgument = attribute.ConstructorArguments[0]; + attribute.ConstructorArguments[0] = new CustomAttributeArgument(keyArgument.Type, "invalid\0key"); + assembly.Write(result.InputAssemblyPath.ToString()); + } + + public static void CheckArtifact(string path, bool nativeAot) + { + Dictionary> groups = ReadArtifact(path, nativeAot); + string target = nativeAot ? Prefix + "Nested+Target+With,Reserved=\"value\"" : Target; + Assert.Equal(new[] + { + LibraryPrefix + "TypeMapOutputGroup,Maps+Library", + Prefix + "Empty,test", + Prefix + "ExternalOnly,test", + Prefix + "GenericUniverse`1[[System.Int32[],System.Private.CoreLib]],test", + Prefix + "Group,test", + Prefix + "Invalid,test", + Prefix + "ProxyOnly,test" + }.Select(FormatName), groups.Keys); + Dictionary group = groups[FormatName(Prefix + "Group,test")]; + Assert.Equal(new[] { "external", "proxy" }, group.Keys); + Assert.Equal(new[] + { + ("conditional", target + ",test"), + ("keep<&\"\t\r\n", target + ",test") + }.Select(entry => (entry.Item1, FormatName(entry.Item2))), group["external"].Entries); + Assert.Equal(new[] + { + (Prefix + "Source,test", Prefix + "Nested+Generic`1[[System.Int32[],System.Private.CoreLib]],test") + }.Select(entry => (FormatName(entry.Item1), FormatName(entry.Item2))), group["proxy"].Entries); + + Dictionary externalOnly = groups[FormatName(Prefix + "ExternalOnly,test")]; + Assert.Equal(new[] { "external" }, externalOnly.Keys); + Assert.Equal(new[] + { + ("", Prefix + "Nested+Generic`1[[System.Int32[],System.Private.CoreLib]],test"), + ("[Lexample/Outer$Inner;", target + "[],test"), + ("array", "System.Int32[,],System.Private.CoreLib"), + ("byref", "System.Int32&,System.Private.CoreLib"), + ("example/Outer$Inner[0]", target + ",test"), + ("pointer", "System.Int32*,System.Private.CoreLib"), + ("unicode/\u0130\u4e2d\U0001f600", target + ",test") + }.Select(entry => (entry.Item1, FormatName(entry.Item2))), externalOnly["external"].Entries); + + Dictionary proxyOnly = groups[FormatName(Prefix + "ProxyOnly,test")]; + Assert.Equal(new[] { "proxy" }, proxyOnly.Keys); + Assert.Equal(new[] + { + (Prefix + "Nested+Generic`1[[System.Int32[],System.Private.CoreLib]],test", target + ",test"), + (Prefix + "Source,test", "System.Object,System.Private.CoreLib") + }.Select(entry => (FormatName(entry.Item1), FormatName(entry.Item2))), proxyOnly["proxy"].Entries); + + Dictionary genericUniverse = groups[FormatName(Prefix + "GenericUniverse`1[[System.Int32[],System.Private.CoreLib]],test")]; + Assert.Equal(new[] { "external" }, genericUniverse.Keys); + Assert.Equal(new[] + { + ("generic-universe", Prefix + "Nested+Generic`1[[System.String[,],System.Private.CoreLib]],test") + }.Select(entry => (entry.Item1, FormatName(entry.Item2))), genericUniverse["external"].Entries); + + Dictionary empty = groups[FormatName(Prefix + "Empty,test")]; + Assert.Equal(new[] { "external", "proxy" }, empty.Keys); + Assert.All(empty.Values, map => Assert.Empty(map.Entries)); + + Dictionary invalid = groups[FormatName(Prefix + "Invalid,test")]; + Assert.Equal(new[] { "external", "proxy" }, invalid.Keys); + Assert.Equal(nativeAot ? [] : new[] + { + ("duplicate", "System.Object,System.Private.CoreLib"), + ("duplicate", "System.String,System.Private.CoreLib") + }.Select(entry => (entry.Item1, FormatName(entry.Item2))), invalid["external"].Entries); + Assert.Equal(nativeAot ? [] : new[] + { + (Prefix + "Source,test", "System.Object,System.Private.CoreLib"), + (Prefix + "Source,test", "System.String,System.Private.CoreLib") + }.Select(entry => (FormatName(entry.Item1), FormatName(entry.Item2))), invalid["proxy"].Entries); + + Dictionary copied = groups[FormatName(LibraryPrefix + "TypeMapOutputGroup,Maps+Library")]; + Assert.Equal(new[] { "external" }, copied.Keys); + Assert.Equal(new[] + { + ("copied", LibraryPrefix + "TypeMapOutputTarget,Maps+Library") + }.Select(entry => (entry.Item1, FormatName(entry.Item2))), copied["external"].Entries); + + string FormatName(string name) => nativeAot ? name : CanonicalName(name); + } + + private static string CanonicalName(string name) => TypeName.Parse(name).AssemblyQualifiedName; + + private static Dictionary> ReadArtifact(string path, bool nativeAot) + { + using XmlReader reader = XmlReader.Create(path, new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit, + IgnoreWhitespace = true + }); + Assert.Equal(XmlNodeType.Element, reader.MoveToContent()); + Assert.Equal("typemaps", reader.Name); + Assert.Equal("1", reader.GetAttribute("version")); + var groups = new Dictionary>(StringComparer.Ordinal); + Dictionary? maps = null; + Map? map = null; + string? mapKind = null; + while (reader.Read()) + { + if (reader.NodeType != XmlNodeType.Element) + continue; + + switch (reader.Name) + { + case "group": + Assert.Equal(1, reader.Depth); + string? groupType = reader.GetAttribute("type"); + Assert.NotNull(groupType); + maps = new Dictionary(StringComparer.Ordinal); + map = null; + mapKind = null; + Assert.True(groups.TryAdd(FormatName(groupType), maps), $"Duplicate universe '{groupType}'."); + break; + case "external": + case "proxy": + Assert.Equal(2, reader.Depth); + Assert.Equal(0, reader.AttributeCount); + Assert.NotNull(maps); + map = new Map(); + mapKind = reader.Name; + Assert.True(maps.TryAdd(reader.Name, map), $"Duplicate '{reader.Name}' section."); + break; + case "entry": + Assert.Equal(3, reader.Depth); + Assert.Equal(2, reader.AttributeCount); + Assert.NotNull(map); + string? key = reader.GetAttribute("key"); + string? target = reader.GetAttribute("value"); + Assert.NotNull(key); + Assert.NotNull(target); + map.Entries.Add((mapKind == "proxy" ? FormatName(key) : key, FormatName(target))); + break; + default: + Assert.Fail($"Unexpected element '{reader.Name}'."); + break; + } + } + + return groups; + + string FormatName(string name) => nativeAot ? name : CanonicalName(name); + } +} From 175e5376e82cd0bc705bd15476f5696d72868d7d Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 9 Oct 2026 11:23:50 +0200 Subject: [PATCH 2/7] Simplify typemap XML output and share trimming coverage Integrate NativeAOT output with ObjectDumper, pass the configured output path to both tools, and use shared library trimming tests for runtime, XML, and build-lifecycle coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/tools/illink/data-formats.md | 23 +- docs/tools/illink/illink-options.md | 19 +- .../SupportFiles/Directory.Build.targets | 6 + eng/testing/linker/project.csproj.template | 1 + eng/testing/linker/trimmingTests.targets | 33 ++- .../Microsoft.NETCore.Native.targets | 5 +- .../Compiler/ExternalTypeMapObjectNode.cs | 4 + .../Common/Compiler/IExternalTypeMapNode.cs | 1 + .../Common/Compiler/IProxyTypeMapNode.cs | 1 + .../Common/Compiler/ProxyTypeMapObjectNode.cs | 4 + .../Compiler/Compilation.cs | 10 - .../AnalyzedExternalTypeMapNode.cs | 2 +- .../AnalyzedProxyTypeMapNode.cs | 2 +- .../DependencyAnalysis/ExternalTypeMapNode.cs | 6 + .../InvalidExternalTypeMapNode.cs | 2 + .../InvalidProxyTypeMapNode.cs | 2 + .../DependencyAnalysis/ProxyTypeMapNode.cs | 6 + .../Compiler/TypeMapObjectDumper.cs | 64 +++++ .../Compiler/TypeMapOutput.cs | 58 ---- .../ILCompiler.Compiler.csproj | 2 +- .../TestCases/TypeMapArtifactTests.cs | 89 ------ .../aot/ILCompiler/ILCompilerRootCommand.cs | 8 +- src/coreclr/tools/aot/ILCompiler/Program.cs | 20 +- .../Dependencies/TypeMapTestLibrary.cs | 21 ++ .../Dependencies/TypeMapTestLibrary.csproj | 10 + ...Runtime.InteropServices.TrimmingTests.proj | 8 +- .../tests/TrimmingTests/TypeMap.cs | 219 ++++++++++++++- .../tests/TrimmingTests/TypeMapTests.targets | 52 ++++ .../src/ILLink.Shared/SharedStrings.resx | 3 - .../src/ILLink.Shared/TypeMapXmlWriter.cs | 16 -- .../build/Microsoft.NET.ILLink.targets | 5 +- .../src/linker/Linker.Steps/OutputStep.cs | 19 +- .../Linker.Steps/RootAssemblyInputStep.cs | 2 +- src/tools/illink/src/linker/Linker/Driver.cs | 6 +- .../illink/src/linker/Linker/LinkContext.cs | 2 +- .../illink/src/linker/Linker/TypeMapOutput.cs | 18 +- .../Reflection.IndividualTests.g.cs | 19 -- .../ILLink.Tasks.Tests.csproj | 7 - .../ILLink.Tasks.Tests/TypeMapFileTests.cs | 263 ----------------- .../Dependencies/TypeMapOutputLibrary.cs | 12 - .../Individual/CanOutputTypeMaps.cs | 76 ----- .../TestCases/IndividualTests.cs | 67 ----- .../TypeMapOutputTests.cs | 265 ------------------ 43 files changed, 493 insertions(+), 965 deletions(-) create mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapObjectDumper.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapOutput.cs delete mode 100644 src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TypeMapArtifactTests.cs create mode 100644 src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.cs create mode 100644 src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj create mode 100644 src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMapTests.targets delete mode 100644 src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Reflection.IndividualTests.g.cs delete mode 100644 src/tools/illink/test/ILLink.Tasks.Tests/TypeMapFileTests.cs delete mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapOutputLibrary.cs delete mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Individual/CanOutputTypeMaps.cs delete mode 100644 src/tools/illink/test/Trimming.Tests.Shared/TypeMapOutputTests.cs diff --git a/docs/tools/illink/data-formats.md b/docs/tools/illink/data-formats.md index e654eeecc856e9..dbe754382d3a84 100644 --- a/docs/tools/illink/data-formats.md +++ b/docs/tools/illink/data-formats.md @@ -15,16 +15,16 @@ ILLink uses several data formats to control or influence the trimming process. T ## Type Map Output Format -ILLink and NativeAOT ILC accept `--output-typemaps` to write a UTF-8 XML +ILLink and NativeAOT ILC accept `--output-typemaps ` to write a UTF-8 XML sidecar describing their retained type maps. This is an opt-in intermediate build artifact, independent of dependency tracing. It does not root any additional types or maps. -The filename and location are derived from the producer output. In SDK builds, -the sidecar stays in the intermediate output directory; it is not published or -bundled with the application. +In SDK builds, `TypeMapXmlFile` specifies the output path and defaults to the +intermediate output directory. The sidecar is not published or bundled with the +application. ```xml - + @@ -39,7 +39,6 @@ bundled with the application. ``` -The root `typemaps` element declares format version `1`. Each `group` identifies a universe by its type. Its optional `external` and `proxy` sections contain the retained mappings of the corresponding kind. An absent section means that map kind was not requested/retained. An empty @@ -53,14 +52,10 @@ Every `entry` has the same two required attributes, `key` and `value`: | `proxy` | The managed source type name. | The mapped proxy type name. | Managed type names (`group.type`, every `entry.value`, and proxy `entry.key`) -use reflection type-name syntax with simple assembly qualification. Unusual -metadata names are not guaranteed to round-trip through a reflection type-name -parser. - -External keys are preserved verbatim, without normalization. Keys containing -characters that XML 1.0 cannot represent cause artifact generation to fail. -Consumers should check the format version and only use artifacts from successful -builds. +use reflection type-name syntax with simple assembly qualification. + +External keys are preserved verbatim, without normalization. +Consumers should only use artifacts from successful builds. # Format Details diff --git a/docs/tools/illink/illink-options.md b/docs/tools/illink/illink-options.md index 8e1152e2b6e0f8..25b051fb799e31 100644 --- a/docs/tools/illink/illink-options.md +++ b/docs/tools/illink/illink-options.md @@ -101,17 +101,16 @@ Example: ### Writing retained type maps -`--output-typemaps` enables an XML sidecar containing the retained external +`--output-typemaps ` writes an XML sidecar containing the retained external (string-to-type) and proxy (type-to-type) maps, grouped by universe. No report is -generated by default, and there is no separate report-path option. - -ILLink writes `.typemaps.xml` beside the trimmed assembly in its -output directory, including when the application is copied rather than trimmed. -NativeAOT ILC writes `.typemaps.xml` beside its native object. -For SDK builds, set `TypeMapGenerateXmlFile=true`. The XML remains an intermediate -artifact under `obj`: in the native intermediate directory for ILC, or the linked -assembly directory for ILLink. It is not copied to the publish directory or -included in a single-file bundle. +generated by default. + +Both tools write to the path supplied on the command line. +For SDK builds, set `TypeMapGenerateXmlFile=true`. `TypeMapXmlFile` specifies the +path passed to the tool and defaults to +`$(IntermediateOutputPath)$(TargetName).typemaps.xml`. +The default location is under `obj`. The XML is not copied to the publish +directory or included in a single-file bundle. The sidecar is an expected output of the trimming/compilation step. Deleting it causes that step to run again. An unchanged build skips the producer, while an diff --git a/eng/testing/linker/SupportFiles/Directory.Build.targets b/eng/testing/linker/SupportFiles/Directory.Build.targets index 9687763d2fdee6..640ca11f57c250 100644 --- a/eng/testing/linker/SupportFiles/Directory.Build.targets +++ b/eng/testing/linker/SupportFiles/Directory.Build.targets @@ -21,4 +21,10 @@ + + + + diff --git a/eng/testing/linker/project.csproj.template b/eng/testing/linker/project.csproj.template index e52a544f8087a0..8779078dc48229 100644 --- a/eng/testing/linker/project.csproj.template +++ b/eng/testing/linker/project.csproj.template @@ -86,6 +86,7 @@ + diff --git a/eng/testing/linker/trimmingTests.targets b/eng/testing/linker/trimmingTests.targets index eee97561702308..d6911e18e1ab77 100644 --- a/eng/testing/linker/trimmingTests.targets +++ b/eng/testing/linker/trimmingTests.targets @@ -53,7 +53,7 @@ <_projectDir>%(TestConsoleApps.ProjectDir)\ @@ -70,6 +70,16 @@ <_additionalProjectReferencesString>@(_additionalProjectReference, '%0a') + + <_testProjectReference Remove="@(_testProjectReference)" /> + <_testProjectReference Include="%(TestConsoleApps.AdditionalProjectReferences)" /> + + + + <_additionalProjectReferencesString>$(_additionalProjectReferencesString) + @(_testProjectReference->'<ProjectReference Include="%(FullPath)" SkipUseReferenceAssembly="true" />', '%0a') + + <_additionalProjectSourceFiles Include="%(TestConsoleApps.AdditionalSourceFiles)" /> @@ -104,6 +114,7 @@ .Replace('{CoreCLRBuildIntegrationDir}', '$(CoreCLRBuildIntegrationDir)') .Replace('{RuntimeHostConfigurationOptions}', '$(_runtimeHostConfigurationOptionsString)') .Replace('{AdditionalProjectReferences}', '$(_additionalProjectReferencesString)') + .Replace('{AdditionalTargets}', '%(TestConsoleApps.AdditionalTargets)') .Replace('{RepositoryEngineeringDir}', '$(RepositoryEngineeringDir)') .Replace('{MonoAOTCompilerDir}', '$(MonoAOTCompilerDir)') .Replace('{MonoProjectRoot}', '$(MonoProjectRoot)') @@ -143,9 +154,27 @@ DependsOnTargets="GenerateProjects" Returns="@(TestConsoleApps)" /> - + + <_testProjectToBuild Include="%(TestConsoleApps.AdditionalProjectReferences)" /> + + + + + + + + + $(NativeIntermediateOutputPath)$(TargetName)$(ExportsFileExt) $(NativeObject) - <_IlcTypeMapXmlFile Condition="'$(TypeMapGenerateXmlFile)' == 'true' and '$(BuildingFrameworkLibrary)' != 'true'">$(NativeIntermediateOutputPath)$(TargetName).typemaps.xml + $([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)', '$(TargetName).typemaps.xml')) + <_IlcTypeMapXmlFile Condition="'$(TypeMapGenerateXmlFile)' == 'true' and '$(BuildingFrameworkLibrary)' != 'true'">$(TypeMapXmlFile) IlcCompile @@ -271,7 +272,7 @@ The .NET Foundation licenses this file to you under the MIT license. - + diff --git a/src/coreclr/tools/Common/Compiler/ExternalTypeMapObjectNode.cs b/src/coreclr/tools/Common/Compiler/ExternalTypeMapObjectNode.cs index 93e0d89e62260b..a66a675749f39f 100644 --- a/src/coreclr/tools/Common/Compiler/ExternalTypeMapObjectNode.cs +++ b/src/coreclr/tools/Common/Compiler/ExternalTypeMapObjectNode.cs @@ -23,6 +23,10 @@ public ExternalTypeMapObjectNode(TypeMapManager manager, INativeFormatTypeRefere _externalReferences = externalReferences; } +#if !READYTORUN + internal IEnumerable GetTypeMaps() => _manager.GetExternalTypeMaps(); +#endif + public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) { // This node does not trigger generation of other nodes. diff --git a/src/coreclr/tools/Common/Compiler/IExternalTypeMapNode.cs b/src/coreclr/tools/Common/Compiler/IExternalTypeMapNode.cs index e4e571f4da091e..2979c93441beec 100644 --- a/src/coreclr/tools/Common/Compiler/IExternalTypeMapNode.cs +++ b/src/coreclr/tools/Common/Compiler/IExternalTypeMapNode.cs @@ -17,6 +17,7 @@ internal interface IExternalTypeMapNode : IDependencyNode, ISortableNode Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section section, INativeFormatTypeReferenceProvider externalReferences); #if !READYTORUN IExternalTypeMapNode ToAnalysisBasedNode(NodeFactory factory); + IEnumerable> GetEntries(NodeFactory factory); #endif } } diff --git a/src/coreclr/tools/Common/Compiler/IProxyTypeMapNode.cs b/src/coreclr/tools/Common/Compiler/IProxyTypeMapNode.cs index 13961085edd497..2b2a97bbbc4153 100644 --- a/src/coreclr/tools/Common/Compiler/IProxyTypeMapNode.cs +++ b/src/coreclr/tools/Common/Compiler/IProxyTypeMapNode.cs @@ -17,6 +17,7 @@ internal interface IProxyTypeMapNode : IDependencyNode, ISortableNode Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section section, INativeFormatTypeReferenceProvider externalReferences); #if !READYTORUN IProxyTypeMapNode ToAnalysisBasedNode(NodeFactory factory); + IEnumerable> GetEntries(NodeFactory factory); #endif } } diff --git a/src/coreclr/tools/Common/Compiler/ProxyTypeMapObjectNode.cs b/src/coreclr/tools/Common/Compiler/ProxyTypeMapObjectNode.cs index 2bb32ca498dc53..b6f2839a970918 100644 --- a/src/coreclr/tools/Common/Compiler/ProxyTypeMapObjectNode.cs +++ b/src/coreclr/tools/Common/Compiler/ProxyTypeMapObjectNode.cs @@ -24,6 +24,10 @@ public ProxyTypeMapObjectNode(TypeMapManager manager, INativeFormatTypeReference _externalReferences = externalReferences; } +#if !READYTORUN + internal IEnumerable GetTypeMaps() => _manager.GetProxyTypeMaps(); +#endif + public override ObjectData GetData(NodeFactory factory, bool relocsOnly = false) { // This node does not trigger generation of other nodes. diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Compilation.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Compilation.cs index 7110f412608cfd..9a2ffae5c102e3 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Compilation.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/Compilation.cs @@ -650,16 +650,6 @@ public void WriteDependencyLog(string fileName) } } - public void WriteTypeMapArtifact(string fileName) - { - using (FileStream output = new FileStream(fileName, FileMode.Create)) - { - var adapter = new TypeMapOutput(); - ILLink.Shared.TypeMapXmlWriter.WriteTypeMapsToStream(output, adapter.GetTypeMapGroups(_factory)); - output.Flush(); - } - } - public IEnumerable CompiledMethodBodies { get diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedExternalTypeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedExternalTypeMapNode.cs index 6c28c00ca89d61..4ebdeaf7056488 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedExternalTypeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedExternalTypeMapNode.cs @@ -14,7 +14,7 @@ internal sealed class AnalyzedExternalTypeMapNode(TypeDesc typeMapGroup, IReadOn { public TypeDesc TypeMapGroup => typeMapGroup; - public IReadOnlyDictionary Entries => entries; + public IEnumerable> GetEntries(NodeFactory factory) => entries; public Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section section, INativeFormatTypeReferenceProvider externalReferences) { diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedProxyTypeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedProxyTypeMapNode.cs index 66a3842dbdd3cc..cbef927b9fe04c 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedProxyTypeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/AnalyzedProxyTypeMapNode.cs @@ -15,7 +15,7 @@ internal sealed class AnalyzedProxyTypeMapNode(TypeDesc typeMapGroup, IReadOnlyD { public TypeDesc TypeMapGroup => typeMapGroup; - public IReadOnlyDictionary Entries => entries; + public IEnumerable> GetEntries(NodeFactory factory) => entries; public Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section section, INativeFormatTypeReferenceProvider externalReferences) { diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs index 70ed4b62fb8598..b88ef44893831b 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ExternalTypeMapNode.cs @@ -101,6 +101,12 @@ public override int CompareToImpl(ISortableNode other, CompilerComparer comparer private static IEETypeNode GetEffectiveTrimTargetType(NodeFactory factory, TypeDesc trimmingTargetType) => RuntimeConstructableTypeDependencies.GetEffectiveTrimTargetType(factory, trimmingTargetType, conditionConstructed: false); + public IEnumerable> GetEntries(NodeFactory factory) + { + foreach ((string name, IEETypeNode target) in GetMarkedEntries(factory)) + yield return new KeyValuePair(name, target.Type); + } + public Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section section, INativeFormatTypeReferenceProvider externalReferences) { VertexHashtable typeMapHashTable = new(); diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/InvalidExternalTypeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/InvalidExternalTypeMapNode.cs index fa527fdfae76c5..1549781fdcfeb9 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/InvalidExternalTypeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/InvalidExternalTypeMapNode.cs @@ -48,5 +48,7 @@ public Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section se } public IExternalTypeMapNode ToAnalysisBasedNode(NodeFactory factory) => new InvalidExternalTypeMapNode(TypeMapGroup, ThrowingMethodStub); + + public IEnumerable> GetEntries(NodeFactory factory) => []; } } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/InvalidProxyTypeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/InvalidProxyTypeMapNode.cs index f0035a2beb3e08..992ecfe9dd9643 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/InvalidProxyTypeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/InvalidProxyTypeMapNode.cs @@ -47,5 +47,7 @@ public Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section se } public IProxyTypeMapNode ToAnalysisBasedNode(NodeFactory factory) => new InvalidProxyTypeMapNode(TypeMapGroup, ThrowingMethodStub); + + public IEnumerable> GetEntries(NodeFactory factory) => []; } } diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ProxyTypeMapNode.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ProxyTypeMapNode.cs index 46059fb6484fe4..cbff28cfd86d84 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ProxyTypeMapNode.cs +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/DependencyAnalysis/ProxyTypeMapNode.cs @@ -78,6 +78,12 @@ public override IEnumerable GetConditionalStaticDep } } + public IEnumerable> GetEntries(NodeFactory factory) + { + foreach ((IEETypeNode key, IEETypeNode value) in GetMarkedEntries(factory)) + yield return new KeyValuePair(key.Type, value.Type); + } + public Vertex CreateTypeMap(NodeFactory factory, NativeWriter writer, Section section, INativeFormatTypeReferenceProvider externalReferences) { VertexHashtable typeMapHashTable = new VertexHashtable(); diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapObjectDumper.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapObjectDumper.cs new file mode 100644 index 00000000000000..792b23d63280d2 --- /dev/null +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapObjectDumper.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; + +using ILCompiler.DependencyAnalysis; +using Internal.TypeSystem; + +using Map = ILLink.Shared.TypeMapXmlWriter.Map; + +namespace ILCompiler; + +public sealed class TypeMapObjectDumper(string fileName) : ObjectDumper +{ + private readonly CustomAttributeTypeNameFormatter _typeNameFormatter = new CustomAttributeTypeNameFormatter(); + private readonly SortedDictionary _groups = new(StringComparer.Ordinal); + + internal override void Begin() + { + } + + protected override void DumpObjectNode(NodeFactory factory, ObjectNode node, ObjectNode.ObjectData objectData) + { + if (node is ExternalTypeMapObjectNode externalMaps) + { + foreach (IExternalTypeMapNode mapNode in externalMaps.GetTypeMaps()) + { + var map = new Map(); + foreach (KeyValuePair entry in mapNode.GetEntries(factory)) + map.Entries.Add((entry.Key, GetName(entry.Value))); + + string name = GetName(mapNode.TypeMapGroup); + _groups.TryGetValue(name, out var maps); + maps.External = map; + _groups[name] = maps; + } + } + + if (node is ProxyTypeMapObjectNode proxyMaps) + { + foreach (IProxyTypeMapNode mapNode in proxyMaps.GetTypeMaps()) + { + var map = new Map(); + foreach (KeyValuePair entry in mapNode.GetEntries(factory)) + map.Entries.Add((GetName(entry.Key), GetName(entry.Value))); + + string name = GetName(mapNode.TypeMapGroup); + _groups.TryGetValue(name, out var maps); + maps.Proxy = map; + _groups[name] = maps; + } + } + } + + internal override void End() + { + using FileStream output = new FileStream(fileName, FileMode.Create); + ILLink.Shared.TypeMapXmlWriter.WriteTypeMapsToStream(output, _groups); + } + + private string GetName(TypeDesc type) => _typeNameFormatter.FormatName(type, options: true); +} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapOutput.cs b/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapOutput.cs deleted file mode 100644 index dd4353ae02035a..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/Compiler/TypeMapOutput.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; - -using ILCompiler.DependencyAnalysis; -using Internal.TypeSystem; - -using Map = ILLink.Shared.TypeMapXmlWriter.Map; - -namespace ILCompiler; - -internal sealed class TypeMapOutput -{ - private readonly CustomAttributeTypeNameFormatter _typeNameFormatter = new CustomAttributeTypeNameFormatter(); - - public SortedDictionary GetTypeMapGroups(NodeFactory factory) - { - var groups = new SortedDictionary(StringComparer.Ordinal); - - foreach (IExternalTypeMapNode original in factory.TypeMapManager.GetExternalTypeMaps()) - { - IExternalTypeMapNode node = original.ToAnalysisBasedNode(factory); - string name = GetName(node.TypeMapGroup); - var map = new Map(); - - if (node is AnalyzedExternalTypeMapNode analyzed) - { - foreach (KeyValuePair entry in analyzed.Entries) - map.Entries.Add((entry.Key, GetName(entry.Value))); - } - - groups[name] = (map, null); - } - - foreach (IProxyTypeMapNode original in factory.TypeMapManager.GetProxyTypeMaps()) - { - IProxyTypeMapNode node = original.ToAnalysisBasedNode(factory); - string name = GetName(node.TypeMapGroup); - var map = new Map(); - - if (node is AnalyzedProxyTypeMapNode analyzed) - { - foreach (KeyValuePair entry in analyzed.Entries) - map.Entries.Add((GetName(entry.Key), GetName(entry.Value))); - } - - groups.TryGetValue(name, out var maps); - maps.Proxy = map; - groups[name] = maps; - } - - return groups; - } - - private string GetName(TypeDesc type) => _typeNameFormatter.FormatName(type, options: true); -} diff --git a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj index d6a0db15368c21..fcbd460edb3157 100644 --- a/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Compiler/ILCompiler.Compiler.csproj @@ -726,7 +726,7 @@ - + diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TypeMapArtifactTests.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TypeMapArtifactTests.cs deleted file mode 100644 index e8a9974e388b35..00000000000000 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCases/TypeMapArtifactTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading.Tasks; -using Mono.Linker.Tests.TestCasesRunner; -using Xunit; - -namespace Mono.Linker.Tests.TestCases; - -public class TypeMapArtifactTests -{ - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task CompilerProducesBuildArtifact(bool useScanner) - { - TrimmedTestCaseResult fixture = TypeMapOutputTests.CreateFixture(new TestRunner(new ObjectFactory())); - string outputDirectory = Path.Combine(fixture.Sandbox.OutputDirectory.ToString(), "native output"); - Directory.CreateDirectory(outputDirectory); - string objectPath = Path.Combine(outputDirectory, "test.o"); - string reportPath = Path.ChangeExtension(objectPath, ".typemaps.xml"); - - await RunCompiler(generateArtifact: false, expectSuccess: true); - Assert.Empty(Directory.EnumerateFiles(outputDirectory, "*typemap*.xml", SearchOption.AllDirectories)); - - await RunCompiler(generateArtifact: true, expectSuccess: true); - TypeMapOutputTests.CheckArtifact(reportPath, nativeAot: true); - - byte[] firstReport = File.ReadAllBytes(reportPath); - File.WriteAllText(reportPath, "stale output"); - await RunCompiler(generateArtifact: true, expectSuccess: true); - Assert.Equal(firstReport, File.ReadAllBytes(reportPath)); - - File.Delete(reportPath); - await RunCompiler(generateArtifact: true, expectSuccess: true); - Assert.Equal(firstReport, File.ReadAllBytes(reportPath)); - - TypeMapOutputTests.UseUnrepresentableKey(fixture); - await RunCompiler(generateArtifact: true, expectSuccess: false); - Assert.False(File.Exists(reportPath)); - - async Task RunCompiler(bool generateArtifact, bool expectSuccess) - { - string runtimeDirectory = (string)AppContext.GetData("Mono.Linker.Tests.RuntimeBinDirectory")!; - string compiler = Path.Combine(runtimeDirectory, "ilc", "ilc.dll"); - Assert.True(File.Exists(compiler), $"The built compiler was not found at '{compiler}'."); - var arguments = new List - { - fixture.InputAssemblyPath.ToString(), - $"-r:{Path.Combine(runtimeDirectory, "aotsdk", "*.dll")}", - $"-r:{Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll")}", - $"-r:{Path.Combine(fixture.Sandbox.InputDirectory.ToString(), "*.dll")}", - "--initassembly:System.Private.CoreLib", - "--initassembly:System.Private.StackTraceMetadata", - "--initassembly:System.Private.TypeLoader", - "--initassembly:System.Private.Reflection.Execution", - "--feature:System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization=false", - "--feature:System.Resources.ResourceManager.AllowCustomResourceTypes=false", - "--feature:System.Linq.Expressions.CanEmitObjectArrayDelegate=false", - "--feature:System.Runtime.CompilerServices.RuntimeFeature.IsDynamicCodeSupported=false", - "--feature:System.Diagnostics.Debugger.IsSupported=false", - "--feature:System.Text.Encoding.EnableUnsafeUTF7Encoding=false", - "--feature:System.Diagnostics.Tracing.EventSource.IsSupported=false", - "--feature:System.Globalization.Invariant=true", - "--feature:System.Resources.UseSystemResourceKeys=true", - "--scanreflection", - useScanner ? "-O" : "--noscan", - $"-o:{objectPath}" - }; - if (generateArtifact) - arguments.Add("--output-typemaps"); - (int exitCode, string diagnostics) = await TypeMapOutputTests.RunTool(compiler, outputDirectory, arguments); - if (expectSuccess) - { - Assert.True(exitCode == 0, $"Compiler failed with exit code {exitCode}.\n{diagnostics}"); - Assert.True(File.Exists(objectPath), "Compilation succeeded without producing its native object."); - } - else - { - Assert.True(exitCode != 0, "The compiler reported success despite failing to write the build artifact."); - Assert.Contains(reportPath, diagnostics, StringComparison.Ordinal); - } - } - } -} diff --git a/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs b/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs index 1bdfb9df110c48..bcb0d13f5cb35e 100644 --- a/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs +++ b/src/coreclr/tools/aot/ILCompiler/ILCompilerRootCommand.cs @@ -186,8 +186,8 @@ internal sealed class ILCompilerRootCommand : RootCommand new("--disable-generated-code-heuristics") { Description = "Disable heuristics for detecting compiler-generated code" }; public Option TypeMapEntryAssembly { get; } = new("--typemap-entry-assembly") { Description = "Assembly name to use as entry point for TypeMap generation" }; - public Option TypeMapGenerateXmlFile { get; } = - new("--output-typemaps") { Description = "Write retained type maps to a .typemaps.xml sidecar beside the native object" }; + public Option TypeMapFileName { get; } = + new("--output-typemaps") { Description = "Write retained type maps to the specified XML file" }; public OptimizationMode OptimizationMode { get; private set; } public ParseResult Result; @@ -279,7 +279,7 @@ public ILCompilerRootCommand(string[] args) : base(".NET Native IL Compiler") Options.Add(UnmanagedEntryPointsAssemblies); Options.Add(DisableGeneratedCodeHeuristics); Options.Add(TypeMapEntryAssembly); - Options.Add(TypeMapGenerateXmlFile); + Options.Add(TypeMapFileName); this.SetAction(result => { @@ -315,7 +315,7 @@ public ILCompilerRootCommand(string[] args) : base(".NET Native IL Compiler") #pragma warning disable CA1861 // Avoid constant arrays as arguments. Only executed once during the execution of the program. Helpers.MakeReproPackage(makeReproPath, result.GetValue(OutputFilePath), args, result, inputOptions : new[] { "-r", "--reference", "-m", "--mibc", "--rdxml", "--directpinvokelist", "--descriptor", "--satellite", "--order" }, - outputOptions : new[] { "-o", "--out", "--exportsfile", "--dgmllog", "--scandgmllog", "--mstat", "--sourcelink" }); + outputOptions : new[] { "-o", "--out", "--exportsfile", "--dgmllog", "--scandgmllog", "--mstat", "--sourcelink", "--output-typemaps" }); #pragma warning restore CA1861 // Avoid constant arrays as arguments } diff --git a/src/coreclr/tools/aot/ILCompiler/Program.cs b/src/coreclr/tools/aot/ILCompiler/Program.cs index fcf5fda1f270c2..a6295a578c1f59 100644 --- a/src/coreclr/tools/aot/ILCompiler/Program.cs +++ b/src/coreclr/tools/aot/ILCompiler/Program.cs @@ -649,6 +649,10 @@ void RunScanner() if (sourceLinkFileName != null) dumpers.Add(new SourceLinkWriter(sourceLinkFileName)); + string typeMapFileName = Get(_command.TypeMapFileName); + if (typeMapFileName != null) + dumpers.Add(new TypeMapObjectDumper(typeMapFileName)); + // Write to a temporary file and rename on success to avoid leaving partial files on failure string tempOutputFilePath = outputFilePath + ".tmp"; CompilationResults compilationResults = compilation.Compile(tempOutputFilePath, ObjectDumper.Compose(dumpers)); @@ -746,22 +750,6 @@ static bool IsRelatedToInvalidInput(MethodDesc method) // Rename the temporary file to the final output file File.Move(tempOutputFilePath, outputFilePath, overwrite: true); - if (Get(_command.TypeMapGenerateXmlFile)) - { - string typeMapOutputFile = Path.ChangeExtension(outputFilePath, ".typemaps.xml"); - try - { - compilationResults.WriteTypeMapArtifact(typeMapOutputFile); - } - catch (Exception e) when (e is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or InvalidOperationException or XmlException) - { - logger.LogError(new DiagnosticString(DiagnosticId.FailedToWriteOutput).GetMessage(typeMapOutputFile) + " " + e.Message, (int)DiagnosticId.FailedToWriteOutput); - if (File.Exists(typeMapOutputFile)) - File.Delete(typeMapOutputFile); - return 1; - } - } - return 0; } diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.cs b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.cs new file mode 100644 index 00000000000000..69a9149a99aff7 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; +using TypeMapTestLibrary; + +[assembly: TypeMap("dependency/Target", typeof(DependencyTarget))] +[assembly: TypeMap("dependency/Trimmed", typeof(UnusedTarget), typeof(UnusedSource))] +[assembly: TypeMapAssociation(typeof(DependencySource), typeof(DependencyProxy))] +[assembly: TypeMapAssociation(typeof(UnusedSource), typeof(UnusedTarget))] +[assembly: TypeMap("wrong/universe", typeof(DependencyTarget))] + +namespace TypeMapTestLibrary; + +public class DependencyUniverse; +public class OtherUniverse; +public class DependencySource; +public class DependencyTarget; +public class DependencyProxy; +public class UnusedSource; +public class UnusedTarget; diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj new file mode 100644 index 00000000000000..f9d39601b32c95 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj @@ -0,0 +1,10 @@ + + + $(NetCoreAppCurrent) + true + + + + + + diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj index 9aa8090bd00f56..53e0e6a966e06b 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj @@ -23,8 +23,12 @@ IlcTrimMetadata - - IlcGenerateMstatFile;IlcGenerateDgmlFile + + IlcGenerateMstatFile;IlcGenerateDgmlFile;TypeMapGenerateXmlFile;Optimize + browser-wasm + $(MSBuildProjectDirectory)/Dependencies/TypeMapTestLibrary.csproj + $(MSBuildProjectDirectory)/TypeMapTests.targets + TypeMapTests.targets diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs index 5dcf5e35009ecd..36750ce437825e 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs @@ -3,8 +3,12 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Xml; +using TypeMapTestLibrary; [assembly: TypeMap("TrimTargetIsTarget", typeof(TargetAndTrimTarget), typeof(TargetAndTrimTarget))] [assembly: TypeMap("TrimTargetIsUnrelated", typeof(TargetType), typeof(TrimTarget))] @@ -14,6 +18,14 @@ [assembly: TypeMap("UnusedName", typeof(UnusedTargetType), typeof(TrimTarget))] [assembly: TypeMapAssociation(typeof(UnusedSourceClass), typeof(UnusedProxyType))] +[assembly: TypeMap("example/Target", typeof(TargetType))] +[assembly: TypeMap("example/\u0130Class", typeof(TargetType))] +[assembly: TypeMap("[Lexample/Target;", typeof(TargetType[]))] +[assembly: TypeMapAssociation(typeof(SourceClass), typeof(GenericProxy))] +[assembly: TypeMap("unused", typeof(UnreferencedTargetType), typeof(UnreferencedTrimTarget))] +[assembly: TypeMapAssemblyTarget("TypeMapTestLibrary")] +[assembly: TypeMap("local/Target", typeof(TargetType))] + if (args.Length > 1 && args[0] == "instantiate") { Console.WriteLine("This code path should never actually be called. It exists exclusively for the trimmer to see that types are used in a way that it can't fully analyze."); @@ -58,7 +70,7 @@ return 4; } -if (GetTypeWithoutTrimAnalysis(nameof(TrimTarget)) is not null) +if (!RuntimeFeature.IsDynamicCodeSupported && GetTypeWithoutTrimAnalysis(nameof(TrimTarget)) is not null) { Console.WriteLine("TrimTarget should not be preserved if the only place that would preserve it is a check that is optimized away."); return 5; @@ -95,8 +107,205 @@ return 10; } +IReadOnlyDictionary nestedTypeMap = TypeMapping.GetOrCreateExternalTypeMapping(); +if (!nestedTypeMap.TryGetValue("example/Target", out Type nestedTarget) || + nestedTarget != targetType || + !nestedTypeMap.TryGetValue("example/\u0130Class", out Type unicodeTarget) || + unicodeTarget != targetType || + !nestedTypeMap.TryGetValue("[Lexample/Target;", out Type arrayTarget) || + arrayTarget != targetType.MakeArrayType()) +{ + Console.WriteLine("Nested universe external mappings do not match expected types."); + return 11; +} + +IReadOnlyDictionary nestedProxyMap = TypeMapping.GetOrCreateProxyTypeMapping(); +if (!nestedProxyMap.TryGetValue(typeof(SourceClass), out Type genericProxy) || + genericProxy != GetTypeWithoutTrimAnalysis("GenericProxy`1[System.Int32]")) +{ + Console.WriteLine("Nested universe proxy mapping does not match expected type."); + return 12; +} + +IReadOnlyDictionary emptyTypeMap = TypeMapping.GetOrCreateExternalTypeMapping(); +if (emptyTypeMap.TryGetValue("unused", out _)) +{ + Console.WriteLine("The empty map contains a trimmed entry."); + return 13; +} + +Console.WriteLine(new DependencySource().GetHashCode()); +IReadOnlyDictionary dependencyMap = TypeMapping.GetOrCreateExternalTypeMapping(); +if (!dependencyMap.TryGetValue("dependency/Target", out Type dependencyTarget) || + dependencyTarget.FullName != "TypeMapTestLibrary.DependencyTarget" || + dependencyMap.TryGetValue("dependency/Trimmed", out _)) +{ + Console.WriteLine("Cross-assembly external mappings do not match expected types."); + return 15; +} + +IReadOnlyDictionary dependencyProxyMap = TypeMapping.GetOrCreateProxyTypeMapping(); +if (!dependencyProxyMap.TryGetValue(typeof(DependencySource), out Type dependencyProxy) || + dependencyProxy.FullName != "TypeMapTestLibrary.DependencyProxy") +{ + Console.WriteLine("Cross-assembly proxy mapping does not match expected type."); + return 16; +} + +IReadOnlyDictionary otherMap = TypeMapping.GetOrCreateExternalTypeMapping(); +if (!otherMap.TryGetValue("local/Target", out Type otherTarget) || otherTarget != targetType || + otherMap.TryGetValue("wrong/universe", out _)) +{ + Console.WriteLine("An assembly target for one universe included entries from another universe."); + return 17; +} + +if (!CheckArtifact()) + return 14; + return 100; +static bool CheckArtifact() +{ + Dictionary External, Dictionary Proxy)> groups = + ReadArtifact(Path.Combine(AppContext.BaseDirectory, "typemaps.xml")); + if (groups.Count != 5) + { + Console.WriteLine("Unexpected type map universe count."); + return false; + } + + string assemblyName = typeof(UsedTypeMap).Assembly.GetName().Name; + var used = groups[NormalizeTypeName($"{nameof(UsedTypeMap)},{assemblyName}")]; + var nested = groups[NormalizeTypeName($"NestedContainer+Universe,{assemblyName}")]; + var empty = groups[NormalizeTypeName($"{nameof(EmptyTypeMap)},{assemblyName}")]; + var dependency = groups[NormalizeTypeName("TypeMapTestLibrary.DependencyUniverse,TypeMapTestLibrary")]; + var other = groups[NormalizeTypeName("TypeMapTestLibrary.OtherUniverse,TypeMapTestLibrary")]; + + return CheckMap(used.External, proxy: false, + ("TrimTargetIsTarget", nameof(TargetAndTrimTarget)), + ("TrimTargetIsUnrelated", nameof(TargetType))) && + CheckMap(used.Proxy, proxy: true, (nameof(SourceClass), nameof(ProxyType))) && + CheckMap(nested.External, proxy: false, + ("example/Target", nameof(TargetType)), + ("example/\u0130Class", nameof(TargetType)), + ("[Lexample/Target;", nameof(TargetType) + "[]")) && + CheckMap(nested.Proxy, proxy: true, + (nameof(SourceClass), "GenericProxy`1[[System.Int32,System.Private.CoreLib]]")) && + CheckMap(empty.External, proxy: false) && + empty.Proxy is null && + CheckMap(dependency.External, proxy: false, + ("dependency/Target", "TypeMapTestLibrary.DependencyTarget,TypeMapTestLibrary")) && + CheckMap(dependency.Proxy, proxy: true, + ("TypeMapTestLibrary.DependencySource,TypeMapTestLibrary", "TypeMapTestLibrary.DependencyProxy,TypeMapTestLibrary")) && + CheckMap(other.External, proxy: false, ("local/Target", nameof(TargetType))) && + other.Proxy is null; + + bool CheckMap(Dictionary map, bool proxy, params (string Key, string Type)[] expected) + { + if (map is null || map.Count != expected.Length) + { + Console.WriteLine($"Unexpected {(proxy ? "proxy" : "external")} map entry count."); + return false; + } + + foreach ((string key, string type) in expected) + { + string expectedKey = proxy ? QualifyTypeName(key) : key; + string expectedType = QualifyTypeName(type); + if (!map.TryGetValue(expectedKey, out string actualType) || actualType != expectedType) + { + Console.WriteLine($"Missing mapping '{key}' to '{type}'."); + return false; + } + } + + return true; + } + + string QualifyTypeName(string type) + { + TypeName name = TypeName.Parse(type); + + return name.AssemblyName is null + ? NormalizeTypeName($"{type},{assemblyName}") + : name.AssemblyQualifiedName; + } +} + +static Dictionary External, Dictionary Proxy)> ReadArtifact(string path) +{ + using XmlReader reader = XmlReader.Create(path); + reader.MoveToContent(); + if (reader.Name != "typemaps" || reader.HasAttributes) + throw new InvalidDataException("Unexpected type map XML root."); + + var groups = new Dictionary External, Dictionary Proxy)>(StringComparer.Ordinal); + bool emptyRoot = reader.IsEmptyElement; + reader.ReadStartElement("typemaps"); + while (!emptyRoot && reader.MoveToContent() == XmlNodeType.Element) + { + if (reader.Name != "group" || reader.AttributeCount != 1) + throw new InvalidDataException("Unexpected type map group."); + + string type = reader.GetAttribute("type") ?? throw new InvalidDataException("Missing type map group type."); + bool emptyGroup = reader.IsEmptyElement; + reader.ReadStartElement("group"); + (Dictionary External, Dictionary Proxy) maps = (null, null); + while (!emptyGroup && reader.MoveToContent() == XmlNodeType.Element) + { + switch (reader.Name) + { + case "external" when maps.External is null: + maps.External = ReadMap(reader, proxy: false); + break; + case "proxy" when maps.Proxy is null: + maps.Proxy = ReadMap(reader, proxy: true); + break; + default: + throw new InvalidDataException($"Unexpected or duplicate map section '{reader.Name}'."); + } + } + if (!emptyGroup) + reader.ReadEndElement(); + groups.Add(NormalizeTypeName(type), maps); + } + if (!emptyRoot) + reader.ReadEndElement(); + reader.MoveToContent(); + + return groups; + + static Dictionary ReadMap(XmlReader reader, bool proxy) + { + if (reader.HasAttributes) + throw new InvalidDataException("Unexpected map section attributes."); + + var map = new Dictionary(StringComparer.Ordinal); + bool emptyMap = reader.IsEmptyElement; + reader.ReadStartElement(); + while (!emptyMap && reader.MoveToContent() == XmlNodeType.Element) + { + if (reader.Name != "entry" || reader.AttributeCount != 2) + throw new InvalidDataException("Unexpected type map entry."); + + string key = reader.GetAttribute("key") ?? throw new InvalidDataException("Missing type map entry key."); + string value = reader.GetAttribute("value") ?? throw new InvalidDataException("Missing type map entry value."); + map.Add(proxy ? NormalizeTypeName(key) : key, NormalizeTypeName(value)); + bool emptyEntry = reader.IsEmptyElement; + reader.ReadStartElement("entry"); + if (!emptyEntry) + reader.ReadEndElement(); + } + if (!emptyMap) + reader.ReadEndElement(); + + return map; + } +} + +static string NormalizeTypeName(string name) => TypeName.Parse(name).AssemblyQualifiedName; + [MethodImpl(MethodImplOptions.NoInlining)] static Type GetTypeWithoutTrimAnalysis(string typeName) { @@ -116,3 +325,11 @@ class UnusedTypeMap; class UnusedTargetType; class UnusedSourceClass; class UnusedProxyType; + +class NestedContainer +{ + public class Universe; +} + +class GenericProxy; +class EmptyTypeMap; diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMapTests.targets b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMapTests.targets new file mode 100644 index 00000000000000..80ef87a973e15c --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMapTests.targets @@ -0,0 +1,52 @@ + + + + <_TypeMapContents>$([System.IO.File]::ReadAllText('$(TypeMapXmlFile)')) + <_TypeMapWritten>$([System.IO.File]::GetLastWriteTimeUtc('$(TypeMapXmlFile)').Ticks) + <_TypeMapCustomFile>$(IntermediateOutputPath)reports with spaces/type maps.xml + <_TypeMapDisabledFile>$(IntermediateOutputPath)disabled.typemaps.xml + + + + <_PublishedTypeMap Include="@(ResolvedFileToPublish)" + Condition="'%(FullPath)' == '$(TypeMapXmlFile)'" /> + + + + + + + + + + + + + + <_TypeMapCustomWritten>$([System.IO.File]::GetLastWriteTimeUtc('$(_TypeMapCustomFile)').Ticks) + + + + + + + + + + + + + + diff --git a/src/tools/illink/src/ILLink.Shared/SharedStrings.resx b/src/tools/illink/src/ILLink.Shared/SharedStrings.resx index 81602304012266..1264358126553e 100644 --- a/src/tools/illink/src/ILLink.Shared/SharedStrings.resx +++ b/src/tools/illink/src/ILLink.Shared/SharedStrings.resx @@ -1122,9 +1122,6 @@ Cannot write type map output because attribute '{0}' has invalid constructor arguments. - - Cannot serialize type '{0}' in type map output using the reflection type name format. - Avoid accessing Assembly file path when publishing as a single file diff --git a/src/tools/illink/src/ILLink.Shared/TypeMapXmlWriter.cs b/src/tools/illink/src/ILLink.Shared/TypeMapXmlWriter.cs index 87e558f92879b9..3a202ac0b2efde 100644 --- a/src/tools/illink/src/ILLink.Shared/TypeMapXmlWriter.cs +++ b/src/tools/illink/src/ILLink.Shared/TypeMapXmlWriter.cs @@ -27,21 +27,6 @@ private TypeMapXmlWriter(XmlWriter writer) _writer.WriteStartDocument(); _writer.WriteStartElement("typemaps"); - _writer.WriteAttributeString("version", "1"); - } - - // Reflection type identifiers escape a different set of characters than assembly display names. - public static string EscapeTypeName(string name) - { - var builder = new StringBuilder(); - foreach (char character in name) - { - if (character is '[' or ']' or '&' or '*' or ',' or '+' or '\\') - builder.Append('\\'); - builder.Append(character); - } - - return builder.ToString(); } public static void WriteTypeMapsToStream(Stream output, SortedDictionary groups) @@ -50,7 +35,6 @@ public static void WriteTypeMapsToStream(Stream output, SortedDictionary - <_TrimmerTypeMapXmlFile>$(IntermediateLinkDir)$(TargetName).typemaps.xml - <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --output-typemaps + $([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)', '$(TargetName).typemaps.xml')) + <_TrimmerTypeMapXmlFile>$(TypeMapXmlFile) + <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --output-typemaps "$(TypeMapXmlFile)" diff --git a/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs b/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs index a3f9c65d4becb7..e4d2142cd61d36 100644 --- a/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs +++ b/src/tools/illink/src/linker/Linker.Steps/OutputStep.cs @@ -63,23 +63,8 @@ protected override void Process() protected override void EndProcess() { - if (Context.TypeMapGenerateXmlFile) - { - AssemblyDefinition? entryAssembly = Annotations.GetEntryPointAssembly() ?? Context.TypeMapOutputAssembly ?? Context.TypeMapHandler?.EntryPointAssembly; - string fileName = entryAssembly is null ? "typemaps.xml" - : Path.ChangeExtension(GetOriginalAssemblyFileInfo(entryAssembly).Name, ".typemaps.xml"); - string outputPath = Path.GetFullPath(Path.Combine(Context.OutputDirectory, fileName)); - try - { - TypeMapOutput.Write(Context, outputPath); - } - catch (Exception e) when (e is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or InvalidOperationException or BadImageFormatException or System.Xml.XmlException) - { - if (File.Exists(outputPath)) - File.Delete(outputPath); - throw new LinkerFatalErrorException(MessageContainer.CreateErrorMessage(null, DiagnosticId.FailedToWriteOutput, outputPath), e); - } - } + if (Context.TypeMapFileName is string typeMapFileName) + TypeMapOutput.Write(Context, typeMapFileName); if (Context.AssemblyListFile != null) { diff --git a/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs b/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs index 03a2e88c345928..1e4b95396186b3 100644 --- a/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs +++ b/src/tools/illink/src/linker/Linker.Steps/RootAssemblyInputStep.cs @@ -24,7 +24,7 @@ protected override void Process() return; #if !ILTRIM - if (Context.TypeMapGenerateXmlFile && (rootMode == AssemblyRootMode.EntryPoint || Context.TypeMapOutputAssembly is null)) + if (Context.TypeMapFileName is not null && (rootMode == AssemblyRootMode.EntryPoint || Context.TypeMapOutputAssembly is null)) Context.TypeMapOutputAssembly = assembly; #endif diff --git a/src/tools/illink/src/linker/Linker/Driver.cs b/src/tools/illink/src/linker/Linker/Driver.cs index a7b2d2c6060517..3d9b6eae322341 100644 --- a/src/tools/illink/src/linker/Linker/Driver.cs +++ b/src/tools/illink/src/linker/Linker/Driver.cs @@ -597,9 +597,11 @@ protected int SetupContext(ILogger? customLogger = null) #if !ILTRIM case "--output-typemaps": - if (!GetBoolParam(token, value => context.TypeMapGenerateXmlFile = value)) + if (!GetStringParam(token, out string? typeMapFileName)) return -1; + context.TypeMapFileName = typeMapFileName; + continue; #endif @@ -1555,7 +1557,7 @@ static void Usage() Console.WriteLine(" --skip-unresolved Ignore unresolved types, methods, and assemblies. Defaults to true"); Console.WriteLine(" --output-pinvokes PATH Output a JSON file with all modules and entry points of the P/Invokes found"); #if !ILTRIM - Console.WriteLine(" --output-typemaps Write retained type maps to a .typemaps.xml sidecar in the output directory"); + Console.WriteLine(" --output-typemaps Write retained type maps to the specified XML file"); #endif Console.WriteLine(" --verbose Log messages indicating progress and warnings"); Console.WriteLine(" --nowarn WARN Disable specific warning messages"); diff --git a/src/tools/illink/src/linker/Linker/LinkContext.cs b/src/tools/illink/src/linker/Linker/LinkContext.cs index 85fc3a84bbd10a..8c3c17a8f1543b 100644 --- a/src/tools/illink/src/linker/Linker/LinkContext.cs +++ b/src/tools/illink/src/linker/Linker/LinkContext.cs @@ -142,7 +142,7 @@ public Pipeline Pipeline public string? TypeMapEntryAssembly { get; set; } #if !ILTRIM - internal bool TypeMapGenerateXmlFile { get; set; } + internal string? TypeMapFileName { get; set; } internal AssemblyDefinition? TypeMapOutputAssembly { get; set; } internal TypeMapHandler? TypeMapHandler { get; set; } #endif diff --git a/src/tools/illink/src/linker/Linker/TypeMapOutput.cs b/src/tools/illink/src/linker/Linker/TypeMapOutput.cs index a6669116ae4b88..e48867eddad9f3 100644 --- a/src/tools/illink/src/linker/Linker/TypeMapOutput.cs +++ b/src/tools/illink/src/linker/Linker/TypeMapOutput.cs @@ -106,7 +106,6 @@ Map GetMap(TypeReference groupType, bool proxy) ByReferenceType byRef => GetTypeName(byRef.ElementType).MakeByRefTypeName(), GenericInstanceType instance => GetTypeName(instance.ElementType) .MakeGenericTypeName(instance.GenericArguments.Select(GetTypeName).ToImmutableArray()), - TypeSpecification or GenericParameter => throw new NotSupportedException(string.Format(SharedStrings.TypeMapOutputUnsupportedType, type.FullName)), _ => GetSimpleTypeName(context.TryResolve(type) ?? type) }; @@ -116,12 +115,25 @@ TypeName GetSimpleTypeName(TypeReference type) if (type.DeclaringType is TypeReference declaringType) name.Append(GetTypeName(declaringType).FullName).Append('+'); else if (type.Namespace.Length > 0) - name.Append(TypeMapXmlWriter.EscapeTypeName(type.Namespace)).Append('.'); - name.Append(TypeMapXmlWriter.EscapeTypeName(type.Name)); + name.Append(EscapeTypeName(type.Namespace)).Append('.'); + name.Append(EscapeTypeName(type.Name)); string assemblyName = type.Scope is AssemblyNameReference assembly ? assembly.Name : type.Module.Assembly.Name.Name; return TypeName.Parse(name.ToString()).WithAssemblyName(new AssemblyNameInfo(assemblyName)); } } + + private static string EscapeTypeName(string name) + { + var builder = new StringBuilder(); + foreach (char character in name) + { + if (character is '[' or ']' or '&' or '*' or ',' or '+' or '\\') + builder.Append('\\'); + builder.Append(character); + } + + return builder.ToString(); + } } diff --git a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Reflection.IndividualTests.g.cs b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Reflection.IndividualTests.g.cs deleted file mode 100644 index c59d81c3eeb03c..00000000000000 --- a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/generated/ILLink.RoslynAnalyzer.Tests.Generator/ILLink.RoslynAnalyzer.Tests.TestCaseGenerator/Reflection.IndividualTests.g.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Threading.Tasks; -using Xunit; - -namespace ILLink.RoslynAnalyzer.Tests.Reflection -{ - public sealed partial class IndividualTests : LinkerTestBase - { - - protected override string TestSuiteName => "Reflection.Individual"; - - [Fact] - public Task CanOutputTypeMaps() - { - return RunTest(allowMissingWarnings: true); - } - - } -} diff --git a/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.csproj b/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.csproj index 5d43a05601fbf8..5f3daab5629a06 100644 --- a/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.csproj +++ b/src/tools/illink/test/ILLink.Tasks.Tests/ILLink.Tasks.Tests.csproj @@ -9,11 +9,4 @@ - - - - - - - diff --git a/src/tools/illink/test/ILLink.Tasks.Tests/TypeMapFileTests.cs b/src/tools/illink/test/ILLink.Tasks.Tests/TypeMapFileTests.cs deleted file mode 100644 index ccc1f63dd2fd84..00000000000000 --- a/src/tools/illink/test/ILLink.Tasks.Tests/TypeMapFileTests.cs +++ /dev/null @@ -1,263 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using System.Xml.Linq; -using Xunit; - -namespace ILLink.Tasks.Tests; - -public class TypeMapFileTests -{ - [Theory] - [InlineData("full", true)] - [InlineData("partial", true)] - [InlineData("partial", false)] - public async Task SDKProducesIncrementalTypeMapSidecar(string trimMode, bool isTrimmable) - { - string directory = Path.Combine(Path.GetTempPath(), nameof(TypeMapFileTests), Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(directory); - try - { - string input = Path.Combine(directory, "App.dll"); - string linkedDirectory = Path.Combine(directory, "linked"); - string sidecar = Path.Combine(linkedDirectory, "App.typemaps.xml"); - string publishedSidecar = Path.Combine(directory, "publish", "App.typemaps.xml"); - string project = Path.Combine(directory, "App.proj"); - string tasksAssembly = (string)AppContext.GetData("ILLink.Tasks.Tests.ILLinkTasksAssembly"); - string msbuild = (string)AppContext.GetData("ILLink.Tasks.Tests.MSBuildPath"); - string sdkTasksAssembly = (string)AppContext.GetData("ILLink.Tasks.Tests.SDKTasksAssembly"); - string corelib = typeof(object).Assembly.Location; - await CreateApplication(input, corelib, "kept"); - new XElement("Project", - new XElement("UsingTask", new XAttribute("TaskName", "NETSdkInformation"), new XAttribute("AssemblyFile", sdkTasksAssembly)), - new XElement("UsingTask", new XAttribute("TaskName", "NETSdkError"), new XAttribute("AssemblyFile", sdkTasksAssembly)), - new XElement("PropertyGroup", - new XElement("PublishTrimmed", "true"), - new XElement("TypeMapGenerateXmlFile", "true"), - new XElement("TargetFrameworkIdentifier", ".NETCoreApp"), - new XElement("_TargetFrameworkVersionWithoutV", Environment.Version.ToString(2)), - new XElement("AssemblyName", "App"), - new XElement("TargetName", "App"), - new XElement("IntermediateOutputPath", Path.Combine(directory, "obj") + Path.DirectorySeparatorChar), - new XElement("IntermediateLinkDir", linkedDirectory + Path.DirectorySeparatorChar), - new XElement("ILLinkTasksAssembly", tasksAssembly), - new XElement("MicrosoftNETBuildTasksAssembly", sdkTasksAssembly), - new XElement("TrimMode", trimMode), - new XElement("_ExtraTrimmerArgs", "--ignore-descriptors true --ignore-substitutions true --ignore-link-attributes false --deterministic true")), - new XElement("ItemGroup", - new XElement("ManagedAssemblyToLink", new XAttribute("Include", input), new XElement("IsTrimmable", isTrimmable)), - new XElement("ManagedAssemblyToLink", new XAttribute("Include", corelib), new XElement("IsTrimmable", true)), - new XElement("ReferencePath", new XAttribute("Include", Path.Combine(Path.GetDirectoryName(corelib), "*.dll"))), - new XElement("TrimmerRootAssembly", new XAttribute("Include", "App"), new XElement("RootMode", "EntryPoint"))), - new XElement("Import", new XAttribute("Project", Path.Combine(Path.GetDirectoryName(msbuild), "Sdks", "Microsoft.NET.Sdk", "targets", "Microsoft.NET.Publish.targets"))), - new XElement("Import", new XAttribute("Project", Path.Combine(Path.GetDirectoryName(tasksAssembly), "build", "Microsoft.NET.ILLink.targets"))), - new XElement("Target", new XAttribute("Name", "_ComputeManagedAssemblyToLink")), - new XElement("Target", new XAttribute("Name", "PrepareForILLink")), - new XElement("Target", new XAttribute("Name", "_HandleFileConflictsForPublish")), - new XElement("Target", new XAttribute("Name", "Publish"), new XAttribute("DependsOnTargets", "ILLink;PrepareForBundle"), - new XElement("ItemGroup", - new XElement("_PublishedTypeMap", new XAttribute("Include", "@(ResolvedFileToPublish);@(FilesToBundle)"), - new XAttribute("Condition", "'%(Filename)%(Extension)' == 'App.typemaps.xml'"))), - new XElement("Error", new XAttribute("Condition", "'@(_PublishedTypeMap)' != ''"), - new XAttribute("Text", "The intermediate type map artifact must not be published or bundled.")), - new XElement("Copy", - new XAttribute("SourceFiles", "@(ResolvedFileToPublish)"), - new XAttribute("DestinationFiles", "@(ResolvedFileToPublish->'$(MSBuildProjectDirectory)/publish/%(RelativePath)')"), - new XAttribute("SkipUnchangedFiles", "true")))).Save(project); - - await Publish(); - Assert.True(File.Exists(Path.Combine(linkedDirectory, "App.dll"))); - Assert.False(File.Exists(Path.Combine(linkedDirectory, "typemaps.xml"))); - CheckArtifact("kept"); - byte[] original = File.ReadAllBytes(sidecar); - DateTime written = File.GetLastWriteTimeUtc(sidecar); - - await Publish(); - Assert.Equal(written, File.GetLastWriteTimeUtc(sidecar)); - Assert.Equal(original, File.ReadAllBytes(sidecar)); - - await Task.Delay(TimeSpan.FromSeconds(1)); - File.SetLastWriteTimeUtc(input, DateTime.UtcNow); - await Publish(); - Assert.True(File.GetLastWriteTimeUtc(sidecar) > written); - Assert.Equal(original, File.ReadAllBytes(sidecar)); - written = File.GetLastWriteTimeUtc(sidecar); - await Publish(); - Assert.Equal(written, File.GetLastWriteTimeUtc(sidecar)); - - File.Delete(sidecar); - await Publish(); - Assert.Equal(original, File.ReadAllBytes(sidecar)); - CheckArtifact("kept"); - written = File.GetLastWriteTimeUtc(sidecar); - await Publish(); - Assert.Equal(written, File.GetLastWriteTimeUtc(sidecar)); - - await Task.Delay(TimeSpan.FromSeconds(1)); - await CreateApplication(input, corelib, "changed"); - await Publish(); - CheckArtifact("changed"); - Assert.NotEqual(original, File.ReadAllBytes(sidecar)); - written = File.GetLastWriteTimeUtc(sidecar); - await Publish(); - Assert.Equal(written, File.GetLastWriteTimeUtc(sidecar)); - - foreach (bool includeAllContent in new[] { false, true }) - { - await Publish(publishSingleFile: true, includeAllContent: includeAllContent); - CheckArtifact("changed"); - } - - await CreateApplication(input, corelib, "invalid\0key"); - File.SetLastWriteTimeUtc(input, written.AddSeconds(-1)); - File.Delete(sidecar); - - await Publish(expectSuccess: false); - Assert.False(File.Exists(sidecar)); - await Publish(expectSuccess: false); - Assert.False(File.Exists(sidecar)); - - await CreateApplication(input, corelib, "changed"); - await Publish(); - CheckArtifact("changed"); - - await Publish(typeMapGenerateXmlFile: false, output: Path.Combine(directory, "disabled")); - Assert.Empty(Directory.EnumerateFiles(Path.Combine(directory, "disabled"), "*.typemaps.xml")); - - void CheckArtifact(string expectedKey) - { - Assert.False(File.Exists(publishedSidecar)); - XElement root = XElement.Load(sidecar); - Assert.Equal("typemaps", root.Name); - Assert.Equal("1", (string)root.Attribute("version")); - XElement group = Assert.Single(root.Elements("group")); - Assert.Equal("Example.Universe, App", (string)group.Attribute("type")); - Assert.Equal(new[] { "external", "proxy" }, group.Elements().Select(map => map.Name.LocalName)); - XElement external = Assert.Single(group.Elements("external")); - Assert.Equal(isTrimmable - ? new[] { (expectedKey, "Example.Target, App") } - : new[] { (expectedKey, "Example.Target, App"), ("trimmed", "Example.Unused, App") }, - external.Elements("entry").Select(entry => ((string)entry.Attribute("key"), (string)entry.Attribute("value")))); - XElement proxy = Assert.Single(group.Elements("proxy")); - Assert.Equal(isTrimmable - ? new[] { ("Example.Source, App", "Example.Target, App") } - : new[] { ("Example.Source, App", "Example.Target, App"), ("Example.Unused, App", "Example.Unused, App") }, - proxy.Elements("entry").Select(entry => ((string)entry.Attribute("key"), (string)entry.Attribute("value")))); - if (!isTrimmable) - Assert.Equal(File.ReadAllBytes(input), File.ReadAllBytes(Path.Combine(linkedDirectory, "App.dll"))); - } - - async Task Publish(bool typeMapGenerateXmlFile = true, string output = null, bool publishSingleFile = false, bool includeAllContent = false, bool expectSuccess = true) - { - var arguments = new List - { - msbuild, - project, - "-t:Publish", - "-nologo", - "-v:minimal", - $"-p:TypeMapGenerateXmlFile={typeMapGenerateXmlFile}", - $"-p:PublishSingleFile={publishSingleFile}", - $"-p:IncludeAllContentForSelfExtract={includeAllContent}" - }; - - if (output is not null) - arguments.Add($"-p:IntermediateLinkDir={output}{Path.DirectorySeparatorChar}"); - - (int exitCode, string diagnostics) = await RunTool(directory, arguments); - if (expectSuccess) - { - Assert.True(exitCode == 0, $"MSBuild failed with exit code {exitCode}.\n{diagnostics}"); - } - else - { - Assert.True(exitCode != 0, "MSBuild skipped the failed artifact generation and reported success."); - Assert.Contains(sidecar, diagnostics, StringComparison.Ordinal); - } - } - } - finally - { - Directory.Delete(directory, recursive: true); - } - } - - private static async Task CreateApplication(string path, string corelibPath, string key) - { - string source = Path.ChangeExtension(path, ".cs"); - await File.WriteAllTextAsync(source, $$""" - using System.Runtime.InteropServices; - - [assembly: TypeMap({{JsonSerializer.Serialize(key)}}, typeof(Example.Target))] - [assembly: TypeMap("trimmed", typeof(Example.Unused), typeof(Example.Unused))] - [assembly: TypeMapAssociation(typeof(Example.Source), typeof(Example.Target))] - [assembly: TypeMapAssociation(typeof(Example.Unused), typeof(Example.Unused))] - - namespace Example; - - public class Program - { - public static void Main() - { - _ = new Source(); - _ = TypeMapping.GetOrCreateExternalTypeMapping(); - _ = TypeMapping.GetOrCreateProxyTypeMapping(); - } - } - - public class Universe; - public class Target; - public class Source; - public class Unused; - """); - - string msbuild = (string)AppContext.GetData("ILLink.Tasks.Tests.MSBuildPath"); - string compiler = Path.Combine(Path.GetDirectoryName(msbuild), "Roslyn", "bincore", "csc.dll"); - (int exitCode, string diagnostics) = await RunTool(Path.GetDirectoryName(path), new[] - { - compiler, "-nologo", "-noconfig", "-nostdlib", "-target:exe", - $"-out:{path}", $"-reference:{corelibPath}", source - }); - Assert.True(exitCode == 0, $"Fixture compilation failed with exit code {exitCode}.\n{diagnostics}"); - } - - private static async Task<(int ExitCode, string Diagnostics)> RunTool(string workingDirectory, IEnumerable arguments) - { - string dotnet = (string)AppContext.GetData("ILLink.Tasks.Tests.DotNetHostPath"); - var startInfo = new ProcessStartInfo(dotnet) - { - WorkingDirectory = workingDirectory, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true - }; - startInfo.Environment["DOTNET_HOST_PATH"] = dotnet; - foreach (string argument in arguments) - startInfo.ArgumentList.Add(argument); - - using var process = Process.Start(startInfo); - Task stdout = process.StandardOutput.ReadToEndAsync(); - Task stderr = process.StandardError.ReadToEndAsync(); - using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - try - { - await process.WaitForExitAsync(timeout.Token); - } - catch (OperationCanceledException) when (timeout.IsCancellationRequested) - { - process.Kill(entireProcessTree: true); - await process.WaitForExitAsync(); - Assert.Fail($"Tool timed out.\n{await stdout}\n{await stderr}"); - } - - return (process.ExitCode, await stdout + await stderr); - } -} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapOutputLibrary.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapOutputLibrary.cs deleted file mode 100644 index 553a55a02a05d3..00000000000000 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Dependencies/TypeMapOutputLibrary.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.InteropServices; -using Mono.Linker.Tests.Cases.Reflection.Dependencies; - -[assembly: TypeMap("copied", typeof(TypeMapOutputTarget))] - -namespace Mono.Linker.Tests.Cases.Reflection.Dependencies; - -public class TypeMapOutputGroup; -public class TypeMapOutputTarget; diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Individual/CanOutputTypeMaps.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Individual/CanOutputTypeMaps.cs deleted file mode 100644 index bd752627ad5b24..00000000000000 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/Individual/CanOutputTypeMaps.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.InteropServices; -using Mono.Linker.Tests.Cases.Expectations.Metadata; -using Mono.Linker.Tests.Cases.Reflection.Dependencies; -using Mono.Linker.Tests.Cases.Reflection.Individual; - -[assembly: TypeMap("keep<&\"\t\r\n", typeof(CanOutputTypeMaps.Nested.Target))] -[assembly: TypeMap("conditional", typeof(CanOutputTypeMaps.Nested.Target), typeof(CanOutputTypeMaps.Source))] -[assembly: TypeMap("trimmed", typeof(CanOutputTypeMaps.Unused), typeof(CanOutputTypeMaps.Unused))] -[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(CanOutputTypeMaps.Nested.Generic))] -[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Unused), typeof(CanOutputTypeMaps.Unused))] -[assembly: TypeMap("array", typeof(int[,]))] -[assembly: TypeMap("byref", typeof(int))] -[assembly: TypeMap("example/Outer$Inner[0]", typeof(CanOutputTypeMaps.Nested.Target))] -[assembly: TypeMap("[Lexample/Outer$Inner;", typeof(CanOutputTypeMaps.Nested.Target[]))] -[assembly: TypeMap("", typeof(CanOutputTypeMaps.Nested.Generic))] -[assembly: TypeMap("pointer", typeof(int*))] -[assembly: TypeMap("unicode/\u0130\u4e2d\U0001f600", typeof(CanOutputTypeMaps.Nested.Target))] -[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(object))] -[assembly: TypeMap("unrequested", typeof(object))] -[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(object))] -[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Nested.Generic), typeof(CanOutputTypeMaps.Nested.Target))] -[assembly: TypeMap>("generic-universe", typeof(CanOutputTypeMaps.Nested.Generic))] -[assembly: TypeMap("trimmed", typeof(CanOutputTypeMaps.Unused), typeof(CanOutputTypeMaps.Unused))] -[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Unused), typeof(object))] -[assembly: TypeMap("unrequested", typeof(object))] -[assembly: TypeMap("duplicate", typeof(object))] -[assembly: TypeMap("duplicate", typeof(string))] -[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(object))] -[assembly: TypeMapAssociation(typeof(CanOutputTypeMaps.Source), typeof(string))] -[assembly: TypeMapAssemblyTarget("Maps+Library")] - -namespace Mono.Linker.Tests.Cases.Reflection.Individual; - -[SetupCompileArgument("/unsafe")] -[SetupLinkerAction("link", "System.Private.CoreLib")] -[SetupLinkerArgument("--ignore-link-attributes", "false")] -[SetupCompileBefore("Maps+Library.dll", new[] { "../Dependencies/TypeMapOutputLibrary.cs" })] -[SetupLinkerAction("copy", "Maps+Library")] -public class CanOutputTypeMaps -{ - public static void Main() - { - Console.WriteLine(new Source()); - Console.WriteLine(new Nested.Generic()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping>()); - } - - public class Group; - public class ExternalOnly; - public class ProxyOnly; - public class Empty; - public class Unrequested; - public class Invalid; - public class GenericUniverse; - public class Source; - public class Unused; - - public class Nested - { - public class Target; - public class Generic; - } -} diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCases/IndividualTests.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCases/IndividualTests.cs index 260f0049a48932..e77533cec7d8d7 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCases/IndividualTests.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCases/IndividualTests.cs @@ -6,7 +6,6 @@ using System.IO; using System.Linq; using System.Runtime.Serialization.Json; -using System.Threading.Tasks; using System.Xml; using Mono.Cecil; using Mono.Linker.Tests.Cases.CommandLine.Mvid; @@ -38,72 +37,6 @@ public void CanSkipUnresolved() Assert.Fail($"The linked assembly is missing. Should have existed at {result.OutputAssemblyPath}"); } - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task CanOutputTypeMaps(bool absoluteOutputDirectory) - { - var runner = new TypeMapArtifactTestRunner(); - TrimmedTestCaseResult result = TypeMapOutputTests.CreateFixture(runner); - string outputDirectory = result.Sandbox.OutputDirectory.ToString(); - string reportPath = Path.Combine(outputDirectory, "test.typemaps.xml"); - await RunLinker(generateArtifact: false, expectSuccess: true); - byte[] assemblyWithoutReport = File.ReadAllBytes(result.OutputAssemblyPath.ToString()); - Assert.Empty(Directory.EnumerateFiles(result.Sandbox.OutputDirectory.ToString(), "*typemap*.xml", SearchOption.AllDirectories)); - - await RunLinker(generateArtifact: true, expectSuccess: true); - TypeMapOutputTests.CheckArtifact(reportPath, nativeAot: false); - Assert.Equal(assemblyWithoutReport, File.ReadAllBytes(result.OutputAssemblyPath.ToString())); - - byte[] firstReport = File.ReadAllBytes(reportPath); - File.WriteAllText(reportPath, "stale output"); - await RunLinker(generateArtifact: true, expectSuccess: true); - Assert.Equal(firstReport, File.ReadAllBytes(reportPath)); - - File.Delete(reportPath); - await RunLinker(generateArtifact: true, expectSuccess: true); - Assert.Equal(firstReport, File.ReadAllBytes(reportPath)); - - TypeMapOutputTests.UseUnrepresentableKey(result); - await RunLinker(generateArtifact: true, expectSuccess: false); - Assert.False(File.Exists(reportPath)); - - async Task RunLinker(bool generateArtifact, bool expectSuccess) - { - string[] arguments = (string[])runner.Arguments.Clone(); - int outputOption = Array.IndexOf(arguments, "-o"); - Assert.True(outputOption >= 0); - arguments[outputOption + 1] = absoluteOutputDirectory ? outputDirectory - : Path.GetRelativePath(result.Sandbox.InputDirectory.ToString(), outputDirectory); - IEnumerable invocation = generateArtifact ? arguments.Concat(new[] { "--output-typemaps" }) : arguments; - (int exitCode, string diagnostics) = await TypeMapOutputTests.RunTool(typeof(Driver).Assembly.Location, - result.Sandbox.InputDirectory.ToString(), invocation); - if (expectSuccess) - { - Assert.True(exitCode == 0, $"ILLink failed with exit code {exitCode}.\n{diagnostics}"); - Assert.True(File.Exists(result.OutputAssemblyPath.ToString())); - } - else - { - Assert.True(exitCode != 0, "ILLink reported success despite failing to write the build artifact."); - Assert.Contains(reportPath, diagnostics, StringComparison.Ordinal); - } - } - } - - private sealed class TypeMapArtifactTestRunner() : TestRunner(new ObjectFactory()) - { - public string[] Arguments { get; private set; } = []; - - protected override void AddTrimmingOptions(TestCaseSandbox sandbox, ManagedCompilationResult compilationResult, - TrimmingArgumentBuilder builder, TestCaseMetadataProvider metadataProvider) - { - base.AddTrimmingOptions(sandbox, compilationResult, builder, metadataProvider); - builder.AddAdditionalArgument("--deterministic", ["true"]); - Arguments = builder.Build(); - } - } - [Fact] public void CanOutputPInvokes() { diff --git a/src/tools/illink/test/Trimming.Tests.Shared/TypeMapOutputTests.cs b/src/tools/illink/test/Trimming.Tests.Shared/TypeMapOutputTests.cs deleted file mode 100644 index d8c986e73536ef..00000000000000 --- a/src/tools/illink/test/Trimming.Tests.Shared/TypeMapOutputTests.cs +++ /dev/null @@ -1,265 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using System.Xml; -using Mono.Cecil; -using Mono.Linker.Tests.Cases.Reflection.Individual; -using Mono.Linker.Tests.Extensions; -using Mono.Linker.Tests.TestCasesRunner; -using Xunit; -using TypeName = System.Reflection.Metadata.TypeName; - -#nullable enable - -namespace Mono.Linker.Tests.TestCases; - -internal static class TypeMapOutputTests -{ - private const string Prefix = "Mono.Linker.Tests.Cases.Reflection.Individual.CanOutputTypeMaps+"; - private const string LibraryPrefix = "Mono.Linker.Tests.Cases.Reflection.Dependencies."; - private const string Target = Prefix + @"Nested+Target\+With\,Reserved=""value"""; - - private sealed class Map - { - public List<(string Key, string Target)> Entries { get; } = []; - } - - public static async Task<(int ExitCode, string Diagnostics)> RunTool(string tool, string workingDirectory, IEnumerable arguments) - { - string dotnet = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH") - ?? Path.GetFullPath(Path.Combine(RuntimeEnvironment.GetRuntimeDirectory(), "..", "..", "..", - OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet")); - Assert.True(File.Exists(dotnet), $"The dotnet host was not found at '{dotnet}'."); - var startInfo = new ProcessStartInfo(dotnet) - { - WorkingDirectory = workingDirectory, - UseShellExecute = false, - RedirectStandardOutput = true, - RedirectStandardError = true - }; - startInfo.ArgumentList.Add("exec"); - startInfo.ArgumentList.Add("--runtimeconfig"); - startInfo.ArgumentList.Add(Path.ChangeExtension(typeof(TypeMapOutputTests).Assembly.Location, ".runtimeconfig.json")); - startInfo.ArgumentList.Add(tool); - foreach (string argument in arguments) - startInfo.ArgumentList.Add(argument); - - using var process = new Process { StartInfo = startInfo }; - Assert.True(process.Start()); - Task stdout = process.StandardOutput.ReadToEndAsync(); - Task stderr = process.StandardError.ReadToEndAsync(); - using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(5)); - try - { - await process.WaitForExitAsync(timeout.Token); - } - catch (OperationCanceledException) when (timeout.IsCancellationRequested) - { - process.Kill(entireProcessTree: true); - await process.WaitForExitAsync(); - Assert.Fail($"Tool timed out.\n{await stdout}\n{await stderr}"); - } - - return (process.ExitCode, await stdout + await stderr); - } - - public static TrimmedTestCaseResult CreateFixture(TestRunner runner) - { - NPath casesRoot = TestDatabase.TestCasesRootDirectory; - var testCase = new TestCase(casesRoot.Combine("Reflection/Individual/CanOutputTypeMaps.cs"), casesRoot, - typeof(CanOutputTypeMaps).Assembly.Location.ToNPath()); - TrimmedTestCaseResult? result = runner.Run(testCase); - Assert.NotNull(result); - - // C# cannot declare identifiers containing these legal metadata characters. - using var assembly = AssemblyDefinition.ReadAssembly(result.InputAssemblyPath.ToString(), new ReaderParameters { InMemory = true }); - TypeDefinition declaringType = assembly.MainModule.GetType(typeof(CanOutputTypeMaps).FullName); - TypeDefinition nestedType = Assert.Single(declaringType.NestedTypes, type => type.Name == "Nested"); - TypeDefinition target = Assert.Single(nestedType.NestedTypes, type => type.Name == "Target"); - string originalName = target.FullName; - target.Name = "Target+With,Reserved=\"value\""; - foreach (CustomAttribute attribute in assembly.CustomAttributes) - { - // A byref typeof expression also requires constructing metadata rather than C# syntax. - if (attribute.AttributeType.Name == "TypeMapAttribute`1" && - attribute.ConstructorArguments[0].Value is string key && key == "byref") - { - CustomAttributeArgument argument = attribute.ConstructorArguments[1]; - attribute.ConstructorArguments[1] = new CustomAttributeArgument(argument.Type, new ByReferenceType((TypeReference)argument.Value)); - } - for (int i = 0; i < attribute.ConstructorArguments.Count; i++) - { - CustomAttributeArgument argument = attribute.ConstructorArguments[i]; - if (argument.Value is TypeReference reference && reference.FullName == originalName) - attribute.ConstructorArguments[i] = new CustomAttributeArgument(argument.Type, target); - else if (argument.Value is ArrayType array && array.ElementType.FullName == originalName) - attribute.ConstructorArguments[i] = new CustomAttributeArgument(argument.Type, new ArrayType(target)); - } - } - assembly.Write(result.InputAssemblyPath.ToString()); - - return result; - } - - public static void UseUnrepresentableKey(TrimmedTestCaseResult result) - { - using var assembly = AssemblyDefinition.ReadAssembly(result.InputAssemblyPath.ToString(), new ReaderParameters { InMemory = true }); - CustomAttribute attribute = Assert.Single(assembly.CustomAttributes, attribute => - attribute.AttributeType.Name == "TypeMapAttribute`1" && - attribute.ConstructorArguments[0].Value is string key && key == "conditional"); - CustomAttributeArgument keyArgument = attribute.ConstructorArguments[0]; - attribute.ConstructorArguments[0] = new CustomAttributeArgument(keyArgument.Type, "invalid\0key"); - assembly.Write(result.InputAssemblyPath.ToString()); - } - - public static void CheckArtifact(string path, bool nativeAot) - { - Dictionary> groups = ReadArtifact(path, nativeAot); - string target = nativeAot ? Prefix + "Nested+Target+With,Reserved=\"value\"" : Target; - Assert.Equal(new[] - { - LibraryPrefix + "TypeMapOutputGroup,Maps+Library", - Prefix + "Empty,test", - Prefix + "ExternalOnly,test", - Prefix + "GenericUniverse`1[[System.Int32[],System.Private.CoreLib]],test", - Prefix + "Group,test", - Prefix + "Invalid,test", - Prefix + "ProxyOnly,test" - }.Select(FormatName), groups.Keys); - Dictionary group = groups[FormatName(Prefix + "Group,test")]; - Assert.Equal(new[] { "external", "proxy" }, group.Keys); - Assert.Equal(new[] - { - ("conditional", target + ",test"), - ("keep<&\"\t\r\n", target + ",test") - }.Select(entry => (entry.Item1, FormatName(entry.Item2))), group["external"].Entries); - Assert.Equal(new[] - { - (Prefix + "Source,test", Prefix + "Nested+Generic`1[[System.Int32[],System.Private.CoreLib]],test") - }.Select(entry => (FormatName(entry.Item1), FormatName(entry.Item2))), group["proxy"].Entries); - - Dictionary externalOnly = groups[FormatName(Prefix + "ExternalOnly,test")]; - Assert.Equal(new[] { "external" }, externalOnly.Keys); - Assert.Equal(new[] - { - ("", Prefix + "Nested+Generic`1[[System.Int32[],System.Private.CoreLib]],test"), - ("[Lexample/Outer$Inner;", target + "[],test"), - ("array", "System.Int32[,],System.Private.CoreLib"), - ("byref", "System.Int32&,System.Private.CoreLib"), - ("example/Outer$Inner[0]", target + ",test"), - ("pointer", "System.Int32*,System.Private.CoreLib"), - ("unicode/\u0130\u4e2d\U0001f600", target + ",test") - }.Select(entry => (entry.Item1, FormatName(entry.Item2))), externalOnly["external"].Entries); - - Dictionary proxyOnly = groups[FormatName(Prefix + "ProxyOnly,test")]; - Assert.Equal(new[] { "proxy" }, proxyOnly.Keys); - Assert.Equal(new[] - { - (Prefix + "Nested+Generic`1[[System.Int32[],System.Private.CoreLib]],test", target + ",test"), - (Prefix + "Source,test", "System.Object,System.Private.CoreLib") - }.Select(entry => (FormatName(entry.Item1), FormatName(entry.Item2))), proxyOnly["proxy"].Entries); - - Dictionary genericUniverse = groups[FormatName(Prefix + "GenericUniverse`1[[System.Int32[],System.Private.CoreLib]],test")]; - Assert.Equal(new[] { "external" }, genericUniverse.Keys); - Assert.Equal(new[] - { - ("generic-universe", Prefix + "Nested+Generic`1[[System.String[,],System.Private.CoreLib]],test") - }.Select(entry => (entry.Item1, FormatName(entry.Item2))), genericUniverse["external"].Entries); - - Dictionary empty = groups[FormatName(Prefix + "Empty,test")]; - Assert.Equal(new[] { "external", "proxy" }, empty.Keys); - Assert.All(empty.Values, map => Assert.Empty(map.Entries)); - - Dictionary invalid = groups[FormatName(Prefix + "Invalid,test")]; - Assert.Equal(new[] { "external", "proxy" }, invalid.Keys); - Assert.Equal(nativeAot ? [] : new[] - { - ("duplicate", "System.Object,System.Private.CoreLib"), - ("duplicate", "System.String,System.Private.CoreLib") - }.Select(entry => (entry.Item1, FormatName(entry.Item2))), invalid["external"].Entries); - Assert.Equal(nativeAot ? [] : new[] - { - (Prefix + "Source,test", "System.Object,System.Private.CoreLib"), - (Prefix + "Source,test", "System.String,System.Private.CoreLib") - }.Select(entry => (FormatName(entry.Item1), FormatName(entry.Item2))), invalid["proxy"].Entries); - - Dictionary copied = groups[FormatName(LibraryPrefix + "TypeMapOutputGroup,Maps+Library")]; - Assert.Equal(new[] { "external" }, copied.Keys); - Assert.Equal(new[] - { - ("copied", LibraryPrefix + "TypeMapOutputTarget,Maps+Library") - }.Select(entry => (entry.Item1, FormatName(entry.Item2))), copied["external"].Entries); - - string FormatName(string name) => nativeAot ? name : CanonicalName(name); - } - - private static string CanonicalName(string name) => TypeName.Parse(name).AssemblyQualifiedName; - - private static Dictionary> ReadArtifact(string path, bool nativeAot) - { - using XmlReader reader = XmlReader.Create(path, new XmlReaderSettings - { - DtdProcessing = DtdProcessing.Prohibit, - IgnoreWhitespace = true - }); - Assert.Equal(XmlNodeType.Element, reader.MoveToContent()); - Assert.Equal("typemaps", reader.Name); - Assert.Equal("1", reader.GetAttribute("version")); - var groups = new Dictionary>(StringComparer.Ordinal); - Dictionary? maps = null; - Map? map = null; - string? mapKind = null; - while (reader.Read()) - { - if (reader.NodeType != XmlNodeType.Element) - continue; - - switch (reader.Name) - { - case "group": - Assert.Equal(1, reader.Depth); - string? groupType = reader.GetAttribute("type"); - Assert.NotNull(groupType); - maps = new Dictionary(StringComparer.Ordinal); - map = null; - mapKind = null; - Assert.True(groups.TryAdd(FormatName(groupType), maps), $"Duplicate universe '{groupType}'."); - break; - case "external": - case "proxy": - Assert.Equal(2, reader.Depth); - Assert.Equal(0, reader.AttributeCount); - Assert.NotNull(maps); - map = new Map(); - mapKind = reader.Name; - Assert.True(maps.TryAdd(reader.Name, map), $"Duplicate '{reader.Name}' section."); - break; - case "entry": - Assert.Equal(3, reader.Depth); - Assert.Equal(2, reader.AttributeCount); - Assert.NotNull(map); - string? key = reader.GetAttribute("key"); - string? target = reader.GetAttribute("value"); - Assert.NotNull(key); - Assert.NotNull(target); - map.Entries.Add((mapKind == "proxy" ? FormatName(key) : key, FormatName(target))); - break; - default: - Assert.Fail($"Unexpected element '{reader.Name}'."); - break; - } - } - - return groups; - - string FormatName(string name) => nativeAot ? name : CanonicalName(name); - } -} From 642a02184463c2efb27f0ff88d3283a94e62c395 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 9 Oct 2026 15:54:58 +0200 Subject: [PATCH 3/7] Omit unconfigured trimming test target imports Generate the optional Import element only when additional targets are specified, avoiding empty Project attributes in other trimming test projects. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- eng/testing/linker/project.csproj.template | 2 +- eng/testing/linker/trimmingTests.targets | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/eng/testing/linker/project.csproj.template b/eng/testing/linker/project.csproj.template index 8779078dc48229..a125c14e0d88cf 100644 --- a/eng/testing/linker/project.csproj.template +++ b/eng/testing/linker/project.csproj.template @@ -86,7 +86,7 @@ - + {AdditionalTargetsImport} diff --git a/eng/testing/linker/trimmingTests.targets b/eng/testing/linker/trimmingTests.targets index d6911e18e1ab77..ba183a4206fa13 100644 --- a/eng/testing/linker/trimmingTests.targets +++ b/eng/testing/linker/trimmingTests.targets @@ -59,6 +59,8 @@ <_projectDir>%(TestConsoleApps.ProjectDir)\ <_projectFile>%(TestConsoleApps.ProjectFile) <_projectSourceFile>%(TestConsoleApps.ProjectCompileItems) + <_additionalTargetsImport> + <_additionalTargetsImport Condition="'%(TestConsoleApps.AdditionalTargets)' != ''"><Import Project="%(TestConsoleApps.AdditionalTargets)" /> @@ -114,7 +116,7 @@ .Replace('{CoreCLRBuildIntegrationDir}', '$(CoreCLRBuildIntegrationDir)') .Replace('{RuntimeHostConfigurationOptions}', '$(_runtimeHostConfigurationOptionsString)') .Replace('{AdditionalProjectReferences}', '$(_additionalProjectReferencesString)') - .Replace('{AdditionalTargets}', '%(TestConsoleApps.AdditionalTargets)') + .Replace('{AdditionalTargetsImport}', '$(_additionalTargetsImport)') .Replace('{RepositoryEngineeringDir}', '$(RepositoryEngineeringDir)') .Replace('{MonoAOTCompilerDir}', '$(MonoAOTCompilerDir)') .Replace('{MonoProjectRoot}', '$(MonoProjectRoot)') From 9af6503f5bbde3cc78aac135aa61ce6582bbbba5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 13 Oct 2026 16:09:10 +0200 Subject: [PATCH 4/7] Simplify typemap output opt-in and remove added trimming tests Enable typemap output with a non-empty TypeMapXmlFile instead of a separate boolean, and update the documentation. Remove the added XML and SDK lifecycle test setup, restoring the original TypeMap runtime test and shared trimming-test harness. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/tools/illink/data-formats.md | 7 +- docs/tools/illink/illink-options.md | 9 +- .../SupportFiles/Directory.Build.targets | 6 - eng/testing/linker/project.csproj.template | 1 - eng/testing/linker/trimmingTests.targets | 35 +-- .../Microsoft.NETCore.Native.targets | 3 +- .../Dependencies/TypeMapTestLibrary.cs | 21 -- .../Dependencies/TypeMapTestLibrary.csproj | 10 - ...Runtime.InteropServices.TrimmingTests.proj | 8 +- .../tests/TrimmingTests/TypeMap.cs | 219 +----------------- .../tests/TrimmingTests/TypeMapTests.targets | 52 ----- .../build/Microsoft.NET.ILLink.targets | 3 +- 12 files changed, 15 insertions(+), 359 deletions(-) delete mode 100644 src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.cs delete mode 100644 src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj delete mode 100644 src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMapTests.targets diff --git a/docs/tools/illink/data-formats.md b/docs/tools/illink/data-formats.md index dbe754382d3a84..a30cdf93380d5e 100644 --- a/docs/tools/illink/data-formats.md +++ b/docs/tools/illink/data-formats.md @@ -19,9 +19,10 @@ ILLink and NativeAOT ILC accept `--output-typemaps ` to write a UTF-8 XML sidecar describing their retained type maps. This is an opt-in intermediate build artifact, independent of dependency tracing. It does not root any additional types or maps. -In SDK builds, `TypeMapXmlFile` specifies the output path and defaults to the -intermediate output directory. The sidecar is not published or bundled with the -application. +In SDK builds, setting `TypeMapXmlFile` to a non-empty path enables generation +and selects the output path. If it is unset or empty, no artifact is written. +SDK consumers can choose an intermediate path so the sidecar is not published +or bundled with the application. ```xml diff --git a/docs/tools/illink/illink-options.md b/docs/tools/illink/illink-options.md index 25b051fb799e31..2f1e868eb8fa5c 100644 --- a/docs/tools/illink/illink-options.md +++ b/docs/tools/illink/illink-options.md @@ -106,11 +106,10 @@ Example: generated by default. Both tools write to the path supplied on the command line. -For SDK builds, set `TypeMapGenerateXmlFile=true`. `TypeMapXmlFile` specifies the -path passed to the tool and defaults to -`$(IntermediateOutputPath)$(TargetName).typemaps.xml`. -The default location is under `obj`. The XML is not copied to the publish -directory or included in a single-file bundle. +For SDK builds, setting `TypeMapXmlFile` to a non-empty path enables output and +specifies the path passed to the tool. Leaving it unset or empty disables +output. SDK consumers should choose an intermediate path under `obj`; the XML +is not copied to the publish directory or included in a single-file bundle. The sidecar is an expected output of the trimming/compilation step. Deleting it causes that step to run again. An unchanged build skips the producer, while an diff --git a/eng/testing/linker/SupportFiles/Directory.Build.targets b/eng/testing/linker/SupportFiles/Directory.Build.targets index 640ca11f57c250..9687763d2fdee6 100644 --- a/eng/testing/linker/SupportFiles/Directory.Build.targets +++ b/eng/testing/linker/SupportFiles/Directory.Build.targets @@ -21,10 +21,4 @@ - - - - diff --git a/eng/testing/linker/project.csproj.template b/eng/testing/linker/project.csproj.template index a125c14e0d88cf..e52a544f8087a0 100644 --- a/eng/testing/linker/project.csproj.template +++ b/eng/testing/linker/project.csproj.template @@ -86,7 +86,6 @@ - {AdditionalTargetsImport} diff --git a/eng/testing/linker/trimmingTests.targets b/eng/testing/linker/trimmingTests.targets index ba183a4206fa13..eee97561702308 100644 --- a/eng/testing/linker/trimmingTests.targets +++ b/eng/testing/linker/trimmingTests.targets @@ -53,14 +53,12 @@ <_projectDir>%(TestConsoleApps.ProjectDir)\ <_projectFile>%(TestConsoleApps.ProjectFile) <_projectSourceFile>%(TestConsoleApps.ProjectCompileItems) - <_additionalTargetsImport> - <_additionalTargetsImport Condition="'%(TestConsoleApps.AdditionalTargets)' != ''"><Import Project="%(TestConsoleApps.AdditionalTargets)" /> @@ -72,16 +70,6 @@ <_additionalProjectReferencesString>@(_additionalProjectReference, '%0a') - - <_testProjectReference Remove="@(_testProjectReference)" /> - <_testProjectReference Include="%(TestConsoleApps.AdditionalProjectReferences)" /> - - - - <_additionalProjectReferencesString>$(_additionalProjectReferencesString) - @(_testProjectReference->'<ProjectReference Include="%(FullPath)" SkipUseReferenceAssembly="true" />', '%0a') - - <_additionalProjectSourceFiles Include="%(TestConsoleApps.AdditionalSourceFiles)" /> @@ -116,7 +104,6 @@ .Replace('{CoreCLRBuildIntegrationDir}', '$(CoreCLRBuildIntegrationDir)') .Replace('{RuntimeHostConfigurationOptions}', '$(_runtimeHostConfigurationOptionsString)') .Replace('{AdditionalProjectReferences}', '$(_additionalProjectReferencesString)') - .Replace('{AdditionalTargetsImport}', '$(_additionalTargetsImport)') .Replace('{RepositoryEngineeringDir}', '$(RepositoryEngineeringDir)') .Replace('{MonoAOTCompilerDir}', '$(MonoAOTCompilerDir)') .Replace('{MonoProjectRoot}', '$(MonoProjectRoot)') @@ -156,26 +143,8 @@ DependsOnTargets="GenerateProjects" Returns="@(TestConsoleApps)" /> - - - - <_testProjectToBuild Include="%(TestConsoleApps.AdditionalProjectReferences)" /> - - - - - - - + DependsOnTargets="GenerateProjects"> $(NativeIntermediateOutputPath)$(TargetName)$(ExportsFileExt) $(NativeObject) - $([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)', '$(TargetName).typemaps.xml')) - <_IlcTypeMapXmlFile Condition="'$(TypeMapGenerateXmlFile)' == 'true' and '$(BuildingFrameworkLibrary)' != 'true'">$(TypeMapXmlFile) + <_IlcTypeMapXmlFile Condition="'$(TypeMapXmlFile)' != '' and '$(BuildingFrameworkLibrary)' != 'true'">$(TypeMapXmlFile) IlcCompile diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.cs b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.cs deleted file mode 100644 index 69a9149a99aff7..00000000000000 --- a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.InteropServices; -using TypeMapTestLibrary; - -[assembly: TypeMap("dependency/Target", typeof(DependencyTarget))] -[assembly: TypeMap("dependency/Trimmed", typeof(UnusedTarget), typeof(UnusedSource))] -[assembly: TypeMapAssociation(typeof(DependencySource), typeof(DependencyProxy))] -[assembly: TypeMapAssociation(typeof(UnusedSource), typeof(UnusedTarget))] -[assembly: TypeMap("wrong/universe", typeof(DependencyTarget))] - -namespace TypeMapTestLibrary; - -public class DependencyUniverse; -public class OtherUniverse; -public class DependencySource; -public class DependencyTarget; -public class DependencyProxy; -public class UnusedSource; -public class UnusedTarget; diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj deleted file mode 100644 index f9d39601b32c95..00000000000000 --- a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - $(NetCoreAppCurrent) - true - - - - - - diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj index 53e0e6a966e06b..9aa8090bd00f56 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj @@ -23,12 +23,8 @@ IlcTrimMetadata - - IlcGenerateMstatFile;IlcGenerateDgmlFile;TypeMapGenerateXmlFile;Optimize - browser-wasm - $(MSBuildProjectDirectory)/Dependencies/TypeMapTestLibrary.csproj - $(MSBuildProjectDirectory)/TypeMapTests.targets - TypeMapTests.targets + + IlcGenerateMstatFile;IlcGenerateDgmlFile diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs index 36750ce437825e..5dcf5e35009ecd 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs @@ -3,12 +3,8 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Reflection.Metadata; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using System.Xml; -using TypeMapTestLibrary; [assembly: TypeMap("TrimTargetIsTarget", typeof(TargetAndTrimTarget), typeof(TargetAndTrimTarget))] [assembly: TypeMap("TrimTargetIsUnrelated", typeof(TargetType), typeof(TrimTarget))] @@ -18,14 +14,6 @@ [assembly: TypeMap("UnusedName", typeof(UnusedTargetType), typeof(TrimTarget))] [assembly: TypeMapAssociation(typeof(UnusedSourceClass), typeof(UnusedProxyType))] -[assembly: TypeMap("example/Target", typeof(TargetType))] -[assembly: TypeMap("example/\u0130Class", typeof(TargetType))] -[assembly: TypeMap("[Lexample/Target;", typeof(TargetType[]))] -[assembly: TypeMapAssociation(typeof(SourceClass), typeof(GenericProxy))] -[assembly: TypeMap("unused", typeof(UnreferencedTargetType), typeof(UnreferencedTrimTarget))] -[assembly: TypeMapAssemblyTarget("TypeMapTestLibrary")] -[assembly: TypeMap("local/Target", typeof(TargetType))] - if (args.Length > 1 && args[0] == "instantiate") { Console.WriteLine("This code path should never actually be called. It exists exclusively for the trimmer to see that types are used in a way that it can't fully analyze."); @@ -70,7 +58,7 @@ return 4; } -if (!RuntimeFeature.IsDynamicCodeSupported && GetTypeWithoutTrimAnalysis(nameof(TrimTarget)) is not null) +if (GetTypeWithoutTrimAnalysis(nameof(TrimTarget)) is not null) { Console.WriteLine("TrimTarget should not be preserved if the only place that would preserve it is a check that is optimized away."); return 5; @@ -107,205 +95,8 @@ return 10; } -IReadOnlyDictionary nestedTypeMap = TypeMapping.GetOrCreateExternalTypeMapping(); -if (!nestedTypeMap.TryGetValue("example/Target", out Type nestedTarget) || - nestedTarget != targetType || - !nestedTypeMap.TryGetValue("example/\u0130Class", out Type unicodeTarget) || - unicodeTarget != targetType || - !nestedTypeMap.TryGetValue("[Lexample/Target;", out Type arrayTarget) || - arrayTarget != targetType.MakeArrayType()) -{ - Console.WriteLine("Nested universe external mappings do not match expected types."); - return 11; -} - -IReadOnlyDictionary nestedProxyMap = TypeMapping.GetOrCreateProxyTypeMapping(); -if (!nestedProxyMap.TryGetValue(typeof(SourceClass), out Type genericProxy) || - genericProxy != GetTypeWithoutTrimAnalysis("GenericProxy`1[System.Int32]")) -{ - Console.WriteLine("Nested universe proxy mapping does not match expected type."); - return 12; -} - -IReadOnlyDictionary emptyTypeMap = TypeMapping.GetOrCreateExternalTypeMapping(); -if (emptyTypeMap.TryGetValue("unused", out _)) -{ - Console.WriteLine("The empty map contains a trimmed entry."); - return 13; -} - -Console.WriteLine(new DependencySource().GetHashCode()); -IReadOnlyDictionary dependencyMap = TypeMapping.GetOrCreateExternalTypeMapping(); -if (!dependencyMap.TryGetValue("dependency/Target", out Type dependencyTarget) || - dependencyTarget.FullName != "TypeMapTestLibrary.DependencyTarget" || - dependencyMap.TryGetValue("dependency/Trimmed", out _)) -{ - Console.WriteLine("Cross-assembly external mappings do not match expected types."); - return 15; -} - -IReadOnlyDictionary dependencyProxyMap = TypeMapping.GetOrCreateProxyTypeMapping(); -if (!dependencyProxyMap.TryGetValue(typeof(DependencySource), out Type dependencyProxy) || - dependencyProxy.FullName != "TypeMapTestLibrary.DependencyProxy") -{ - Console.WriteLine("Cross-assembly proxy mapping does not match expected type."); - return 16; -} - -IReadOnlyDictionary otherMap = TypeMapping.GetOrCreateExternalTypeMapping(); -if (!otherMap.TryGetValue("local/Target", out Type otherTarget) || otherTarget != targetType || - otherMap.TryGetValue("wrong/universe", out _)) -{ - Console.WriteLine("An assembly target for one universe included entries from another universe."); - return 17; -} - -if (!CheckArtifact()) - return 14; - return 100; -static bool CheckArtifact() -{ - Dictionary External, Dictionary Proxy)> groups = - ReadArtifact(Path.Combine(AppContext.BaseDirectory, "typemaps.xml")); - if (groups.Count != 5) - { - Console.WriteLine("Unexpected type map universe count."); - return false; - } - - string assemblyName = typeof(UsedTypeMap).Assembly.GetName().Name; - var used = groups[NormalizeTypeName($"{nameof(UsedTypeMap)},{assemblyName}")]; - var nested = groups[NormalizeTypeName($"NestedContainer+Universe,{assemblyName}")]; - var empty = groups[NormalizeTypeName($"{nameof(EmptyTypeMap)},{assemblyName}")]; - var dependency = groups[NormalizeTypeName("TypeMapTestLibrary.DependencyUniverse,TypeMapTestLibrary")]; - var other = groups[NormalizeTypeName("TypeMapTestLibrary.OtherUniverse,TypeMapTestLibrary")]; - - return CheckMap(used.External, proxy: false, - ("TrimTargetIsTarget", nameof(TargetAndTrimTarget)), - ("TrimTargetIsUnrelated", nameof(TargetType))) && - CheckMap(used.Proxy, proxy: true, (nameof(SourceClass), nameof(ProxyType))) && - CheckMap(nested.External, proxy: false, - ("example/Target", nameof(TargetType)), - ("example/\u0130Class", nameof(TargetType)), - ("[Lexample/Target;", nameof(TargetType) + "[]")) && - CheckMap(nested.Proxy, proxy: true, - (nameof(SourceClass), "GenericProxy`1[[System.Int32,System.Private.CoreLib]]")) && - CheckMap(empty.External, proxy: false) && - empty.Proxy is null && - CheckMap(dependency.External, proxy: false, - ("dependency/Target", "TypeMapTestLibrary.DependencyTarget,TypeMapTestLibrary")) && - CheckMap(dependency.Proxy, proxy: true, - ("TypeMapTestLibrary.DependencySource,TypeMapTestLibrary", "TypeMapTestLibrary.DependencyProxy,TypeMapTestLibrary")) && - CheckMap(other.External, proxy: false, ("local/Target", nameof(TargetType))) && - other.Proxy is null; - - bool CheckMap(Dictionary map, bool proxy, params (string Key, string Type)[] expected) - { - if (map is null || map.Count != expected.Length) - { - Console.WriteLine($"Unexpected {(proxy ? "proxy" : "external")} map entry count."); - return false; - } - - foreach ((string key, string type) in expected) - { - string expectedKey = proxy ? QualifyTypeName(key) : key; - string expectedType = QualifyTypeName(type); - if (!map.TryGetValue(expectedKey, out string actualType) || actualType != expectedType) - { - Console.WriteLine($"Missing mapping '{key}' to '{type}'."); - return false; - } - } - - return true; - } - - string QualifyTypeName(string type) - { - TypeName name = TypeName.Parse(type); - - return name.AssemblyName is null - ? NormalizeTypeName($"{type},{assemblyName}") - : name.AssemblyQualifiedName; - } -} - -static Dictionary External, Dictionary Proxy)> ReadArtifact(string path) -{ - using XmlReader reader = XmlReader.Create(path); - reader.MoveToContent(); - if (reader.Name != "typemaps" || reader.HasAttributes) - throw new InvalidDataException("Unexpected type map XML root."); - - var groups = new Dictionary External, Dictionary Proxy)>(StringComparer.Ordinal); - bool emptyRoot = reader.IsEmptyElement; - reader.ReadStartElement("typemaps"); - while (!emptyRoot && reader.MoveToContent() == XmlNodeType.Element) - { - if (reader.Name != "group" || reader.AttributeCount != 1) - throw new InvalidDataException("Unexpected type map group."); - - string type = reader.GetAttribute("type") ?? throw new InvalidDataException("Missing type map group type."); - bool emptyGroup = reader.IsEmptyElement; - reader.ReadStartElement("group"); - (Dictionary External, Dictionary Proxy) maps = (null, null); - while (!emptyGroup && reader.MoveToContent() == XmlNodeType.Element) - { - switch (reader.Name) - { - case "external" when maps.External is null: - maps.External = ReadMap(reader, proxy: false); - break; - case "proxy" when maps.Proxy is null: - maps.Proxy = ReadMap(reader, proxy: true); - break; - default: - throw new InvalidDataException($"Unexpected or duplicate map section '{reader.Name}'."); - } - } - if (!emptyGroup) - reader.ReadEndElement(); - groups.Add(NormalizeTypeName(type), maps); - } - if (!emptyRoot) - reader.ReadEndElement(); - reader.MoveToContent(); - - return groups; - - static Dictionary ReadMap(XmlReader reader, bool proxy) - { - if (reader.HasAttributes) - throw new InvalidDataException("Unexpected map section attributes."); - - var map = new Dictionary(StringComparer.Ordinal); - bool emptyMap = reader.IsEmptyElement; - reader.ReadStartElement(); - while (!emptyMap && reader.MoveToContent() == XmlNodeType.Element) - { - if (reader.Name != "entry" || reader.AttributeCount != 2) - throw new InvalidDataException("Unexpected type map entry."); - - string key = reader.GetAttribute("key") ?? throw new InvalidDataException("Missing type map entry key."); - string value = reader.GetAttribute("value") ?? throw new InvalidDataException("Missing type map entry value."); - map.Add(proxy ? NormalizeTypeName(key) : key, NormalizeTypeName(value)); - bool emptyEntry = reader.IsEmptyElement; - reader.ReadStartElement("entry"); - if (!emptyEntry) - reader.ReadEndElement(); - } - if (!emptyMap) - reader.ReadEndElement(); - - return map; - } -} - -static string NormalizeTypeName(string name) => TypeName.Parse(name).AssemblyQualifiedName; - [MethodImpl(MethodImplOptions.NoInlining)] static Type GetTypeWithoutTrimAnalysis(string typeName) { @@ -325,11 +116,3 @@ class UnusedTypeMap; class UnusedTargetType; class UnusedSourceClass; class UnusedProxyType; - -class NestedContainer -{ - public class Universe; -} - -class GenericProxy; -class EmptyTypeMap; diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMapTests.targets b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMapTests.targets deleted file mode 100644 index 80ef87a973e15c..00000000000000 --- a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMapTests.targets +++ /dev/null @@ -1,52 +0,0 @@ - - - - <_TypeMapContents>$([System.IO.File]::ReadAllText('$(TypeMapXmlFile)')) - <_TypeMapWritten>$([System.IO.File]::GetLastWriteTimeUtc('$(TypeMapXmlFile)').Ticks) - <_TypeMapCustomFile>$(IntermediateOutputPath)reports with spaces/type maps.xml - <_TypeMapDisabledFile>$(IntermediateOutputPath)disabled.typemaps.xml - - - - <_PublishedTypeMap Include="@(ResolvedFileToPublish)" - Condition="'%(FullPath)' == '$(TypeMapXmlFile)'" /> - - - - - - - - - - - - - - <_TypeMapCustomWritten>$([System.IO.File]::GetLastWriteTimeUtc('$(_TypeMapCustomFile)').Ticks) - - - - - - - - - - - - - - diff --git a/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets index cd174aa56a7e5d..dd513a1029e953 100644 --- a/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets +++ b/src/tools/illink/src/ILLink.Tasks/build/Microsoft.NET.ILLink.targets @@ -85,8 +85,7 @@ Copyright (c) .NET Foundation. All rights reserved. <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --typemap-entry-assembly "$(TypeMapEntryAssembly)" - - $([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(IntermediateOutputPath)', '$(TargetName).typemaps.xml')) + <_TrimmerTypeMapXmlFile>$(TypeMapXmlFile) <_ExtraTrimmerArgs>$(_ExtraTrimmerArgs) --output-typemaps "$(TypeMapXmlFile)" From c2c213f451ef4da14f23a0ff959fb97776c9e140 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 19 Oct 2026 18:17:44 +0200 Subject: [PATCH 5/7] Add shared typemap XML output coverage Verify ILLink and NativeAOT typemap XML using a shared testcase and XPath expectations. Run production ILC compilation for output checks with a bounded compiler process lifetime. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ILTrim.Tests/ILTrimExpectedFailures.txt | 1 + .../ILCompiler.Trimming.Tests.csproj | 4 + .../TestCasesRunner/ILCompilerOptions.cs | 2 + .../TestCasesRunner/ResultChecker.cs | 1 + .../TrimmingArgumentBuilder.cs | 5 + .../TestCasesRunner/TrimmingDriver.cs | 95 +++++++++++++++++- .../ReflectionTests.cs | 6 ++ .../Assertions/ExpectedXmlElementAttribute.cs | 17 ++++ .../Reflection/TypeMapOutput.cs | 96 +++++++++++++++++++ .../TestCasesRunner/ResultChecker.cs | 1 + .../ResultChecker.Xml.cs | 44 +++++++++ .../test/Trimming.Tests.Shared/TestRunner.cs | 6 ++ 12 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedXmlElementAttribute.cs create mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapOutput.cs create mode 100644 src/tools/illink/test/Trimming.Tests.Shared/ResultChecker.Xml.cs diff --git a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt index 56245f38bd010a..5d8c12cc057e29 100644 --- a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt +++ b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt @@ -350,6 +350,7 @@ Reflection.TypeHierarchyLibraryModeSuppressions Reflection.TypeHierarchyReflectionWarnings Reflection.TypeMap Reflection.TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed +Reflection.TypeMapOutput Reflection.TypeUsedViaReflection Reflection.UnsafeAccessor RequiresCapability.BasicRequires diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/ILCompiler.Trimming.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/ILCompiler.Trimming.Tests.csproj index e8cef2b9014c91..11dbf0ef9c6259 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/ILCompiler.Trimming.Tests.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/ILCompiler.Trimming.Tests.csproj @@ -22,6 +22,7 @@ + @@ -32,6 +33,9 @@ + + $(DotNetTool) + $(RuntimeBinDir) diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ILCompilerOptions.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ILCompilerOptions.cs index 681fd981a3f4d8..a91d6e887934f9 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ILCompilerOptions.cs +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ILCompilerOptions.cs @@ -22,6 +22,8 @@ public class ILCompilerOptions public Dictionary WarningsAsErrors = new Dictionary(); public List SuppressedWarningCategories = new List(); public bool DisableGeneratedCodeHeuristics; + public string? OutputFileName; public string? TypeMapEntryAssembly; + public string? TypeMapFileName; } } diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ResultChecker.cs index 66703476bcbd9a..76e48debbf1ab3 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ResultChecker.cs @@ -86,6 +86,7 @@ public virtual void Check(TrimmedTestCaseResult testResult) } } + VerifyExpectedXmlOutput(testResult, original); AdditionalChecking(testResult, original); } finally diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingArgumentBuilder.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingArgumentBuilder.cs index afdac96827e93a..a9553d15635f37 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingArgumentBuilder.cs +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingArgumentBuilder.cs @@ -65,6 +65,7 @@ public virtual void AddReference(NPath path) public virtual void AddOutputDirectory(NPath directory) { + Options.OutputFileName = directory.ToString(); } public virtual void AddLinkXmlFile(string file) @@ -167,6 +168,10 @@ public virtual void AddAdditionalArgument(string flag, string[] values) { Options.TypeMapEntryAssembly = values[0]; } + else if (flag == "--output-typemaps") + { + Options.TypeMapFileName = values[0]; + } else if (flag == "--singlewarn") { Options.SingleWarn = true; diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingDriver.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingDriver.cs index acf155339d2055..0a7b8707fb684b 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingDriver.cs +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingDriver.cs @@ -3,18 +3,23 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Runtime.InteropServices; +using System.Threading.Tasks; using System.Xml; using ILCompiler; using ILCompiler.Dataflow; +using ILLink.Shared; using ILLink.Shared.TrimAnalysis; using Internal.IL; using Internal.TypeSystem; using Internal.TypeSystem.Ecma; +using Xunit; namespace Mono.Linker.Tests.TestCasesRunner { @@ -170,7 +175,95 @@ public ILScanResults Trim(ILCompilerOptions options, TrimmingCustomizations? cus .UseTypeMapManager(typeMapManager) .ToILScanner(); - return scanner.Scan(); + ILScanResults results = scanner.Scan(); + if (options.TypeMapFileName is string typeMapFileName) + Compile(options, typeMapFileName); + + return results; + } + + private static void Compile(ILCompilerOptions options, string typeMapFileName) + { + string runtimeBinDirectory = Assert.IsType(AppContext.GetData("Mono.Linker.Tests.RuntimeBinDirectory")); + string compilerPath = Path.Combine(runtimeBinDirectory, "ilc", "ilc.dll"); + Assert.True(File.Exists(compilerPath), $"ILC was not found at '{compilerPath}'."); + Assert.NotNull(options.OutputFileName); + + // RyuJIT permits only one compiler host per process. + var startInfo = new ProcessStartInfo + { + FileName = Assert.IsType(AppContext.GetData("Mono.Linker.Tests.DotNetHostPath")), + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + startInfo.ArgumentList.Add(compilerPath); + foreach (string inputFile in options.InputFilePaths.Values) + startInfo.ArgumentList.Add(inputFile); + AddArguments("--out", [options.OutputFileName]); + AddArguments("--output-typemaps", [typeMapFileName]); + startInfo.ArgumentList.Add("-O"); + startInfo.ArgumentList.Add("--scanreflection"); + AddArguments("--reference", options.ReferenceFilePaths.Values); + AddArguments("--initassembly", options.InitAssemblies); + AddArguments("--trim", options.TrimAssemblies); + AddArguments("--root", options.AdditionalRootAssemblies); + AddArguments("--conditionalroot", options.RootEntireAssemblies); + AddArguments("--descriptor", options.Descriptors); + AddArguments("--substitution", options.SubstitutionFiles); + foreach (var feature in options.FeatureSwitches) + AddArguments("--feature", [$"{feature.Key}={(feature.Value ? "true" : "false")}"]); + if (options.TypeMapEntryAssembly is string entryAssembly) + AddArguments("--typemap-entry-assembly", [entryAssembly]); + if (options.SingleWarn) + startInfo.ArgumentList.Add("--singlewarn"); + if (options.TreatWarningsAsErrors) + startInfo.ArgumentList.Add("--warnaserror"); + foreach (var warning in options.WarningsAsErrors) + { + string code = "IL" + warning.Key.ToString("D4", CultureInfo.InvariantCulture); + AddArguments(warning.Value ? "--warnaserr" : "--nowarnaserr", [code]); + } + foreach (string category in options.SuppressedWarningCategories) + { + startInfo.ArgumentList.Add(category switch + { + MessageSubCategory.TrimAnalysis => "--notrimwarn", + MessageSubCategory.AotAnalysis => "--noaotwarn", + _ => throw new InvalidOperationException($"Unsupported warning category '{category}'.") + }); + } + if (options.DisableGeneratedCodeHeuristics) + startInfo.ArgumentList.Add("--disable-generated-code-heuristics"); + + using var process = new Process { StartInfo = startInfo }; + Assert.True(process.Start(), $"Failed to start ILC at '{compilerPath}'."); + Task outputTask = process.StandardOutput.ReadToEndAsync(); + Task errorTask = process.StandardError.ReadToEndAsync(); + const int TimeoutMinutes = 2; + bool exited = process.WaitForExit(TimeSpan.FromMinutes(TimeoutMinutes)); + if (!exited) + process.Kill(entireProcessTree: true); + + string output = outputTask.GetAwaiter().GetResult(); + string error = errorTask.GetAwaiter().GetResult(); + File.WriteAllText(Path.ChangeExtension(options.OutputFileName, ".log"), output + error); + Assert.True(exited, + $"ILC compilation timed out after {TimeoutMinutes} minutes:{Environment.NewLine}{output}{error}"); + Assert.True(process.ExitCode == 0, + $"ILC compilation failed with exit code {process.ExitCode}:{Environment.NewLine}{output}{error}"); + Assert.True(File.Exists(options.OutputFileName), $"ILC did not produce the native object file '{options.OutputFileName}'."); + Assert.True(new FileInfo(options.OutputFileName).Length > 0, $"ILC produced an empty native object file '{options.OutputFileName}'."); + + void AddArguments(string flag, IEnumerable values) + { + foreach (string value in values) + { + startInfo.ArgumentList.Add(flag); + startInfo.ArgumentList.Add(value); + } + } } public static void ComputeDefaultOptions(out TargetOS os, out TargetArchitecture arch) diff --git a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ReflectionTests.cs b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ReflectionTests.cs index 2f0ee4a081df57..c665026c41d3d8 100644 --- a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ReflectionTests.cs +++ b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ReflectionTests.cs @@ -190,6 +190,12 @@ public Task TypeMapEntryAssembly() return RunTest(); } + [Fact] + public Task TypeMapOutput() + { + return RunTest(); + } + [Fact] public Task TypeBaseTypeUseViaReflection() { diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedXmlElementAttribute.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedXmlElementAttribute.cs new file mode 100644 index 00000000000000..d224f413234047 --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedXmlElementAttribute.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Mono.Linker.Tests.Cases.Expectations.Assertions; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public class ExpectedXmlElementAttribute : BaseExpectedLinkedBehaviorAttribute +{ + public ExpectedXmlElementAttribute(string fileName, string xpath, int count = 1) + { + ArgumentException.ThrowIfNullOrEmpty(fileName); + ArgumentException.ThrowIfNullOrEmpty(xpath); + ArgumentOutOfRangeException.ThrowIfNegative(count); + } +} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapOutput.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapOutput.cs new file mode 100644 index 00000000000000..ccd2bbbb49548c --- /dev/null +++ b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapOutput.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Cases.Expectations.Metadata; +using Mono.Linker.Tests.Cases.Reflection; +using Mono.Linker.Tests.Cases.Reflection.Dependencies; + +[assembly: TypeMap("example/Target", typeof(TypeMapOutput.Target))] +[assembly: TypeMap("example/\u0130<&\"Target", typeof(TypeMapOutput.Target[]))] +[assembly: TypeMap("trimmed", typeof(TypeMapOutput.UnusedTarget), typeof(TypeMapOutput.UnusedSource))] +[assembly: TypeMapAssociation(typeof(TypeMapOutput.Source), typeof(TypeMapOutput.Proxy))] +[assembly: TypeMapAssociation(typeof(TypeMapOutput.UnusedSource), typeof(TypeMapOutput.UnusedTarget))] +[assembly: TypeMap("trimmed", typeof(TypeMapOutput.UnusedTarget), typeof(TypeMapOutput.UnusedSource))] +[assembly: TypeMap("external", typeof(TypeMapOutput.Target))] +[assembly: TypeMapAssociation(typeof(TypeMapOutput.Source), typeof(TypeMapOutput.Proxy))] +[assembly: TypeMap("external", typeof(TypeMapOutput.Target))] +[assembly: TypeMapAssociation(typeof(TypeMapOutput.Source), typeof(TypeMapOutput.Proxy))] +[assembly: TypeMap("unused", typeof(TypeMapOutput.UnusedTarget))] +[assembly: TypeMapAssemblyTarget("TypeMapEntryAssemblyLib")] + +namespace Mono.Linker.Tests.Cases.Reflection; + +[SetupCompileBefore("TypeMapEntryAssemblyLib.dll", new[] { "Dependencies/TypeMapEntryAssemblyLib.cs" })] +[SetupLinkerAction("link", "System.Private.CoreLib")] +[SetupLinkerArgument("--ignore-link-attributes", "false")] +[SetupLinkerArgument("--output-typemaps", FileName)] +#if NATIVEAOT +[SetupLinkerArgument("--warnaserror")] +[SetupLinkerArgument("--feature", "System.StartupHookProvider.IsSupported", "false")] +#endif +[SkipKeptItemsValidation] +[ExpectedNoWarnings] +[ExpectedXmlElement(FileName, "/typemaps")] +[ExpectedXmlElement(FileName, "/typemaps[@*]", 0)] +[ExpectedXmlElement(FileName, "/typemaps/group", 5)] +[ExpectedXmlElement(FileName, "/typemaps/group[count(@*) != 1 or not(@type)]", 0)] +[ExpectedXmlElement(FileName, "/typemaps/group[count(external) > 1 or count(proxy) > 1]", 0)] +[ExpectedXmlElement(FileName, "/typemaps/group/*[not(self::external or self::proxy) or @*]", 0)] +[ExpectedXmlElement(FileName, "/typemaps/group/*/*[not(self::entry)]", 0)] +[ExpectedXmlElement(FileName, "/typemaps/group/*/entry", 9)] +[ExpectedXmlElement(FileName, "/typemaps/group/*/entry[count(@*) != 2 or not(@key) or not(@value)]", 0)] +// The producer formatters differ in whitespace after assembly-name separators. +[ExpectedXmlElement(FileName, UsedGroup + "/external/entry", 2)] +[ExpectedXmlElement(FileName, UsedGroup + "/external/entry[@key='example/Target' and translate(@value, ' ', '')='" + TypePrefix + "Target,test']")] +[ExpectedXmlElement(FileName, UsedGroup + "/external/entry[@key='example/\u0130<&\"Target' and translate(@value, ' ', '')='" + TypePrefix + "Target[],test']")] +[ExpectedXmlElement(FileName, UsedGroup + "/proxy/" + ProxyEntry)] +[ExpectedXmlElement(FileName, EmptyGroup + "/external[not(entry)]")] +[ExpectedXmlElement(FileName, EmptyGroup + "/proxy", 0)] +[ExpectedXmlElement(FileName, ExternalOnlyGroup + "/external/entry[@key='external' and translate(@value, ' ', '')='" + TypePrefix + "Target,test']")] +[ExpectedXmlElement(FileName, ExternalOnlyGroup + "/proxy", 0)] +[ExpectedXmlElement(FileName, ProxyOnlyGroup + "/external", 0)] +[ExpectedXmlElement(FileName, ProxyOnlyGroup + "/proxy/" + ProxyEntry)] +[ExpectedXmlElement(FileName, DependencyGroup + "/external/entry[@key='entry_type1' and translate(@value, ' ', '')='" + DependencyPrefix + "EntryType1,TypeMapEntryAssemblyLib']")] +[ExpectedXmlElement(FileName, DependencyGroup + "/external/entry[@key='entry_type2' and translate(@value, ' ', '')='" + DependencyPrefix + "EntryType2,TypeMapEntryAssemblyLib']")] +[ExpectedXmlElement(FileName, DependencyGroup + "/proxy/entry[translate(@key, ' ', '')='" + DependencyPrefix + "EntrySource1,TypeMapEntryAssemblyLib' and translate(@value, ' ', '')='" + DependencyPrefix + "EntryProxy1,TypeMapEntryAssemblyLib']")] +[ExpectedXmlElement(FileName, DependencyGroup + "/proxy/entry[translate(@key, ' ', '')='" + DependencyPrefix + "EntrySource2,TypeMapEntryAssemblyLib' and translate(@value, ' ', '')='" + DependencyPrefix + "EntryProxy2,TypeMapEntryAssemblyLib']")] +class TypeMapOutput +{ + private const string FileName = "type maps.xml"; + private const string TypePrefix = "Mono.Linker.Tests.Cases.Reflection.TypeMapOutput+"; + private const string DependencyPrefix = "Mono.Linker.Tests.Cases.Reflection.Dependencies."; + private const string UsedGroup = "/typemaps/group[translate(@type, ' ', '')='" + TypePrefix + "UsedUniverse,test']"; + private const string EmptyGroup = "/typemaps/group[translate(@type, ' ', '')='" + TypePrefix + "EmptyUniverse,test']"; + private const string ExternalOnlyGroup = "/typemaps/group[translate(@type, ' ', '')='" + TypePrefix + "ExternalOnlyUniverse,test']"; + private const string ProxyOnlyGroup = "/typemaps/group[translate(@type, ' ', '')='" + TypePrefix + "ProxyOnlyUniverse,test']"; + private const string DependencyGroup = "/typemaps/group[translate(@type, ' ', '')='" + DependencyPrefix + "TypeMapEntryAssemblyGroup,TypeMapEntryAssemblyLib']"; + private const string ProxyEntry = "entry[translate(@key, ' ', '')='" + TypePrefix + "Source,test' and translate(@value, ' ', '')='" + TypePrefix + "Proxy`1[[System.Int32,System.Private.CoreLib]],test']"; + + public static void Main() + { + Console.WriteLine(new Source()); + Console.WriteLine(new EntrySource1()); + Console.WriteLine(new EntrySource2()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); + Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); + } + + public class UsedUniverse; + public class EmptyUniverse; + public class ExternalOnlyUniverse; + public class ProxyOnlyUniverse; + public class UnusedUniverse; + public class Target; + public class Source; + public class Proxy; + public class UnusedSource; + public class UnusedTarget; +} diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs index e7dbad5bc01eed..d158c147b5aaa2 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs @@ -127,6 +127,7 @@ public virtual void Check(TrimmedTestCaseResult linkResult) VerifyExpectedDependencyTrace(linkResult.MetadataProvider, linkResult.OutputAssemblyPath); } + VerifyExpectedXmlOutput(linkResult, original); VerifyLinkingOfOtherAssemblies(original); VerifyILOfOtherAssemblies(linkResult); AdditionalChecking(linkResult, original); diff --git a/src/tools/illink/test/Trimming.Tests.Shared/ResultChecker.Xml.cs b/src/tools/illink/test/Trimming.Tests.Shared/ResultChecker.Xml.cs new file mode 100644 index 00000000000000..709591146de17d --- /dev/null +++ b/src/tools/illink/test/Trimming.Tests.Shared/ResultChecker.Xml.cs @@ -0,0 +1,44 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Xml; +using Mono.Cecil; +using Mono.Linker.Tests.Cases.Expectations.Assertions; +using Mono.Linker.Tests.Extensions; +using Xunit; + +namespace Mono.Linker.Tests.TestCasesRunner; + +public partial class ResultChecker +{ + private static void VerifyExpectedXmlOutput(TrimmedTestCaseResult result, AssemblyDefinition original) + { + var documents = new Dictionary(StringComparer.Ordinal); + foreach (CustomAttribute attribute in result.TestCase.FindTypeDefinition(original).CustomAttributes) + { + if (attribute.AttributeType.Name != nameof(ExpectedXmlElementAttribute)) + continue; + + string fileName = (string)attribute.ConstructorArguments[0].Value; + string xpath = (string)attribute.ConstructorArguments[1].Value; + int expectedCount = (int)attribute.ConstructorArguments[2].Value; + if (!documents.TryGetValue(fileName, out XmlDocument? document)) + { + NPath path = result.Sandbox.OutputDirectory.Combine(fileName); + Assert.True(path.FileExists(), $"Expected XML output was not found at '{path}'."); + document = new XmlDocument(); + document.Load(path.ToString()); + documents.Add(fileName, document); + } + + XmlNodeList? matches = document.SelectNodes(xpath); + Assert.NotNull(matches); + Assert.True(matches.Count == expectedCount, + $"Expected {expectedCount} XML nodes matching '{xpath}' in '{fileName}', but found {matches.Count}."); + } + } +} diff --git a/src/tools/illink/test/Trimming.Tests.Shared/TestRunner.cs b/src/tools/illink/test/Trimming.Tests.Shared/TestRunner.cs index b86b1e12ac2c95..8c3e5affc1bf9d 100644 --- a/src/tools/illink/test/Trimming.Tests.Shared/TestRunner.cs +++ b/src/tools/illink/test/Trimming.Tests.Shared/TestRunner.cs @@ -178,6 +178,12 @@ protected virtual void AddTrimmingOptions(TestCaseSandbox sandbox, ManagedCompil AddOutputDirectory(sandbox, compilationResult, builder); + foreach (var argument in caseDefinedOptions.AdditionalArguments) + { + if (argument.Key == "--output-typemaps" && argument.Value is [string fileName]) + argument.Value[0] = sandbox.OutputDirectory.Combine(fileName).ToString(); + } + foreach (var rspFile in sandbox.ResponseFiles) builder.AddResponseFile(rspFile); From ea0e2cebada4344bb6f0c51b3468cc158277b566 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 21 Oct 2026 09:43:58 +0200 Subject: [PATCH 6/7] Revert "Add shared typemap XML output coverage" This reverts commit c2c213f451ef4da14f23a0ff959fb97776c9e140. The shared testcase needed an out-of-process Compile helper in the ILC trimming test driver. Per review (https://github.com/dotnet/runtime/pull/135275#discussion_r4319408175) the XML output coverage moves to a library TrimmingTest instead, which is restored in the next commit. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../ILTrim.Tests/ILTrimExpectedFailures.txt | 1 - .../ILCompiler.Trimming.Tests.csproj | 4 - .../TestCasesRunner/ILCompilerOptions.cs | 2 - .../TestCasesRunner/ResultChecker.cs | 1 - .../TrimmingArgumentBuilder.cs | 5 - .../TestCasesRunner/TrimmingDriver.cs | 95 +----------------- .../ReflectionTests.cs | 6 -- .../Assertions/ExpectedXmlElementAttribute.cs | 17 ---- .../Reflection/TypeMapOutput.cs | 96 ------------------- .../TestCasesRunner/ResultChecker.cs | 1 - .../ResultChecker.Xml.cs | 44 --------- .../test/Trimming.Tests.Shared/TestRunner.cs | 6 -- 12 files changed, 1 insertion(+), 277 deletions(-) delete mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedXmlElementAttribute.cs delete mode 100644 src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapOutput.cs delete mode 100644 src/tools/illink/test/Trimming.Tests.Shared/ResultChecker.Xml.cs diff --git a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt index 5d8c12cc057e29..56245f38bd010a 100644 --- a/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt +++ b/src/coreclr/tools/ILTrim.Tests/ILTrimExpectedFailures.txt @@ -350,7 +350,6 @@ Reflection.TypeHierarchyLibraryModeSuppressions Reflection.TypeHierarchyReflectionWarnings Reflection.TypeMap Reflection.TypeMapAssemblyTargetRemovedWhenAllEntriesTrimmed -Reflection.TypeMapOutput Reflection.TypeUsedViaReflection Reflection.UnsafeAccessor RequiresCapability.BasicRequires diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/ILCompiler.Trimming.Tests.csproj b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/ILCompiler.Trimming.Tests.csproj index 11dbf0ef9c6259..e8cef2b9014c91 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/ILCompiler.Trimming.Tests.csproj +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/ILCompiler.Trimming.Tests.csproj @@ -22,7 +22,6 @@ - @@ -33,9 +32,6 @@ - - $(DotNetTool) - $(RuntimeBinDir) diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ILCompilerOptions.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ILCompilerOptions.cs index a91d6e887934f9..681fd981a3f4d8 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ILCompilerOptions.cs +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ILCompilerOptions.cs @@ -22,8 +22,6 @@ public class ILCompilerOptions public Dictionary WarningsAsErrors = new Dictionary(); public List SuppressedWarningCategories = new List(); public bool DisableGeneratedCodeHeuristics; - public string? OutputFileName; public string? TypeMapEntryAssembly; - public string? TypeMapFileName; } } diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ResultChecker.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ResultChecker.cs index 76e48debbf1ab3..66703476bcbd9a 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ResultChecker.cs +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/ResultChecker.cs @@ -86,7 +86,6 @@ public virtual void Check(TrimmedTestCaseResult testResult) } } - VerifyExpectedXmlOutput(testResult, original); AdditionalChecking(testResult, original); } finally diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingArgumentBuilder.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingArgumentBuilder.cs index a9553d15635f37..afdac96827e93a 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingArgumentBuilder.cs +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingArgumentBuilder.cs @@ -65,7 +65,6 @@ public virtual void AddReference(NPath path) public virtual void AddOutputDirectory(NPath directory) { - Options.OutputFileName = directory.ToString(); } public virtual void AddLinkXmlFile(string file) @@ -168,10 +167,6 @@ public virtual void AddAdditionalArgument(string flag, string[] values) { Options.TypeMapEntryAssembly = values[0]; } - else if (flag == "--output-typemaps") - { - Options.TypeMapFileName = values[0]; - } else if (flag == "--singlewarn") { Options.SingleWarn = true; diff --git a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingDriver.cs b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingDriver.cs index 0a7b8707fb684b..acf155339d2055 100644 --- a/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingDriver.cs +++ b/src/coreclr/tools/aot/ILCompiler.Trimming.Tests/TestCasesRunner/TrimmingDriver.cs @@ -3,23 +3,18 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Reflection.Metadata; using System.Runtime.InteropServices; -using System.Threading.Tasks; using System.Xml; using ILCompiler; using ILCompiler.Dataflow; -using ILLink.Shared; using ILLink.Shared.TrimAnalysis; using Internal.IL; using Internal.TypeSystem; using Internal.TypeSystem.Ecma; -using Xunit; namespace Mono.Linker.Tests.TestCasesRunner { @@ -175,95 +170,7 @@ public ILScanResults Trim(ILCompilerOptions options, TrimmingCustomizations? cus .UseTypeMapManager(typeMapManager) .ToILScanner(); - ILScanResults results = scanner.Scan(); - if (options.TypeMapFileName is string typeMapFileName) - Compile(options, typeMapFileName); - - return results; - } - - private static void Compile(ILCompilerOptions options, string typeMapFileName) - { - string runtimeBinDirectory = Assert.IsType(AppContext.GetData("Mono.Linker.Tests.RuntimeBinDirectory")); - string compilerPath = Path.Combine(runtimeBinDirectory, "ilc", "ilc.dll"); - Assert.True(File.Exists(compilerPath), $"ILC was not found at '{compilerPath}'."); - Assert.NotNull(options.OutputFileName); - - // RyuJIT permits only one compiler host per process. - var startInfo = new ProcessStartInfo - { - FileName = Assert.IsType(AppContext.GetData("Mono.Linker.Tests.DotNetHostPath")), - UseShellExecute = false, - CreateNoWindow = true, - RedirectStandardOutput = true, - RedirectStandardError = true - }; - startInfo.ArgumentList.Add(compilerPath); - foreach (string inputFile in options.InputFilePaths.Values) - startInfo.ArgumentList.Add(inputFile); - AddArguments("--out", [options.OutputFileName]); - AddArguments("--output-typemaps", [typeMapFileName]); - startInfo.ArgumentList.Add("-O"); - startInfo.ArgumentList.Add("--scanreflection"); - AddArguments("--reference", options.ReferenceFilePaths.Values); - AddArguments("--initassembly", options.InitAssemblies); - AddArguments("--trim", options.TrimAssemblies); - AddArguments("--root", options.AdditionalRootAssemblies); - AddArguments("--conditionalroot", options.RootEntireAssemblies); - AddArguments("--descriptor", options.Descriptors); - AddArguments("--substitution", options.SubstitutionFiles); - foreach (var feature in options.FeatureSwitches) - AddArguments("--feature", [$"{feature.Key}={(feature.Value ? "true" : "false")}"]); - if (options.TypeMapEntryAssembly is string entryAssembly) - AddArguments("--typemap-entry-assembly", [entryAssembly]); - if (options.SingleWarn) - startInfo.ArgumentList.Add("--singlewarn"); - if (options.TreatWarningsAsErrors) - startInfo.ArgumentList.Add("--warnaserror"); - foreach (var warning in options.WarningsAsErrors) - { - string code = "IL" + warning.Key.ToString("D4", CultureInfo.InvariantCulture); - AddArguments(warning.Value ? "--warnaserr" : "--nowarnaserr", [code]); - } - foreach (string category in options.SuppressedWarningCategories) - { - startInfo.ArgumentList.Add(category switch - { - MessageSubCategory.TrimAnalysis => "--notrimwarn", - MessageSubCategory.AotAnalysis => "--noaotwarn", - _ => throw new InvalidOperationException($"Unsupported warning category '{category}'.") - }); - } - if (options.DisableGeneratedCodeHeuristics) - startInfo.ArgumentList.Add("--disable-generated-code-heuristics"); - - using var process = new Process { StartInfo = startInfo }; - Assert.True(process.Start(), $"Failed to start ILC at '{compilerPath}'."); - Task outputTask = process.StandardOutput.ReadToEndAsync(); - Task errorTask = process.StandardError.ReadToEndAsync(); - const int TimeoutMinutes = 2; - bool exited = process.WaitForExit(TimeSpan.FromMinutes(TimeoutMinutes)); - if (!exited) - process.Kill(entireProcessTree: true); - - string output = outputTask.GetAwaiter().GetResult(); - string error = errorTask.GetAwaiter().GetResult(); - File.WriteAllText(Path.ChangeExtension(options.OutputFileName, ".log"), output + error); - Assert.True(exited, - $"ILC compilation timed out after {TimeoutMinutes} minutes:{Environment.NewLine}{output}{error}"); - Assert.True(process.ExitCode == 0, - $"ILC compilation failed with exit code {process.ExitCode}:{Environment.NewLine}{output}{error}"); - Assert.True(File.Exists(options.OutputFileName), $"ILC did not produce the native object file '{options.OutputFileName}'."); - Assert.True(new FileInfo(options.OutputFileName).Length > 0, $"ILC produced an empty native object file '{options.OutputFileName}'."); - - void AddArguments(string flag, IEnumerable values) - { - foreach (string value in values) - { - startInfo.ArgumentList.Add(flag); - startInfo.ArgumentList.Add(value); - } - } + return scanner.Scan(); } public static void ComputeDefaultOptions(out TargetOS os, out TargetArchitecture arch) diff --git a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ReflectionTests.cs b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ReflectionTests.cs index c665026c41d3d8..2f0ee4a081df57 100644 --- a/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ReflectionTests.cs +++ b/src/tools/illink/test/ILLink.RoslynAnalyzer.Tests/ReflectionTests.cs @@ -190,12 +190,6 @@ public Task TypeMapEntryAssembly() return RunTest(); } - [Fact] - public Task TypeMapOutput() - { - return RunTest(); - } - [Fact] public Task TypeBaseTypeUseViaReflection() { diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedXmlElementAttribute.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedXmlElementAttribute.cs deleted file mode 100644 index d224f413234047..00000000000000 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases.Expectations/Assertions/ExpectedXmlElementAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; - -namespace Mono.Linker.Tests.Cases.Expectations.Assertions; - -[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] -public class ExpectedXmlElementAttribute : BaseExpectedLinkedBehaviorAttribute -{ - public ExpectedXmlElementAttribute(string fileName, string xpath, int count = 1) - { - ArgumentException.ThrowIfNullOrEmpty(fileName); - ArgumentException.ThrowIfNullOrEmpty(xpath); - ArgumentOutOfRangeException.ThrowIfNegative(count); - } -} diff --git a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapOutput.cs b/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapOutput.cs deleted file mode 100644 index ccd2bbbb49548c..00000000000000 --- a/src/tools/illink/test/Mono.Linker.Tests.Cases/Reflection/TypeMapOutput.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.InteropServices; -using Mono.Linker.Tests.Cases.Expectations.Assertions; -using Mono.Linker.Tests.Cases.Expectations.Metadata; -using Mono.Linker.Tests.Cases.Reflection; -using Mono.Linker.Tests.Cases.Reflection.Dependencies; - -[assembly: TypeMap("example/Target", typeof(TypeMapOutput.Target))] -[assembly: TypeMap("example/\u0130<&\"Target", typeof(TypeMapOutput.Target[]))] -[assembly: TypeMap("trimmed", typeof(TypeMapOutput.UnusedTarget), typeof(TypeMapOutput.UnusedSource))] -[assembly: TypeMapAssociation(typeof(TypeMapOutput.Source), typeof(TypeMapOutput.Proxy))] -[assembly: TypeMapAssociation(typeof(TypeMapOutput.UnusedSource), typeof(TypeMapOutput.UnusedTarget))] -[assembly: TypeMap("trimmed", typeof(TypeMapOutput.UnusedTarget), typeof(TypeMapOutput.UnusedSource))] -[assembly: TypeMap("external", typeof(TypeMapOutput.Target))] -[assembly: TypeMapAssociation(typeof(TypeMapOutput.Source), typeof(TypeMapOutput.Proxy))] -[assembly: TypeMap("external", typeof(TypeMapOutput.Target))] -[assembly: TypeMapAssociation(typeof(TypeMapOutput.Source), typeof(TypeMapOutput.Proxy))] -[assembly: TypeMap("unused", typeof(TypeMapOutput.UnusedTarget))] -[assembly: TypeMapAssemblyTarget("TypeMapEntryAssemblyLib")] - -namespace Mono.Linker.Tests.Cases.Reflection; - -[SetupCompileBefore("TypeMapEntryAssemblyLib.dll", new[] { "Dependencies/TypeMapEntryAssemblyLib.cs" })] -[SetupLinkerAction("link", "System.Private.CoreLib")] -[SetupLinkerArgument("--ignore-link-attributes", "false")] -[SetupLinkerArgument("--output-typemaps", FileName)] -#if NATIVEAOT -[SetupLinkerArgument("--warnaserror")] -[SetupLinkerArgument("--feature", "System.StartupHookProvider.IsSupported", "false")] -#endif -[SkipKeptItemsValidation] -[ExpectedNoWarnings] -[ExpectedXmlElement(FileName, "/typemaps")] -[ExpectedXmlElement(FileName, "/typemaps[@*]", 0)] -[ExpectedXmlElement(FileName, "/typemaps/group", 5)] -[ExpectedXmlElement(FileName, "/typemaps/group[count(@*) != 1 or not(@type)]", 0)] -[ExpectedXmlElement(FileName, "/typemaps/group[count(external) > 1 or count(proxy) > 1]", 0)] -[ExpectedXmlElement(FileName, "/typemaps/group/*[not(self::external or self::proxy) or @*]", 0)] -[ExpectedXmlElement(FileName, "/typemaps/group/*/*[not(self::entry)]", 0)] -[ExpectedXmlElement(FileName, "/typemaps/group/*/entry", 9)] -[ExpectedXmlElement(FileName, "/typemaps/group/*/entry[count(@*) != 2 or not(@key) or not(@value)]", 0)] -// The producer formatters differ in whitespace after assembly-name separators. -[ExpectedXmlElement(FileName, UsedGroup + "/external/entry", 2)] -[ExpectedXmlElement(FileName, UsedGroup + "/external/entry[@key='example/Target' and translate(@value, ' ', '')='" + TypePrefix + "Target,test']")] -[ExpectedXmlElement(FileName, UsedGroup + "/external/entry[@key='example/\u0130<&\"Target' and translate(@value, ' ', '')='" + TypePrefix + "Target[],test']")] -[ExpectedXmlElement(FileName, UsedGroup + "/proxy/" + ProxyEntry)] -[ExpectedXmlElement(FileName, EmptyGroup + "/external[not(entry)]")] -[ExpectedXmlElement(FileName, EmptyGroup + "/proxy", 0)] -[ExpectedXmlElement(FileName, ExternalOnlyGroup + "/external/entry[@key='external' and translate(@value, ' ', '')='" + TypePrefix + "Target,test']")] -[ExpectedXmlElement(FileName, ExternalOnlyGroup + "/proxy", 0)] -[ExpectedXmlElement(FileName, ProxyOnlyGroup + "/external", 0)] -[ExpectedXmlElement(FileName, ProxyOnlyGroup + "/proxy/" + ProxyEntry)] -[ExpectedXmlElement(FileName, DependencyGroup + "/external/entry[@key='entry_type1' and translate(@value, ' ', '')='" + DependencyPrefix + "EntryType1,TypeMapEntryAssemblyLib']")] -[ExpectedXmlElement(FileName, DependencyGroup + "/external/entry[@key='entry_type2' and translate(@value, ' ', '')='" + DependencyPrefix + "EntryType2,TypeMapEntryAssemblyLib']")] -[ExpectedXmlElement(FileName, DependencyGroup + "/proxy/entry[translate(@key, ' ', '')='" + DependencyPrefix + "EntrySource1,TypeMapEntryAssemblyLib' and translate(@value, ' ', '')='" + DependencyPrefix + "EntryProxy1,TypeMapEntryAssemblyLib']")] -[ExpectedXmlElement(FileName, DependencyGroup + "/proxy/entry[translate(@key, ' ', '')='" + DependencyPrefix + "EntrySource2,TypeMapEntryAssemblyLib' and translate(@value, ' ', '')='" + DependencyPrefix + "EntryProxy2,TypeMapEntryAssemblyLib']")] -class TypeMapOutput -{ - private const string FileName = "type maps.xml"; - private const string TypePrefix = "Mono.Linker.Tests.Cases.Reflection.TypeMapOutput+"; - private const string DependencyPrefix = "Mono.Linker.Tests.Cases.Reflection.Dependencies."; - private const string UsedGroup = "/typemaps/group[translate(@type, ' ', '')='" + TypePrefix + "UsedUniverse,test']"; - private const string EmptyGroup = "/typemaps/group[translate(@type, ' ', '')='" + TypePrefix + "EmptyUniverse,test']"; - private const string ExternalOnlyGroup = "/typemaps/group[translate(@type, ' ', '')='" + TypePrefix + "ExternalOnlyUniverse,test']"; - private const string ProxyOnlyGroup = "/typemaps/group[translate(@type, ' ', '')='" + TypePrefix + "ProxyOnlyUniverse,test']"; - private const string DependencyGroup = "/typemaps/group[translate(@type, ' ', '')='" + DependencyPrefix + "TypeMapEntryAssemblyGroup,TypeMapEntryAssemblyLib']"; - private const string ProxyEntry = "entry[translate(@key, ' ', '')='" + TypePrefix + "Source,test' and translate(@value, ' ', '')='" + TypePrefix + "Proxy`1[[System.Int32,System.Private.CoreLib]],test']"; - - public static void Main() - { - Console.WriteLine(new Source()); - Console.WriteLine(new EntrySource1()); - Console.WriteLine(new EntrySource2()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateExternalTypeMapping()); - Console.WriteLine(TypeMapping.GetOrCreateProxyTypeMapping()); - } - - public class UsedUniverse; - public class EmptyUniverse; - public class ExternalOnlyUniverse; - public class ProxyOnlyUniverse; - public class UnusedUniverse; - public class Target; - public class Source; - public class Proxy; - public class UnusedSource; - public class UnusedTarget; -} diff --git a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs index d158c147b5aaa2..e7dbad5bc01eed 100644 --- a/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs +++ b/src/tools/illink/test/Mono.Linker.Tests/TestCasesRunner/ResultChecker.cs @@ -127,7 +127,6 @@ public virtual void Check(TrimmedTestCaseResult linkResult) VerifyExpectedDependencyTrace(linkResult.MetadataProvider, linkResult.OutputAssemblyPath); } - VerifyExpectedXmlOutput(linkResult, original); VerifyLinkingOfOtherAssemblies(original); VerifyILOfOtherAssemblies(linkResult); AdditionalChecking(linkResult, original); diff --git a/src/tools/illink/test/Trimming.Tests.Shared/ResultChecker.Xml.cs b/src/tools/illink/test/Trimming.Tests.Shared/ResultChecker.Xml.cs deleted file mode 100644 index 709591146de17d..00000000000000 --- a/src/tools/illink/test/Trimming.Tests.Shared/ResultChecker.Xml.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -#nullable enable - -using System; -using System.Collections.Generic; -using System.Xml; -using Mono.Cecil; -using Mono.Linker.Tests.Cases.Expectations.Assertions; -using Mono.Linker.Tests.Extensions; -using Xunit; - -namespace Mono.Linker.Tests.TestCasesRunner; - -public partial class ResultChecker -{ - private static void VerifyExpectedXmlOutput(TrimmedTestCaseResult result, AssemblyDefinition original) - { - var documents = new Dictionary(StringComparer.Ordinal); - foreach (CustomAttribute attribute in result.TestCase.FindTypeDefinition(original).CustomAttributes) - { - if (attribute.AttributeType.Name != nameof(ExpectedXmlElementAttribute)) - continue; - - string fileName = (string)attribute.ConstructorArguments[0].Value; - string xpath = (string)attribute.ConstructorArguments[1].Value; - int expectedCount = (int)attribute.ConstructorArguments[2].Value; - if (!documents.TryGetValue(fileName, out XmlDocument? document)) - { - NPath path = result.Sandbox.OutputDirectory.Combine(fileName); - Assert.True(path.FileExists(), $"Expected XML output was not found at '{path}'."); - document = new XmlDocument(); - document.Load(path.ToString()); - documents.Add(fileName, document); - } - - XmlNodeList? matches = document.SelectNodes(xpath); - Assert.NotNull(matches); - Assert.True(matches.Count == expectedCount, - $"Expected {expectedCount} XML nodes matching '{xpath}' in '{fileName}', but found {matches.Count}."); - } - } -} diff --git a/src/tools/illink/test/Trimming.Tests.Shared/TestRunner.cs b/src/tools/illink/test/Trimming.Tests.Shared/TestRunner.cs index 8c3e5affc1bf9d..b86b1e12ac2c95 100644 --- a/src/tools/illink/test/Trimming.Tests.Shared/TestRunner.cs +++ b/src/tools/illink/test/Trimming.Tests.Shared/TestRunner.cs @@ -178,12 +178,6 @@ protected virtual void AddTrimmingOptions(TestCaseSandbox sandbox, ManagedCompil AddOutputDirectory(sandbox, compilationResult, builder); - foreach (var argument in caseDefinedOptions.AdditionalArguments) - { - if (argument.Key == "--output-typemaps" && argument.Value is [string fileName]) - argument.Value[0] = sandbox.OutputDirectory.Combine(fileName).ToString(); - } - foreach (var rspFile in sandbox.ResponseFiles) builder.AddResponseFile(rspFile); From 2eeb741b7c32807636a5d4dd8a16ad116fd71ee1 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 21 Oct 2026 09:44:10 +0200 Subject: [PATCH 7/7] Validate retained typemap XML in the TypeMap trimming test Restore the TypeMap library trimming test, which reads the XML written by --output-typemaps in both the ILLink and NativeAOT legs and checks the external and proxy maps of every universe, including a dependency assembly reached through TypeMapAssemblyTarget. Unlike the earlier version, the test needs no custom MSBuild targets file. A test entry opts in with AdditionalProperties metadata (TypeMapXmlFile=typemaps.xml); the shared trimming test harness builds the entry's AdditionalProjectReferences and copies the file next to the published app so the test can read it. The entry now also runs in the ILLink leg (it was NativeAotOnly) and sets Optimize so it passes in non-Release local runs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../SupportFiles/Directory.Build.targets | 7 + eng/testing/linker/trimmingTests.targets | 32 ++- .../Dependencies/TypeMapTestLibrary.cs | 21 ++ .../Dependencies/TypeMapTestLibrary.csproj | 10 + ...Runtime.InteropServices.TrimmingTests.proj | 9 +- .../tests/TrimmingTests/TypeMap.cs | 219 +++++++++++++++++- 6 files changed, 293 insertions(+), 5 deletions(-) create mode 100644 src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.cs create mode 100644 src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj diff --git a/eng/testing/linker/SupportFiles/Directory.Build.targets b/eng/testing/linker/SupportFiles/Directory.Build.targets index 9687763d2fdee6..9697d13e4f762a 100644 --- a/eng/testing/linker/SupportFiles/Directory.Build.targets +++ b/eng/testing/linker/SupportFiles/Directory.Build.targets @@ -21,4 +21,11 @@ + + + + + diff --git a/eng/testing/linker/trimmingTests.targets b/eng/testing/linker/trimmingTests.targets index eee97561702308..0e10a6805996e9 100644 --- a/eng/testing/linker/trimmingTests.targets +++ b/eng/testing/linker/trimmingTests.targets @@ -47,7 +47,7 @@ %(FullPath) - + @@ -70,6 +70,16 @@ <_additionalProjectReferencesString>@(_additionalProjectReference, '%0a') + + <_testProjectReference Remove="@(_testProjectReference)" /> + <_testProjectReference Include="%(TestConsoleApps.AdditionalProjectReferences)" /> + + + + <_additionalProjectReferencesString>$(_additionalProjectReferencesString) + @(_testProjectReference->'<ProjectReference Include="%(FullPath)" SkipUseReferenceAssembly="true" />', '%0a') + + <_additionalProjectSourceFiles Include="%(TestConsoleApps.AdditionalSourceFiles)" /> @@ -143,9 +153,27 @@ DependsOnTargets="GenerateProjects" Returns="@(TestConsoleApps)" /> - + + <_testProjectToBuild Include="%(TestConsoleApps.AdditionalProjectReferences)" /> + + + + + + + + + ("dependency/Target", typeof(DependencyTarget))] +[assembly: TypeMap("dependency/Trimmed", typeof(UnusedTarget), typeof(UnusedSource))] +[assembly: TypeMapAssociation(typeof(DependencySource), typeof(DependencyProxy))] +[assembly: TypeMapAssociation(typeof(UnusedSource), typeof(UnusedTarget))] +[assembly: TypeMap("wrong/universe", typeof(DependencyTarget))] + +namespace TypeMapTestLibrary; + +public class DependencyUniverse; +public class OtherUniverse; +public class DependencySource; +public class DependencyTarget; +public class DependencyProxy; +public class UnusedSource; +public class UnusedTarget; diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj new file mode 100644 index 00000000000000..f9d39601b32c95 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/Dependencies/TypeMapTestLibrary.csproj @@ -0,0 +1,10 @@ + + + $(NetCoreAppCurrent) + true + + + + + + diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj index 9aa8090bd00f56..d0fd3614c47f1d 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/System.Runtime.InteropServices.TrimmingTests.proj @@ -23,8 +23,13 @@ IlcTrimMetadata - - IlcGenerateMstatFile;IlcGenerateDgmlFile + + + IlcGenerateMstatFile;IlcGenerateDgmlFile;Optimize + browser-wasm + $(MSBuildProjectDirectory)/Dependencies/TypeMapTestLibrary.csproj + + TypeMapXmlFile=typemaps.xml diff --git a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs index 5dcf5e35009ecd..36750ce437825e 100644 --- a/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs +++ b/src/libraries/System.Runtime.InteropServices/tests/TrimmingTests/TypeMap.cs @@ -3,8 +3,12 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Reflection.Metadata; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Xml; +using TypeMapTestLibrary; [assembly: TypeMap("TrimTargetIsTarget", typeof(TargetAndTrimTarget), typeof(TargetAndTrimTarget))] [assembly: TypeMap("TrimTargetIsUnrelated", typeof(TargetType), typeof(TrimTarget))] @@ -14,6 +18,14 @@ [assembly: TypeMap("UnusedName", typeof(UnusedTargetType), typeof(TrimTarget))] [assembly: TypeMapAssociation(typeof(UnusedSourceClass), typeof(UnusedProxyType))] +[assembly: TypeMap("example/Target", typeof(TargetType))] +[assembly: TypeMap("example/\u0130Class", typeof(TargetType))] +[assembly: TypeMap("[Lexample/Target;", typeof(TargetType[]))] +[assembly: TypeMapAssociation(typeof(SourceClass), typeof(GenericProxy))] +[assembly: TypeMap("unused", typeof(UnreferencedTargetType), typeof(UnreferencedTrimTarget))] +[assembly: TypeMapAssemblyTarget("TypeMapTestLibrary")] +[assembly: TypeMap("local/Target", typeof(TargetType))] + if (args.Length > 1 && args[0] == "instantiate") { Console.WriteLine("This code path should never actually be called. It exists exclusively for the trimmer to see that types are used in a way that it can't fully analyze."); @@ -58,7 +70,7 @@ return 4; } -if (GetTypeWithoutTrimAnalysis(nameof(TrimTarget)) is not null) +if (!RuntimeFeature.IsDynamicCodeSupported && GetTypeWithoutTrimAnalysis(nameof(TrimTarget)) is not null) { Console.WriteLine("TrimTarget should not be preserved if the only place that would preserve it is a check that is optimized away."); return 5; @@ -95,8 +107,205 @@ return 10; } +IReadOnlyDictionary nestedTypeMap = TypeMapping.GetOrCreateExternalTypeMapping(); +if (!nestedTypeMap.TryGetValue("example/Target", out Type nestedTarget) || + nestedTarget != targetType || + !nestedTypeMap.TryGetValue("example/\u0130Class", out Type unicodeTarget) || + unicodeTarget != targetType || + !nestedTypeMap.TryGetValue("[Lexample/Target;", out Type arrayTarget) || + arrayTarget != targetType.MakeArrayType()) +{ + Console.WriteLine("Nested universe external mappings do not match expected types."); + return 11; +} + +IReadOnlyDictionary nestedProxyMap = TypeMapping.GetOrCreateProxyTypeMapping(); +if (!nestedProxyMap.TryGetValue(typeof(SourceClass), out Type genericProxy) || + genericProxy != GetTypeWithoutTrimAnalysis("GenericProxy`1[System.Int32]")) +{ + Console.WriteLine("Nested universe proxy mapping does not match expected type."); + return 12; +} + +IReadOnlyDictionary emptyTypeMap = TypeMapping.GetOrCreateExternalTypeMapping(); +if (emptyTypeMap.TryGetValue("unused", out _)) +{ + Console.WriteLine("The empty map contains a trimmed entry."); + return 13; +} + +Console.WriteLine(new DependencySource().GetHashCode()); +IReadOnlyDictionary dependencyMap = TypeMapping.GetOrCreateExternalTypeMapping(); +if (!dependencyMap.TryGetValue("dependency/Target", out Type dependencyTarget) || + dependencyTarget.FullName != "TypeMapTestLibrary.DependencyTarget" || + dependencyMap.TryGetValue("dependency/Trimmed", out _)) +{ + Console.WriteLine("Cross-assembly external mappings do not match expected types."); + return 15; +} + +IReadOnlyDictionary dependencyProxyMap = TypeMapping.GetOrCreateProxyTypeMapping(); +if (!dependencyProxyMap.TryGetValue(typeof(DependencySource), out Type dependencyProxy) || + dependencyProxy.FullName != "TypeMapTestLibrary.DependencyProxy") +{ + Console.WriteLine("Cross-assembly proxy mapping does not match expected type."); + return 16; +} + +IReadOnlyDictionary otherMap = TypeMapping.GetOrCreateExternalTypeMapping(); +if (!otherMap.TryGetValue("local/Target", out Type otherTarget) || otherTarget != targetType || + otherMap.TryGetValue("wrong/universe", out _)) +{ + Console.WriteLine("An assembly target for one universe included entries from another universe."); + return 17; +} + +if (!CheckArtifact()) + return 14; + return 100; +static bool CheckArtifact() +{ + Dictionary External, Dictionary Proxy)> groups = + ReadArtifact(Path.Combine(AppContext.BaseDirectory, "typemaps.xml")); + if (groups.Count != 5) + { + Console.WriteLine("Unexpected type map universe count."); + return false; + } + + string assemblyName = typeof(UsedTypeMap).Assembly.GetName().Name; + var used = groups[NormalizeTypeName($"{nameof(UsedTypeMap)},{assemblyName}")]; + var nested = groups[NormalizeTypeName($"NestedContainer+Universe,{assemblyName}")]; + var empty = groups[NormalizeTypeName($"{nameof(EmptyTypeMap)},{assemblyName}")]; + var dependency = groups[NormalizeTypeName("TypeMapTestLibrary.DependencyUniverse,TypeMapTestLibrary")]; + var other = groups[NormalizeTypeName("TypeMapTestLibrary.OtherUniverse,TypeMapTestLibrary")]; + + return CheckMap(used.External, proxy: false, + ("TrimTargetIsTarget", nameof(TargetAndTrimTarget)), + ("TrimTargetIsUnrelated", nameof(TargetType))) && + CheckMap(used.Proxy, proxy: true, (nameof(SourceClass), nameof(ProxyType))) && + CheckMap(nested.External, proxy: false, + ("example/Target", nameof(TargetType)), + ("example/\u0130Class", nameof(TargetType)), + ("[Lexample/Target;", nameof(TargetType) + "[]")) && + CheckMap(nested.Proxy, proxy: true, + (nameof(SourceClass), "GenericProxy`1[[System.Int32,System.Private.CoreLib]]")) && + CheckMap(empty.External, proxy: false) && + empty.Proxy is null && + CheckMap(dependency.External, proxy: false, + ("dependency/Target", "TypeMapTestLibrary.DependencyTarget,TypeMapTestLibrary")) && + CheckMap(dependency.Proxy, proxy: true, + ("TypeMapTestLibrary.DependencySource,TypeMapTestLibrary", "TypeMapTestLibrary.DependencyProxy,TypeMapTestLibrary")) && + CheckMap(other.External, proxy: false, ("local/Target", nameof(TargetType))) && + other.Proxy is null; + + bool CheckMap(Dictionary map, bool proxy, params (string Key, string Type)[] expected) + { + if (map is null || map.Count != expected.Length) + { + Console.WriteLine($"Unexpected {(proxy ? "proxy" : "external")} map entry count."); + return false; + } + + foreach ((string key, string type) in expected) + { + string expectedKey = proxy ? QualifyTypeName(key) : key; + string expectedType = QualifyTypeName(type); + if (!map.TryGetValue(expectedKey, out string actualType) || actualType != expectedType) + { + Console.WriteLine($"Missing mapping '{key}' to '{type}'."); + return false; + } + } + + return true; + } + + string QualifyTypeName(string type) + { + TypeName name = TypeName.Parse(type); + + return name.AssemblyName is null + ? NormalizeTypeName($"{type},{assemblyName}") + : name.AssemblyQualifiedName; + } +} + +static Dictionary External, Dictionary Proxy)> ReadArtifact(string path) +{ + using XmlReader reader = XmlReader.Create(path); + reader.MoveToContent(); + if (reader.Name != "typemaps" || reader.HasAttributes) + throw new InvalidDataException("Unexpected type map XML root."); + + var groups = new Dictionary External, Dictionary Proxy)>(StringComparer.Ordinal); + bool emptyRoot = reader.IsEmptyElement; + reader.ReadStartElement("typemaps"); + while (!emptyRoot && reader.MoveToContent() == XmlNodeType.Element) + { + if (reader.Name != "group" || reader.AttributeCount != 1) + throw new InvalidDataException("Unexpected type map group."); + + string type = reader.GetAttribute("type") ?? throw new InvalidDataException("Missing type map group type."); + bool emptyGroup = reader.IsEmptyElement; + reader.ReadStartElement("group"); + (Dictionary External, Dictionary Proxy) maps = (null, null); + while (!emptyGroup && reader.MoveToContent() == XmlNodeType.Element) + { + switch (reader.Name) + { + case "external" when maps.External is null: + maps.External = ReadMap(reader, proxy: false); + break; + case "proxy" when maps.Proxy is null: + maps.Proxy = ReadMap(reader, proxy: true); + break; + default: + throw new InvalidDataException($"Unexpected or duplicate map section '{reader.Name}'."); + } + } + if (!emptyGroup) + reader.ReadEndElement(); + groups.Add(NormalizeTypeName(type), maps); + } + if (!emptyRoot) + reader.ReadEndElement(); + reader.MoveToContent(); + + return groups; + + static Dictionary ReadMap(XmlReader reader, bool proxy) + { + if (reader.HasAttributes) + throw new InvalidDataException("Unexpected map section attributes."); + + var map = new Dictionary(StringComparer.Ordinal); + bool emptyMap = reader.IsEmptyElement; + reader.ReadStartElement(); + while (!emptyMap && reader.MoveToContent() == XmlNodeType.Element) + { + if (reader.Name != "entry" || reader.AttributeCount != 2) + throw new InvalidDataException("Unexpected type map entry."); + + string key = reader.GetAttribute("key") ?? throw new InvalidDataException("Missing type map entry key."); + string value = reader.GetAttribute("value") ?? throw new InvalidDataException("Missing type map entry value."); + map.Add(proxy ? NormalizeTypeName(key) : key, NormalizeTypeName(value)); + bool emptyEntry = reader.IsEmptyElement; + reader.ReadStartElement("entry"); + if (!emptyEntry) + reader.ReadEndElement(); + } + if (!emptyMap) + reader.ReadEndElement(); + + return map; + } +} + +static string NormalizeTypeName(string name) => TypeName.Parse(name).AssemblyQualifiedName; + [MethodImpl(MethodImplOptions.NoInlining)] static Type GetTypeWithoutTrimAnalysis(string typeName) { @@ -116,3 +325,11 @@ class UnusedTypeMap; class UnusedTargetType; class UnusedSourceClass; class UnusedProxyType; + +class NestedContainer +{ + public class Universe; +} + +class GenericProxy; +class EmptyTypeMap;