Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net5.0-platform support for Pack #3479

Merged
merged 3 commits into from
Jul 10, 2020
Merged

net5.0-platform support for Pack #3479

merged 3 commits into from
Jul 10, 2020

Conversation

zkat
Copy link
Contributor

@zkat zkat commented Jun 26, 2020

So #3339 had to be reverted because it caused some nasty failures during integration with dotnet, because our Pack tests weren't covering net5.0 scenarios, much less with the updated net5 tfm format changes.

This PR fixes this specific issue and adds some tests so we're not caught by surprise in the future.

Review of this PR might be better off being focused on the changes in 22b5836e8e66448e332515d33f5e9204ea0118aa, since the other commit is what was in #3339 originally, since I'm reverting the revert.

Fixes: NuGet/Home#9682


namespace NuGet.Packaging
{
public static class NuGetFrameworkNameUtility
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is used a few times within the same project/assembly, but also once from NuGet.CommandLine. Is there a way to refactor a little so it doesn't need to be used by NuGet.CommandLine, and change the class to internal?

NuGetFrameworkNameUtility doesn't seem like an API that should be public for customers using NuGet.Packaging in their own apps.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FrameworkNameUtility vs FrameworkNameUtility.

I think we can combine them into 1.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One returns FrameworkName, this one returns NuGetFramework.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've merged them into a single utility.


effectivePath = path;

if (String.IsNullOrEmpty(targetFrameworkString))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string.IsNullOrEmpty instead of String.IsNullOrEmpty? Guideline#11

@@ -60,8 +60,8 @@ interface IFrameworkMappings
IEnumerable<OneWayCompatibilityMappingEntry> CompatibilityMappings { get; }

/// <summary>
/// Ordered list of framework identifiers. The first framework in the list will be preferred over other
/// framework identifiers. This is enable better tie breaking in scenarios where legacy frameworks are
/// Ordered list of framework identifiers. The first framework in the list will be preferred over other
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: You can consider reverting this givent hat there are no other changes.


namespace NuGet.Packaging
{
public static class NuGetFrameworkNameUtility
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FrameworkNameUtility vs FrameworkNameUtility.

I think we can combine them into 1.

{
public static class NuGetFrameworkNameUtility
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "1#")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is CA1021 still raised?
I don't think we have this enabled anymore.


namespace NuGet.Packaging
{
public static class NuGetFrameworkNameUtility
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One returns FrameworkName, this one returns NuGetFramework.

/// <param name="strictParsing">if set to <c>true</c>, parse the first folder of path even if it is unrecognized framework.</param>
/// <param name="effectivePath">returns the path after the parsed target framework</param>
/// <returns></returns>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I'm not sure C1021 is enabled in our current analyzers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, we can use a value tuple instead of out params cause they are more explicit.

@@ -160,8 +214,8 @@ public void CreatePackageWithNuspecIncludeExclude()
var net45 = new PackageDependencyGroup(new NuGetFramework(".NETFramework", new Version(4, 5)), dependencies45);
builder.DependencyGroups.Add(net45);

var net46 = new PackageDependencyGroup(new NuGetFramework(".NETFramework", new Version(4, 6)), dependencies46);
builder.DependencyGroups.Add(net46);
var net50win = new PackageDependencyGroup(new NuGetFramework(".NETCoreApp", new Version(5, 0), "windows", new Version(0, 0)), dependencies50);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most full proof test to ensure net50 can be packed is to add a test in https://github.com/NuGet/NuGet.Client/blob/dev/test/NuGet.Core.FuncTests/Dotnet.Integration.Test/PackCommandTests.cs.

Add the test only for net5.0 projects.

@zkat zkat force-pushed the dev-zkat-net5.0-platforms branch from 1a5176b to c43aa89 Compare July 1, 2020 21:56
@zkat zkat force-pushed the dev-zkat-net5.0-platforms branch 2 times, most recently from 3c35510 to 0fb3255 Compare July 3, 2020 02:41
@zkat zkat force-pushed the dev-zkat-net5.0-platforms branch from 0fb3255 to 6e1356b Compare July 3, 2020 02:52
@wli3
Copy link

wli3 commented Jul 6, 2020

Any update?

@zkat
Copy link
Contributor Author

zkat commented Jul 6, 2020

Just waiting for final approvals. Everything is passing, but I need another review (/cc @nkolev92 @zivkan )

@wli3
Copy link

wli3 commented Jul 7, 2020

@nkolev92 @zivkan could you approve it soon? We have several works being blocked by this change. And the deadline is next week.

nkolev92
nkolev92 previously approved these changes Jul 7, 2020
Copy link
Member

@nkolev92 nkolev92 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good.

A few comments to address before merging and a nit or two for you consider.

src/NuGet.Core/NuGet.Frameworks/FrameworkNameProvider.cs Outdated Show resolved Hide resolved
@@ -40,3 +40,4 @@
[assembly: SuppressMessage("Build", "CA1062:In externally visible method 'bool NuGetFrameworkUtility.IsNetCore50AndUp(NuGetFramework framework)', validate parameter 'framework' is non-null before using it. If appropriate, throw an ArgumentNullException when the argument is null or add a Code Contract precondition asserting non-null argument.", Justification = "<Pending>", Scope = "member", Target = "~M:NuGet.Frameworks.NuGetFrameworkUtility.IsNetCore50AndUp(NuGet.Frameworks.NuGetFramework)~System.Boolean")]
[assembly: SuppressMessage("Build", "CA2237:Add [Serializable] to FrameworkException as this type implements ISerializable", Justification = "<Pending>", Scope = "type", Target = "~T:NuGet.Frameworks.FrameworkException")]
[assembly: SuppressMessage("Build", "CA1067:Type NuGet.Frameworks.OneWayCompatibilityMappingEntry should override Equals because it implements IEquatable<T>", Justification = "<Pending>", Scope = "type", Target = "~T:NuGet.Frameworks.OneWayCompatibilityMappingEntry")]
[assembly: SuppressMessage("Globalization", "CA1308:Normalize strings to uppercase", Justification = "<Pending>", Scope = "member", Target = "~M:NuGet.Frameworks.FrameworkNameProvider.TryGetShortPlatform(System.String,System.Version,System.String,System.String@)~System.Boolean")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Add a justification.

/// <summary>
/// Creates a new NuGetFramework instance, with an optional platform and platformVersion (only available for net5.0+)
/// </summary>
public NuGetFramework(string frameworkIdentifier, Version frameworkVersion, string platform, Version platformVersion)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same feedback as above, just confirm that platform and version are still the correct concept names.

public static string GetFrameworkString(this NuGetFramework self)
{
if (self.Version.Major >= 5)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels very fragile and likely incorrect. We should check the TargetFrameworkIdentifier too.

That's what the IsNet5Era check does:

            IsNet5Era = (_frameworkVersion.Major >= Version5 && StringComparer.OrdinalIgnoreCase.Equals(FrameworkConstants.FrameworkIdentifiers.NetCoreApp, _frameworkIdentifier));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've decided to go ahead and export IsNet5Era as a public property. I think it's going to be useful for more than just our own needs. I don't like the idea of copying this stuff all over the place when special-casing net5 stuff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather have a duplicated logic tbh.

NuGet.Frameworks and NuGet.Versioning are arguably the most important core libraries in all of NuGet.
ISNet5Era screams like it's something that we couldn't properly design, very much an internal detail.

@zivkan thoughts?

Copy link
Member

@zivkan zivkan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry about the late feedback. Too many things going on :(

src/NuGet.Core/NuGet.Frameworks/FrameworkNameProvider.cs Outdated Show resolved Hide resolved
{
if (frameworkIdentifier == null)
{
throw new ArgumentNullException("frameworkIdentifier");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new ArgumentNullException("frameworkIdentifier");
throw new ArgumentNullException(nameof(frameworkIdentifier));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intentional, in order to keep it consistent with the existing API. Are you sure I should change it? I thought we had this discussion before. I'm happy to make this change, though, if you want.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember a discussion where someone was using nameof for strings that are compared to command line arguments customers pass in. I argued that it shouldn't use nameof because renaming the variable in code should not break functional behaviour. I lost that argument.

But for validating method parameters, I'd expect it to match the parameter name. If I use NuGet.Frameworks in my app, and I get an argument null exception saying that parameter x should not be null, but none of the method parameters are named x, then it's confusing.

You're right that the exception message will change between package versions if we change the parameter name, so it's a tradeoff of making the error message be consistent with previous versions, or be consistent with the current code. In either case we'll break anyone using named parameters, so it's ideal to avoid renaming parameters if at all possible.

overall, it's more of a nitpick, so I don't care very much. The mandatory/optional TPV is a much more important issue.


if (frameworkVersion == null)
{
throw new ArgumentNullException("frameworkVersion");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new ArgumentNullException("frameworkVersion");
throw new ArgumentNullException(nameof(frameworkVersion));


if (platform == null)
{
throw new ArgumentNullException("platform");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new ArgumentNullException("platform");
throw new ArgumentNullException(nameof(platform));


if (platformVersion == null)
{
throw new ArgumentNullException("platformVersion");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
throw new ArgumentNullException("platformVersion");
throw new ArgumentNullException(nameof(platformVersion));

Comment on lines 69 to 70
Assert.Throws<System.ArgumentException>(() => target.GetNearest(targetFramework, frameworks));

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tailing whitespace should be removed.

But also, why isn't .NETCoreApp,Version=v4.0 compatible with net5? I mean, I know it's not a real .NET Core version, but according to the rule of supporting the same platform identifier with a lower version number, it should be compatible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just to verify that we do, indeed, fail on .NET5+ TFMs with this API. This is one of the IVs APIs that need to be updated or have a sibling API created that supports Platforms.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(the error it checks for is the argument error for trying to do new FrameworkName(nearest.DotNetFrameworkName))

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not a big deal, but I still don't understand.

net4.0 is not compatible with net5.0, but that's because net4.0 == .NETFramework,Version=4.0, whereas net5.0 == .NETCoreApp,Version=5.0. In other words the TargetFrameworkIdentifier (TFI) are different, even though the "short folder name" looks the same.

But when the TFI is the same (.NETCoreApp), version 5.0 should generally be compatible with lower versions, including 4.0.

If net5.0 (.NETCoreApp,Version=5.0) isn't compatible with netcoreapp3.1 (.NETCoreApp,Version=3.1) then we have a bug that will impact customers.

[InlineData("net5.0-watchos", "net5.0", false)]
[InlineData("net5.0-windows", "net5.0", false)]
// net5.0-<osname> is compatible with net5.0 but not the inverse
[InlineData("net5.0-android", "net5.0", true)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as commented elsewhere, are TFMs with a platform identifier, but no platform version even supported/allowed? How can NuGet do compat checks, when we don't know the version?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are indeed supported, and NuGetFrameworks without an OsVersion are perfectly valid -- they're evaluated later based on information we get from the SDK (that's tbd and it's one of the future things to do)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the occasion where the NuGetFramework API needs to support one thing, but restore will actually require the min version :)

Copy link
Member

@zivkan zivkan Jul 7, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't want to delay this PR being merged, but just as Nikolche wrote, what needs to work from a user point of view working with their csproj file is not necesserily the same as what the NuGetFramework class needs to implement.

Another way to look at it: I'm writing a custom desktop app that knows nothing about MSBuild or the .NET SDK, but uses NuGet.Framework from the package on nuget.org. I want to check if two TFMs are compatible. If we don't enforce TPV being mandatory, how can we do the check?

In my opinion, the TPV being optional for csproj restore is something we should handle in the msbuild "goo", but by the time we start using NuGetFramework instances, we always have the version.

@@ -224,6 +226,14 @@ public void NuGetFramework_ProfileName(string folder, string expected)
[InlineData("netcoreapp1.5", ".NETCoreApp,Version=v1.5")]
[InlineData("netcoreapp2.0", ".NetCoreApp,Version=v2.0")]
[InlineData("netcoreapp3.0", ".NetCoreApp,Version=v3.0")]
[InlineData("net5.0-android", "net5.0-android")]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm repeating myself many times :) are net5.0 TFMs with platform identifier, but no platform version allowed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above :)

@@ -186,7 +240,7 @@ public void CreatePackageWithNuspecIncludeExclude()
<group targetFramework="".NETFramework4.5"">
<dependency id=""packageB"" version=""1.0.0"" exclude=""z"" />
</group>
<group targetFramework="".NETFramework4.6"">
<group targetFramework=""net5.0-windows"">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nuspecs should definately never allow a net5.0 TFM with a TPI, but no TPV.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually totally valid! There's going to be a default version provided by the sdk for each major .net version

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that it's valid for a project file (csproj). But in the future when we restore a project targeting net6.0-windows10.0.12345, and the package has binaries for net5.0-windows, we won't know what the default platform version was for windows for net5.0-windows at the time that the package author packed the project.

So, the csproj uses net5.0-windows, but restore gets the default version from the SDK, and writes that in the assets file. I'm not sure if pack reads the project TFMs from the assets file, or if it also needs to get the default platform version from the SDK, but it's resolved at pack time and burned into the nuspec and the package.

Otherwise every year, the SDK will need to pass an array of every previous .net's version default platform versions. Imagine net20 trying to figure out what the net5.0-windows default platform version was.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @zivkan brings out a good point.
Pack needs to hard code the version here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

....or maybe I misunderstood :( I guess this is THE place where we shouldn't allow it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a meeting invite for later today, so I hope we can discuss it more quickly than these comments going back and forth, but @dsplaisted's comment in another issue: NuGet/Home#9444 (comment) appears to be evidence that once NuGet starts reading the 4/5 MSBuild properties, rather than parsing TargetFramework directly, by the time we get to C# code, TPV will always be defined. My understanding is still that the c# class NuGet.Frameworks.NuGetFramework can treat TPV as mandatory when TPI is not the empty string/null.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is still that the c# class NuGet.Frameworks.NuGetFramework can treat TPV as mandatory when TPI is not the empty string/null.

@zivkan I don't think that will work. We're going to be using NuGetFramework to parse out those MSBuild properties from the TargetFramework. For example (from this PR):

    <TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier>
    <TargetPlatformVersion>$([MSBuild]::GetTargetPlatformVersion('$(TargetFramework)', 3))</TargetPlatformVersion>

Those intrinsic MSBuild functions will be implemented by calling the NuGetFramework APIs, so they need to work when you have the platform name but not the version.

You might be able to require that the platform version be set if the platform name is set when doing compatibility checks for a NuGetFramework.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I was reading the SDK's master branch targets file, which does string manipulation, so thought the SDK wouldn't be using our API. This indeed makes things more complex, but for a very good reason.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zkat @nkolev92 is adding a new ParseProjectTargetFramework(string) that allows optional TPV, but all the other Parse* methods keeping it mandatory a horrible idea?

Maybe I should give up on this idea and accept that the rest of our code is going to have higher risk of bugs if we don't check if TPV is defined correctly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently NuGetFramework.Parse("netcoreapp") works and returns an object with TargetFrameworkfVersion=0.0. So maybe this isn't a good suggestion. I feel like I needlessly delayed this PR now.

I think it would be good for the nuspec and nupkg's lib/tfm directory to contain the version 0.0 TPV. It's a signal that something went wrong. I don't know if GetShortFolderName should always return TPV, or if we should limit it to pack. My feeling is always, but I don't know if netcoreapp0.0 or netcoreapp is returned by GetShortFoldName in the example above.

Copy link
Member

@zivkan zivkan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the SDK is going to start using NuGetFramework to parse project's TargetFramework, my hope to have a model that doesn't allow invalid states is not possible. We're just going to have to be careful everywhere we use it (particularly pack, but maybe other places too)

Copy link
Member

@nkolev92 nkolev92 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not expose the IsNet5Era property.

The rest looks fine.

@@ -350,7 +431,7 @@ public bool IsSpecificFramework
/// <summary>
/// True if this framework is Net5 or later, until we invent something new.
/// </summary>
internal bool IsNet5Era { get; set; }
public bool IsNet5Era { get; private set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please keep this internal?

This is a very low level API and I'd like us to keep it as clean as possible.
Personally I'd rather have code duplication than expose a concept that's really a consequence of a business decision.

@zivkan thought?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed that this changed. I agree, we should keep it internal. If we get feedback from the SDK team or others that they hate having to do the TFI & TFV comparisons themselves and duplicating this check, we can add it without it being a breaking change (and also try to come up with a better name)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed :)

@zkat zkat merged commit 1f12139 into dev Jul 10, 2020
@zkat zkat deleted the dev-zkat-net5.0-platforms branch July 10, 2020 18:22
@sfoslund
Copy link

@zkat great to see this has been merged! When can we expect a new build to be promoted?

@rrelyea
Copy link
Contributor

rrelyea commented Jul 10, 2020

this has been merged into our dev branch. We will likely be starting a VS master branch (16.8) insertion early next week. And once that succeeds, then following up with SDK insertions. Feel free to start an internal conversation with @aortiz-msft , @zkat and myself if that timing doesn't work for any reason.

@zivkan
Copy link
Member

zivkan commented Jul 16, 2020

@sfoslund It's in now! dotnet/sdk#12496 🥳

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

net5.0-platform test failures
8 participants