From bed586d16b899325f69404721227a5dc639f5ad6 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Tue, 24 Jan 2023 11:37:17 -0800 Subject: [PATCH 1/8] Introduce intrinsic to 'intersect' target frameworks This is the initial implementation of an experimental intrinsic designed to support the ability to build subsets of a repo/solution/project. This ability can reduce build time and support the ability to build projects based on available target resources per-platforms. The idea is that this intrinsic would be used in a set of targets that are imported early, before standard .NET SDK targets, and could be used to filter the target frameworks that a project is going to build for. .NET's arcade will contain an initial implementation of those targets, looking something like this: ``` <_OriginalTargetFrameworks Condition="'$(TargetFrameworks)' != ''">$(TargetFrameworks) <_OriginalTargetFrameworks Condition="'$(TargetFramework)' != ''">$(TargetFramework) <_FilteredTargetFrameworks>$([MSBuild]::Unescape($([MSBuild]::IntersectTargetFrameworks('$(_OriginalTargetFrameworks)', '$(DotNetTargetFrameworkFilter)')))) $(_FilteredTargetFrameworks) $(_FilteredTargetFrameworks) true ``` Initially, the intrinsic is implemented as follows: - A framework is maintained if the Framework name (Framework property of parsed nuget property) exists in both lists AND - The version matches, if specified. Thus, if a TFM list is: `net6.0-windows;netstandard2.0;net472` and the intersect list is `net6.0;netstandard2.0`, the result would be net6.0;netstandard2.0. It's possible that before this MSBuild version goes out of preview. There are two other options: - **An implementation that selects based on compatibilty**. The inherent problem with a compatibility check (based on NuGet's notion) is that some TFMs are compatible with TFMs you may want to remove. For instance, you may want to keep netstandard2.0, but not net472. However, since net472 is compatible with netstandard2.0, it would not be eliminated. - **Strict intersection based on all TFM properties**. This is simpler and more straightforward, but requires a more verbose input list. For instance, if you wanted to preserve net6 targets, especially in cross-targeting situations, you may have list many TFMs (e.g. specific OS versions like net6.0-windows). Of these alternate approaches, I think strict is the most likely to be usable and understandable. --- src/Build/Evaluation/IntrinsicFunctions.cs | 5 +++ src/Build/Utilities/NuGetFrameworkWrapper.cs | 41 ++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/Build/Evaluation/IntrinsicFunctions.cs b/src/Build/Evaluation/IntrinsicFunctions.cs index ce0f37bbd56..dd7d23de77e 100644 --- a/src/Build/Evaluation/IntrinsicFunctions.cs +++ b/src/Build/Evaluation/IntrinsicFunctions.cs @@ -549,6 +549,11 @@ internal static string GetTargetPlatformVersion(string tfm, int versionPartCount return NuGetFramework.Value.GetTargetPlatformVersion(tfm, versionPartCount); } + internal static string IntersectTargetFrameworks(string left, string right) + { + return NuGetFramework.Value.IntersectTargetFrameworks(left, right); + } + internal static bool AreFeaturesEnabled(Version wave) { return ChangeWaves.AreFeaturesEnabled(wave); diff --git a/src/Build/Utilities/NuGetFrameworkWrapper.cs b/src/Build/Utilities/NuGetFrameworkWrapper.cs index 9d3546fcf37..116d030cb26 100644 --- a/src/Build/Utilities/NuGetFrameworkWrapper.cs +++ b/src/Build/Utilities/NuGetFrameworkWrapper.cs @@ -4,6 +4,8 @@ using System; using System.IO; using System.Reflection; +using System.Linq; +using System.Collections.Generic; using Microsoft.Build.Framework; using Microsoft.Build.Shared; @@ -27,6 +29,7 @@ internal class NuGetFrameworkWrapper private static PropertyInfo VersionProperty; private static PropertyInfo PlatformProperty; private static PropertyInfo PlatformVersionProperty; + private static PropertyInfo AllFrameworkVersionsProperty; public NuGetFrameworkWrapper() { @@ -47,6 +50,7 @@ public NuGetFrameworkWrapper() VersionProperty = NuGetFramework.GetProperty("Version"); PlatformProperty = NuGetFramework.GetProperty("Platform"); PlatformVersionProperty = NuGetFramework.GetProperty("PlatformVersion"); + AllFrameworkVersionsProperty = NuGetFramework.GetProperty("AllFrameworkVersions"); } catch { @@ -91,5 +95,42 @@ private string GetNonZeroVersionParts(Version version, int minVersionPartCount) var nonZeroVersionParts = version.Revision == 0 ? version.Build == 0 ? version.Minor == 0 ? 1 : 2 : 3 : 4; return version.ToString(Math.Max(nonZeroVersionParts, minVersionPartCount)); } + + public string IntersectTargetFrameworks(string left, string right) + { + IEnumerable<(string originalTfm, object parsedTfm)> leftFrameworks = ParseTfms(left); + IEnumerable<(string originalTfm, object parsedTfm)> rightFrameworks = ParseTfms(right); + string tfmList = ""; + + // An incoming target framework from 'left' is kept if it is compatible with any of the desired target frameworks on 'right' + foreach (var l in leftFrameworks) + { + if (rightFrameworks.Any(r => + (FrameworkProperty.GetValue(l.parsedTfm) as string).Equals(FrameworkProperty.GetValue(r.parsedTfm) as string, StringComparison.OrdinalIgnoreCase) && + (((Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(l.parsedTfm))) && (Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(r.parsedTfm)))) || + ((VersionProperty.GetValue(l.parsedTfm) as Version) == (VersionProperty.GetValue(r.parsedTfm) as Version))))) + { + if (string.IsNullOrEmpty(tfmList)) + { + tfmList = l.originalTfm; + } + else + { + tfmList += $";{l.originalTfm}"; + } + } + } + + return tfmList; + + IEnumerable<(string originalTfm, object parsedTfm)> ParseTfms(string desiredTargetFrameworks) + { + return desiredTargetFrameworks.Split(new char[] {';'}, StringSplitOptions.RemoveEmptyEntries).Select(tfm => + { + (string originalTfm, object parsedTfm) parsed = (tfm, Parse(tfm)); + return parsed; + }); + } + } } } From 5541ef2867250eeda2768be589be84ca80765b23 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 25 Jan 2023 09:50:51 -0800 Subject: [PATCH 2/8] Rename intrinsic and add a few tests --- .../Evaluation/Expander_Tests.cs | 17 +++++++++++++++++ src/Build/Evaluation/IntrinsicFunctions.cs | 4 ++-- src/Build/Utilities/NuGetFrameworkWrapper.cs | 18 +++++++++--------- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index 8501b7297f9..a8271b103d6 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -4295,6 +4295,23 @@ private void TestPropertyFunction(string expression, string propertyName, string result.ShouldBe(expected); } + [Theory] + [InlineData("net6.0", "netstandard2.0", "")] + [InlineData("net6.0-windows", "netstandard2.0", "")] + [InlineData("net6.0-windows", "net6.0", "net6.0-windows")] + [InlineData("netstandard2.0;net6.0", "net6.0", "net6.0")] + [InlineData("netstandard2.0;net6.0-windows", "net6.0", "net6.0-windows")] + [InlineData("netstandard2.0;net6.0-windows", "net6.0;netstandard2.0;net472", "netstandard2.0;net6.0-windows")] + [InlineData("netstandard2.0;net472", "net6.0;netstandard2.0;net472", "netstandard2.0;net472")] + [InlineData("netstandard2.0;net472", "net6.0;netstandard2.0;net472", "netstandard2.0;net472")] + public void PropertyFunctionIntersectTargetFrameworks(string left, string right, string expected) + { + var pg = new PropertyDictionary(); + var expander = new Expander(pg, FileSystems.Default); + + AssertSuccess(expander, $"$([MSBuild]::FilterTargetFrameworks('{left}', '{right}'))", expected); + } + [Fact] public void ExpandItemVectorFunctions_GetPathsOfAllDirectoriesAbove() { diff --git a/src/Build/Evaluation/IntrinsicFunctions.cs b/src/Build/Evaluation/IntrinsicFunctions.cs index dd7d23de77e..cf17320a952 100644 --- a/src/Build/Evaluation/IntrinsicFunctions.cs +++ b/src/Build/Evaluation/IntrinsicFunctions.cs @@ -549,9 +549,9 @@ internal static string GetTargetPlatformVersion(string tfm, int versionPartCount return NuGetFramework.Value.GetTargetPlatformVersion(tfm, versionPartCount); } - internal static string IntersectTargetFrameworks(string left, string right) + internal static string FilterTargetFrameworks(string incoming, string filter) { - return NuGetFramework.Value.IntersectTargetFrameworks(left, right); + return NuGetFramework.Value.FilterTargetFrameworks(incoming, filter); } internal static bool AreFeaturesEnabled(Version wave) diff --git a/src/Build/Utilities/NuGetFrameworkWrapper.cs b/src/Build/Utilities/NuGetFrameworkWrapper.cs index 116d030cb26..e904e80c756 100644 --- a/src/Build/Utilities/NuGetFrameworkWrapper.cs +++ b/src/Build/Utilities/NuGetFrameworkWrapper.cs @@ -96,27 +96,27 @@ private string GetNonZeroVersionParts(Version version, int minVersionPartCount) return version.ToString(Math.Max(nonZeroVersionParts, minVersionPartCount)); } - public string IntersectTargetFrameworks(string left, string right) + public string FilterTargetFrameworks(string incoming, string filter) { - IEnumerable<(string originalTfm, object parsedTfm)> leftFrameworks = ParseTfms(left); - IEnumerable<(string originalTfm, object parsedTfm)> rightFrameworks = ParseTfms(right); - string tfmList = ""; + IEnumerable<(string originalTfm, object parsedTfm)> incomingFrameworks = ParseTfms(incoming); + IEnumerable<(string originalTfm, object parsedTfm)> filterFrameworks = ParseTfms(filter); + StringBuilder tfmList = new StringBuilder(); - // An incoming target framework from 'left' is kept if it is compatible with any of the desired target frameworks on 'right' - foreach (var l in leftFrameworks) + // An incoming target framework from 'incoming' is kept if it is compatible with any of the desired target frameworks on 'filter' + foreach (var l in incomingFrameworks) { - if (rightFrameworks.Any(r => + if (filterFrameworks.Any(r => (FrameworkProperty.GetValue(l.parsedTfm) as string).Equals(FrameworkProperty.GetValue(r.parsedTfm) as string, StringComparison.OrdinalIgnoreCase) && (((Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(l.parsedTfm))) && (Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(r.parsedTfm)))) || ((VersionProperty.GetValue(l.parsedTfm) as Version) == (VersionProperty.GetValue(r.parsedTfm) as Version))))) { if (string.IsNullOrEmpty(tfmList)) { - tfmList = l.originalTfm; + tfmList.Append(l.originalTfm); } else { - tfmList += $";{l.originalTfm}"; + tfmList.Append($";{l.originalTfm}"); } } } From 8154a2415cea22bea9bc3d6e27acaf7ed56f48b4 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 25 Jan 2023 10:00:06 -0800 Subject: [PATCH 3/8] Fix usings --- src/Build/Utilities/NuGetFrameworkWrapper.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Build/Utilities/NuGetFrameworkWrapper.cs b/src/Build/Utilities/NuGetFrameworkWrapper.cs index e904e80c756..bb2bae2cc3c 100644 --- a/src/Build/Utilities/NuGetFrameworkWrapper.cs +++ b/src/Build/Utilities/NuGetFrameworkWrapper.cs @@ -2,10 +2,11 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; +using System.Collections.Generic; using System.IO; -using System.Reflection; using System.Linq; -using System.Collections.Generic; +using System.Reflection; +using System.Text; using Microsoft.Build.Framework; using Microsoft.Build.Shared; From 55f8703d1e1cf31094cf68ad92b12c3b5d437203 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 25 Jan 2023 10:10:00 -0800 Subject: [PATCH 4/8] And...one more stringbuilder fix --- src/Build/Utilities/NuGetFrameworkWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Utilities/NuGetFrameworkWrapper.cs b/src/Build/Utilities/NuGetFrameworkWrapper.cs index bb2bae2cc3c..49698f3ccf1 100644 --- a/src/Build/Utilities/NuGetFrameworkWrapper.cs +++ b/src/Build/Utilities/NuGetFrameworkWrapper.cs @@ -122,7 +122,7 @@ public string FilterTargetFrameworks(string incoming, string filter) } } - return tfmList; + return tfmList.ToString(); IEnumerable<(string originalTfm, object parsedTfm)> ParseTfms(string desiredTargetFrameworks) { From e65afce2a668eae61448791a400069a68300fc4e Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 25 Jan 2023 10:19:33 -0800 Subject: [PATCH 5/8] I'm bad at this --- src/Build/Utilities/NuGetFrameworkWrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Build/Utilities/NuGetFrameworkWrapper.cs b/src/Build/Utilities/NuGetFrameworkWrapper.cs index 49698f3ccf1..e7760b0ae76 100644 --- a/src/Build/Utilities/NuGetFrameworkWrapper.cs +++ b/src/Build/Utilities/NuGetFrameworkWrapper.cs @@ -111,7 +111,7 @@ public string FilterTargetFrameworks(string incoming, string filter) (((Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(l.parsedTfm))) && (Convert.ToBoolean(AllFrameworkVersionsProperty.GetValue(r.parsedTfm)))) || ((VersionProperty.GetValue(l.parsedTfm) as Version) == (VersionProperty.GetValue(r.parsedTfm) as Version))))) { - if (string.IsNullOrEmpty(tfmList)) + if (tfmList.Length == 0) { tfmList.Append(l.originalTfm); } From 819ff06f704f32a26e655edc0de6d696491823d1 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 25 Jan 2023 10:34:42 -0800 Subject: [PATCH 6/8] Fix test --- src/Build.UnitTests/Evaluation/Expander_Tests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index a8271b103d6..ada3954652c 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -4303,8 +4303,7 @@ private void TestPropertyFunction(string expression, string propertyName, string [InlineData("netstandard2.0;net6.0-windows", "net6.0", "net6.0-windows")] [InlineData("netstandard2.0;net6.0-windows", "net6.0;netstandard2.0;net472", "netstandard2.0;net6.0-windows")] [InlineData("netstandard2.0;net472", "net6.0;netstandard2.0;net472", "netstandard2.0;net472")] - [InlineData("netstandard2.0;net472", "net6.0;netstandard2.0;net472", "netstandard2.0;net472")] - public void PropertyFunctionIntersectTargetFrameworks(string left, string right, string expected) + public void PropertyFunctionFilterTargetFrameworks(string left, string right, string expected) { var pg = new PropertyDictionary(); var expander = new Expander(pg, FileSystems.Default); From 8eabc06ce6ffb76768832587e182b94644bfa594 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 25 Jan 2023 15:30:00 -0800 Subject: [PATCH 7/8] Try a different approach --- src/Build.UnitTests/Evaluation/Expander_Tests.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index ada3954652c..6ab0e263494 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -4303,12 +4303,9 @@ private void TestPropertyFunction(string expression, string propertyName, string [InlineData("netstandard2.0;net6.0-windows", "net6.0", "net6.0-windows")] [InlineData("netstandard2.0;net6.0-windows", "net6.0;netstandard2.0;net472", "netstandard2.0;net6.0-windows")] [InlineData("netstandard2.0;net472", "net6.0;netstandard2.0;net472", "netstandard2.0;net472")] - public void PropertyFunctionFilterTargetFrameworks(string left, string right, string expected) + public void PropertyFunctionFilterTargetFrameworks(string incoming, string filter, string expected) { - var pg = new PropertyDictionary(); - var expander = new Expander(pg, FileSystems.Default); - - AssertSuccess(expander, $"$([MSBuild]::FilterTargetFrameworks('{left}', '{right}'))", expected); + TestPropertyFunction($"$([MSBuild]::FilterTargetFrameworks('{incoming}', '{filter}'))", "_", "_", expected); } [Fact] From bde9b146a9a1927bf4b4b3692b5d0b72ebe7b319 Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 25 Jan 2023 15:56:04 -0800 Subject: [PATCH 8/8] I made some tests --- src/Build.UnitTests/Evaluation/Expander_Tests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Build.UnitTests/Evaluation/Expander_Tests.cs b/src/Build.UnitTests/Evaluation/Expander_Tests.cs index 6ab0e263494..9864deef770 100644 --- a/src/Build.UnitTests/Evaluation/Expander_Tests.cs +++ b/src/Build.UnitTests/Evaluation/Expander_Tests.cs @@ -4301,8 +4301,8 @@ private void TestPropertyFunction(string expression, string propertyName, string [InlineData("net6.0-windows", "net6.0", "net6.0-windows")] [InlineData("netstandard2.0;net6.0", "net6.0", "net6.0")] [InlineData("netstandard2.0;net6.0-windows", "net6.0", "net6.0-windows")] - [InlineData("netstandard2.0;net6.0-windows", "net6.0;netstandard2.0;net472", "netstandard2.0;net6.0-windows")] - [InlineData("netstandard2.0;net472", "net6.0;netstandard2.0;net472", "netstandard2.0;net472")] + [InlineData("netstandard2.0;net6.0-windows", "net6.0;netstandard2.0;net472", "netstandard2.0%3bnet6.0-windows")] + [InlineData("netstandard2.0;net472", "net6.0;netstandard2.0;net472", "netstandard2.0%3bnet472")] public void PropertyFunctionFilterTargetFrameworks(string incoming, string filter, string expected) { TestPropertyFunction($"$([MSBuild]::FilterTargetFrameworks('{incoming}', '{filter}'))", "_", "_", expected);