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..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 @@ -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. --> + + + %(ManagedAssemblyToLink.TrimMode) + + + + + - + + + true + + + + + copy + @@ -141,22 +163,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..d63838164e8b 100644 --- a/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs +++ b/src/Tests/Microsoft.NET.Publish.Tests/GivenThatWeWantToRunILLink.cs @@ -99,6 +99,86 @@ public void ILLink_runs_and_creates_linked_app(string targetFramework, bool refe DoesDepsFileHaveAssembly(depsFile, unusedFrameworkAssembly).Should().BeFalse(); } + [Theory] + [InlineData("net5.0")] + public void PrepareForILLink_can_set_IsTrimmable(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 => 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(); + } + + [Theory] [InlineData("netcoreapp3.0")] public void ILLink_accepts_root_descriptor(string targetFramework) @@ -494,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 @@ -501,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", "_SetILLinkDefaults"), + new XAttribute("BeforeTargets", "PrepareForILLink"), new XAttribute("Name", "_EnableNonFrameworkTrimming")); project.Root.Add(target); - target.Add(new XElement(ns + "PropertyGroup", - new XElement("_TrimmerDefaultAction", "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")))); + 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";