From 8fd70956d1f4bb29a1d0f0b7a84a855999cf8ab7 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 25 Feb 2021 16:14:00 -0800 Subject: [PATCH 1/2] Enable additional ways to opt in to trimming This implements the SDK side of the behavior described at https://github.com/mono/linker/blob/main/docs/design/trimmed-assemblies.md#net-6. This includes a linker update with https://github.com/mono/linker/pull/1839. --- eng/Version.Details.xml | 8 +- eng/Versions.props | 2 +- .../targets/Microsoft.NET.ILLink.targets | 48 ++- .../GivenThatWeWantToRunILLink.cs | 296 +++++++++++++++++- 4 files changed, 337 insertions(+), 17 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 04b6854fcf73..2c2661ab1329 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -102,13 +102,13 @@ https://github.com/microsoft/vstest 8ee2efd12c3781c5cf850f113f2252fda6a3312b - + https://github.com/mono/linker - 6b3a3050c70577bd1b3fd7611eef56679e22a4f1 + 44907d98e524f65db0a0edc2cab8afe077ba812a - + https://github.com/mono/linker - 6b3a3050c70577bd1b3fd7611eef56679e22a4f1 + 44907d98e524f65db0a0edc2cab8afe077ba812a https://github.com/dotnet/runtime diff --git a/eng/Versions.props b/eng/Versions.props index 3a0dcbed42a3..20ffc790cd91 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -73,7 +73,7 @@ - 6.0.100-preview.2.21124.3 + 6.0.100-preview.2.21125.1 $(MicrosoftNETILLinkTasksPackageVersion) 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 859b9bf84620..8ef33798129a 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 @@ -118,6 +118,7 @@ Copyright (c) .NET Foundation. All rights reserved. ReferenceAssemblyPaths="@(ReferencePath)" RootAssemblyNames="@(TrimmerRootAssembly)" TrimMode="$(TrimMode)" + DefaultAction="$(_TrimmerDefaultAction)" RemoveSymbols="$(TrimmerRemoveSymbols)" FeatureSettings="@(_TrimmerFeatureSettings)" CustomData="@(_TrimmerCustomData)" @@ -171,15 +172,24 @@ Copyright (c) .NET Foundation. All rights reserved. in some cases. --> - - + 5 0 + + + $(TreatWarningsAsErrors) <_ExtraTrimmerArgs>--skip-unresolved true $(_ExtraTrimmerArgs) copyused + + + + <_TrimmerDefaultAction Condition=" $([MSBuild]::VersionLessThan('$(TargetFrameworkVersion)', '6.0')) ">$(TrimMode) + + <_TrimmerDefaultAction Condition=" '$(_TrimmerDefaultAction)' == '' ">copy + @@ -221,17 +231,45 @@ Copyright (c) .NET Foundation. All rights reserved. - + true - + + + <_ManagedAssemblyToLinkName Include="@(ManagedAssemblyToLink->'%(Filename)')"> + %(Identity) + + <_NonTrimmableManagedAssemblyToLinkName Include="@(_ManagedAssemblyToLinkName)" Exclude="@(TrimmableAssembly)" /> + <_TrimmableManagedAssemblyToLinkName Include="@(_ManagedAssemblyToLinkName)" Exclude="@(_NonTrimmableManagedAssemblyToLinkName)" /> + <_TrimmableManagedAssemblyToLink Include="@(_TrimmableManagedAssemblyToLinkName->'%(OriginalItemSpec)')" /> + + + true + + + + + + + + copy + + + + $(TrimMode) + + + $(_TrimmerDefaultAction) + + + <_TrimmerFeatureSettings Include="@(RuntimeHostConfigurationOption)" Condition="'%(RuntimeHostConfigurationOption.Trim)' == 'true'" /> diff --git a/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs b/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs index 1c7b06e3a20d..25c67bc7b44d 100644 --- a/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs +++ b/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs @@ -68,6 +68,8 @@ public void ILLink_only_runs_when_switch_is_enabled(string targetFramework) [Theory] [InlineData("netcoreapp3.0", true)] [InlineData("netcoreapp3.0", false)] + [InlineData("net5.0", false)] + [InlineData("net6.0", false)] public void ILLink_runs_and_creates_linked_app(string targetFramework, bool referenceClassLibAsPackage) { var projectName = "HelloWorld"; @@ -157,6 +159,7 @@ public void PrepareForILLink_can_set_IsTrimmable(string targetFramework) [RequiresMSBuildVersionTheory("16.8.0")] [InlineData("net5.0")] + [InlineData("net6.0")] public void PrepareForILLink_can_set_TrimMode(string targetFramework) { var projectName = "HelloWorld"; @@ -182,6 +185,7 @@ public void PrepareForILLink_can_set_TrimMode(string targetFramework) [RequiresMSBuildVersionTheory("16.8.0")] [InlineData("net5.0")] + [InlineData("net6.0")] public void ILLink_respects_global_TrimMode(string targetFramework) { var projectName = "HelloWorld"; @@ -209,6 +213,160 @@ public void ILLink_respects_global_TrimMode(string targetFramework) DoesImageHaveMethod(isTrimmableDll, "UnusedMethod").Should().BeFalse(); } + [RequiresMSBuildVersionTheory("16.8.0")] + [InlineData("net5.0")] + [InlineData("net6.0")] + public void ILLink_roots_IntermediateAssembly(string targetFramework) + { + var projectName = "HelloWorld"; + var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); + + var testProject = CreateTestProjectForILLinkTesting(targetFramework, projectName); + var testAsset = _testAssetsManager.CreateTestProject(testProject) + .WithProjectChanges(project => SetGlobalTrimMode(project, "link")) + .WithProjectChanges(project => SetIsTrimmable(project, projectName)); + + var publishCommand = new PublishCommand(testAsset); + 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"); + + // The assembly is trimmed but its entry point is kept + DoesImageHaveMethod(publishedDll, "UnusedMethod").Should().BeFalse(); + DoesImageHaveMethod(publishedDll, "Main").Should().BeTrue(); + } + + [RequiresMSBuildVersionTheory("16.8.0")] + [InlineData("net6.0")] + public void ILLink_respects_TrimmableAssembly(string targetFramework) + { + var projectName = "HelloWorld"; + var referenceProjectName = "ClassLibForILLink"; + var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); + + var testProject = CreateTestProjectForILLinkTesting(targetFramework, projectName, referenceProjectName); + testProject.AdditionalItems["TrimmableAssembly"] = new Dictionary { ["Include"] = referenceProjectName }; + var testAsset = _testAssetsManager.CreateTestProject(testProject); + + var publishCommand = new PublishCommand(testAsset); + 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 unusedTrimmableDll = Path.Combine(publishDirectory, $"{referenceProjectName}.dll"); + + File.Exists(publishedDll).Should().BeTrue(); + // Check that the unused assembly was removed. + File.Exists(unusedTrimmableDll).Should().BeFalse(); + } + + [RequiresMSBuildVersionTheory("16.8.0")] + [InlineData("net6.0")] + public void ILLink_respects_IsTrimmable_attribute(string targetFramework) + { + string projectName = "HelloWorld"; + var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); + var testProject = CreateTestProjectWithIsTrimmableAttributes(targetFramework, projectName); + var testAsset = _testAssetsManager.CreateTestProject(testProject); + + var publishCommand = new PublishCommand(testAsset); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}").Should().Pass(); + + var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; + + var unusedTrimmableDll = Path.Combine(publishDirectory, "UnusedTrimmableAssembly.dll"); + var unusedNonTrimmableDll = Path.Combine(publishDirectory, "UnusedNonTrimmableAssembly.dll"); + + // Only unused non-trimmable assemblies are kept + File.Exists(unusedTrimmableDll).Should().BeFalse(); + DoesImageHaveMethod(unusedNonTrimmableDll, "UnusedMethod").Should().BeTrue(); + } + + [RequiresMSBuildVersionTheory("16.8.0")] + [InlineData("net6.0")] + public void ILLink_IsTrimmable_metadata_can_override_attribute(string targetFramework) + { + string projectName = "HelloWorld"; + var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); + var testProject = CreateTestProjectWithIsTrimmableAttributes(targetFramework, projectName); + var testAsset = _testAssetsManager.CreateTestProject(testProject) + .WithProjectChanges(project => SetIsTrimmable(project, "UnusedTrimmableAssembly", false)) + .WithProjectChanges(project => SetIsTrimmable(project, "UnusedNonTrimmableAssembly", true)); + + var publishCommand = new PublishCommand(testAsset); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}", "/v:n").Should().Pass(); + + var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; + + var unusedTrimmableDll = Path.Combine(publishDirectory, "UnusedTrimmableAssembly.dll"); + var unusedNonTrimmableDll = Path.Combine(publishDirectory, "UnusedNonTrimmableAssembly.dll"); + + // Attributed IsTrimmable assembly with IsTrimmable=false metadata should be kept + DoesImageHaveMethod(unusedTrimmableDll, "UnusedMethod").Should().BeTrue(); + // Unattributed assembly with IsTrimmable=true should be trimmed + File.Exists(unusedNonTrimmableDll).Should().BeFalse(); + } + + [RequiresMSBuildVersionTheory("16.8.0")] + [InlineData("net6.0")] + public void ILLink_TrimMode_applies_to_IsTrimmable_assemblies(string targetFramework) + { + string projectName = "HelloWorld"; + var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); + var testProject = CreateTestProjectWithIsTrimmableAttributes(targetFramework, projectName); + var testAsset = _testAssetsManager.CreateTestProject(testProject) + .WithProjectChanges(project => SetGlobalTrimMode(project, "link")); + + var publishCommand = new PublishCommand(testAsset); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}").Should().Pass(); + + var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; + + var trimmableDll = Path.Combine(publishDirectory, "TrimmableAssembly.dll"); + var nonTrimmableDll = Path.Combine(publishDirectory, "NonTrimmableAssembly.dll"); + var unusedTrimmableDll = Path.Combine(publishDirectory, "UnusedTrimmableAssembly.dll"); + var unusedNonTrimmableDll = Path.Combine(publishDirectory, "UnusedNonTrimmableAssembly.dll"); + + // Trimmable assemblies are trimmed at member level + DoesImageHaveMethod(trimmableDll, "UnusedMethod").Should().BeFalse(); + DoesImageHaveMethod(trimmableDll, "UsedMethod").Should().BeTrue(); + File.Exists(unusedTrimmableDll).Should().BeFalse(); + // Non-trimmable assemblies still get copied + DoesImageHaveMethod(nonTrimmableDll, "UnusedMethod").Should().BeTrue(); + DoesImageHaveMethod(unusedNonTrimmableDll, "UnusedMethod").Should().BeTrue(); + } + + [RequiresMSBuildVersionTheory("16.8.0")] + [InlineData("net6.0")] + public void ILLink_can_set_TrimmerDefaultAction(string targetFramework) + { + string projectName = "HelloWorld"; + var rid = EnvironmentInfo.GetCompatibleRid(targetFramework); + var testProject = CreateTestProjectWithIsTrimmableAttributes(targetFramework, projectName); + var testAsset = _testAssetsManager.CreateTestProject(testProject) + .WithProjectChanges(project => SetTrimmerDefaultAction(project, "link")); + + var publishCommand = new PublishCommand(testAsset); + publishCommand.Execute($"/p:RuntimeIdentifier={rid}").Should().Pass(); + + var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; + + var trimmableDll = Path.Combine(publishDirectory, "TrimmableAssembly.dll"); + var nonTrimmableDll = Path.Combine(publishDirectory, "NonTrimmableAssembly.dll"); + var unusedTrimmableDll = Path.Combine(publishDirectory, "UnusedTrimmableAssembly.dll"); + var unusedNonTrimmableDll = Path.Combine(publishDirectory, "UnusedNonTrimmableAssembly.dll"); + + // Trimmable assemblies are trimmed at assembly level + DoesImageHaveMethod(trimmableDll, "UnusedMethod").Should().BeTrue(); + File.Exists(unusedTrimmableDll).Should().BeFalse(); + // Unattributed assemblies are trimmed at member level + DoesImageHaveMethod(nonTrimmableDll, "UnusedMethod").Should().BeFalse(); + File.Exists(unusedNonTrimmableDll).Should().BeFalse(); + } + [RequiresMSBuildVersionTheory("16.8.0")] [InlineData("net5.0")] public void ILLink_analysis_warnings_are_disabled_by_default(string targetFramework) @@ -445,7 +603,7 @@ public void ILLink_accepts_root_descriptor(string targetFramework) // keeps all used assemblies, but in this case we want to // check whether the root descriptor actually roots only // the specified method. - var extraArgs = $"-p link {referenceProjectName}"; + var extraArgs = $"--action link {referenceProjectName}"; publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true", $"/p:_ExtraTrimmerArgs={extraArgs}", "/v:n").Should().Pass(); @@ -502,7 +660,7 @@ public void ILLink_respects_feature_settings_from_host_config() var publishCommand = new PublishCommand(testAsset); publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true", - $"/p:_ExtraTrimmerArgs=-p link {referenceProjectName}").Should().Pass(); + $"/p:_ExtraTrimmerArgs=--action link {referenceProjectName}").Should().Pass(); var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; var referenceDll = Path.Combine(publishDirectory, $"{referenceProjectName}.dll"); @@ -535,7 +693,7 @@ public void ILLink_ignores_host_config_settings_with_link_false() var publishCommand = new PublishCommand(testAsset); publishCommand.Execute($"/p:RuntimeIdentifier={rid}", $"/p:SelfContained=true", "/p:PublishTrimmed=true", - $"/p:_ExtraTrimmerArgs=-p link {referenceProjectName}").Should().Pass(); + $"/p:_ExtraTrimmerArgs=--action link {referenceProjectName}").Should().Pass(); var publishDirectory = publishCommand.GetOutputDirectory(targetFramework: targetFramework, runtimeIdentifier: rid).FullName; var referenceDll = Path.Combine(publishDirectory, $"{referenceProjectName}.dll"); @@ -1055,18 +1213,25 @@ public void It_warns_when_targetting_netcoreapp_2_x_illink() .HaveStdOutContaining(Strings.PublishTrimmedRequiresVersion30); } - private void SetIsTrimmable(XDocument project, string assemblyName) + private void SetIsTrimmable(XDocument project, string assemblyName, bool value = true) { var ns = project.Root.Name.Namespace; - var target = new XElement(ns + "Target", + var target = project.Root.Elements(ns + "Target") + .Where(e => e.Attribute("Name")?.Value == "SetIsTrimmable") + .FirstOrDefault(); + + if (target == null) { + target = new XElement(ns + "Target", new XAttribute("BeforeTargets", "PrepareForILLink"), new XAttribute("Name", "SetIsTrimmable")); - project.Root.Add(target); + project.Root.Add(target); + } + target.Add(new XElement(ns + "ItemGroup", new XElement("ManagedAssemblyToLink", new XAttribute("Condition", $"'%(FileName)' == '{assemblyName}'"), - new XElement("IsTrimmable", "true")))); + new XElement("IsTrimmable", value.ToString())))); } private void SetTrimMode(XDocument project, string assemblyName, string trimMode) @@ -1093,6 +1258,15 @@ private void SetGlobalTrimMode(XDocument project, string trimMode) trimMode)); } + private void SetTrimmerDefaultAction(XDocument project, string action) + { + var ns = project.Root.Name.Namespace; + + var properties = new XElement(ns + "PropertyGroup"); + project.Root.Add(properties); + properties.Add(new XElement(ns + "_TrimmerDefaultAction", action)); + } + private void EnableNonFrameworkTrimming(XDocument project) { // Used to override the default linker options for testing @@ -1211,6 +1385,110 @@ public override void IL_2046() {} return testProject; } + private TestProject CreateTestProjectWithIsTrimmableAttributes ( + string targetFramework, + string projectName) + { + var testProject = new TestProject() + { + Name = projectName, + TargetFrameworks = targetFramework, + IsExe = true + }; + testProject.AdditionalProperties["PublishTrimmed"] = "true"; + + testProject.SourceFiles[$"{projectName}.cs"] = @" +using System; +public class Program +{ + public static void Main() + { + TrimmableAssembly.UsedMethod(); + NonTrimmableAssembly.UsedMethod(); + } +}"; + + var trimmableProject = new TestProject() + { + Name = "TrimmableAssembly", + TargetFrameworks = targetFramework + }; + + trimmableProject.SourceFiles["TrimmableAssembly.cs"] = @" +using System.Reflection; + +[assembly: AssemblyMetadata(""IsTrimmable"", ""True"")] + +public static class TrimmableAssembly +{ + public static void UsedMethod() + { + } + + public static void UnusedMethod() + { + } +}"; + testProject.ReferencedProjects.Add (trimmableProject); + + var nonTrimmableProject = new TestProject() + { + Name = "NonTrimmableAssembly", + TargetFrameworks = targetFramework + }; + + nonTrimmableProject.SourceFiles["NonTrimmableAssembly.cs"] = @" +public static class NonTrimmableAssembly +{ + public static void UsedMethod() + { + } + + public static void UnusedMethod() + { + } +}"; + testProject.ReferencedProjects.Add (nonTrimmableProject); + + var unusedTrimmableProject = new TestProject() + { + Name = "UnusedTrimmableAssembly", + TargetFrameworks = targetFramework + }; + + unusedTrimmableProject.SourceFiles["UnusedTrimmableAssembly.cs"] = @" +using System.Reflection; + +[assembly: AssemblyMetadata(""IsTrimmable"", ""True"")] + +public static class UnusedTrimmableAssembly +{ + public static void UnusedMethod() + { + } +} +"; + testProject.ReferencedProjects.Add (unusedTrimmableProject); + + var unusedNonTrimmableProject = new TestProject() + { + Name = "UnusedNonTrimmableAssembly", + TargetFrameworks = targetFramework + }; + + unusedNonTrimmableProject.SourceFiles["UnusedNonTrimmableAssembly.cs"] = @" +public static class UnusedNonTrimmableAssembly +{ + public static void UnusedMethod() + { + } +} +"; + testProject.ReferencedProjects.Add (unusedNonTrimmableProject); + + return testProject; + } + private TestProject CreateTestProjectForILLinkTesting( string targetFramework, string mainProjectName, @@ -1236,6 +1514,10 @@ public static void Main() { Console.WriteLine(""Hello world""); } + + public static void UnusedMethod() + { + } "; if (addAssemblyReference) From fd66656b94eb1d71bba7fdac4210716ff32ee7ee Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Thu, 25 Feb 2021 18:10:10 -0800 Subject: [PATCH 2/2] PR feedback - use JoinItems task :) - always set _TrimmerDefaultAction --- .../targets/Microsoft.NET.ILLink.targets | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 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 8ef33798129a..b01902233296 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 @@ -182,9 +182,6 @@ Copyright (c) .NET Foundation. All rights reserved. $(TreatWarningsAsErrors) <_ExtraTrimmerArgs>--skip-unresolved true $(_ExtraTrimmerArgs) copyused - - - <_TrimmerDefaultAction Condition=" $([MSBuild]::VersionLessThan('$(TargetFrameworkVersion)', '6.0')) ">$(TrimMode) @@ -230,25 +227,26 @@ Copyright (c) .NET Foundation. All rights reserved. false + - true + - - <_ManagedAssemblyToLinkName Include="@(ManagedAssemblyToLink->'%(Filename)')"> - %(Identity) - - <_NonTrimmableManagedAssemblyToLinkName Include="@(_ManagedAssemblyToLinkName)" Exclude="@(TrimmableAssembly)" /> - <_TrimmableManagedAssemblyToLinkName Include="@(_ManagedAssemblyToLinkName)" Exclude="@(_NonTrimmableManagedAssemblyToLinkName)" /> - <_TrimmableManagedAssemblyToLink Include="@(_TrimmableManagedAssemblyToLinkName->'%(OriginalItemSpec)')" /> + + + + + - - true - + + - + +