Skip to content

Commit

Permalink
Align TargetFramework pkg with NuGet Task (#12041)
Browse files Browse the repository at this point in the history
- Align the ChooseBestP2PTargetFrameworkTask more closely with the NuGet
  task that is used in the SDK to resolve best matching project references.
- Enable nullable reference type warnings
- Remove logic from BinPlace.targets that isn't needed anymore
- Make the TargetFramework msbuild files pick up the SDK's runtime graph
  instead of passing it in explicitly.
  • Loading branch information
ViktorHofer authored Dec 30, 2022
1 parent ea2db8a commit e82404f
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 178 deletions.
2 changes: 2 additions & 0 deletions src/Common/Internal/AssemblyResolver.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System;
using System.Diagnostics;
using System.IO;
Expand Down
2 changes: 2 additions & 0 deletions src/Common/Internal/BuildTask.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System;
Expand Down
12 changes: 4 additions & 8 deletions src/Microsoft.DotNet.Build.Tasks.TargetFramework/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Supports copying to additional paths based on which `TargetFramework` among `Tar

- BinPlaceItem
- Typically computed by the BinPlacing targets to determine what assets to binplace.
- Identity: source of file to binplace. For example: the built output dll, pdb, content files, etc.
- Identity: source of file to binplace. For example: the built output dll, pdb, content files, etc.
- Metadata:
- TargetPath: when specified can indicate the relative path, including filename, to place the item.

Expand All @@ -26,14 +26,10 @@ Supports copying to additional paths based on which `TargetFramework` among `Tar
- RefPath: directory to copy `BinPlaceItem`s when `BinPlaceRef` is set to true.
- RuntimePath: directory to copy `BinPlaceItem`s when `BinPlaceRuntime` is set to true.
- TestPath: directory to copy `BinPlaceItem`s when `BinPlaceTest` is set to true.
- PackageFileNativePath: directory to write props file containing `BinPlaceItem`s when `BinPlaceNative` is set to true.
- PackageFileRefPath: directory to write props file containing `BinPlaceItem`s when `BinPlaceRef` is set to true.
- PackageFileRuntimePath: directory to write props file containing `BinPlaceItem`s when `BinPlaceRuntime` is set to true.
- ItemName: An item name to use instead of `BinPlaceItem` for the source of items for this `BinPlaceTargetFramework`.
- SetProperties: Name=Value pairs of properties that should be set.

## BinPlacing Properties
- BinPlaceNative: When set to true `BinPlaceItem`s are copied to the `NativePath` of active `BinPlaceTargetFramework`s. Props are written to the `PackageFileNativePath` directory.
- BinPlaceRef: When set to true `BinPlaceItem`s are copied to the `RefPath` of active `BinPlaceTargetFramework`s. Props are written to the `PackageFileRefPath` directory.
- BinPlaceRuntime: When set to true `BinPlaceItem`s are copied to the `RuntimePath` of active `BinPlaceTargetFramework`s. Props are written to the `PackageFileRuntimePath` directory.
- BinPlaceNative: When set to true `BinPlaceItem`s are copied to the `NativePath` of active `BinPlaceTargetFramework`s.
- BinPlaceRef: When set to true `BinPlaceItem`s are copied to the `RefPath` of active `BinPlaceTargetFramework`s.
- BinPlaceRuntime: When set to true `BinPlaceItem`s are copied to the `RuntimePath` of active `BinPlaceTargetFramework`s.
- BinPlaceTest: When set to true `BinPlaceItem`s are copied to the `TestPath` of active `BinPlaceTargetFramework`s.
Original file line number Diff line number Diff line change
@@ -1,87 +1,134 @@
// 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;
// Keep in sync with https://raw.githubusercontent.com/NuGet/NuGet.Client/dccbd304b11103e08b97abf4cf4bcc1499d9235a/src/NuGet.Core/NuGet.Frameworks/NuGetFrameworkUtility.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Globalization;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NuGet.Common;
using NuGet.Frameworks;

namespace Microsoft.DotNet.Build.Tasks.TargetFramework
{
public class ChooseBestP2PTargetFrameworkTask : BuildTask
{
[Required]
public ITaskItem[] ProjectReferencesWithTargetFrameworks { get; set; }
private const string NEAREST_TARGET_FRAMEWORK = "NearestTargetFramework";
private const string TARGET_FRAMEWORKS = "TargetFrameworks";

[Required]
public string RuntimeGraph { get; set; }
public string? RuntimeGraph { get; set; }

/// <summary>
/// The current project's target framework.
/// </summary>
[Required]
public string TargetFramework { get; set; }
public string? CurrentProjectTargetFramework { get; set; }

/// <summary>
/// Optional TargetPlatformMoniker
/// </summary>
public string? CurrentProjectTargetPlatform { get; set; }

public bool OmitIncompatibleProjectReferences { get; set; }

/// <summary>
/// The project references for property lookup.
/// </summary>
public ITaskItem[]? AnnotatedProjectReferences { get; set; }

/// <summary>
/// The project references with assigned properties.
/// </summary>
[Output]
public ITaskItem[] AnnotatedProjectReferencesWithSetTargetFramework { get; set; }
public ITaskItem[]? AssignedProjects { get; set; }

public override bool Execute()
{
var annotatedProjectReferencesWithSetTargetFramework = new List<ITaskItem>(ProjectReferencesWithTargetFrameworks.Length);
var targetFrameworkResolver = new TargetFrameworkResolver(RuntimeGraph);
if (AnnotatedProjectReferences == null)
{
return !Log.HasLoggedErrors;
}

for (int i = 0; i < ProjectReferencesWithTargetFrameworks.Length; i++)
// validate current project framework
string errorMessage = string.Format(CultureInfo.CurrentCulture, "The project target framework '{0}' is not a supported target framework.", $"TargetFrameworkMoniker: {CurrentProjectTargetFramework}, TargetPlatformMoniker:{CurrentProjectTargetPlatform}");
if (!TryParseFramework(CurrentProjectTargetFramework!, CurrentProjectTargetPlatform, errorMessage, Log, out var projectNuGetFramework))
{
ITaskItem projectReference = ProjectReferencesWithTargetFrameworks[i];
string targetFrameworksValue = projectReference.GetMetadata("TargetFrameworks");
return false;
}

TargetFrameworkResolver targetFrameworkResolver = TargetFrameworkResolver.CreateOrGet(RuntimeGraph!);
List<ITaskItem> assignedProjects = new(AnnotatedProjectReferences.Length);

// Allow referencing projects with TargetFrameworks explicitely cleared out, i.e. Microsoft.Build.Traversal.
if (!string.IsNullOrWhiteSpace(targetFrameworksValue))
foreach (ITaskItem annotatedProjectReference in AnnotatedProjectReferences)
{
ITaskItem? assignedProject = AssignNearestFrameworkForSingleReference(annotatedProjectReference, projectNuGetFramework, targetFrameworkResolver);
if (assignedProject != null)
{
string[] targetFrameworks = targetFrameworksValue.Split(';');

string referringTargetFramework = projectReference.GetMetadata("ReferringTargetFramework");
if (string.IsNullOrWhiteSpace(referringTargetFramework))
{
referringTargetFramework = TargetFramework;
}

string bestTargetFramework = targetFrameworkResolver.GetBestSupportedTargetFramework(targetFrameworks, referringTargetFramework);
if (bestTargetFramework == null)
{
if (OmitIncompatibleProjectReferences)
{
continue;
}
Log.LogError($"Not able to find a compatible supported target framework for {referringTargetFramework} in Project {Path.GetFileName(projectReference.ItemSpec)}. The Supported Configurations are {string.Join(", ", targetFrameworks)}");
}

// Mimic msbuild's Common.targets behavior: https://github.com/dotnet/msbuild/blob/3c8fb11a080a5a15199df44fabf042a22e9ad4da/src/Tasks/Microsoft.Common.CurrentVersion.targets#L1842-L1853
if (projectReference.GetMetadata("HasSingleTargetFramework") != "true")
{
projectReference.SetMetadata("SetTargetFramework", "TargetFramework=" + bestTargetFramework);
}
else
{
// If the project has a single TargetFramework, we need to Undefine TargetFramework to avoid another project evaluation.
string undefineProperties = projectReference.GetMetadata("UndefineProperties");
projectReference.SetMetadata("UndefineProperties", undefineProperties + ";TargetFramework");
}

if (projectReference.GetMetadata("IsRidAgnostic") == "true")
{
// If the project is RID agnostic, undefine the RuntimeIdentifier property to avoid another evaluation. -->
string undefineProperties = projectReference.GetMetadata("UndefineProperties");
projectReference.SetMetadata("UndefineProperties", undefineProperties + ";RuntimeIdentifier");
}

projectReference.SetMetadata("SkipGetTargetFrameworkProperties", "true");
assignedProjects.Add(assignedProject);
}

annotatedProjectReferencesWithSetTargetFramework.Add(projectReference);
}

AnnotatedProjectReferencesWithSetTargetFramework = annotatedProjectReferencesWithSetTargetFramework.ToArray();
AssignedProjects = assignedProjects.ToArray();
return !Log.HasLoggedErrors;
}
}

private ITaskItem? AssignNearestFrameworkForSingleReference(ITaskItem project,
NuGetFramework projectNuGetFramework,
TargetFrameworkResolver targetFrameworkResolver)
{
TaskItem itemWithProperties = new(project);
string referencedProjectFrameworkString = project.GetMetadata(TARGET_FRAMEWORKS);

if (string.IsNullOrEmpty(referencedProjectFrameworkString))
{
// No target frameworks set, nothing to do.
return itemWithProperties;
}

string[] referencedProjectFrameworks = MSBuildStringUtility.Split(referencedProjectFrameworkString!);

// try project framework
string? nearestNuGetFramework = targetFrameworkResolver.GetNearest(referencedProjectFrameworks, projectNuGetFramework);
if (nearestNuGetFramework != null)
{
itemWithProperties.SetMetadata(NEAREST_TARGET_FRAMEWORK, nearestNuGetFramework);
return itemWithProperties;
}

if (OmitIncompatibleProjectReferences)
{
return null;
}

// no match found
Log.LogError(string.Format(CultureInfo.CurrentCulture, "Project '{0}' targets '{1}'. It cannot be referenced by a project that targets '{2}{3}'.", project.ItemSpec, referencedProjectFrameworkString, projectNuGetFramework.DotNetFrameworkName, projectNuGetFramework.HasPlatform ? "-" + projectNuGetFramework.DotNetPlatformName : string.Empty));
return itemWithProperties;
}

private static bool TryParseFramework(string targetFrameworkMoniker, string? targetPlatformMoniker, string errorMessage, Log logger, out NuGetFramework nugetFramework)
{
// Check if we have a long name.
#if NETFRAMEWORK || NETSTANDARD
nugetFramework = targetFrameworkMoniker.Contains(",")
? NuGetFramework.ParseComponents(targetFrameworkMoniker, targetPlatformMoniker)
: NuGetFramework.Parse(targetFrameworkMoniker);
#else
nugetFramework = targetFrameworkMoniker.Contains(',', System.StringComparison.Ordinal)
? NuGetFramework.ParseComponents(targetFrameworkMoniker, targetPlatformMoniker)
: NuGetFramework.Parse(targetFrameworkMoniker);
#endif

// validate framework
if (nugetFramework.IsUnsupported)
{
logger.LogError(errorMessage);
return false;
}

return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using NuGet.Frameworks;
using System.Collections.Generic;
using System.Linq;

Expand All @@ -11,31 +12,32 @@ namespace Microsoft.DotNet.Build.Tasks.TargetFramework
public class ChooseBestTargetFrameworksTask : BuildTask
{
[Required]
public ITaskItem[] BuildTargetFrameworks { get; set; }
public ITaskItem[]? BuildTargetFrameworks { get; set; }

[Required]
public string RuntimeGraph { get; set; }
public string? RuntimeGraph { get; set; }

[Required]
public string[] SupportedTargetFrameworks { get; set; }
public string[]? SupportedTargetFrameworks { get; set; }

// Returns distinct items only. Compares the include values. Metadata is ignored.
public bool Distinct { get; set; }

[Output]
public ITaskItem[] BestTargetFrameworks { get; set; }
public ITaskItem[]? BestTargetFrameworks { get; set; }

public override bool Execute()
{
var bestTargetFrameworkList = new List<ITaskItem>(BuildTargetFrameworks.Length);
var targetframeworkResolver = new TargetFrameworkResolver(RuntimeGraph);
List<ITaskItem> bestTargetFrameworkList = new(BuildTargetFrameworks!.Length);
TargetFrameworkResolver targetframeworkResolver = TargetFrameworkResolver.CreateOrGet(RuntimeGraph!);

foreach (ITaskItem buildTargetFramework in BuildTargetFrameworks)
{
string bestTargetFramework = targetframeworkResolver.GetBestSupportedTargetFramework(SupportedTargetFrameworks, buildTargetFramework.ItemSpec);
NuGetFramework framework = NuGetFramework.ParseFolder(buildTargetFramework.ItemSpec);
string? bestTargetFramework = targetframeworkResolver.GetNearest(SupportedTargetFrameworks!, framework);
if (bestTargetFramework != null && (!Distinct || !bestTargetFrameworkList.Any(b => b.ItemSpec == bestTargetFramework)))
{
var item = new TaskItem(bestTargetFramework);
TaskItem item = new(bestTargetFramework);
buildTargetFramework.CopyMetadataTo(item);
bestTargetFrameworkList.Add(item);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<Title>Configuration system for cross-targeting projects.</Title>
<PackageDescription>This package provides the following MSBuild tasks: ChooseBestTargetFrameworksTask and ChooseBestP2PTargetFrameworkTask.</PackageDescription>
<DefaultItemExcludes Condition="'$(TargetFramework)' != 'net472'">**/*.Desktop.*</DefaultItemExcludes>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net472'">
Expand All @@ -24,7 +25,7 @@
<PackageReference Include="Microsoft.Build.Tasks.Core" Version="$(MicrosoftBuildTasksCoreVersion)" />
<PackageReference Include="NuGet.Packaging" Version="$(NugetVersion)" />

<!-- This is here so that we agree with the project's transitive references to NewtonSoft.Json -->
<!-- This is here so that we agree with the project's transitive references to Newtonsoft.Json -->
<PackageReference Include="Newtonsoft.Json" Version="$(NewtonsoftJsonVersion)" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,20 @@ namespace Microsoft.DotNet.Build.Tasks.TargetFramework
/// This class uses NuGet's asset selection logic to choose the best TargetFramework given the list of supported TargetFrameworks.
/// This behaves in a same way as NuGet selects lib files from a nuget package for a particular TargetFramework.
/// </summary>
public class TargetFrameworkResolver
internal class TargetFrameworkResolver
{
private static readonly Dictionary<string, TargetFrameworkResolver> s_targetFrameworkResolverCache = new();
private readonly ManagedCodeConventions _conventions;
private readonly PatternSet _configStringPattern;

public TargetFrameworkResolver(string runtimeGraph)
private TargetFrameworkResolver(string runtimeGraph)
{
_conventions = new ManagedCodeConventions(JsonRuntimeFormat.ReadRuntimeGraph(runtimeGraph));
_configStringPattern = new PatternSet(
_conventions.Properties,
groupPatterns: new PatternDefinition[]
{
// In order to use Nuget's asset allocation, the input needs to file paths and should contain a trailing slash.
// In order to use Nuget's asset allocation, the input needs to be file paths and should contain a trailing slash.
new PatternDefinition("{tfm}/"),
new PatternDefinition("{tfm}-{rid}/")
},
Expand All @@ -37,17 +38,28 @@ public TargetFrameworkResolver(string runtimeGraph)
});
}

public string GetBestSupportedTargetFramework(IEnumerable<string> supportedTargetFrameworks, string targetFramework)
public static TargetFrameworkResolver CreateOrGet(string runtimeGraph)
{
var contentCollection = new ContentItemCollection();
contentCollection.Load(supportedTargetFrameworks.Select(t => t + '/').ToArray());
if (!s_targetFrameworkResolverCache.TryGetValue(runtimeGraph, out TargetFrameworkResolver? targetFrameworkResolver))
{
targetFrameworkResolver = new TargetFrameworkResolver(runtimeGraph);
s_targetFrameworkResolverCache.Add(runtimeGraph, targetFrameworkResolver);
}

string[] splitStrings = targetFramework.Split('-');
string targetFrameworkWithoutSuffix = splitStrings[0];
string targetFrameworkSuffix = splitStrings.Length > 1 ? splitStrings[1] : string.Empty;
return targetFrameworkResolver!;
}

public string? GetNearest(IEnumerable<string> frameworks, NuGetFramework framework)
{
NuGetFramework frameworkWithoutPlatform = NuGetFramework.Parse(framework.DotNetFrameworkName);

ContentItemCollection contentCollection = new();
contentCollection.Load(frameworks.Select(f => f + '/').ToArray());

// The platform is expected to be passed-in lower-case but the SDK normalizes "windows" to "Windows" which is why it is lowered again.
SelectionCriteria criteria = _conventions.Criteria.ForFrameworkAndRuntime(frameworkWithoutPlatform, framework.Platform.ToLowerInvariant());
string? bestTargetFrameworkString = contentCollection.FindBestItemGroup(criteria, _configStringPattern)?.Items[0].Path;

SelectionCriteria criteria = _conventions.Criteria.ForFrameworkAndRuntime(NuGetFramework.Parse(targetFrameworkWithoutSuffix), targetFrameworkSuffix);
string bestTargetFrameworkString = contentCollection.FindBestItemGroup(criteria, _configStringPattern)?.Items[0].Path;
return bestTargetFrameworkString?.Remove(bestTargetFrameworkString.Length - 1);
}
}
Expand Down
Loading

0 comments on commit e82404f

Please sign in to comment.