diff --git a/documentation/wiki/ChangeWaves.md b/documentation/wiki/ChangeWaves.md index 46d1d61b4e5..dc6273622ff 100644 --- a/documentation/wiki/ChangeWaves.md +++ b/documentation/wiki/ChangeWaves.md @@ -25,6 +25,7 @@ The opt-out comes in the form of setting the environment variable `MSBuildDisabl - [Don't expand full drive globs with false condition](https://github.com/dotnet/msbuild/pull/5669) ### 16.10 - [Error when a property expansion in a condition has whitespace](https://github.com/dotnet/msbuild/pull/5672) +- [Allow Custom CopyToOutputDirectory Location With TargetPath](https://github.com/dotnet/msbuild/pull/6237) ### 17.0 ## Change Waves No Longer In Rotation diff --git a/src/Shared/Constants.cs b/src/Shared/Constants.cs index d8b2c66c98d..eea2401dca9 100644 --- a/src/Shared/Constants.cs +++ b/src/Shared/Constants.cs @@ -166,6 +166,10 @@ internal static class ItemMetadataNames internal const string subType = "SubType"; internal const string executableExtension = "ExecutableExtension"; internal const string embedInteropTypes = "EmbedInteropTypes"; + + /// + /// The output path for a given item. + /// internal const string targetPath = "TargetPath"; internal const string dependentUpon = "DependentUpon"; internal const string msbuildSourceProjectFile = "MSBuildSourceProjectFile"; diff --git a/src/Tasks.UnitTests/AssignTargetPath_Tests.cs b/src/Tasks.UnitTests/AssignTargetPath_Tests.cs index 8f159985a94..3f9dc087274 100644 --- a/src/Tasks.UnitTests/AssignTargetPath_Tests.cs +++ b/src/Tasks.UnitTests/AssignTargetPath_Tests.cs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using System.Collections.Generic; using Microsoft.Build.Framework; using Microsoft.Build.Shared; using Microsoft.Build.Tasks; using Microsoft.Build.Utilities; +using Shouldly; using Xunit; namespace Microsoft.Build.UnitTests @@ -20,15 +22,10 @@ public void Regress314791() { new TaskItem(NativeMethodsShared.IsWindows ? @"c:\bin2\abc.efg" : "/bin2/abc.efg") }; t.RootFolder = NativeMethodsShared.IsWindows ? @"c:\bin" : "/bin"; - bool success = t.Execute(); - - Assert.True(success); - - Assert.Single(t.AssignedFiles); - Assert.Equal( - NativeMethodsShared.IsWindows ? @"c:\bin2\abc.efg" : "/bin2/abc.efg", - t.AssignedFiles[0].ItemSpec); - Assert.Equal(@"abc.efg", t.AssignedFiles[0].GetMetadata("TargetPath")); + t.Execute().ShouldBeTrue(); + t.AssignedFiles.Length.ShouldBe(1); + t.AssignedFiles[0].ItemSpec.ShouldBe(NativeMethodsShared.IsWindows ? @"c:\bin2\abc.efg" : "/bin2/abc.efg"); + t.AssignedFiles[0].GetMetadata("TargetPath").ShouldBe("abc.efg"); } [Fact] @@ -40,12 +37,9 @@ public void AtConeRoot() { new TaskItem(NativeMethodsShared.IsWindows ? @"c:\f1\f2\file.txt" : "/f1/f2/file.txt") }; t.RootFolder = NativeMethodsShared.IsWindows ? @"c:\f1\f2" : "/f1/f2"; - bool success = t.Execute(); - - Assert.True(success); - - Assert.Single(t.AssignedFiles); - Assert.Equal(@"file.txt", t.AssignedFiles[0].GetMetadata("TargetPath")); + t.Execute().ShouldBeTrue(); + t.AssignedFiles.Length.ShouldBe(1); + t.AssignedFiles[0].GetMetadata("TargetPath").ShouldBe("file.txt"); } [Fact] @@ -64,12 +58,9 @@ public void OutOfCone() // /f1 to /x1 t.RootFolder = NativeMethodsShared.IsWindows ? @"c:\f1" : "/x1"; - bool success = t.Execute(); - - Assert.True(success); - - Assert.Single(t.AssignedFiles); - Assert.Equal("file.txt", t.AssignedFiles[0].GetMetadata("TargetPath")); + t.Execute().ShouldBeTrue(); + t.AssignedFiles.Length.ShouldBe(1); + t.AssignedFiles[0].GetMetadata("TargetPath").ShouldBe("file.txt"); } [Fact] @@ -84,14 +75,69 @@ public void InConeButAbsolute() }; t.RootFolder = NativeMethodsShared.IsWindows ? @"c:\f1\f2" : "/f1/f2"; - bool success = t.Execute(); + t.Execute().ShouldBeTrue(); + t.AssignedFiles.Length.ShouldBe(1); + t.AssignedFiles[0].GetMetadata("TargetPath").ShouldBe(NativeMethodsShared.IsWindows ? @"f3\f4\file.txt" : "f3/f4/file.txt"); + } + + [Theory] + [InlineData("c:/fully/qualified/path.txt")] + [InlineData("test/output/file.txt")] + [InlineData(@"some\dir\to\file.txt")] + [InlineData("file.txt")] + [InlineData("file")] + public void TargetPathAlreadySet(string targetPath) + { + AssignTargetPath t = new AssignTargetPath(); + t.BuildEngine = new MockEngine(); + Dictionary metaData = new Dictionary(); + metaData.Add("TargetPath", targetPath); + metaData.Add("Link", "c:/foo/bar"); + t.Files = new ITaskItem[] + { + new TaskItem( + itemSpec: NativeMethodsShared.IsWindows ? @"c:\f1\f2\file.txt" : "/f1/f2/file.txt", + itemMetadata: metaData) + }; + t.RootFolder = NativeMethodsShared.IsWindows ? @"c:\f1\f2" : "/f1/f2"; + + t.Execute().ShouldBeTrue(); + t.AssignedFiles.Length.ShouldBe(1); + t.AssignedFiles[0].GetMetadata("TargetPath").ShouldBe(targetPath); + } + + [Theory] + [InlineData("c:/fully/qualified/path.txt")] + [InlineData("test/output/file.txt")] + [InlineData(@"some\dir\to\file.txt")] + [InlineData("file.txt")] + [InlineData("file")] + public void TargetPathAlreadySet_DisabledUnderChangeWave16_10(string targetPath) + { + using TestEnvironment env = TestEnvironment.Create(); + string link = "c:/some/path"; + + ChangeWaves.ResetStateForTests(); + env.SetEnvironmentVariable("MSBUILDDISABLEFEATURESFROMVERSION", ChangeWaves.Wave16_10.ToString()); + BuildEnvironmentHelper.ResetInstance_ForUnitTestsOnly(); - Assert.True(success); + AssignTargetPath t = new AssignTargetPath(); + t.BuildEngine = new MockEngine(); + Dictionary metaData = new Dictionary(); + metaData.Add("TargetPath", targetPath); + metaData.Add("Link", link); + t.Files = new ITaskItem[] + { + new TaskItem( + itemSpec: NativeMethodsShared.IsWindows ? @"c:\f1\f2\file.txt" : "/f1/f2/file.txt", + itemMetadata: metaData) + }; + t.RootFolder = NativeMethodsShared.IsWindows ? @"c:\f1\f2" : "/f1/f2"; - Assert.Single(t.AssignedFiles); - Assert.Equal( - NativeMethodsShared.IsWindows ? @"f3\f4\file.txt" : "f3/f4/file.txt", - t.AssignedFiles[0].GetMetadata("TargetPath")); + t.Execute().ShouldBeTrue(); + t.AssignedFiles.Length.ShouldBe(1); + t.AssignedFiles[0].GetMetadata("TargetPath").ShouldBe(link); + ChangeWaves.ResetStateForTests(); } } } diff --git a/src/Tasks/AssignTargetPath.cs b/src/Tasks/AssignTargetPath.cs index ffb085cfcf6..6b033ae1fb0 100644 --- a/src/Tasks/AssignTargetPath.cs +++ b/src/Tasks/AssignTargetPath.cs @@ -71,13 +71,19 @@ public override bool Execute() for (int i = 0; i < Files.Length; ++i) { - string link = Files[i].GetMetadata(ItemMetadataNames.link); AssignedFiles[i] = new TaskItem(Files[i]); - // If file has a link, use that. - string targetPath = link; + // If TargetPath is already set, it takes priority. + // https://github.com/dotnet/msbuild/issues/2795 + string targetPath = ChangeWaves.AreFeaturesEnabled(ChangeWaves.Wave16_10) ? Files[i].GetMetadata(ItemMetadataNames.targetPath) : null; - if (string.IsNullOrEmpty(link)) + // If TargetPath not already set, fall back to default behavior. + if (string.IsNullOrEmpty(targetPath)) + { + targetPath = Files[i].GetMetadata(ItemMetadataNames.link); + } + + if (string.IsNullOrEmpty(targetPath)) { if (// if the file path is relative !Path.IsPathRooted(Files[i].ItemSpec) &&