diff --git a/DotnetCLIVersion.txt b/DotnetCLIVersion.txt
index 59cc8a9c2a18..ae306034599e 100644
--- a/DotnetCLIVersion.txt
+++ b/DotnetCLIVersion.txt
@@ -1 +1 @@
-2.0.0-preview1-005685
+2.0.0-preview1-005722
\ No newline at end of file
diff --git a/build/DependencyVersions.props b/build/DependencyVersions.props
index 0a3cecd46953..846ac9f0576f 100644
--- a/build/DependencyVersions.props
+++ b/build/DependencyVersions.props
@@ -2,6 +2,10 @@
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+
+
15.1.548
1.0.3
@@ -9,11 +13,11 @@
4.3.0-beta1-2418
9.0.1
6.0.4
+ 1.4.2
- 2.0.0-beta-001783-00
2.1.0
4.19.2
4.19.0
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAConflictResolver.cs b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAConflictResolver.cs
new file mode 100644
index 000000000000..364c05e37655
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/GivenAConflictResolver.cs
@@ -0,0 +1,417 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using FluentAssertions;
+using Microsoft.Build.Framework;
+using Microsoft.Extensions.DependencyModel;
+using Xunit;
+using Microsoft.NET.Build.Tasks.ConflictResolution;
+using System.Linq;
+
+namespace Microsoft.NET.Build.Tasks.UnitTests
+{
+ public class GivenAConflictResolver
+ {
+ [Fact]
+ public void ItemsWithDifferentKeysDontConflict()
+ {
+ var item1 = new MockConflictItem("System.Ben");
+ var item2 = new MockConflictItem("System.Immo");
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().BeEmpty();
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenOnlyOneItemExistsAWinnerCannotBeDetermined()
+ {
+ var item1 = new MockConflictItem() { Exists = false };
+ var item2 = new MockConflictItem() { Exists = true };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().BeEmpty();
+ result.UnresolvedConflicts.Should().Equal(item2);
+ }
+
+ [Fact]
+ public void WhenNeitherItemExistsAWinnerCannotBeDetermined()
+ {
+ var item1 = new MockConflictItem() { Exists = false, AssemblyVersion = new Version("1.0.0.0") };
+ var item2 = new MockConflictItem() { Exists = false, AssemblyVersion = new Version("2.0.0.0") };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().BeEmpty();
+ result.UnresolvedConflicts.Should().Equal(item2);
+ }
+
+
+ [Fact]
+ public void WhenAnItemDoesntExistButDoesNotConflictWithAnythingItIsNotReported()
+ {
+ var result = GetConflicts(
+ new MockConflictItem("System.Ben"),
+ new MockConflictItem("System.Immo") { Exists = false },
+ new MockConflictItem("System.Dave")
+ );
+
+ result.Conflicts.Should().BeEmpty();
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndDontHaveAssemblyVersionsTheFileVersionIsUsedToResolveTheConflict()
+ {
+ var item1 = new MockConflictItem() { AssemblyVersion = null, FileVersion = new Version("1.0.0.0") };
+ var item2 = new MockConflictItem() { AssemblyVersion = null, FileVersion = new Version("3.0.0.0") };
+ var item3 = new MockConflictItem() { AssemblyVersion = null, FileVersion = new Version("2.0.0.0") };
+
+ var result = GetConflicts(item1, item2, item3);
+
+ result.Conflicts.Should().Equal(item1, item3);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndOnlyOneHasAnAssemblyVersionAWinnerCannotBeDetermined()
+ {
+ var item1 = new MockConflictItem() { AssemblyVersion = new Version("1.0.0.0") };
+ var item2 = new MockConflictItem() { AssemblyVersion = null };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().BeEmpty();
+ result.UnresolvedConflicts.Should().Equal(item2);
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndAssemblyVersionsMatchTheFileVersionIsUsedToResolveTheConflict()
+ {
+ var item1 = new MockConflictItem() { FileVersion = new Version("3.0.0.0") };
+ var item2 = new MockConflictItem() { FileVersion = new Version("2.0.0.0") };
+ var item3 = new MockConflictItem() { FileVersion = new Version("1.0.0.0") };
+
+ var result = GetConflicts(item1, item2, item3);
+
+ result.Conflicts.Should().Equal(item2, item3);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictTheAssemblyVersionIsUsedToResolveTheConflict()
+ {
+ var item1 = new MockConflictItem() { AssemblyVersion = new Version("1.0.0.0") };
+ var item2 = new MockConflictItem() { AssemblyVersion = new Version("2.0.0.0") };
+ var item3 = new MockConflictItem() { AssemblyVersion = new Version("3.0.0.0") };
+
+ var result = GetConflicts(item1, item2, item3);
+
+ result.Conflicts.Should().Equal(item1, item2);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndDontHaveFileVersionsThePackageRankIsUsedToResolveTheConflict()
+ {
+ var item1 = new MockConflictItem() { FileVersion = null, PackageId = "Package3" };
+ var item2 = new MockConflictItem() { FileVersion = null, PackageId = "Package2" };
+ var item3 = new MockConflictItem() { FileVersion = null, PackageId = "Package1" };
+
+ var result = GetConflicts(item1, item2, item3);
+
+ result.Conflicts.Should().Equal(item1, item2);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndOnlyOneHasAFileVersionAWinnerCannotBeDetermined()
+ {
+ var item1 = new MockConflictItem() { FileVersion = null };
+ var item2 = new MockConflictItem() { FileVersion = new Version("1.0.0.0") };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().BeEmpty();
+ result.UnresolvedConflicts.Should().Equal(item2);
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndFileVersionsMatchThePackageRankIsUsedToResolveTheConflict()
+ {
+ var item1 = new MockConflictItem() { PackageId = "Package2" };
+ var item2 = new MockConflictItem() { PackageId = "Package3" };
+ var item3 = new MockConflictItem() { PackageId = "Package1" };
+
+ var result = GetConflicts(item1, item2, item3);
+
+ result.Conflicts.Should().Equal(item2, item1);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictTheFileVersionIsUsedToResolveTheConflict()
+ {
+ var item1 = new MockConflictItem() { FileVersion = new Version("2.0.0.0") };
+ var item2 = new MockConflictItem() { FileVersion = new Version("1.0.0.0") };
+ var item3 = new MockConflictItem() { FileVersion = new Version("3.0.0.0") };
+
+ var result = GetConflicts(item1, item2, item3);
+
+ result.Conflicts.Should().Equal(item2, item1);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndDontHaveAPackageRankTheItemTypeIsUsedToResolveTheConflict()
+ {
+ var item1 = new MockConflictItem() { PackageId = "Unranked1", ItemType = ConflictItemType.Platform };
+ var item2 = new MockConflictItem() { PackageId = "Unranked2", ItemType = ConflictItemType.Reference };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().Equal(item2);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndOnlyOneHasAPackageRankItWins()
+ {
+ var item1 = new MockConflictItem() { PackageId = "Unranked1" };
+ var item2 = new MockConflictItem() { PackageId = "Ranked1" };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().Equal(item1);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndPackageRanksMatchTheItemTypeIsUsedToResolveTheConflict()
+ {
+ var item1 = new MockConflictItem() { PackageId = "Package1", ItemType = ConflictItemType.Reference };
+ var item2 = new MockConflictItem() { PackageId = "Package1", ItemType = ConflictItemType.Platform };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().Equal(item1);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+
+ [Fact]
+ public void WhenItemsConflictThePackageRankIsUsedToResolveTheConflict()
+ {
+ var item1 = new MockConflictItem() { PackageId = "Package1" };
+ var item2 = new MockConflictItem() { PackageId = "Package2" };
+ var item3 = new MockConflictItem() { PackageId = "Package3" };
+
+ var result = GetConflicts(item1, item2, item3);
+
+ result.Conflicts.Should().Equal(item2, item3);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndBothArePlatformItemsTheConflictCannotBeResolved()
+ {
+ var item1 = new MockConflictItem() { ItemType = ConflictItemType.Platform };
+ var item2 = new MockConflictItem() { ItemType = ConflictItemType.Platform };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().BeEmpty();
+ result.UnresolvedConflicts.Should().Equal(item2);
+ }
+
+ [Fact]
+ public void WhenItemsConflictAndNeitherArePlatformItemsTheConflictCannotBeResolved()
+ {
+ var item1 = new MockConflictItem() { ItemType = ConflictItemType.Reference };
+ var item2 = new MockConflictItem() { ItemType = ConflictItemType.CopyLocal };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().BeEmpty();
+ result.UnresolvedConflicts.Should().Equal(item2);
+ }
+
+ [Fact]
+ public void WhenItemsConflictAPlatformItemWins()
+ {
+ var item1 = new MockConflictItem() { ItemType = ConflictItemType.Reference };
+ var item2 = new MockConflictItem() { ItemType = ConflictItemType.Platform };
+
+ var result = GetConflicts(item1, item2);
+
+ result.Conflicts.Should().Equal(item1);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenCommitWinnerIsFalseOnlyTheFirstResolvedConflictIsReported()
+ {
+ var committedItem = new MockConflictItem() { AssemblyVersion = new Version("2.0.0.0") } ;
+
+ var uncommittedItem1 = new MockConflictItem() { AssemblyVersion = new Version("3.0.0.0") };
+ var uncommittedItem2 = new MockConflictItem() { AssemblyVersion = new Version("1.0.0.0") };
+ var uncommittedItem3 = new MockConflictItem() { AssemblyVersion = new Version("2.0.0.0") };
+
+ var result = GetConflicts(new[] { committedItem }, uncommittedItem1, uncommittedItem2, uncommittedItem3);
+
+ result.Conflicts.Should().Equal(committedItem);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenCommitWinnerIsFalseAndThereIsNoWinnerEachUnresolvedConflictIsReported()
+ {
+ var committedItem = new MockConflictItem();
+
+ var uncommittedItem1 = new MockConflictItem();
+ var uncommittedItem2 = new MockConflictItem();
+ var uncommittedItem3 = new MockConflictItem();
+
+ var result = GetConflicts(new[] { committedItem }, uncommittedItem1, uncommittedItem2, uncommittedItem3);
+
+ result.Conflicts.Should().BeEmpty();
+ result.UnresolvedConflicts.Should().Equal(uncommittedItem1, uncommittedItem2, uncommittedItem3);
+ }
+
+ [Fact]
+ public void WhenCommitWinnerIsFalseMultipleConflictsAreReportedIfTheCommittedItemWins()
+ {
+ var committedItem = new MockConflictItem() { AssemblyVersion = new Version("4.0.0.0") };
+
+ var uncommittedItem1 = new MockConflictItem() { AssemblyVersion = new Version("3.0.0.0") };
+ var uncommittedItem2 = new MockConflictItem() { AssemblyVersion = new Version("1.0.0.0") };
+ var uncommittedItem3 = new MockConflictItem() { AssemblyVersion = new Version("2.0.0.0") };
+
+ var result = GetConflicts(new[] { committedItem }, uncommittedItem1, uncommittedItem2, uncommittedItem3);
+
+ result.Conflicts.Should().Equal(uncommittedItem1, uncommittedItem2, uncommittedItem3);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ [Fact]
+ public void WhenCommitWinnerIsFalseConflictsWithDifferentKeysAreReported()
+ {
+ var committedItem1 = new MockConflictItem("System.Ben") { AssemblyVersion = new Version("2.0.0.0") };
+ var committedItem2 = new MockConflictItem("System.Immo") { AssemblyVersion = new Version("2.0.0.0") };
+
+ var uncommittedItem1 = new MockConflictItem("System.Ben") { AssemblyVersion = new Version("1.0.0.0") };
+ var uncommittedItem2 = new MockConflictItem("System.Immo") { AssemblyVersion = new Version("3.0.0.0") };
+ var uncommittedItem3 = new MockConflictItem("System.Dave") { AssemblyVersion = new Version("3.0.0.0") };
+ var uncommittedItem4 = new MockConflictItem("System.Ben") { AssemblyVersion = new Version("3.0.0.0") };
+
+ var result = GetConflicts(new[] { committedItem1, committedItem2 }, uncommittedItem1, uncommittedItem2, uncommittedItem3, uncommittedItem4);
+
+ result.Conflicts.Should().Equal(uncommittedItem1, committedItem2, committedItem1);
+ result.UnresolvedConflicts.Should().BeEmpty();
+ }
+
+ static ConflictResults GetConflicts(params MockConflictItem[] items)
+ {
+ return GetConflicts(items, Array.Empty());
+ }
+
+ static ConflictResults GetConflicts(MockConflictItem [] itemsToCommit, params MockConflictItem [] itemsNotToCommit)
+ {
+ ConflictResults ret = new ConflictResults();
+
+ void ConflictHandler(MockConflictItem item)
+ {
+ ret.Conflicts.Add(item);
+ }
+
+ void UnresolvedConflictHandler(MockConflictItem item)
+ {
+ ret.UnresolvedConflicts.Add(item);
+ }
+
+ string[] packagesForRank = itemsToCommit.Concat(itemsNotToCommit)
+ .Select(i => i.PackageId)
+ .Where(id => !id.StartsWith("Unranked", StringComparison.OrdinalIgnoreCase))
+ .Distinct()
+ .OrderBy(id => id)
+ .ToArray();
+
+ var resolver = new ConflictResolver(new PackageRank(packagesForRank), new MockLog());
+
+ resolver.ResolveConflicts(itemsToCommit, GetItemKey, ConflictHandler,
+ unresolvedConflict: UnresolvedConflictHandler);
+
+ resolver.ResolveConflicts(itemsNotToCommit, GetItemKey, ConflictHandler,
+ commitWinner: false,
+ unresolvedConflict: UnresolvedConflictHandler);
+
+ return ret;
+ }
+
+ static string GetItemKey(MockConflictItem item)
+ {
+ return item.Key;
+ }
+
+ class ConflictResults
+ {
+ public List Conflicts { get; set; } = new List();
+ public List UnresolvedConflicts { get; set; } = new List();
+ }
+
+ class MockConflictItem : IConflictItem
+ {
+ public MockConflictItem(string name = "System.Ben")
+ {
+ Key = name + ".dll";
+ AssemblyVersion = new Version("1.0.0.0");
+ ItemType = ConflictItemType.Reference;
+ Exists = true;
+ FileName = name + ".dll";
+ FileVersion = new Version("1.0.0.0");
+ PackageId = name;
+ DisplayName = name;
+ }
+ public string Key { get; set; }
+
+ public Version AssemblyVersion { get; set; }
+
+ public ConflictItemType ItemType { get; set; }
+
+ public bool Exists { get; set; }
+
+ public string FileName { get; set; }
+
+ public Version FileVersion { get; set; }
+
+ public string PackageId { get; set; }
+
+ public string DisplayName { get; set; }
+ }
+
+ class MockLog : ILog
+ {
+ public void LogError(string message, params object[] messageArgs)
+ {
+ }
+
+ public void LogMessage(string message, params object[] messageArgs)
+ {
+ }
+
+ public void LogMessage(LogImportance importance, string message, params object[] messageArgs)
+ {
+ }
+
+ public void LogWarning(string message, params object[] messageArgs)
+ {
+ }
+ }
+
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Microsoft.NET.Build.Tasks.UnitTests.csproj b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Microsoft.NET.Build.Tasks.UnitTests.csproj
index 1bfcdb498c6d..65d575113f75 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Microsoft.NET.Build.Tasks.UnitTests.csproj
+++ b/src/Tasks/Microsoft.NET.Build.Tasks.UnitTests/Microsoft.NET.Build.Tasks.UnitTests.csproj
@@ -42,6 +42,7 @@
+
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ConflictItem.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ConflictItem.cs
new file mode 100644
index 000000000000..0484c7aaa411
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ConflictItem.cs
@@ -0,0 +1,199 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Build.Framework;
+using System;
+using System.IO;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ internal enum ConflictItemType
+ {
+ Reference,
+ CopyLocal,
+ Runtime,
+ Platform
+ }
+
+ internal interface IConflictItem
+ {
+ Version AssemblyVersion { get; }
+ ConflictItemType ItemType { get; }
+ bool Exists { get; }
+ string FileName { get; }
+ Version FileVersion { get; }
+ string PackageId { get; }
+ string DisplayName { get; }
+ }
+
+ // Wraps an ITask item and adds lazy evaluated properties used by Conflict resolution.
+ internal class ConflictItem : IConflictItem
+ {
+ public ConflictItem(ITaskItem originalItem, ConflictItemType itemType)
+ {
+ OriginalItem = originalItem;
+ ItemType = itemType;
+ }
+
+ public ConflictItem(string fileName, string packageId, Version assemblyVersion, Version fileVersion)
+ {
+ OriginalItem = null;
+ ItemType = ConflictItemType.Platform;
+ FileName = fileName;
+ SourcePath = fileName;
+ PackageId = packageId;
+ AssemblyVersion = assemblyVersion;
+ FileVersion = fileVersion;
+ }
+
+ private bool _hasAssemblyVersion;
+ private Version _assemblyVersion;
+ public Version AssemblyVersion
+ {
+ get
+ {
+ if (!_hasAssemblyVersion)
+ {
+ _assemblyVersion = null;
+
+ var assemblyVersionString = OriginalItem?.GetMetadata(nameof(AssemblyVersion)) ?? String.Empty;
+
+ if (assemblyVersionString.Length != 0)
+ {
+ Version.TryParse(assemblyVersionString, out _assemblyVersion);
+ }
+ else
+ {
+ _assemblyVersion = FileUtilities.TryGetAssemblyVersion(SourcePath);
+ }
+
+ // assemblyVersion may be null but don't try to recalculate it
+ _hasAssemblyVersion = true;
+ }
+
+ return _assemblyVersion;
+ }
+ private set
+ {
+ _assemblyVersion = value;
+ _hasAssemblyVersion = true;
+ }
+ }
+
+ public ConflictItemType ItemType { get; }
+
+ private bool? _exists;
+ public bool Exists
+ {
+ get
+ {
+ if (_exists == null)
+ {
+ _exists = ItemType == ConflictItemType.Platform || File.Exists(SourcePath);
+ }
+
+ return _exists.Value;
+ }
+ }
+
+ private string _fileName;
+ public string FileName
+ {
+ get
+ {
+ if (_fileName == null)
+ {
+ _fileName = OriginalItem == null ? String.Empty : OriginalItem.GetMetadata(MetadataNames.FileName) + OriginalItem.GetMetadata(MetadataNames.Extension);
+ }
+ return _fileName;
+ }
+ private set { _fileName = value; }
+ }
+
+ private bool _hasFileVersion;
+ private Version _fileVersion;
+ public Version FileVersion
+ {
+ get
+ {
+ if (!_hasFileVersion)
+ {
+ _fileVersion = null;
+
+ var fileVersionString = OriginalItem?.GetMetadata(nameof(FileVersion)) ?? String.Empty;
+
+ if (fileVersionString.Length != 0)
+ {
+ Version.TryParse(fileVersionString, out _fileVersion);
+ }
+ else
+ {
+ _fileVersion = FileUtilities.GetFileVersion(SourcePath);
+ }
+
+ // fileVersion may be null but don't try to recalculate it
+ _hasFileVersion = true;
+ }
+
+ return _fileVersion;
+ }
+ private set
+ {
+ _fileVersion = value;
+ _hasFileVersion = true;
+ }
+ }
+
+ public ITaskItem OriginalItem { get; }
+
+ private string _packageId;
+ public string PackageId
+ {
+ get
+ {
+ if (_packageId == null)
+ {
+ _packageId = OriginalItem?.GetMetadata(MetadataNames.NuGetPackageId) ?? String.Empty;
+
+ if (_packageId.Length == 0)
+ {
+ _packageId = NuGetUtils.GetPackageIdFromSourcePath(SourcePath) ?? String.Empty;
+ }
+ }
+
+ return _packageId.Length == 0 ? null : _packageId;
+ }
+ private set { _packageId = value; }
+ }
+
+
+ private string _sourcePath;
+ public string SourcePath
+ {
+ get
+ {
+ if (_sourcePath == null)
+ {
+ _sourcePath = ItemUtilities.GetSourcePath(OriginalItem) ?? String.Empty;
+ }
+
+ return _sourcePath.Length == 0 ? null : _sourcePath;
+ }
+ private set { _sourcePath = value; }
+ }
+
+ private string _displayName;
+ public string DisplayName
+ {
+ get
+ {
+ if (_displayName == null)
+ {
+ var itemSpec = OriginalItem == null ? FileName : OriginalItem.ItemSpec;
+ _displayName = $"{ItemType}:{itemSpec}";
+ }
+ return _displayName;
+ }
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ConflictResolver.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ConflictResolver.cs
new file mode 100644
index 000000000000..0fedc03c3079
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ConflictResolver.cs
@@ -0,0 +1,256 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Build.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ // The conflict resolver finds conflicting items, and if there are any of them it reports the "losing" item via the foundConflict callback
+ internal class ConflictResolver where TConflictItem : class, IConflictItem
+ {
+ private Dictionary winningItemsByKey = new Dictionary();
+ private ILog log;
+ private PackageRank packageRank;
+
+ public ConflictResolver(PackageRank packageRank, ILog log)
+ {
+ this.log = log;
+ this.packageRank = packageRank;
+ }
+
+ public void ResolveConflicts(IEnumerable conflictItems, Func getItemKey,
+ Action foundConflict, bool commitWinner = true,
+ Action unresolvedConflict = null)
+ {
+ if (conflictItems == null)
+ {
+ return;
+ }
+
+ foreach (var conflictItem in conflictItems)
+ {
+ var itemKey = getItemKey(conflictItem);
+
+ if (String.IsNullOrEmpty(itemKey))
+ {
+ continue;
+ }
+
+ TConflictItem existingItem;
+
+ if (winningItemsByKey.TryGetValue(itemKey, out existingItem))
+ {
+ // a conflict was found, determine the winner.
+ var winner = ResolveConflict(existingItem, conflictItem);
+
+ if (winner == null)
+ {
+ // no winner, skip it.
+ // don't add to conflict list and just use the existing item for future conflicts.
+
+ // Report unresolved conflict (currently just used as a test hook)
+ unresolvedConflict?.Invoke(conflictItem);
+
+ continue;
+ }
+
+ TConflictItem loser = conflictItem;
+ if (!ReferenceEquals(winner, existingItem))
+ {
+ // replace existing item
+ if (commitWinner)
+ {
+ winningItemsByKey[itemKey] = conflictItem;
+ }
+ else
+ {
+ winningItemsByKey.Remove(itemKey);
+ }
+ loser = existingItem;
+
+ }
+
+ foundConflict(loser);
+ }
+ else if (commitWinner)
+ {
+ winningItemsByKey[itemKey] = conflictItem;
+ }
+ }
+ }
+
+ readonly string SENTENCE_SPACING = " ";
+
+ private TConflictItem ResolveConflict(TConflictItem item1, TConflictItem item2)
+ {
+ string conflictMessage = string.Format(CultureInfo.CurrentCulture, Strings.EncounteredConflict,
+ item1.DisplayName,
+ item2.DisplayName);
+
+
+ var exists1 = item1.Exists;
+ var exists2 = item2.Exists;
+
+ if (!exists1 && !exists2)
+ {
+ // If neither file exists, then don't report a conflict, as both items should be resolved (or not) to the same reference assembly
+ return null;
+ }
+
+ if (!exists1 || !exists2)
+ {
+ string fileMessage = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.CurrentCulture, Strings.CouldNotDetermineWinner_DoesntExist,
+ !exists1 ? item1.DisplayName : item2.DisplayName);
+
+ log.LogMessage(fileMessage);
+ return null;
+ }
+
+ var assemblyVersion1 = item1.AssemblyVersion;
+ var assemblyVersion2 = item2.AssemblyVersion;
+
+ // if only one is missing version stop: something is wrong when we have a conflict between assembly and non-assembly
+ if (assemblyVersion1 == null ^ assemblyVersion2 == null)
+ {
+ var nonAssembly = assemblyVersion1 == null ? item1.DisplayName : item2.DisplayName;
+ string assemblyMessage = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.CurrentCulture, Strings.CouldNotDetermineWinner_NotAnAssembly,
+ nonAssembly);
+
+ log.LogMessage(assemblyMessage);
+ return null;
+ }
+
+ // only handle cases where assembly version is different, and not null (implicit here due to xor above)
+ if (assemblyVersion1 != assemblyVersion2)
+ {
+ string winningDisplayName;
+ Version winningVersion;
+ Version losingVersion;
+ if (assemblyVersion1 > assemblyVersion2)
+ {
+ winningDisplayName = item1.DisplayName;
+ winningVersion = assemblyVersion1;
+ losingVersion = assemblyVersion2;
+ }
+ else
+ {
+ winningDisplayName = item2.DisplayName;
+ winningVersion = assemblyVersion2;
+ losingVersion = assemblyVersion1;
+ }
+
+
+ string assemblyMessage = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.CurrentCulture, Strings.ChoosingAssemblyVersion,
+ winningDisplayName,
+ winningVersion,
+ losingVersion);
+
+ log.LogMessage(assemblyMessage);
+
+ if (assemblyVersion1 > assemblyVersion2)
+ {
+ return item1;
+ }
+
+ if (assemblyVersion2 > assemblyVersion1)
+ {
+ return item2;
+ }
+ }
+
+ var fileVersion1 = item1.FileVersion;
+ var fileVersion2 = item2.FileVersion;
+
+ // if only one is missing version
+ if (fileVersion1 == null ^ fileVersion2 == null)
+ {
+ var nonVersion = fileVersion1 == null ? item1.DisplayName : item2.DisplayName;
+ string fileVersionMessage = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.CurrentCulture, Strings.CouldNotDetermineWinner_FileVersion,
+ nonVersion);
+ return null;
+ }
+
+ if (fileVersion1 != fileVersion2)
+ {
+ string winningDisplayName;
+ Version winningVersion;
+ Version losingVersion;
+ if (fileVersion1 > fileVersion2)
+ {
+ winningDisplayName = item1.DisplayName;
+ winningVersion = fileVersion1;
+ losingVersion = fileVersion2;
+ }
+ else
+ {
+ winningDisplayName = item2.DisplayName;
+ winningVersion = fileVersion2;
+ losingVersion = fileVersion1;
+ }
+
+
+ string fileVersionMessage = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.CurrentCulture, Strings.ChoosingFileVersion,
+ winningDisplayName,
+ winningVersion,
+ losingVersion);
+
+ log.LogMessage(fileVersionMessage);
+
+ if (fileVersion1 > fileVersion2)
+ {
+ return item1;
+ }
+
+ if (fileVersion2 > fileVersion1)
+ {
+ return item2;
+ }
+ }
+
+ var packageRank1 = packageRank.GetPackageRank(item1.PackageId);
+ var packageRank2 = packageRank.GetPackageRank(item2.PackageId);
+
+ if (packageRank1 < packageRank2)
+ {
+ string packageRankMessage = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.CurrentCulture, Strings.ChoosingPreferredPackage,
+ item1.DisplayName);
+ log.LogMessage(packageRankMessage);
+ return item1;
+ }
+
+ if (packageRank2 < packageRank1)
+ {
+ string packageRankMessage = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.CurrentCulture, Strings.ChoosingPreferredPackage,
+ item2.DisplayName);
+ return item2;
+ }
+
+ var isPlatform1 = item1.ItemType == ConflictItemType.Platform;
+ var isPlatform2 = item2.ItemType == ConflictItemType.Platform;
+
+ if (isPlatform1 && !isPlatform2)
+ {
+ string platformMessage = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.CurrentCulture, Strings.ChoosingPlatformItem,
+ item1.DisplayName);
+ log.LogMessage(platformMessage);
+ return item1;
+ }
+
+ if (!isPlatform1 && isPlatform2)
+ {
+ string platformMessage = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.CurrentCulture, Strings.ChoosingPlatformItem,
+ item2.DisplayName);
+ log.LogMessage(platformMessage);
+ return item2;
+ }
+
+ string message = conflictMessage + SENTENCE_SPACING + string.Format(CultureInfo.InvariantCulture, Strings.ConflictCouldNotDetermineWinner);
+
+ log.LogMessage(message);
+ return null;
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/FileUtilities.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/FileUtilities.cs
new file mode 100644
index 000000000000..73bd82e0fe9c
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/FileUtilities.cs
@@ -0,0 +1,34 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ static partial class FileUtilities
+ {
+ public static Version GetFileVersion(string sourcePath)
+ {
+ var fvi = FileVersionInfo.GetVersionInfo(sourcePath);
+
+ if (fvi != null)
+ {
+ return new Version(fvi.FileMajorPart, fvi.FileMinorPart, fvi.FileBuildPart, fvi.FilePrivatePart);
+ }
+
+ return null;
+ }
+
+ static readonly HashSet s_assemblyExtensions = new HashSet(new[] { ".dll", ".exe", ".winmd" }, StringComparer.OrdinalIgnoreCase);
+ public static Version TryGetAssemblyVersion(string sourcePath)
+ {
+ var extension = Path.GetExtension(sourcePath);
+
+ return s_assemblyExtensions.Contains(extension) ? GetAssemblyVersion(sourcePath) : null;
+ }
+
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/FileUtilities.net45.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/FileUtilities.net45.cs
new file mode 100644
index 000000000000..8419c4c36d83
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/FileUtilities.net45.cs
@@ -0,0 +1,27 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+#if NET46
+
+using System;
+using System.Reflection;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ static partial class FileUtilities
+ {
+ private static Version GetAssemblyVersion(string sourcePath)
+ {
+ try
+ {
+ return AssemblyName.GetAssemblyName(sourcePath)?.Version;
+ }
+ catch(BadImageFormatException)
+ {
+ return null;
+ }
+ }
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/FileUtilities.netstandard.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/FileUtilities.netstandard.cs
new file mode 100644
index 000000000000..22c16a588ba6
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/FileUtilities.netstandard.cs
@@ -0,0 +1,45 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+#if NETCOREAPP1_0
+
+using System;
+using System.IO;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ static partial class FileUtilities
+ {
+ private static Version GetAssemblyVersion(string sourcePath)
+ {
+ using (var assemblyStream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.Delete | FileShare.Read))
+ {
+ Version result = null;
+ try
+ {
+ using (PEReader peReader = new PEReader(assemblyStream, PEStreamOptions.LeaveOpen))
+ {
+ if (peReader.HasMetadata)
+ {
+ MetadataReader reader = peReader.GetMetadataReader();
+ if (reader.IsAssembly)
+ {
+ result = reader.GetAssemblyDefinition().Version;
+ }
+ }
+ }
+ }
+ catch (BadImageFormatException)
+ {
+ // not a PE
+ }
+
+ return result;
+ }
+ }
+ }
+}
+
+#endif
\ No newline at end of file
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ILog.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ILog.cs
new file mode 100644
index 000000000000..2fc6b390e071
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ILog.cs
@@ -0,0 +1,85 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Build.Framework;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ public enum LogImportance
+ {
+ Low = MessageImportance.Low,
+ Normal = MessageImportance.Normal,
+ High = MessageImportance.High
+ }
+
+
+ public interface ILog
+ {
+ //
+ // Summary:
+ // Logs an error with the specified message.
+ //
+ // Parameters:
+ // message:
+ // The message.
+ //
+ // messageArgs:
+ // Optional arguments for formatting the message string.
+ //
+ // Exceptions:
+ // T:System.ArgumentNullException:
+ // message is null.
+ void LogError(string message, params object[] messageArgs);
+
+ //
+ // Summary:
+ // Logs a message with the specified string.
+ //
+ // Parameters:
+ // message:
+ // The message.
+ //
+ // messageArgs:
+ // The arguments for formatting the message.
+ //
+ // Exceptions:
+ // T:System.ArgumentNullException:
+ // message is null.
+ void LogMessage(string message, params object[] messageArgs);
+
+ //
+ // Summary:
+ // Logs a message with the specified string and importance.
+ //
+ // Parameters:
+ // importance:
+ // One of the enumeration values that specifies the importance of the message.
+ //
+ // message:
+ // The message.
+ //
+ // messageArgs:
+ // The arguments for formatting the message.
+ //
+ // Exceptions:
+ // T:System.ArgumentNullException:
+ // message is null.
+ void LogMessage(LogImportance importance, string message, params object[] messageArgs);
+
+ //
+ // Summary:
+ // Logs a warning with the specified message.
+ //
+ // Parameters:
+ // message:
+ // The message.
+ //
+ // messageArgs:
+ // Optional arguments for formatting the message string.
+ //
+ // Exceptions:
+ // T:System.ArgumentNullException:
+ // message is null.
+ void LogWarning(string message, params object[] messageArgs);
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/MSBuildLog.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/MSBuildLog.cs
new file mode 100644
index 000000000000..c8b588499829
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/MSBuildLog.cs
@@ -0,0 +1,38 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ internal class MSBuildLog : ILog
+ {
+ private TaskLoggingHelper logger;
+ public MSBuildLog(TaskLoggingHelper logger)
+ {
+ this.logger = logger;
+ }
+
+ public void LogError(string message, params object[] messageArgs)
+ {
+ logger.LogError(message, messageArgs);
+ }
+
+ public void LogMessage(string message, params object[] messageArgs)
+ {
+ logger.LogMessage(message, messageArgs);
+ }
+
+ public void LogMessage(LogImportance importance, string message, params object[] messageArgs)
+ {
+ logger.LogMessage((MessageImportance)importance, message, messageArgs);
+ }
+
+ public void LogWarning(string message, params object[] messageArgs)
+ {
+ logger.LogWarning(message, messageArgs);
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/MSBuildUtilities.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/MSBuildUtilities.cs
new file mode 100644
index 000000000000..bd71a059ead8
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/MSBuildUtilities.cs
@@ -0,0 +1,69 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ ///
+ /// Internal utilties copied from microsoft/MSBuild repo.
+ ///
+ class MSBuildUtilities
+ {
+ ///
+ /// Converts a string to a bool. We consider "true/false", "on/off", and
+ /// "yes/no" to be valid boolean representations in the XML.
+ /// Modified from its original version to not throw, but return a default value.
+ ///
+ /// The string to convert.
+ /// Boolean true or false, corresponding to the string.
+ internal static bool ConvertStringToBool(string parameterValue, bool defaultValue = false)
+ {
+ if (String.IsNullOrEmpty(parameterValue))
+ {
+ return defaultValue;
+ }
+ else if (ValidBooleanTrue(parameterValue))
+ {
+ return true;
+ }
+ else if (ValidBooleanFalse(parameterValue))
+ {
+ return false;
+ }
+ else
+ {
+ // Unsupported boolean representation.
+ return defaultValue;
+ }
+ }
+
+ ///
+ /// Returns true if the string represents a valid MSBuild boolean true value,
+ /// such as "on", "!false", "yes"
+ ///
+ private static bool ValidBooleanTrue(string parameterValue)
+ {
+ return ((String.Compare(parameterValue, "true", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "on", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "yes", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "!false", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "!off", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "!no", StringComparison.OrdinalIgnoreCase) == 0));
+ }
+
+ ///
+ /// Returns true if the string represents a valid MSBuild boolean false value,
+ /// such as "!on" "off" "no" "!true"
+ ///
+ private static bool ValidBooleanFalse(string parameterValue)
+ {
+ return ((String.Compare(parameterValue, "false", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "off", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "no", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "!true", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "!on", StringComparison.OrdinalIgnoreCase) == 0) ||
+ (String.Compare(parameterValue, "!yes", StringComparison.OrdinalIgnoreCase) == 0));
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/MetadataNames.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/MetadataNames.cs
new file mode 100644
index 000000000000..989fa009591d
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/MetadataNames.cs
@@ -0,0 +1,19 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+
+{
+ static class MetadataNames
+ {
+ public const string Aliases = "Aliases";
+ public const string DestinationSubPath = "DestinationSubPath";
+ public const string Extension = "Extension";
+ public const string FileName = "FileName";
+ public const string HintPath = "HintPath";
+ public const string NuGetPackageId = "NuGetPackageId";
+ public const string Path = "Path";
+ public const string Private = "Private";
+ public const string TargetPath = "TargetPath";
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/PackageRank.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/PackageRank.cs
new file mode 100644
index 000000000000..99480a808044
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/PackageRank.cs
@@ -0,0 +1,48 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ class PackageRank
+ {
+ private Dictionary packageRanks;
+
+ public PackageRank(string[] packageIds)
+ {
+ var numPackages = packageIds?.Length ?? 0;
+
+ // cache ranks for fast lookup
+ packageRanks = new Dictionary(numPackages, StringComparer.OrdinalIgnoreCase);
+
+ for (int i = numPackages - 1; i >= 0; i--)
+ {
+ var preferredPackageId = packageIds[i].Trim();
+
+ if (preferredPackageId.Length != 0)
+ {
+ // overwrite any duplicates, lowest rank will win.
+ packageRanks[preferredPackageId] = i;
+ }
+ }
+ }
+
+ ///
+ /// Get's the rank of a package, lower packages are preferred
+ ///
+ /// id of package
+ /// rank of package
+ public int GetPackageRank(string packageId)
+ {
+ int rank;
+ if (packageId != null && packageRanks.TryGetValue(packageId, out rank))
+ {
+ return rank;
+ }
+
+ return int.MaxValue;
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/PlatformManifestReader.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/PlatformManifestReader.cs
new file mode 100644
index 000000000000..2412fc09dcd6
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/PlatformManifestReader.cs
@@ -0,0 +1,86 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Build.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ static class PlatformManifestReader
+ {
+ static readonly char[] s_manifestLineSeparator = new[] { '|' };
+ public static IEnumerable LoadConflictItems(string manifestPath, ILog log)
+ {
+ if (manifestPath == null)
+ {
+ throw new ArgumentNullException(nameof(manifestPath));
+ }
+
+ if (!File.Exists(manifestPath))
+ {
+ string errorMessage = string.Format(CultureInfo.InvariantCulture, Strings.CouldNotLoadPlatformManifest,
+ manifestPath);
+ log.LogError(errorMessage);
+ yield break;
+ }
+
+ using (var manfiestStream = File.Open(manifestPath, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete))
+ using (var manifestReader = new StreamReader(manfiestStream))
+ {
+ for (int lineNumber = 0; !manifestReader.EndOfStream; lineNumber++)
+ {
+ var line = manifestReader.ReadLine().Trim();
+
+ if (line.Length == 0 || line[0] == '#')
+ {
+ continue;
+ }
+
+ var lineParts = line.Split(s_manifestLineSeparator);
+
+ if (lineParts.Length != 4)
+ {
+ string errorMessage = string.Format(CultureInfo.InvariantCulture, Strings.ErrorParsingPlatformManifest,
+ manifestPath,
+ lineNumber,
+ "fileName|packageId|assemblyVersion|fileVersion");
+ log.LogError(errorMessage);
+ yield break;
+ }
+
+ var fileName = lineParts[0].Trim();
+ var packageId = lineParts[1].Trim();
+ var assemblyVersionString = lineParts[2].Trim();
+ var fileVersionString = lineParts[3].Trim();
+
+ Version assemblyVersion = null, fileVersion = null;
+
+ if (assemblyVersionString.Length != 0 && !Version.TryParse(assemblyVersionString, out assemblyVersion))
+ {
+ string errorMessage = string.Format(CultureInfo.InvariantCulture, Strings.ErrorParsingPlatformManifestInvalidValue,
+ manifestPath,
+ lineNumber,
+ "AssemblyVersion",
+ assemblyVersionString);
+ log.LogError(errorMessage);
+ }
+
+ if (fileVersionString.Length != 0 && !Version.TryParse(fileVersionString, out fileVersion))
+ {
+ string errorMessage = string.Format(CultureInfo.InvariantCulture, Strings.ErrorParsingPlatformManifestInvalidValue,
+ manifestPath,
+ lineNumber,
+ "FileVersion",
+ fileVersionString);
+ log.LogError(errorMessage);
+ }
+
+ yield return new ConflictItem(fileName, packageId, assemblyVersion, fileVersion);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ResolvePackageFileConflicts.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ResolvePackageFileConflicts.cs
new file mode 100644
index 000000000000..abe3dfb9c47c
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ConflictResolution/ResolvePackageFileConflicts.cs
@@ -0,0 +1,181 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using Microsoft.Build.Framework;
+using Microsoft.Build.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.NET.Build.Tasks.ConflictResolution
+{
+ public class ResolvePackageFileConflicts : TaskBase
+ {
+ private HashSet referenceConflicts = new HashSet();
+ private HashSet copyLocalConflicts = new HashSet();
+ private HashSet allConflicts = new HashSet();
+
+ public ITaskItem[] References { get; set; }
+
+ public ITaskItem[] ReferenceCopyLocalPaths { get; set; }
+
+ public ITaskItem[] OtherRuntimeItems { get; set; }
+
+ public ITaskItem[] PlatformManifests { get; set; }
+
+ ///
+ /// NuGet3 and later only. In the case of a conflict with identical file version information a file from the most preferred package will be chosen.
+ ///
+ public string[] PreferredPackages { get; set; }
+
+ [Output]
+ public ITaskItem[] ReferencesWithoutConflicts { get; set; }
+
+ [Output]
+ public ITaskItem[] ReferenceCopyLocalPathsWithoutConflicts { get; set; }
+
+ [Output]
+ public ITaskItem[] Conflicts { get; set; }
+
+ protected override void ExecuteCore()
+ {
+ var log = new MSBuildLog(Log);
+ var packageRanks = new PackageRank(PreferredPackages);
+
+ // resolve conflicts at compile time
+ var referenceItems = GetConflictTaskItems(References, ConflictItemType.Reference).ToArray();
+
+ var compileConflictScope = new ConflictResolver(packageRanks, log);
+
+ compileConflictScope.ResolveConflicts(referenceItems,
+ ci => ItemUtilities.GetReferenceFileName(ci.OriginalItem),
+ HandleCompileConflict);
+
+ // resolve conflicts that class in output
+ var runtimeConflictScope = new ConflictResolver(packageRanks, log);
+
+ runtimeConflictScope.ResolveConflicts(referenceItems,
+ ci => ItemUtilities.GetReferenceTargetPath(ci.OriginalItem),
+ HandleRuntimeConflict);
+
+ var copyLocalItems = GetConflictTaskItems(ReferenceCopyLocalPaths, ConflictItemType.CopyLocal).ToArray();
+
+ runtimeConflictScope.ResolveConflicts(copyLocalItems,
+ ci => ItemUtilities.GetTargetPath(ci.OriginalItem),
+ HandleRuntimeConflict);
+
+ var otherRuntimeItems = GetConflictTaskItems(OtherRuntimeItems, ConflictItemType.Runtime).ToArray();
+
+ runtimeConflictScope.ResolveConflicts(otherRuntimeItems,
+ ci => ItemUtilities.GetTargetPath(ci.OriginalItem),
+ HandleRuntimeConflict);
+
+
+ // resolve conflicts with platform (eg: shared framework) items
+ // we only commit the platform items since its not a conflict if other items share the same filename.
+ var platformConflictScope = new ConflictResolver(packageRanks, log);
+ var platformItems = PlatformManifests?.SelectMany(pm => PlatformManifestReader.LoadConflictItems(pm.ItemSpec, log)) ?? Enumerable.Empty();
+
+ platformConflictScope.ResolveConflicts(platformItems, pi => pi.FileName, pi => { });
+ platformConflictScope.ResolveConflicts(referenceItems.Where(ri => !referenceConflicts.Contains(ri.OriginalItem)),
+ ri => ItemUtilities.GetReferenceTargetFileName(ri.OriginalItem),
+ HandleRuntimeConflict,
+ commitWinner:false);
+ platformConflictScope.ResolveConflicts(copyLocalItems.Where(ci => !copyLocalConflicts.Contains(ci.OriginalItem)),
+ ri => ri.FileName,
+ HandleRuntimeConflict,
+ commitWinner: false);
+ platformConflictScope.ResolveConflicts(otherRuntimeItems,
+ ri => ri.FileName,
+ HandleRuntimeConflict,
+ commitWinner: false);
+
+ ReferencesWithoutConflicts = RemoveConflicts(References, referenceConflicts);
+ ReferenceCopyLocalPathsWithoutConflicts = RemoveConflicts(ReferenceCopyLocalPaths, copyLocalConflicts);
+ Conflicts = CreateConflictTaskItems(allConflicts);
+ }
+
+ private ITaskItem[] CreateConflictTaskItems(ICollection conflicts)
+ {
+ var conflictItems = new ITaskItem[conflicts.Count];
+
+ int i = 0;
+ foreach(var conflict in conflicts)
+ {
+ conflictItems[i++] = CreateConflictTaskItem(conflict);
+ }
+
+ return conflictItems;
+ }
+
+ private ITaskItem CreateConflictTaskItem(ConflictItem conflict)
+ {
+ var item = new TaskItem(conflict.SourcePath);
+
+ if (conflict.PackageId != null)
+ {
+ item.SetMetadata(nameof(ConflictItemType), conflict.ItemType.ToString());
+ }
+
+ return item;
+ }
+
+ private IEnumerable GetConflictTaskItems(ITaskItem[] items, ConflictItemType itemType)
+ {
+ return (items != null) ? items.Select(i => new ConflictItem(i, itemType)) : Enumerable.Empty();
+ }
+
+ private void HandleCompileConflict(ConflictItem conflictItem)
+ {
+ if (conflictItem.ItemType == ConflictItemType.Reference)
+ {
+ referenceConflicts.Add(conflictItem.OriginalItem);
+ }
+ allConflicts.Add(conflictItem);
+ }
+
+ private void HandleRuntimeConflict(ConflictItem conflictItem)
+ {
+ if (conflictItem.ItemType == ConflictItemType.Reference)
+ {
+ conflictItem.OriginalItem.SetMetadata(MetadataNames.Private, "False");
+ }
+ else if (conflictItem.ItemType == ConflictItemType.CopyLocal)
+ {
+ copyLocalConflicts.Add(conflictItem.OriginalItem);
+ }
+ allConflicts.Add(conflictItem);
+ }
+
+ ///
+ /// Filters conflicts from original, maintaining order.
+ ///
+ ///
+ ///
+ ///
+ private ITaskItem[] RemoveConflicts(ITaskItem[] original, ICollection conflicts)
+ {
+ if (conflicts.Count == 0)
+ {
+ return original;
+ }
+
+ var result = new ITaskItem[original.Length - conflicts.Count];
+ int index = 0;
+
+ foreach(var originalItem in original)
+ {
+ if (!conflicts.Contains(originalItem))
+ {
+ if (index >= result.Length)
+ {
+ throw new ArgumentException($"Items from {nameof(conflicts)} were missing from {nameof(original)}");
+ }
+ result[index++] = originalItem;
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs
index 2bf4a5189544..eb513ee03cf3 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs
@@ -1,13 +1,15 @@
// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-using System.Collections.Generic;
-using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.Extensions.DependencyModel;
using Newtonsoft.Json;
using NuGet.ProjectModel;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
namespace Microsoft.NET.Build.Tasks
{
@@ -50,6 +52,9 @@ public class GenerateDepsFile : TaskBase
[Required]
public ITaskItem[] ReferenceSatellitePaths { get; set; }
+ [Required]
+ public ITaskItem[] FilesToSkip { get; set; }
+
public ITaskItem CompilerOptions { get; set; }
public ITaskItem[] PrivateAssetsPackageReferences { get; set; }
@@ -64,8 +69,13 @@ public ITaskItem[] FilesWritten
get { return _filesWritten.ToArray(); }
}
+ private Dictionary> compileFilesToSkip = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+ private Dictionary> runtimeFilesToSkip = new Dictionary>(StringComparer.OrdinalIgnoreCase);
+
protected override void ExecuteCore()
{
+ LoadFilesToSkip();
+
LockFile lockFile = new LockFileCache(BuildEngine4).GetLockFile(AssetsFilePath);
CompilationOptions compilationOptions = CompilationOptionsConverter.ConvertFrom(CompilerOptions);
@@ -103,6 +113,11 @@ protected override void ExecuteCore()
.WithReferenceAssembliesPath(FrameworkReferenceResolver.GetDefaultReferenceAssembliesPath())
.Build();
+ if (compileFilesToSkip.Any() || runtimeFilesToSkip.Any())
+ {
+ dependencyContext = TrimFilesToSkip(dependencyContext);
+ }
+
var writer = new DependencyContextWriter();
using (var fileStream = File.Create(DepsFilePath))
{
@@ -111,5 +126,115 @@ protected override void ExecuteCore()
_filesWritten.Add(new TaskItem(DepsFilePath));
}
+
+ private void LoadFilesToSkip()
+ {
+ foreach (var fileToSkip in FilesToSkip)
+ {
+ string packageId, packageSubPath;
+ NuGetUtils.GetPackageParts(fileToSkip.ItemSpec, out packageId, out packageSubPath);
+
+ if (String.IsNullOrEmpty(packageId) || String.IsNullOrEmpty(packageSubPath))
+ {
+ continue;
+ }
+
+ var itemType = fileToSkip.GetMetadata(nameof(ConflictResolution.ConflictItemType));
+ var packagesWithFilesToSkip = (itemType == nameof(ConflictResolution.ConflictItemType.Reference)) ? compileFilesToSkip : runtimeFilesToSkip;
+
+ HashSet filesToSkipForPackage;
+ if (!packagesWithFilesToSkip.TryGetValue(packageId, out filesToSkipForPackage))
+ {
+ packagesWithFilesToSkip[packageId] = filesToSkipForPackage = new HashSet(StringComparer.OrdinalIgnoreCase);
+ }
+
+ filesToSkipForPackage.Add(packageSubPath);
+ }
+ }
+
+ private DependencyContext TrimFilesToSkip(DependencyContext sourceDeps)
+ {
+ return new DependencyContext(sourceDeps.Target,
+ sourceDeps.CompilationOptions,
+ TrimCompilationLibraries(sourceDeps.CompileLibraries),
+ TrimRuntimeLibraries(sourceDeps.RuntimeLibraries),
+ sourceDeps.RuntimeGraph);
+ }
+
+ private IEnumerable TrimRuntimeLibraries(IReadOnlyList runtimeLibraries)
+ {
+ foreach (var runtimeLibrary in runtimeLibraries)
+ {
+ HashSet filesToSkip;
+ if (runtimeFilesToSkip.TryGetValue(runtimeLibrary.Name, out filesToSkip))
+ {
+ yield return new RuntimeLibrary(runtimeLibrary.Type,
+ runtimeLibrary.Name,
+ runtimeLibrary.Version,
+ runtimeLibrary.Hash,
+ TrimAssetGroups(runtimeLibrary.RuntimeAssemblyGroups, filesToSkip).ToArray(),
+ TrimAssetGroups(runtimeLibrary.NativeLibraryGroups, filesToSkip).ToArray(),
+ TrimResourceAssemblies(runtimeLibrary.ResourceAssemblies, filesToSkip),
+ runtimeLibrary.Dependencies,
+ runtimeLibrary.Serviceable);
+ }
+ else
+ {
+ yield return runtimeLibrary;
+ }
+ }
+ }
+
+ private IEnumerable TrimAssetGroups(IEnumerable assetGroups, ISet filesToTrim)
+ {
+ foreach (var assetGroup in assetGroups)
+ {
+ yield return new RuntimeAssetGroup(assetGroup.Runtime, TrimAssemblies(assetGroup.AssetPaths, filesToTrim));
+ }
+ }
+
+ private IEnumerable TrimResourceAssemblies(IEnumerable resourceAssemblies, ISet filesToTrim)
+ {
+ foreach (var resourceAssembly in resourceAssemblies)
+ {
+ if (!filesToTrim.Contains(resourceAssembly.Path))
+ {
+ yield return resourceAssembly;
+ }
+ }
+ }
+
+ private IEnumerable TrimCompilationLibraries(IReadOnlyList compileLibraries)
+ {
+ foreach (var compileLibrary in compileLibraries)
+ {
+ HashSet filesToSkip;
+ if (compileFilesToSkip.TryGetValue(compileLibrary.Name, out filesToSkip))
+ {
+ yield return new CompilationLibrary(compileLibrary.Type,
+ compileLibrary.Name,
+ compileLibrary.Version,
+ compileLibrary.Hash,
+ TrimAssemblies(compileLibrary.Assemblies, filesToSkip),
+ compileLibrary.Dependencies,
+ compileLibrary.Serviceable);
+ }
+ else
+ {
+ yield return compileLibrary;
+ }
+ }
+ }
+
+ private IEnumerable TrimAssemblies(IEnumerable assemblies, ISet filesToTrim)
+ {
+ foreach (var assembly in assemblies)
+ {
+ if (!filesToTrim.Contains(assembly))
+ {
+ yield return assembly;
+ }
+ }
+ }
}
}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ITaskItemExtensions.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ITaskItemExtensions.cs
deleted file mode 100644
index 7c86fee2d33d..000000000000
--- a/src/Tasks/Microsoft.NET.Build.Tasks/ITaskItemExtensions.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) .NET Foundation and contributors. All rights reserved.
-// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-
-using System;
-using Microsoft.Build.Framework;
-
-namespace Microsoft.NET.Build.Tasks
-{
- internal static class ITaskItemExtensions
- {
- public static bool? GetBooleanMetadata(this ITaskItem item, string metadataName)
- {
- bool? result = null;
-
- string value = item.GetMetadata(metadataName);
- bool parsedResult;
- if (bool.TryParse(value, out parsedResult))
- {
- result = parsedResult;
- }
-
- return result;
- }
-
- public static bool HasMetadataValue(this ITaskItem item, string name, string expectedValue)
- {
- string value = item.GetMetadata(name);
-
- return string.Equals(value, expectedValue, StringComparison.OrdinalIgnoreCase);
- }
- }
-}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ItemUtilities.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ItemUtilities.cs
new file mode 100644
index 000000000000..d87a534dca1a
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/ItemUtilities.cs
@@ -0,0 +1,136 @@
+// Copyright (c) .NET Foundation and contributors. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+using System;
+using Microsoft.Build.Framework;
+using Microsoft.NET.Build.Tasks.ConflictResolution;
+using System.IO;
+
+namespace Microsoft.NET.Build.Tasks
+{
+ internal static class ItemUtilities
+ {
+ public static bool? GetBooleanMetadata(this ITaskItem item, string metadataName)
+ {
+ bool? result = null;
+
+ string value = item.GetMetadata(metadataName);
+ bool parsedResult;
+ if (bool.TryParse(value, out parsedResult))
+ {
+ result = parsedResult;
+ }
+
+ return result;
+ }
+
+ public static bool HasMetadataValue(this ITaskItem item, string name, string expectedValue)
+ {
+ string value = item.GetMetadata(name);
+
+ return string.Equals(value, expectedValue, StringComparison.OrdinalIgnoreCase);
+ }
+
+ ///
+ /// Get's the filename to use for identifying reference conflicts
+ ///
+ ///
+ ///
+ public static string GetReferenceFileName(ITaskItem item)
+ {
+ var aliases = item.GetMetadata(MetadataNames.Aliases);
+
+ if (!String.IsNullOrEmpty(aliases))
+ {
+ // skip compile-time conflict detection for aliased assemblies.
+ // An alias is the way to avoid a conflict
+ // eg: System, v1.0.0.0 in global will not conflict with System, v2.0.0.0 in `private` alias
+ // We could model each alias scope and try to check for conflicts within that scope,
+ // but this is a ton of complexity for a fringe feature.
+ // Instead, we'll treat an alias as an indication that the developer has opted out of
+ // conflict resolution.
+ return null;
+ }
+
+ // We only handle references that have path information since we're only concerned
+ // with resolving conflicts between file references. If conflicts exist between
+ // named references that are found from AssemblySearchPaths we'll leave those to
+ // RAR to handle or not as it sees fit.
+ var sourcePath = GetSourcePath(item);
+
+ if (String.IsNullOrEmpty(sourcePath))
+ {
+ return null;
+ }
+
+ try
+ {
+ return Path.GetFileName(sourcePath);
+ }
+ catch (ArgumentException)
+ {
+ // We won't even try to resolve a conflict if we can't open the file, so ignore invalid paths
+ return null;
+ }
+ }
+
+ public static string GetReferenceTargetPath(ITaskItem item)
+ {
+ // Determine if the reference will be copied local.
+ // We're only dealing with primary file references. For these RAR will
+ // copy local if Private is true or unset.
+
+ var isPrivate = MSBuildUtilities.ConvertStringToBool(item.GetMetadata(MetadataNames.Private), defaultValue: true);
+
+ if (!isPrivate)
+ {
+ // Private = false means the reference shouldn't be copied.
+ return null;
+ }
+
+ return GetTargetPath(item);
+ }
+
+ public static string GetReferenceTargetFileName(ITaskItem item)
+ {
+ var targetPath = GetReferenceTargetPath(item);
+
+ return targetPath != null ? Path.GetFileName(targetPath) : null;
+ }
+
+ public static string GetSourcePath(ITaskItem item)
+ {
+ var sourcePath = item.GetMetadata(MetadataNames.HintPath);
+
+ if (String.IsNullOrWhiteSpace(sourcePath))
+ {
+ // assume item-spec points to the file.
+ // this won't work if it comes from a targeting pack or SDK, but
+ // in that case the file won't exist and we'll skip it.
+ sourcePath = item.ItemSpec;
+ }
+
+ return sourcePath;
+ }
+
+ static readonly string[] s_targetPathMetadata = new[] { MetadataNames.TargetPath, MetadataNames.DestinationSubPath };
+ public static string GetTargetPath(ITaskItem item)
+ {
+ // first use TargetPath, DestinationSubPath, then Path, then fallback to filename+extension alone
+ foreach (var metadata in s_targetPathMetadata)
+ {
+ var value = item.GetMetadata(metadata);
+
+ if (!String.IsNullOrWhiteSpace(value))
+ {
+ // normalize path
+ return value.Replace('\\', '/');
+ }
+ }
+
+ var sourcePath = GetSourcePath(item);
+
+ return Path.GetFileName(sourcePath);
+ }
+ }
+}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj b/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj
index 2ef85a0ce908..c1f1180767d0 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/Microsoft.NET.Build.Tasks.csproj
@@ -35,6 +35,9 @@
$(MsBuildPackagesVersion)
Runtime
+
+
+
@@ -61,7 +64,7 @@
-
+
@@ -90,6 +93,7 @@
+
@@ -161,6 +165,9 @@
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/NuGetUtils.cs b/src/Tasks/Microsoft.NET.Build.Tasks/NuGetUtils.cs
index 6c05fb88b463..e96e7bd91bff 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/NuGetUtils.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/NuGetUtils.cs
@@ -37,5 +37,50 @@ public static NuGetFramework ParseFrameworkName(string frameworkName)
{
return frameworkName == null ? null : NuGetFramework.Parse(frameworkName);
}
+
+ ///
+ /// Gets PackageId from sourcePath.
+ ///
+ ///
+ ///
+ public static string GetPackageIdFromSourcePath(string sourcePath)
+ {
+ string packageId, unused;
+ GetPackageParts(sourcePath, out packageId, out unused);
+ return packageId;
+ }
+
+ ///
+ /// Gets PackageId and package subpath from source path
+ ///
+ /// full path to package file
+ /// package ID
+ /// subpath of asset within package
+ public static void GetPackageParts(string fullPath, out string packageId, out string packageSubPath)
+ {
+ packageId = null;
+ packageSubPath = null;
+ try
+ {
+ // this method is just a temporary heuristic until we flow the NuGet metadata through the right items
+ // https://github.com/dotnet/sdk/issues/1091
+ for (var dir = Directory.GetParent(fullPath); dir != null; dir = dir.Parent)
+ {
+ var nuspecs = dir.GetFiles("*.nuspec");
+
+ if (nuspecs.Length > 0)
+ {
+ packageId = Path.GetFileNameWithoutExtension(nuspecs[0].Name);
+ packageSubPath = fullPath.Substring(dir.FullName.Length + 1).Replace('\\', '/');
+ break;
+ }
+ }
+ }
+ catch (Exception)
+ { }
+
+ return;
+
+ }
}
}
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.Designer.cs b/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.Designer.cs
index c888c413ab0a..6cdc7726f942 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.Designer.cs
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.Designer.cs
@@ -142,6 +142,51 @@ internal static string CannotInferTargetFrameworkIdentiferAndVersion {
}
}
+ ///
+ /// Looks up a localized string similar to Choosing '{0}' because AssemblyVersion '{1}' is greater than '{2}'..
+ ///
+ internal static string ChoosingAssemblyVersion {
+ get {
+ return ResourceManager.GetString("ChoosingAssemblyVersion", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Choosing '{0}' because file version '{1}' is greater than '{2}'..
+ ///
+ internal static string ChoosingFileVersion {
+ get {
+ return ResourceManager.GetString("ChoosingFileVersion", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Choosing '{0}' because it is a platform item..
+ ///
+ internal static string ChoosingPlatformItem {
+ get {
+ return ResourceManager.GetString("ChoosingPlatformItem", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Choosing '{0}' because it comes from a package that is preferred..
+ ///
+ internal static string ChoosingPreferredPackage {
+ get {
+ return ResourceManager.GetString("ChoosingPreferredPackage", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Could not determine winner due to equal file and assembly versions..
+ ///
+ internal static string ConflictCouldNotDetermineWinner {
+ get {
+ return ResourceManager.GetString("ConflictCouldNotDetermineWinner", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Content file '{0}' does not contain expected parent package information..
///
@@ -169,6 +214,42 @@ internal static string ContentPreproccessorParameterRequired {
}
}
+ ///
+ /// Looks up a localized string similar to Could not determine winner because '{0}' does not exist..
+ ///
+ internal static string CouldNotDetermineWinner_DoesntExist {
+ get {
+ return ResourceManager.GetString("CouldNotDetermineWinner_DoesntExist", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Could not determine a winner because '{0}' has no file version..
+ ///
+ internal static string CouldNotDetermineWinner_FileVersion {
+ get {
+ return ResourceManager.GetString("CouldNotDetermineWinner_FileVersion", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Could not determine a winner because '{0}' is not an assembly..
+ ///
+ internal static string CouldNotDetermineWinner_NotAnAssembly {
+ get {
+ return ResourceManager.GetString("CouldNotDetermineWinner_NotAnAssembly", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Could not load PlatformManifest from '{0}' because it did not exist..
+ ///
+ internal static string CouldNotLoadPlatformManifest {
+ get {
+ return ResourceManager.GetString("CouldNotLoadPlatformManifest", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Framework not installed: {0} in {1}.
///
@@ -232,6 +313,33 @@ internal static string DuplicatePreprocessorToken {
}
}
+ ///
+ /// Looks up a localized string similar to Encountered conflict between '{0}' and '{1}'..
+ ///
+ internal static string EncounteredConflict {
+ get {
+ return ResourceManager.GetString("EncounteredConflict", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Error parsing PlatformManifest from '{0}' line {1}. Lines must have the format {2}..
+ ///
+ internal static string ErrorParsingPlatformManifest {
+ get {
+ return ResourceManager.GetString("ErrorParsingPlatformManifest", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Error parsing PlatformManifest from '{0}' line {1}. {2} '{3}' was invalid..
+ ///
+ internal static string ErrorParsingPlatformManifestInvalidValue {
+ get {
+ return ResourceManager.GetString("ErrorParsingPlatformManifestInvalidValue", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Errors occured when emitting satellite assembly '{0}'..
///
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.resx b/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.resx
index b38ce09bc87f..06d5c2a7ef25 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.resx
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/Resources/Strings.resx
@@ -258,4 +258,40 @@
It is not supported to build or publish a self-contained application without specifying a RuntimeIdentifier. Please either specify a RuntimeIdentifier or set SelfContained to false.
+
+ Choosing '{0}' because AssemblyVersion '{1}' is greater than '{2}'.
+
+
+ Choosing '{0}' because file version '{1}' is greater than '{2}'.
+
+
+ Choosing '{0}' because it is a platform item.
+
+
+ Choosing '{0}' because it comes from a package that is preferred.
+
+
+ Could not determine winner due to equal file and assembly versions.
+
+
+ Could not determine winner because '{0}' does not exist.
+
+
+ Could not determine a winner because '{0}' has no file version.
+
+
+ Could not determine a winner because '{0}' is not an assembly.
+
+
+ Encountered conflict between '{0}' and '{1}'.
+
+
+ Could not load PlatformManifest from '{0}' because it did not exist.
+
+
+ Error parsing PlatformManifest from '{0}' line {1}. Lines must have the format {2}.
+
+
+ Error parsing PlatformManifest from '{0}' line {1}. {2} '{3}' was invalid.
+
\ No newline at end of file
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.ConflictResolution.targets b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.ConflictResolution.targets
new file mode 100644
index 000000000000..2ab644f75673
--- /dev/null
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.ConflictResolution.targets
@@ -0,0 +1,64 @@
+
+
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+
+
+
+
+
+
+
+
+ <_LockFileAssemblies Include="@(AllCopyLocalItems->WithMetadataValue('Type', 'assembly'))" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Publish.targets b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Publish.targets
index e241c45d3d60..d6e5bd38fa48 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Publish.targets
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Publish.targets
@@ -460,7 +460,9 @@ Copyright (c) .NET Foundation. All rights reserved.
-->
@@ -479,6 +481,7 @@ Copyright (c) .NET Foundation. All rights reserved.
ReferenceSatellitePaths="@(ReferenceSatellitePaths)"
RuntimeIdentifier="$(RuntimeIdentifier)"
PlatformLibraryName="$(MicrosoftNETPlatformLibrary)"
+ FilesToSkip="@(_ConflictPackageFiles);@(_PublishConflictPackageFiles)"
CompilerOptions="@(DependencyFileCompilerOptions)"
PrivateAssetsPackageReferences="@(PrivateAssetsPackageReference)"
IsSelfContained="$(SelfContained)" />
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.props b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.props
index 0447bc652afc..da7c497eb1ba 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.props
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.props
@@ -13,6 +13,9 @@ Copyright (c) .NET Foundation. All rights reserved.
$(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+
+
+ true
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.targets b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.targets
index 2f3b71d3c928..ad3014cb878e 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.targets
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.NET.Sdk.targets
@@ -80,7 +80,7 @@ Copyright (c) .NET Foundation. All rights reserved.
-->
@@ -421,6 +422,7 @@ Copyright (c) .NET Foundation. All rights reserved.
+
diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.PackageDependencyResolution.targets b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.PackageDependencyResolution.targets
index 142ed97d0853..db45689312dd 100644
--- a/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.PackageDependencyResolution.targets
+++ b/src/Tasks/Microsoft.NET.Build.Tasks/build/Microsoft.PackageDependencyResolution.targets
@@ -120,6 +120,7 @@ Copyright (c) .NET Foundation. All rights reserved.
ResolveLockFileAnalyzers;
ResolveLockFileCopyLocalProjectDeps;
IncludeTransitiveProjectReferences;
+ _HandlePackageFileConflicts
+ {
+ var ns = p.Root.Name.Namespace;
+
+ var itemGroup = new XElement(ns + "ItemGroup");
+ p.Root.Add(itemGroup);
+
+ itemGroup.Add(new XElement(ns + "Reference", new XAttribute("Include", "System")));
+ })
+ .Restore(testProject.Name);
+
+ var buildCommand = new BuildCommand(Stage0MSBuild, Path.Combine(testAsset.TestRoot, testProject.Name));
+
+ buildCommand
+ .CaptureStdOut()
+ .Execute("/v:diag")
+ .Should()
+ .Pass()
+ .And
+ .NotHaveStdOutMatching("Encountered conflict", System.Text.RegularExpressions.RegexOptions.CultureInvariant | System.Text.RegularExpressions.RegexOptions.IgnoreCase);
+ }
+
[Fact]
public void It_generates_binding_redirects_if_needed()
{
diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildADesktopLibrary.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildADesktopLibrary.cs
index 9ee2a6b9f889..15eb47480b41 100644
--- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildADesktopLibrary.cs
+++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildADesktopLibrary.cs
@@ -110,5 +110,57 @@ public void It_can_preserve_compilation_context_and_reference_netstandard_librar
dependencyContext.CompileLibraries.Should().NotBeEmpty();
}
}
+
+ [Fact]
+ public void It_resolves_assembly_conflicts_with_a_NETFramework_library()
+ {
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ return;
+ }
+
+ TestProject project = new TestProject()
+ {
+ Name = "NETFrameworkLibrary",
+ TargetFrameworks = "net462",
+ IsSdkProject = true
+ };
+
+ var testAsset = _testAssetsManager.CreateTestProject(project)
+ .WithProjectChanges(p =>
+ {
+ var ns = p.Root.Name.Namespace;
+
+ var itemGroup = new XElement(ns + "ItemGroup");
+ p.Root.Add(itemGroup);
+
+ itemGroup.Add(new XElement(ns + "PackageReference",
+ new XAttribute("Include", "NETStandard.Library"),
+ new XAttribute("Version", "$(BundledNETStandardPackageVersion)")));
+
+ foreach (var dependency in TestAsset.NetStandard1_3Dependencies)
+ {
+ itemGroup.Add(new XElement(ns + "PackageReference",
+ new XAttribute("Include", dependency.Item1),
+ new XAttribute("Version", dependency.Item2)));
+ }
+
+ })
+ .Restore(project.Name);
+
+ string projectFolder = Path.Combine(testAsset.Path, project.Name);
+
+ var buildCommand = new BuildCommand(MSBuildTest.Stage0MSBuild, projectFolder);
+
+ buildCommand
+ .CaptureStdOut()
+ .Execute()
+ .Should()
+ .Pass()
+ .And
+ .NotHaveStdOutContaining("warning")
+ .And
+ .NotHaveStdOutContaining("MSB3243");
+ }
}
}
diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs
index 1cb8f43b69b7..66471486dd5a 100644
--- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs
+++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetCoreApp.cs
@@ -1,4 +1,6 @@
using FluentAssertions;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.Extensions.DependencyModel;
using Microsoft.NET.TestFramework;
using Microsoft.NET.TestFramework.Assertions;
using Microsoft.NET.TestFramework.Commands;
@@ -97,5 +99,162 @@ public void It_restores_only_ridless_tfm()
targetDefs.Count.Should().Be(1);
targetDefs.Should().Contain(".NETCoreApp,Version=v1.1");
}
+
+ [Fact]
+ public void It_runs_the_app_from_the_output_folder()
+ {
+ RunAppFromOutputFolder("RunFromOutputFolder", false, false);
+ }
+
+ [Fact]
+ public void It_runs_a_rid_specific_app_from_the_output_folder()
+ {
+ RunAppFromOutputFolder("RunFromOutputFolderWithRID", true, false);
+ }
+
+ [Fact]
+ public void It_runs_the_app_with_conflicts_from_the_output_folder()
+ {
+ RunAppFromOutputFolder("RunFromOutputFolderConflicts", false, true);
+ }
+
+ [Fact]
+ public void It_runs_a_rid_specific_app_with_conflicts_from_the_output_folder()
+ {
+ RunAppFromOutputFolder("RunFromOutputFolderWithRIDConflicts", true, true);
+ }
+
+ public void RunAppFromOutputFolder(string testName, bool useRid, bool includeConflicts)
+ {
+ if (UsingFullFrameworkMSBuild)
+ {
+ // Disabled on full framework MSBuild until CI machines have VS with bundled .NET Core / .NET Standard versions
+ // See https://github.com/dotnet/sdk/issues/1077
+ return;
+ }
+
+ var targetFramework = "netcoreapp2.0";
+ var runtimeIdentifier = useRid ? EnvironmentInfo.GetCompatibleRid(targetFramework) : null;
+
+ TestProject project = new TestProject()
+ {
+ Name = testName,
+ IsSdkProject = true,
+ TargetFrameworks = targetFramework,
+ RuntimeIdentifier = runtimeIdentifier,
+ IsExe = true,
+ };
+
+ string outputMessage = $"Hello from {project.Name}!";
+
+ project.SourceFiles["Program.cs"] = @"
+using System;
+public static class Program
+{
+ public static void Main()
+ {
+ Console.WriteLine(""" + outputMessage + @""");
+ }
+}
+";
+ var testAsset = _testAssetsManager.CreateTestProject(project, project.Name)
+ .WithProjectChanges(p =>
+ {
+ if (includeConflicts)
+ {
+ var ns = p.Root.Name.Namespace;
+
+ var itemGroup = new XElement(ns + "ItemGroup");
+ p.Root.Add(itemGroup);
+
+ foreach (var dependency in TestAsset.NetStandard1_3Dependencies)
+ {
+ itemGroup.Add(new XElement(ns + "PackageReference",
+ new XAttribute("Include", dependency.Item1),
+ new XAttribute("Version", dependency.Item2)));
+ }
+ }
+ })
+ .Restore(project.Name);
+
+ string projectFolder = Path.Combine(testAsset.Path, project.Name);
+
+ var buildCommand = new BuildCommand(Stage0MSBuild, projectFolder);
+
+ buildCommand
+ .Execute()
+ .Should()
+ .Pass();
+
+ string outputFolder = buildCommand.GetOutputDirectory(project.TargetFrameworks, runtimeIdentifier: runtimeIdentifier ?? "").FullName;
+
+ Command.Create(RepoInfo.DotNetHostPath, new[] { Path.Combine(outputFolder, project.Name + ".dll") })
+ .CaptureStdOut()
+ .Execute()
+ .Should()
+ .Pass()
+ .And
+ .HaveStdOutContaining(outputMessage);
+
+ }
+
+ [Fact]
+ public void It_trims_conflicts_from_the_deps_file()
+ {
+ if (UsingFullFrameworkMSBuild)
+ {
+ // Disabled on full framework MSBuild until CI machines have VS with bundled .NET Core / .NET Standard versions
+ // See https://github.com/dotnet/sdk/issues/1077
+ return;
+ }
+
+ TestProject project = new TestProject()
+ {
+ Name = "NetCore2App",
+ TargetFrameworks = "netcoreapp2.0",
+ IsExe = true,
+ IsSdkProject = true
+ };
+
+ var testAsset = _testAssetsManager.CreateTestProject(project)
+ .WithProjectChanges(p =>
+ {
+ var ns = p.Root.Name.Namespace;
+
+ var itemGroup = new XElement(ns + "ItemGroup");
+ p.Root.Add(itemGroup);
+
+ foreach (var dependency in TestAsset.NetStandard1_3Dependencies)
+ {
+ itemGroup.Add(new XElement(ns + "PackageReference",
+ new XAttribute("Include", dependency.Item1),
+ new XAttribute("Version", dependency.Item2)));
+ }
+
+ })
+ .Restore(project.Name);
+
+ string projectFolder = Path.Combine(testAsset.Path, project.Name);
+
+ var buildCommand = new BuildCommand(Stage0MSBuild, projectFolder);
+
+ buildCommand
+ .Execute()
+ .Should()
+ .Pass();
+
+ string outputFolder = buildCommand.GetOutputDirectory(project.TargetFrameworks).FullName;
+
+ using (var depsJsonFileStream = File.OpenRead(Path.Combine(outputFolder, $"{project.Name}.deps.json")))
+ {
+ var dependencyContext = new DependencyContextJsonReader().Read(depsJsonFileStream);
+ dependencyContext.Should()
+ .OnlyHaveRuntimeAssemblies("", project.Name)
+ .And
+ .HaveNoDuplicateRuntimeAssemblies("")
+ .And
+ .HaveNoDuplicateNativeAssets(""); ;
+ }
+ }
}
}
diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetStandard2Library.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetStandard2Library.cs
new file mode 100644
index 000000000000..f735c783bc3e
--- /dev/null
+++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToBuildANetStandard2Library.cs
@@ -0,0 +1,84 @@
+using System.IO;
+using Microsoft.NET.TestFramework;
+using Microsoft.NET.TestFramework.Assertions;
+using Microsoft.NET.TestFramework.Commands;
+using Xunit;
+using static Microsoft.NET.TestFramework.Commands.MSBuildTest;
+using System.Linq;
+using FluentAssertions;
+using System.Xml.Linq;
+using System.Runtime.Versioning;
+using System.Runtime.InteropServices;
+using System.Collections.Generic;
+using System;
+using Microsoft.NET.TestFramework.ProjectConstruction;
+
+namespace Microsoft.NET.Build.Tests
+{
+ public class GivenThatWeWantToBuildANetStandard2Library : SdkTest
+ {
+ [Fact]
+ public void It_builds_a_netstandard2_library_successfully()
+ {
+ TestProject project = new TestProject()
+ {
+ Name = "NetStandard2Library",
+ TargetFrameworks = "netstandard2.0",
+ IsSdkProject = true
+ };
+
+ var testAsset = _testAssetsManager.CreateTestProject(project)
+ .Restore(project.Name);
+
+ string projectFolder = Path.Combine(testAsset.Path, project.Name);
+
+ var buildCommand = new BuildCommand(Stage0MSBuild, projectFolder);
+
+ buildCommand
+ .Execute()
+ .Should()
+ .Pass();
+
+ }
+
+ [Fact]
+ public void It_resolves_assembly_conflicts()
+ {
+ TestProject project = new TestProject()
+ {
+ Name = "NetStandard2Library",
+ TargetFrameworks = "netstandard2.0",
+ IsSdkProject = true
+ };
+
+
+ var testAsset = _testAssetsManager.CreateTestProject(project)
+ .WithProjectChanges(p =>
+ {
+ var ns = p.Root.Name.Namespace;
+
+ var itemGroup = new XElement(ns + "ItemGroup");
+ p.Root.Add(itemGroup);
+
+ foreach (var dependency in TestAsset.NetStandard1_3Dependencies)
+ {
+ itemGroup.Add(new XElement(ns + "PackageReference",
+ new XAttribute("Include", dependency.Item1),
+ new XAttribute("Version", dependency.Item2)));
+ }
+
+ })
+ .Restore(project.Name);
+
+ string projectFolder = Path.Combine(testAsset.Path, project.Name);
+
+ var buildCommand = new BuildCommand(Stage0MSBuild, projectFolder);
+
+ buildCommand
+ .Execute()
+ .Should()
+ .Pass();
+
+ }
+ }
+}
diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToReferenceAnAssembly.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToReferenceAnAssembly.cs
index c7f390a72b96..3ac6eba67371 100644
--- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToReferenceAnAssembly.cs
+++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToReferenceAnAssembly.cs
@@ -26,6 +26,13 @@ public void ItRunsAppsDirectlyReferencingAssemblies(
string referencerTarget,
string dependencyTarget)
{
+ if (UsingFullFrameworkMSBuild)
+ {
+ // Disabled on full framework MSBuild until CI machines have VS with bundled .NET Core / .NET Standard versions
+ // See https://github.com/dotnet/sdk/issues/1077
+ return;
+ }
+
string identifier = referencerTarget.ToString() + "_" + dependencyTarget.ToString();
TestProject dependencyProject = new TestProject()
@@ -59,7 +66,6 @@ public static string GetMessage()
Name = "Referencer",
IsSdkProject = true,
TargetFrameworks = referencerTarget,
- RuntimeFrameworkVersion = RepoInfo.NetCoreApp20Version,
// Need to use a self-contained app for now because we don't use a CLI that has a "2.0" shared framework
RuntimeIdentifier = EnvironmentInfo.GetCompatibleRid(referencerTarget),
IsExe = true,
diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToVerifyNuGetReferenceCompat.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToVerifyNuGetReferenceCompat.cs
index 7ddbb5c3f5ab..621d226d87e6 100644
--- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToVerifyNuGetReferenceCompat.cs
+++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToVerifyNuGetReferenceCompat.cs
@@ -82,9 +82,7 @@ public class GivenThatWeWantToVerifyNuGetReferenceCompat : SdkTest, IClassFixtur
[InlineData("netcoreapp1.0", "Full", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netcoreapp1.0", true, true)]
[InlineData("netcoreapp1.1", "Full", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netcoreapp1.0 netcoreapp1.1", true, true)]
[InlineData("netcoreapp2.0", "PartM1", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netcoreapp1.0 netcoreapp1.1 netcoreapp2.0", true, true)]
- // Fullframework NuGet versioning on Jenkins infrastructure issue
- // https://github.com/dotnet/sdk/issues/1041
- //[InlineData("netcoreapp2.0", "Full", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netstandard2.0 netcoreapp1.0 netcoreapp1.1 netcoreapp2.0", true, true)]
+ [InlineData("netcoreapp2.0", "Full", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netstandard2.0 netcoreapp1.0 netcoreapp1.1 netcoreapp2.0", true, true)]
// OptIn matrix throws an exception for each permutation
// https://github.com/dotnet/sdk/issues/1025
@@ -94,6 +92,17 @@ public class GivenThatWeWantToVerifyNuGetReferenceCompat : SdkTest, IClassFixtur
public void Nuget_reference_compat(string referencerTarget, string testDescription, string rawDependencyTargets,
bool restoreSucceeds, bool buildSucceeds)
{
+ if (UsingFullFrameworkMSBuild &&
+ (referencerTarget == "netcoreapp2.0" || referencerTarget == "netstandard2.0"))
+ {
+ // Fullframework NuGet versioning on Jenkins infrastructure issue
+ // https://github.com/dotnet/sdk/issues/1041
+
+ // Disabled on full framework MSBuild until CI machines have VS with bundled .NET Core / .NET Standard versions
+ // See https://github.com/dotnet/sdk/issues/1077
+ return;
+ }
+
string referencerDirectoryNamePostfix = "_" + referencerTarget + "_" + testDescription;
TestProject referencerProject = GetTestProject(ConstantStringValues.ReferencerDirectoryName, referencerTarget, true);
diff --git a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToVerifyProjectReferenceCompat.cs b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToVerifyProjectReferenceCompat.cs
index 3b91f6125768..99e302e5977b 100644
--- a/test/Microsoft.NET.Build.Tests/GivenThatWeWantToVerifyProjectReferenceCompat.cs
+++ b/test/Microsoft.NET.Build.Tests/GivenThatWeWantToVerifyProjectReferenceCompat.cs
@@ -38,13 +38,21 @@ public class GivenThatWeWantToVerifyProjectReferenceCompat : SdkTest
[InlineData("netcoreapp1.0", "FullMatrix", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netcoreapp1.0", true, true)]
[InlineData("netcoreapp1.1", "FullMatrix", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netcoreapp1.0 netcoreapp1.1", true, true)]
[InlineData("netcoreapp2.0", "PartialM1", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netcoreapp1.0 netcoreapp1.1 netcoreapp2.0", true, true)]
- // Fullframework NuGet versioning on Jenkins infrastructure issue
- // https://github.com/dotnet/sdk/issues/1041
- //[InlineData("netcoreapp2.0", "FullMatrix", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netstandard2.0 netcoreapp1.0 netcoreapp1.1 netcoreapp2.0", true, true)]
+ [InlineData("netcoreapp2.0", "FullMatrix", "netstandard1.0 netstandard1.1 netstandard1.2 netstandard1.3 netstandard1.4 netstandard1.5 netstandard1.6 netstandard2.0 netcoreapp1.0 netcoreapp1.1 netcoreapp2.0", true, true)]
public void Project_reference_compat(string referencerTarget, string testIDPostFix, string rawDependencyTargets,
bool restoreSucceeds, bool buildSucceeds)
{
+ if (UsingFullFrameworkMSBuild && referencerTarget == "netcoreapp2.0")
+ {
+ // Fullframework NuGet versioning on Jenkins infrastructure issue
+ // https://github.com/dotnet/sdk/issues/1041
+
+ // Disabled on full framework MSBuild until CI machines have VS with bundled .NET Core / .NET Standard versions
+ // See https://github.com/dotnet/sdk/issues/1077
+ return;
+ }
+
string identifier = "_TestID_" + referencerTarget + "_" + testIDPostFix;
TestProject referencerProject = GetTestProject("Referencer", referencerTarget, true);
diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs
index 7e410dec6b22..04054b8e958c 100644
--- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs
+++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAHelloWorldProject.cs
@@ -11,6 +11,10 @@
using Microsoft.NET.TestFramework.ProjectConstruction;
using Xunit;
using static Microsoft.NET.TestFramework.Commands.MSBuildTest;
+using System.Xml.Linq;
+using System.Runtime.CompilerServices;
+using System;
+using Microsoft.Extensions.DependencyModel;
namespace Microsoft.NET.Publish.Tests
{
@@ -98,6 +102,13 @@ public void It_publishes_self_contained_apps_to_the_publish_folder_and_the_app_s
[Fact]
public void Publish_standalone_post_netcoreapp2_app_and_it_should_run()
{
+ if (UsingFullFrameworkMSBuild)
+ {
+ // Disabled on full framework MSBuild until CI machines have VS with bundled .NET Core / .NET Standard versions
+ // See https://github.com/dotnet/sdk/issues/1077
+ return;
+ }
+
var targetFramework = "netcoreapp2.0";
var rid = EnvironmentInfo.GetCompatibleRid(targetFramework);
@@ -107,7 +118,6 @@ public void Publish_standalone_post_netcoreapp2_app_and_it_should_run()
Name = "Hello",
IsSdkProject = true,
TargetFrameworks = targetFramework,
- RuntimeFrameworkVersion = RepoInfo.NetCoreApp20Version,
RuntimeIdentifier = rid,
IsExe = true,
};
@@ -161,6 +171,166 @@ public static void Main()
.HaveStdOutContaining("Hello from a netcoreapp2.0.!");
}
+ [Fact]
+ public void Conflicts_are_resolved_when_publishing_a_portable_app()
+ {
+ Conflicts_are_resolved_when_publishing(selfContained: false, ridSpecific: false);
+ }
+
+ [Fact]
+ public void Conflicts_are_resolved_when_publishing_a_self_contained_app()
+ {
+ Conflicts_are_resolved_when_publishing(selfContained: true, ridSpecific: true);
+ }
+
+ [Fact]
+ public void Conflicts_are_resolved_when_publishing_a_rid_specific_shared_framework_app()
+ {
+ Conflicts_are_resolved_when_publishing(selfContained: false, ridSpecific: true);
+ }
+
+ void Conflicts_are_resolved_when_publishing(bool selfContained, bool ridSpecific, [CallerMemberName] string callingMethod = "")
+ {
+ if (UsingFullFrameworkMSBuild)
+ {
+ // Disabled on full framework MSBuild until CI machines have VS with bundled .NET Core / .NET Standard versions
+ // See https://github.com/dotnet/sdk/issues/1077
+ return;
+ }
+
+ if (selfContained && !ridSpecific)
+ {
+ throw new ArgumentException("Self-contained apps must be rid specific");
+ }
+
+ var targetFramework = "netcoreapp2.0";
+ var rid = ridSpecific ? EnvironmentInfo.GetCompatibleRid(targetFramework) : null;
+
+ TestProject testProject = new TestProject()
+ {
+ Name = selfContained ? "SelfContainedWithConflicts" :
+ (ridSpecific ? "RidSpecificSharedConflicts" : "PortableWithConflicts"),
+ IsSdkProject = true,
+ TargetFrameworks = targetFramework,
+ RuntimeIdentifier = rid,
+ IsExe = true,
+ };
+
+ string outputMessage = $"Hello from {testProject.Name}!";
+
+ testProject.SourceFiles["Program.cs"] = @"
+using System;
+public static class Program
+{
+ public static void Main()
+ {
+ Console.WriteLine(""" + outputMessage + @""");
+ }
+}
+";
+ var testProjectInstance = _testAssetsManager.CreateTestProject(testProject, testProject.Name)
+ .WithProjectChanges(p =>
+ {
+
+ var ns = p.Root.Name.Namespace;
+
+ var itemGroup = new XElement(ns + "ItemGroup");
+ p.Root.Add(itemGroup);
+
+ foreach (var dependency in TestAsset.NetStandard1_3Dependencies)
+ {
+ itemGroup.Add(new XElement(ns + "PackageReference",
+ new XAttribute("Include", dependency.Item1),
+ new XAttribute("Version", dependency.Item2)));
+ }
+
+ if (!selfContained && ridSpecific)
+ {
+ var propertyGroup = new XElement(ns + "PropertyGroup");
+ p.Root.Add(propertyGroup);
+
+ propertyGroup.Add(new XElement(ns + "SelfContained",
+ "false"));
+ }
+ })
+ .Restore(testProject.Name);
+
+ var publishCommand = new PublishCommand(Stage0MSBuild, Path.Combine(testProjectInstance.TestRoot, testProject.Name));
+ var publishResult = publishCommand.Execute();
+
+ publishResult.Should().Pass();
+
+ var publishDirectory = publishCommand.GetOutputDirectory(
+ targetFramework: targetFramework,
+ runtimeIdentifier: rid ?? string.Empty);
+
+ DependencyContext dependencyContext;
+ using (var depsJsonFileStream = File.OpenRead(Path.Combine(publishDirectory.FullName, $"{testProject.Name}.deps.json")))
+ {
+ dependencyContext = new DependencyContextJsonReader().Read(depsJsonFileStream);
+ }
+
+ dependencyContext.Should()
+ .HaveNoDuplicateRuntimeAssemblies(rid ?? "")
+ .And
+ .HaveNoDuplicateNativeAssets(rid ?? "");
+
+ ICommand runCommand;
+
+ if (selfContained)
+ {
+ var selfContainedExecutable = testProject.Name + Constants.ExeSuffix;
+
+ string selfContainedExecutableFullPath = Path.Combine(publishDirectory.FullName, selfContainedExecutable);
+
+ var libPrefix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "" : "lib";
+
+ publishDirectory.Should().HaveFiles(new[] {
+ selfContainedExecutable,
+ $"{testProject.Name}.dll",
+ $"{testProject.Name}.pdb",
+ $"{testProject.Name}.deps.json",
+ $"{testProject.Name}.runtimeconfig.json",
+ $"{libPrefix}coreclr{Constants.DynamicLibSuffix}",
+ $"{libPrefix}hostfxr{Constants.DynamicLibSuffix}",
+ $"{libPrefix}hostpolicy{Constants.DynamicLibSuffix}",
+ $"mscorlib.dll",
+ $"System.Private.CoreLib.dll",
+ });
+
+ dependencyContext.Should()
+ .OnlyHaveRuntimeAssembliesWhichAreInFolder(rid, publishDirectory.FullName)
+ .And
+ .OnlyHaveNativeAssembliesWhichAreInFolder(rid, publishDirectory.FullName, testProject.Name);
+
+ runCommand = Command.Create(selfContainedExecutableFullPath, new string[] { })
+ .EnsureExecutable();
+ }
+ else
+ {
+ publishDirectory.Should().OnlyHaveFiles(new[] {
+ $"{testProject.Name}.dll",
+ $"{testProject.Name}.pdb",
+ $"{testProject.Name}.deps.json",
+ $"{testProject.Name}.runtimeconfig.json"
+ });
+
+ dependencyContext.Should()
+ .OnlyHaveRuntimeAssemblies(rid ?? "", testProject.Name);
+
+ runCommand = Command.Create(RepoInfo.DotNetHostPath, new[] { Path.Combine(publishDirectory.FullName, $"{testProject.Name}.dll") });
+ }
+
+ runCommand
+ .CaptureStdOut()
+ .Execute()
+ .Should()
+ .Pass()
+ .And
+ .HaveStdOutContaining(outputMessage);
+
+ }
+
[Fact]
public void A_deployment_project_can_reference_the_hello_world_project()
{
diff --git a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAProjectWithDependencies.cs b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAProjectWithDependencies.cs
index a9de1f584dec..7ad5f16f8e8a 100644
--- a/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAProjectWithDependencies.cs
+++ b/test/Microsoft.NET.Publish.Tests/GivenThatWeWantToPublishAProjectWithDependencies.cs
@@ -236,6 +236,13 @@ public void It_publishes_documentation_files(string properties, bool expectAppDo
[InlineData("PublishReferencesDocumentationFiles=true", true)]
public void It_publishes_referenced_assembly_documentation(string property, bool expectAssemblyDocumentationFilePublished)
{
+ if (UsingFullFrameworkMSBuild)
+ {
+ // Disabled on full framework MSBuild until CI machines have VS with bundled .NET Core / .NET Standard versions
+ // See https://github.com/dotnet/sdk/issues/1077
+ return;
+ }
+
var identifier = property.Replace("=", "");
var libProject = new TestProject
@@ -259,7 +266,6 @@ public void It_publishes_referenced_assembly_documentation(string property, bool
IsSdkProject = true,
IsExe = true,
TargetFrameworks = "netcoreapp2.0",
- RuntimeFrameworkVersion = RepoInfo.NetCoreApp20Version,
References = { publishedLibPath }
};
diff --git a/test/Microsoft.NET.TestFramework/Assertions/DependencyContextAssertions.cs b/test/Microsoft.NET.TestFramework/Assertions/DependencyContextAssertions.cs
new file mode 100644
index 000000000000..4b5820b77b64
--- /dev/null
+++ b/test/Microsoft.NET.TestFramework/Assertions/DependencyContextAssertions.cs
@@ -0,0 +1,87 @@
+using FluentAssertions;
+using FluentAssertions.Execution;
+using Microsoft.DotNet.Cli.Utils;
+using Microsoft.Extensions.DependencyModel;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Microsoft.NET.TestFramework.Assertions
+{
+ public class DependencyContextAssertions
+ {
+ private DependencyContext _dependencyContext;
+
+ public DependencyContextAssertions(DependencyContext dependencyContext)
+ {
+ _dependencyContext = dependencyContext;
+ }
+
+ public AndConstraint HaveNoDuplicateRuntimeAssemblies(string runtimeIdentifier)
+ {
+ var assemblyNames = _dependencyContext.GetRuntimeAssemblyNames(runtimeIdentifier);
+
+ var duplicateAssemblies = assemblyNames.GroupBy(n => n.Name).Where(g => g.Count() > 1);
+ duplicateAssemblies.Select(g => g.Key).Should().BeEmpty();
+
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint HaveNoDuplicateNativeAssets(string runtimeIdentifier)
+ {
+ var nativeAssets = _dependencyContext.GetRuntimeNativeAssets(runtimeIdentifier);
+ var nativeFilenames = nativeAssets.Select(n => Path.GetFileName(n));
+ var duplicateNativeAssets = nativeFilenames.GroupBy(n => n).Where(g => g.Count() > 1);
+ duplicateNativeAssets.Select(g => g.Key).Should().BeEmpty();
+
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint OnlyHaveRuntimeAssemblies(string runtimeIdentifier, params string[] runtimeAssemblyNames)
+ {
+ var assemblyNames = _dependencyContext.GetRuntimeAssemblyNames(runtimeIdentifier);
+
+ assemblyNames.Select(n => n.Name)
+ .Should()
+ .BeEquivalentTo(runtimeAssemblyNames);
+
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint OnlyHaveRuntimeAssembliesWhichAreInFolder(string runtimeIdentifier, string folder)
+ {
+ var assemblyNames = _dependencyContext.GetRuntimeAssemblyNames(runtimeIdentifier);
+
+ var assemblyFiles = assemblyNames.Select(an => Path.Combine(folder, an.Name + ".dll"));
+
+ var missingFiles = assemblyFiles.Where(f => !File.Exists(f));
+
+ missingFiles.Should().BeEmpty();
+
+ return new AndConstraint(this);
+ }
+
+ public AndConstraint OnlyHaveNativeAssembliesWhichAreInFolder(string runtimeIdentifier, string folder, string appName)
+ {
+ var nativeAssets = _dependencyContext.GetRuntimeNativeAssets(runtimeIdentifier);
+ var nativeAssetsWithPath = nativeAssets.Select(f =>
+ {
+ // apphost gets renamed to the name of the app in self-contained publish
+ if (Path.GetFileNameWithoutExtension(f) == "apphost")
+ {
+ return Path.Combine(folder, appName + Constants.ExeSuffix);
+ }
+ else
+ {
+ return Path.Combine(folder, Path.GetFileName(f));
+ }
+ });
+ var missingNativeAssets = nativeAssetsWithPath.Where(f => !File.Exists(f));
+ missingNativeAssets.Should().BeEmpty();
+
+ return new AndConstraint(this);
+ }
+ }
+}
diff --git a/test/Microsoft.NET.TestFramework/Assertions/DependencyContextExtensions.cs b/test/Microsoft.NET.TestFramework/Assertions/DependencyContextExtensions.cs
new file mode 100644
index 000000000000..ad0c5a1e74d2
--- /dev/null
+++ b/test/Microsoft.NET.TestFramework/Assertions/DependencyContextExtensions.cs
@@ -0,0 +1,15 @@
+using Microsoft.Extensions.DependencyModel;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Microsoft.NET.TestFramework.Assertions
+{
+ public static class DependencyContextExtensions
+ {
+ public static DependencyContextAssertions Should(this DependencyContext dependencyContext)
+ {
+ return new DependencyContextAssertions(dependencyContext);
+ }
+ }
+}
diff --git a/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs b/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs
index cb49146232d9..eebe90d7f954 100644
--- a/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs
+++ b/test/Microsoft.NET.TestFramework/ProjectConstruction/TestProject.cs
@@ -142,12 +142,6 @@ internal void Create(TestAsset targetTestAsset, string testProjectsSourceFolder)
{
propertyGroup.Add(new XElement(ns + "TargetFramework", this.TargetFrameworks));
}
- // Workaround for .NET Core 2.0
- //
- if (this.TargetFrameworks.Contains("netcoreapp2.0") && this.RuntimeFrameworkVersion == null)
- {
- this.RuntimeFrameworkVersion = RepoInfo.NetCoreApp20Version;
- }
if (!string.IsNullOrEmpty(this.RuntimeFrameworkVersion))
{
diff --git a/test/Microsoft.NET.TestFramework/RepoInfo.cs b/test/Microsoft.NET.TestFramework/RepoInfo.cs
index aaa1732e8ac8..c66df5cade78 100644
--- a/test/Microsoft.NET.TestFramework/RepoInfo.cs
+++ b/test/Microsoft.NET.TestFramework/RepoInfo.cs
@@ -86,28 +86,6 @@ public static string DotNetHostPath
}
}
- public static string NetCoreApp20Version { get; } = ReadNetCoreApp20Version();
-
- private static string ReadNetCoreApp20Version()
- {
- var dependencyVersionsPath = Path.Combine(RepoRoot, "build", "DependencyVersions.props");
- var root = XDocument.Load(dependencyVersionsPath).Root;
- var ns = root.Name.Namespace;
-
- var version = root
- .Elements(ns + "PropertyGroup")
- .Elements(ns + "MicrosoftNETCoreApp20Version")
- .FirstOrDefault()
- ?.Value;
-
- if (string.IsNullOrEmpty(version))
- {
- throw new InvalidOperationException($"Could not find a property named 'MicrosoftNETCoreApp20Version' in {dependencyVersionsPath}");
- }
-
- return version;
- }
-
private static string FindConfigurationInBasePath()
{
// assumes tests are always executed from the "bin/$Configuration/Tests" directory
diff --git a/test/Microsoft.NET.TestFramework/SdkTest.cs b/test/Microsoft.NET.TestFramework/SdkTest.cs
index 867ff42326a8..d4a56a444a4e 100644
--- a/test/Microsoft.NET.TestFramework/SdkTest.cs
+++ b/test/Microsoft.NET.TestFramework/SdkTest.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Runtime.InteropServices;
using System.Text;
namespace Microsoft.NET.TestFramework
@@ -22,8 +23,8 @@ public void Dispose()
{
// Skip path length validation if running on full framework MSBuild. We do the path length validation
// to avoid getting path to long errors when copying the test drop in our build infrastructure. However,
- // those builds are only built with .NET Core MSBuild.
- if (!UsingFullFrameworkMSBuild)
+ // those builds are only built with .NET Core MSBuild running on Windows
+ if (!UsingFullFrameworkMSBuild && RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
_testAssetsManager.ValidateDestinationDirectories();
}
diff --git a/test/Microsoft.NET.TestFramework/TestAsset.cs b/test/Microsoft.NET.TestFramework/TestAsset.cs
index a7d253194291..a5d6658620a0 100644
--- a/test/Microsoft.NET.TestFramework/TestAsset.cs
+++ b/test/Microsoft.NET.TestFramework/TestAsset.cs
@@ -175,5 +175,66 @@ private bool IsInBinOrObjFolder(string path)
return path.Contains(binFolderWithTrailingSlash)
|| path.Contains(objFolderWithTrailingSlash);
}
+
+ public static IEnumerable> NetStandard1_3Dependencies
+ {
+ get
+ {
+ string netstandardDependenciesXml = @"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ";
+
+ XElement netStandardDependencies = XElement.Parse(netstandardDependenciesXml);
+
+ foreach (var dependency in netStandardDependencies.Elements("dependency"))
+ {
+ yield return Tuple.Create(dependency.Attribute("id").Value, dependency.Attribute("version").Value);
+ }
+ }
+ }
}
}
diff --git a/test/Microsoft.NET.TestFramework/TestAssetsManager.cs b/test/Microsoft.NET.TestFramework/TestAssetsManager.cs
index 3cf4e452b26f..9dbf3c813e28 100644
--- a/test/Microsoft.NET.TestFramework/TestAssetsManager.cs
+++ b/test/Microsoft.NET.TestFramework/TestAssetsManager.cs
@@ -111,7 +111,16 @@ private string GetTestDestinationDirectoryPath(
#else
string baseDirectory = AppContext.BaseDirectory;
#endif
- string ret = Path.Combine(baseDirectory, callingMethod + identifier, testProjectName);
+ string ret;
+ if (testProjectName == callingMethod)
+ {
+ // If testProjectName and callingMethod are the same, don't duplicate it in the test path
+ ret = Path.Combine(baseDirectory, callingMethod + identifier);
+ }
+ else
+ {
+ ret = Path.Combine(baseDirectory, callingMethod + identifier, testProjectName);
+ }
TestDestinationDirectories.Add(ret);