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