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) &&