From e339d94e8869fd86cb69ae1719dd7ed96316e1c1 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 19 Jun 2020 23:19:10 +0000 Subject: [PATCH 1/4] Add ILLink extension points See https://github.com/dotnet/sdk/issues/12035 - PrepareForILLink target - ManagedAssemblyToLink ItemGroup - TrimMode property and metadata - private _TrimmerCustomData (https://github.com/mono/linker/issues/1134) - Allow setting `PublishTrimmed` in a late import.targets --- .../targets/Microsoft.NET.ILLink.targets | 72 ++++++++++++------- .../GivenThatWeWantToRunILLink.cs | 20 ++++-- 2 files changed, 58 insertions(+), 34 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets index 19d94d859de4..ca5e0d4d60f4 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets @@ -13,7 +13,9 @@ Copyright (c) .NET Foundation. All rights reserved. - + + $(IntermediateOutputPath)linked\ $(IntermediateLinkDir)\ @@ -37,21 +39,21 @@ Copyright (c) .NET Foundation. All rights reserved. - <_LinkedResolvedFileToPublish Include="@(_LinkedResolvedFileToPublishCandidates)" Condition="Exists('%(Identity)')" /> - + <_LinkedResolvedFileToPublish Include="@(_LinkedResolvedFileToPublishCandidate)" Condition="Exists('%(Identity)')" /> + - <_RemovedManagedAssemblies Include="@(_ManagedAssembliesToLink)" Condition="!Exists('$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + <_RemovedManagedAssembly Include="@(ManagedAssemblyToLink)" Condition="!Exists('$(IntermediateLinkDir)%(Filename)%(Extension)')" /> - - - - - + + + + + @@ -67,8 +69,8 @@ Copyright (c) .NET Foundation. All rights reserved. --> - + + + true + + + + + copy + + + + - - <_ManagedAssembliesToLink Condition=" '%(_ManagedAssembliesToLink.IsTrimmable)' != 'true' "> - copy - + + %(ManagedAssemblyToLink.TrimMode) + @@ -141,22 +159,22 @@ Copyright (c) .NET Foundation. All rights reserved. - + - + - <_LinkedResolvedFileToPublishCandidates Include="@(_ManagedAssembliesToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" /> + <_LinkedResolvedFileToPublishCandidate Include="@(ManagedAssemblyToLink->'$(IntermediateLinkDir)%(Filename)%(Extension)')" /> diff --git a/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs b/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs index 941b3117ac23..d5b0db821301 100644 --- a/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs +++ b/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs @@ -74,7 +74,7 @@ public void ILLink_runs_and_creates_linked_app(string targetFramework, bool refe .WithProjectChanges(project => EnableNonFrameworkTrimming(project)); var publishCommand = new PublishCommand(Log, Path.Combine(testAsset.TestRoot, testProject.Name)); - publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true").Should().Pass(); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true", "/v:d").Should().Pass(); var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; var intermediateDirectory = publishCommand.GetIntermediateDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; @@ -99,6 +99,12 @@ public void ILLink_runs_and_creates_linked_app(string targetFramework, bool refe DoesDepsFileHaveAssembly(depsFile, unusedFrameworkAssembly).Should().BeFalse(); } + // PrepareForILLink can be used to set IsTrimmable + + // PrepareForILLink can be used to set per-assembly TrimMode + + // TrimMode can be used to control global defaults + [Theory] [InlineData("netcoreapp3.0")] public void ILLink_accepts_root_descriptor(string targetFramework) @@ -306,7 +312,7 @@ public void ILLink_does_not_include_leftover_artifacts_on_second_run(string targ var linkSemaphore = Path.Combine(intermediateDirectory, "Link.semaphore"); // Link, keeping classlib - publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true").Should().Pass(); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true", "/v:d").Should().Pass(); DateTime semaphoreFirstModifiedTime = File.GetLastWriteTimeUtc(linkSemaphore); var publishedDllKeptFirstTimeOnly = Path.Combine(publishDirectory, $"{referenceProjectName}.dll"); @@ -505,19 +511,19 @@ private void EnableNonFrameworkTrimming(XDocument project) var ns = project.Root.Name.Namespace; var target = new XElement(ns + "Target", - new XAttribute("AfterTargets", "_SetILLinkDefaults"), + new XAttribute("AfterTargets", "PrepareForILLink"), new XAttribute("Name", "_EnableNonFrameworkTrimming")); project.Root.Add(target); target.Add(new XElement(ns + "PropertyGroup", - new XElement("_TrimmerDefaultAction", "link"))); + new XElement("TrimMode", "link"))); target.Add(new XElement(ns + "ItemGroup", new XElement("TrimmerRootAssembly", new XAttribute("Remove", "@(TrimmerRootAssembly)")), new XElement("TrimmerRootAssembly", new XAttribute("Include", "@(IntermediateAssembly->'%(FileName)')")), - new XElement("_ManagedAssembliesToLink", - new XAttribute("Update", "@(_ManagedAssembliesToLink)"), - new XElement("action")))); + new XElement("ManagedAssemblyToLink", + new XAttribute("Update", "@(ManagedAssemblyToLink)"), + new XElement("TrimMode")))); } static readonly string substitutionsFilename = "ILLink.Substitutions.xml"; From 2b7c44fdb7a38509183b95c9bc8e101bfb5ab8c2 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 23 Jun 2020 16:29:07 +0000 Subject: [PATCH 2/4] Add tests for extension points --- .../GivenThatWeWantToRunILLink.cs | 143 +++++++++++++++--- 1 file changed, 126 insertions(+), 17 deletions(-) diff --git a/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs b/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs index d5b0db821301..d63838164e8b 100644 --- a/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs +++ b/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs @@ -74,7 +74,7 @@ public void ILLink_runs_and_creates_linked_app(string targetFramework, bool refe .WithProjectChanges(project => EnableNonFrameworkTrimming(project)); var publishCommand = new PublishCommand(Log, Path.Combine(testAsset.TestRoot, testProject.Name)); - publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true", "/v:d").Should().Pass(); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true").Should().Pass(); var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; var intermediateDirectory = publishCommand.GetIntermediateDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; @@ -99,11 +99,85 @@ public void ILLink_runs_and_creates_linked_app(string targetFramework, bool refe DoesDepsFileHaveAssembly(depsFile, unusedFrameworkAssembly).Should().BeFalse(); } - // PrepareForILLink can be used to set IsTrimmable + [Theory] + [InlineData("net5.0")] + public void PrepareForILLink_can_set_IsTrimmable(string targetFramework) + { + var projectName = "HelloWorld"; + var referenceProjectName = "ClassLibForILLink"; + var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); - // PrepareForILLink can be used to set per-assembly TrimMode + var testProject = CreateTestProjectForILLinkTesting(targetFramework, projectName, referenceProjectName); + var testAsset = _testAssetsManager.CreateTestProject(testProject) + .WithProjectChanges(project => SetIsTrimmable(project, referenceProjectName)); + + var publishCommand = new PublishCommand(Log, Path.Combine(testAsset.TestRoot, testProject.Name)); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true").Should().Pass(); + + var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; + + var publishedDll = Path.Combine(publishDirectory, $"{projectName}.dll"); + var unusedIsTrimmableDll = Path.Combine(publishDirectory, $"{referenceProjectName}.dll"); + + File.Exists(publishedDll).Should().BeTrue(); + // Check that the unused trimmable assembly was removed + File.Exists(unusedIsTrimmableDll).Should().BeFalse(); + } + + [Theory] + [InlineData("net5.0")] + public void PrepareForILLink_can_set_TrimMode(string targetFramework) + { + var projectName = "HelloWorld"; + var referenceProjectName = "ClassLibForILLink"; + var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); + + var testProject = CreateTestProjectForILLinkTesting(targetFramework, projectName, referenceProjectName); + var testAsset = _testAssetsManager.CreateTestProject(testProject) + .WithProjectChanges(project => SetTrimMode(project, referenceProjectName, "link")); + + var publishCommand = new PublishCommand(Log, Path.Combine(testAsset.TestRoot, testProject.Name)); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true").Should().Pass(); + + var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; + + var publishedDll = Path.Combine(publishDirectory, $"{projectName}.dll"); + var unusedTrimModeLinkDll = Path.Combine(publishDirectory, $"{referenceProjectName}.dll"); + + File.Exists(publishedDll).Should().BeTrue(); + // Check that the unused "link" assembly was removed. + File.Exists(unusedTrimModeLinkDll).Should().BeFalse(); + } + + [Theory] + [InlineData("net5.0")] + public void ILLink_respects_global_TrimMode(string targetFramework) + { + var projectName = "HelloWorld"; + var referenceProjectName = "ClassLibForILLink"; + var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); + + var testProject = CreateTestProjectForILLinkTesting(targetFramework, projectName, referenceProjectName); + var testAsset = _testAssetsManager.CreateTestProject(testProject) + .WithProjectChanges(project => SetGlobalTrimMode(project, "link")) + .WithProjectChanges(project => SetIsTrimmable(project, referenceProjectName)) + .WithProjectChanges(project => AddRootDescriptor(project, $"{referenceProjectName}.xml")); + + var publishCommand = new PublishCommand(Log, Path.Combine(testAsset.TestRoot, testProject.Name)); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true").Should().Pass(); + + var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; + + var publishedDll = Path.Combine(publishDirectory, $"{projectName}.dll"); + var isTrimmableDll = Path.Combine(publishDirectory, $"{referenceProjectName}.dll"); + + File.Exists(publishedDll).Should().BeTrue(); + File.Exists(isTrimmableDll).Should().BeTrue(); + // Check that the assembly was trimmed at the member level + DoesImageHaveMethod(isTrimmableDll, "UnusedMethodToRoot").Should().BeTrue(); + DoesImageHaveMethod(isTrimmableDll, "UnusedMethod").Should().BeFalse(); + } - // TrimMode can be used to control global defaults [Theory] [InlineData("netcoreapp3.0")] @@ -312,7 +386,7 @@ public void ILLink_does_not_include_leftover_artifacts_on_second_run(string targ var linkSemaphore = Path.Combine(intermediateDirectory, "Link.semaphore"); // Link, keeping classlib - publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true", "/v:d").Should().Pass(); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true").Should().Pass(); DateTime semaphoreFirstModifiedTime = File.GetLastWriteTimeUtc(linkSemaphore); var publishedDllKeptFirstTimeOnly = Path.Combine(publishDirectory, $"{referenceProjectName}.dll"); @@ -500,6 +574,44 @@ public void It_warns_when_targetting_netcoreapp_2_x() .HaveStdOutContaining(Strings.PublishTrimmedRequiresVersion30); } + private void SetIsTrimmable(XDocument project, string assemblyName) + { + var ns = project.Root.Name.Namespace; + + var target = new XElement(ns + "Target", + new XAttribute("BeforeTargets", "PrepareForILLink"), + new XAttribute("Name", "SetIsTrimmable")); + project.Root.Add(target); + target.Add(new XElement(ns + "ItemGroup", + new XElement("ManagedAssemblyToLink", + new XAttribute("Condition", $"'%(FileName)' == '{assemblyName}'"), + new XElement("IsTrimmable", "true")))); + } + + private void SetTrimMode(XDocument project, string assemblyName, string trimMode) + { + var ns = project.Root.Name.Namespace; + + var target = new XElement(ns + "Target", + new XAttribute("BeforeTargets", "PrepareForILLink"), + new XAttribute("Name", "SetTrimMode")); + project.Root.Add(target); + target.Add(new XElement(ns + "ItemGroup", + new XElement("ManagedAssemblyToLink", + new XAttribute("Condition", $"'%(FileName)' == '{assemblyName}'"), + new XElement("TrimMode", trimMode)))); + } + + private void SetGlobalTrimMode(XDocument project, string trimMode) + { + var ns = project.Root.Name.Namespace; + + var properties = new XElement(ns + "PropertyGroup"); + project.Root.Add(properties); + properties.Add(new XElement(ns + "TrimMode", + trimMode)); + } + private void EnableNonFrameworkTrimming(XDocument project) { // Used to override the default linker options for testing @@ -507,23 +619,20 @@ private void EnableNonFrameworkTrimming(XDocument project) // but we want to ensure that the linker is running // end-to-end by checking that it strips code from our // test projects. - + SetGlobalTrimMode(project, "link"); var ns = project.Root.Name.Namespace; var target = new XElement(ns + "Target", - new XAttribute("AfterTargets", "PrepareForILLink"), + new XAttribute("BeforeTargets", "PrepareForILLink"), new XAttribute("Name", "_EnableNonFrameworkTrimming")); project.Root.Add(target); - target.Add(new XElement(ns + "PropertyGroup", - new XElement("TrimMode", "link"))); - target.Add(new XElement(ns + "ItemGroup", - new XElement("TrimmerRootAssembly", - new XAttribute("Remove", "@(TrimmerRootAssembly)")), - new XElement("TrimmerRootAssembly", - new XAttribute("Include", "@(IntermediateAssembly->'%(FileName)')")), - new XElement("ManagedAssemblyToLink", - new XAttribute("Update", "@(ManagedAssemblyToLink)"), - new XElement("TrimMode")))); + var items = new XElement(ns + "ItemGroup"); + target.Add(items); + items.Add(new XElement("ManagedAssemblyToLink", + new XElement("Condition", "true"), + new XElement("IsTrimmable", "true"))); + items.Add(new XElement(ns + "TrimmerRootAssembly", + new XAttribute("Include", "@(IntermediateAssembly->'%(FileName)')"))); } static readonly string substitutionsFilename = "ILLink.Substitutions.xml"; From 7be7c7256102be074dfa020a56a0b36c2d70c235 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 23 Jun 2020 17:00:31 +0000 Subject: [PATCH 3/4] Set Action in _RunILLink --- .../targets/Microsoft.NET.ILLink.targets | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets index ca5e0d4d60f4..3d2a2132e31c 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets @@ -81,6 +81,13 @@ Copyright (c) .NET Foundation. All rights reserved. <_DotNetHostFileName Condition=" '$(OS)' == 'Windows_NT' ">dotnet.exe + + + + %(ManagedAssemblyToLink.TrimMode) + + + - - - - %(ManagedAssemblyToLink.TrimMode) - - - <_TrimmerFeatureSettings Include="@(RuntimeHostConfigurationOption)" Condition="'%(RuntimeHostConfigurationOption.Trim)' == 'true'" /> From eb6401f1329aaa9afe817b668eb16e72b513309f Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 23 Jun 2020 23:27:52 +0000 Subject: [PATCH 4/4] Add notes about ManagedAssemblyToLink --- .../targets/Microsoft.NET.ILLink.targets | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets index 3d2a2132e31c..e7fb38b8cae7 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets +++ b/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.ILLink.targets @@ -122,8 +122,12 @@ Copyright (c) .NET Foundation. All rights reserved. PrepareForILLink Set up the default options and inputs to ILLink. Other targets are expected to hook into - this extension point via BeforeTargets/AfterTargets to opt assemblies into or out of trimming, - and to set ILLink global or per-assembly options. + this extension point via BeforeTargets/AfterTargets to opt assemblies into or out of trimming + using global ILLink options, or per-assembly IsTrimmable and TrimMode metadata. + + Note that adding items to or removing items from ManagedAssemblyToLink is unsupported. To change + the set of inputs to the linker, instead use a different extension point to + set PostprocessAssembly metadata on ResolvedFileToPublish. -->