|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Microsoft.NET.Build.Tasks.ConflictResolution
{
public class ResolvePackageFileConflicts : TaskBase
{
private HashSet<ITaskItem?> referenceConflicts = new();
private HashSet<ITaskItem?> analyzerConflicts = new();
private HashSet<ITaskItem?> copyLocalConflicts = new();
private HashSet<ConflictItem> compilePlatformWinners = new();
private HashSet<ConflictItem> allConflicts = new();
public ITaskItem[]? References { get; set; }
public ITaskItem[]? Analyzers { get; set; }
public ITaskItem[]? ReferenceCopyLocalPaths { get; set; }
public ITaskItem[]? OtherRuntimeItems { get; set; }
public ITaskItem[]? PlatformManifests { get; set; }
public ITaskItem[]? TargetFrameworkDirectories { get; set; }
/// <summary>
/// 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.
/// </summary>
public string[]? PreferredPackages { get; set; }
/// <summary>
/// A collection of items that contain information of which packages get overridden
/// by which packages before doing any other conflict resolution.
/// </summary>
/// <remarks>
/// This is an optimization so AssemblyVersions, FileVersions, etc. don't need to be read
/// in the default cases where platform packages (Microsoft.NETCore.App) should override specific packages
/// (System.Console v4.3.0).
/// </remarks>
public ITaskItem[]? PackageOverrides { get; set; }
[Output]
public ITaskItem[]? ReferencesWithoutConflicts { get; set; }
[Output]
public ITaskItem[]? AnalyzersWithoutConflicts { get; set; }
[Output]
public ITaskItem[]? ReferenceCopyLocalPathsWithoutConflicts { get; set; }
[Output]
public ITaskItem[]? Conflicts { get; set; }
protected override void ExecuteCore()
{
var log = Log;
var packageRanks = new PackageRank(PreferredPackages);
var packageOverrides = new PackageOverrideResolver<ConflictItem>(PackageOverrides);
// Treat assemblies from FrameworkList.xml as platform assemblies that also get considered at compile time
IEnumerable<ConflictItem>? compilePlatformItems = null;
if (TargetFrameworkDirectories != null && TargetFrameworkDirectories.Any())
{
var frameworkListReader = new FrameworkListReader(BuildEngine4);
compilePlatformItems = TargetFrameworkDirectories.SelectMany(tfd =>
{
return frameworkListReader.GetConflictItems(Path.Combine(tfd.ItemSpec, "RedistList", "FrameworkList.xml"), log);
}).ToArray();
}
// resolve conflicts at compile time
var referenceItems = GetConflictTaskItems(References, ConflictItemType.Reference).ToArray();
using (var compileConflictScope = new ConflictResolver<ConflictItem>(packageRanks, packageOverrides, log))
{
compileConflictScope.ResolveConflicts(referenceItems,
ci => ItemUtilities.GetReferenceFileName(ci.OriginalItem),
HandleCompileConflict);
if (compilePlatformItems != null)
{
compileConflictScope.ResolveConflicts(compilePlatformItems,
ci => ci.FileName,
HandleCompileConflict);
}
}
// Remove platform items which won a conflict with a reference but subsequently lost to something else
compilePlatformWinners.ExceptWith(allConflicts);
// resolve analyzer conflicts
var analyzerItems = GetConflictTaskItems(Analyzers, ConflictItemType.Analyzer).ToArray();
using (var analyzerConflictScope = new ConflictResolver<ConflictItem>(packageRanks, packageOverrides, log))
{
analyzerConflictScope.ResolveConflicts(analyzerItems, ci => ci.FileName, HandleAnalyzerConflict);
}
// resolve conflicts that clash in output
IEnumerable<ConflictItem> copyLocalItems;
IEnumerable<ConflictItem> otherRuntimeItems;
using (var runtimeConflictScope = new ConflictResolver<ConflictItem>(packageRanks, packageOverrides, log))
{
runtimeConflictScope.ResolveConflicts(referenceItems,
ci => ItemUtilities.GetReferenceTargetPath(ci.OriginalItem),
HandleRuntimeConflict);
copyLocalItems = GetConflictTaskItems(ReferenceCopyLocalPaths, ConflictItemType.CopyLocal).ToArray();
runtimeConflictScope.ResolveConflicts(copyLocalItems,
ci => ItemUtilities.GetTargetPath(ci.OriginalItem),
HandleRuntimeConflict);
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.
using (var platformConflictScope = new ConflictResolver<ConflictItem>(packageRanks, packageOverrides, log))
{
var platformItems = PlatformManifests?.SelectMany(pm => PlatformManifestReader.LoadConflictItems(pm.ItemSpec, log)) ?? Enumerable.Empty<ConflictItem>();
if (compilePlatformItems != null)
{
platformItems = platformItems.Concat(compilePlatformItems);
}
platformConflictScope.ResolveConflicts(platformItems, pi => pi.FileName, (winner, loser) => { });
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);
AnalyzersWithoutConflicts = RemoveConflicts(Analyzers, analyzerConflicts);
ReferenceCopyLocalPathsWithoutConflicts = RemoveConflicts(ReferenceCopyLocalPaths, copyLocalConflicts);
Conflicts = CreateConflictTaskItems(allConflicts);
// This handles the issue described here: https://github.com/dotnet/sdk/issues/2221
// The issue is that before conflict resolution runs, references to assemblies in the framework
// that also match an assembly coming from a NuGet package are either removed (in non-SDK targets
// via the ResolveNuGetPackageAssets target in Microsoft.NuGet.targets) or transformed to refer
// to the assembly coming from the NuGet package (for SDK projects in the ResolveLockFileReferences
// task).
// In either case, there ends up being no Reference item which will resolve to the DLL in
// the reference assemblies. This is a problem if the platform item from the reference
// assemblies wins a conflict in the compile scope, as the reference to the assembly from
// the NuGet package will be removed, but there will be no reference to the Framework assembly
// passed to the compiler.
// So what we do is keep track of Platform items that win conflicts with Reference items in
// the compile scope, and explicitly add references to them here.
var referenceItemSpecs = new HashSet<string>(ReferencesWithoutConflicts?.Select(r => r.ItemSpec) ?? Enumerable.Empty<string>(),
StringComparer.OrdinalIgnoreCase);
ReferencesWithoutConflicts = SafeConcat(ReferencesWithoutConflicts,
// The Reference item we create in this case should be without the .dll extension
// (which is added in FrameworkListReader in order to make the framework items
// correctly conflict with DLLs from NuGet packages)
compilePlatformWinners.Select(c => Path.GetFileNameWithoutExtension(c.FileName) ?? string.Empty)
// Don't add a reference if we already have one (especially in case the existing one has
// metadata we want to keep, such as aliases)
.Where(simpleName => !referenceItemSpecs.Contains(simpleName))
.Select(r => new TaskItem(r)));
}
// Concatenate two things, either of which may be null. Interpret null as empty,
// and return null if the result would be empty.
private ITaskItem[]? SafeConcat(ITaskItem[]? first, IEnumerable<ITaskItem> second)
{
if (first == null || first.Length == 0)
{
return second?.ToArray();
}
if (second == null || !second.Any())
{
return first;
}
return first.Concat(second).ToArray();
}
private ITaskItem[] CreateConflictTaskItems(ICollection<ConflictItem> 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());
item.SetMetadata(MetadataKeys.NuGetPackageId, conflict.PackageId);
}
return item;
}
private IEnumerable<ConflictItem> GetConflictTaskItems(ITaskItem[]? items, ConflictItemType itemType)
{
return (items != null) ? items.Select(i => new ConflictItem(i, itemType)) : Enumerable.Empty<ConflictItem>();
}
private void HandleCompileConflict(ConflictItem winner, ConflictItem loser)
{
if (loser.ItemType == ConflictItemType.Reference)
{
referenceConflicts.Add(loser.OriginalItem);
if (winner.ItemType == ConflictItemType.Platform)
{
compilePlatformWinners.Add(winner);
}
}
allConflicts.Add(loser);
}
private void HandleAnalyzerConflict(ConflictItem winner, ConflictItem loser)
{
analyzerConflicts.Add(loser.OriginalItem);
allConflicts.Add(loser);
}
private void HandleRuntimeConflict(ConflictItem winner, ConflictItem loser)
{
if (loser.ItemType == ConflictItemType.Reference)
{
loser.OriginalItem?.SetMetadata(MetadataNames.Private, "False");
}
else if (loser.ItemType == ConflictItemType.CopyLocal)
{
copyLocalConflicts.Add(loser.OriginalItem);
}
allConflicts.Add(loser);
}
/// <summary>
/// Filters conflicts from original, maintaining order.
/// </summary>
/// <param name="original"></param>
/// <param name="conflicts"></param>
/// <returns></returns>
private ITaskItem[]? RemoveConflicts(ITaskItem[]? original, ICollection<ITaskItem?> conflicts)
{
if (original is null || 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;
}
}
// If there are duplicates in the original list, then our size calculation for the result will have been wrong.
// So we have to re-allocate the array with the right size.
// Duplicates can happen if there are duplicate Reference items that are joined with a reference from a package in ResolveLockFileReferences
if (index != result.Length)
{
return result.Take(index).ToArray();
}
return result;
}
}
}
|