-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Description
As part of the spec for the .NET 5 TFM work we identified an issue with TFM checks in conditions.
Background on MSBuild evaluation
In SDK-style projects there are two kinds of MSBuild files that are automatically included into each project:
-
*.props
: These files are included at the top of the user's project file and are used to define a set of default properties that the user's project file can use. -
*.targets
. These files are included at the bottom of the user's project file, usually meant to define build targets and additional properties/items that need to depend on properties defined by the user.
Furthermore, MSBuild has a multi-pass evaluation model where properties are evaluated before items.
Why is all of this important? Because it controls which properties the user can rely on in their project file.
Often, a user wants to express a condition like "include this file if you're compiling for .NET 5 or higher". Logically one would like to express it like this:
<ItemGroup Condition="'$(TargetFramework)' >= 'net5.0'`">
but this doesn't work because that would be a string comparison, not a version comparison. Instead, the user has to write it like this:
<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND '$(TargetFrameworkVersion)' >= '3.0'">
This works for conditions on item groups because they are evaluated after properties. Since the user's project file defines the TargetFramework
property, the SDK logic that expands it into the other properties such as TargetFrameworkIdentifier
and TargetFrameworkVersion
has to live in *.targets
, i.e. at the bottom of the project file. That means these automatically expanded properties aren't available for the user when defining other properties. This happens to work for items because items are evaluated after all properties are evaluated.
Due to MSBuild evaluation order the user cannot define properties like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp3.1</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard'">
<SomeProperty>Some .NET Standard specific value<SomeProperty>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<SomeProperty>Some .NET Core specific value<SomeProperty>
</PropertyGroup>
</Project>
In the past, we've seen people working this around by using string processing functions against the TargetFramework
property, which is less than ideal.
Option using attributes
Ideally, we'd expose functionality such that the user can do version checks:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp3.1</TargetFramework>
</PropertyGroup>
<PropertyGroup TargetFramework="netstandard">
<SomeProperty>Some value that applies to all versions of .NET Standard<SomeProperty>
</PropertyGroup>
<PropertyGroup TargetFramework=">=netcoreapp2.0">
<SomeProperty>Some value that applies to .NET Core 2.0 and later<SomeProperty>
</PropertyGroup>
<PropertyGroup TargetFramework="==net5.0-ios13.0">
<SomeProperty>Some value that only applies to .NET 5 + iOS 13.0<SomeProperty>
</PropertyGroup>
<PropertyGroup TargetPlatform="windows">
<SomeProperty>Some value that applies to all version of Windows<SomeProperty>
</PropertyGroup>
<PropertyGroup TargetPlatform=">=ios-12.0">
<SomeProperty>Some value that applies to iOS 12.0 and later<SomeProperty>
</PropertyGroup>
</Project>
The idea is:
- Property groups, properties, and item groups get new attributes
TargetFramework
andTargetPlatform
. - The value can be prefixed with an optional conditional operator
==
,!=
,<
,<=
,>
, and>=
. If the operator is omitted,==
is assumed. TargetFramework
supports comparisons with a friendly TFM name. This can include an OS flavor for symmetry. If theTargetFramework
property includes an OS flavor but the attribute doesn't, the comparison only applies to the TFM without the OS flavor. In other words a condition ofTargetFramework=">=net5.0"
will result intrue
if the project targetsnet5.0
,net6.0
, as well asnet6.0-android12.0
.
Option via new syntax
We could also invent new syntax that allows parsing of constitutes like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;netcoreapp3.1</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework::Identifier)=='netstandard'">
<SomeProperty>Some value that applies to all versions of .NET Standard<SomeProperty>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework::Name)>='netcoreapp2.0'">
<SomeProperty>Some value that applies to .NET Core 2.0 and later<SomeProperty>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework::Name)=='net5.0-ios13.0'">
<SomeProperty>Some value that only applies to .NET 5 + iOS 13.0<SomeProperty>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework::Platform)=='windows'">
<SomeProperty>Some value that applies to all version of Windows<SomeProperty>
</PropertyGroup>
<PropertyGroup Condition="$(TargetFramework::Platform)>='ios-12.0'">
<SomeProperty>Some value that applies to iOS 12.0 and later<SomeProperty>
</PropertyGroup>
</Project>
Option via functions
We could also just define new intrinsic functions on some type, but this will make using them a mouthful:
<PropertyGroup Condition="`'$([MSBuild]::TargetFrameworkIdentifier($(TargetFramework)))' == '.NETStandard'`">
<SomeProperty>Some value that applies to all versions of .NET Standard<SomeProperty>
</PropertyGroup>
<PropertyGroup Condition="`'$([MSBuild]::IsTargetFrameworkOrLater($(TargetFramework)))', 'net5.0'))`">
<SomeProperty>Some value that applies to .NET 5 or later<SomeProperty>
</PropertyGroup>
<PropertyGroup Condition="`'$([MSBuild]::IsTargetPlatformOrLater($(TargetFramework)))', 'ios12.0'))`">
<SomeProperty>Some value that applies to iOS 12 or later<SomeProperty>
</PropertyGroup>
I am not married to any of these ideas; I'm just spitballing here. Thoughts?