dotnet / sdk

Core functionality needed to create .NET Core projects, that is shared between Visual Studio and CLI
https://dot.net/core
MIT License
2.73k stars 1.07k forks source link

[Proposal] .NET Tools should default to running on the latest available Runtime #30336

Open baronfel opened 1 year ago

baronfel commented 1 year ago

Is your feature request related to a problem? Please describe.

Currently, .NET applications use a runtimeConfig to determine which runtime they run on. This is something that is determined at build time, but non-tool .NET applications can override this with various flags and environment variables given to the .NET runtime host. The default rollforward policy is LatestPatch, which allows an application to run on any runtime of the same major/minor version that is at least as high a version as the application was built against.

.NET (local) Tools are a specific kind of .NET application that are intended to be distributed and managed via SDK commands. They are generally development tools/utilities. We think the semantics of tools are such that tools should be able to run on any .NET runtime equal or greater than the runtime the application originally targeted. This will reduce friction for users adopting new SDKS/Runtimes by not completely blocking their workflows without some explicit proof on incompatibility.

Without this change, tool authors must either explicitly set a more flexible roll-forward policy, or multitarget. Multitargeting has the side effect of increasing the overall size of the tool, and still not solving the issue - a tool that multitargets net6.0 and net7.0 will still not run on net8.0 runtimes.

Describe the solution you'd like

There are two places we could address this mismatch:

Build Time changes

We would change the RollForward of all tools from LatestPatch to LatestMajor. This would cause the desired behavior for all newly-built apps. We would also track a sentinel value that we set the RollForward, so that at install time we could tell if the user explicitly requested a change to the RollForward.

Install Time changes

We would ensure that tools that did not explicitly override to force a certain RollForward behavior would be written with LatestMajor behavior. This helps us plug the existing behavior gap for tools that haven't been authored with an SDK containing these changes.

Additional context

baronfel commented 1 year ago

Pinging @richlander and @vitek-karas because this behavior change would technically limit the flexibility of the runtime to make breaking changes.

vitek-karas commented 1 year ago

I think it would be really unfortunate if this caused use to change the bar on breaking changes in .NET (it doesn't feel important enough to do that). So far we've been pretty specific about developers making an explicit decision to roll forward over major versions.

That said I agree that for the tools this makes a lot of sense. I think we should be very explicit about this - meaning that installing the tool will "force roll-forward" and what are the ways for tool's developers to avoid that. Basically still trying to keep our existing major version compatibility stance, and .NET tools would be an explicit exception to the rule - where developing a tool is in itself an opt-in to this behavioral change.

richlander commented 1 year ago

This other proposal is getting at much of the same thing, but in a different way. I'd rather see us focus on making it easier to move projects forward to new .NET versions. I think this other proposal has less of the issues that Vitek is concerned about.

I also agree that changing our roll-forward policy (for any binaries) is going to cause us pain. I think it is good we have an opt-in, for both producer and consumer of code.

vitek-karas commented 1 year ago

My understanding is that this issue is trying to solve the problem that we have a slew of tools already on NuGet, but when we release 8.0 P1, only a few of them will work (those who added rollforward manually). I agree that it would be really nice to have a majority of the tools working from day 1 (especially since we rarely break people anymore). There's also the fact that some tools are not maintained frequently so it may take a long time (if ever) to get them to run on 8.0)

https://github.com/dotnet/sdk/issues/29949 will only make it easier to test the update (note that I can't publish the tool with that, since it would modify the min supported version). So the two are only related to the extent that latter makes it easier for people to test major versions.

jander-msft commented 1 year ago

As a single data point, if this automatic roll-forward was applied to .NET tools, the dotnet-monitor tool (which is an ASP.NET application that ships as a .NET tool) would likely opt-out of it. There was a change (I believe it was this) between .NET 6 and .NET 7 regarding the NTAuthentication class on which the Microsoft.AspNetCore.Authentication.Negotiate assembly used reflection to access internal APIs. It was changed in .NET 7 in such a way that the Negotiate assembly could no longer lookup the API it was attempting to reflect over. We had to disable roll-forward due to this change.

We're okay with disabling roll-forward because we are actively working on dotnet-monitor and are releasing previews in lock-step with .NET 8 all up.

richlander commented 1 year ago

You are right @vitek-karas. The thing I'm getting at doesn't solve the problem. However, one of the reasons why some people have pushed back on my proposal is compatibility. If we cannot enable an easier source upgrade option, then binary roll-forward is off the table. The two options are on a compatibility spectrum.

richlander commented 1 year ago

Our compat bar is based on the idea that no scenarios auto roll-forward. I think we should keep to that. Otherwise, we probably need to reframe our compat bar, which I believe hasn't happened.

An alternate premise to tools auto rollforward is that users lack sufficient UX to make on-demand rollforward tenable. Said differently, the tools auto roll-forward change is a band-aid change over poor UX. We should fix the UX and hold fixing the experience (by reverting the roll-forward change) until we can invest in changes like the ones proposed as follows.

Another philosophy point is that we've tried to make tools just a opinionated delivery of an app, but everything else is the same. If tools need some improved UX (beyond delivery), it is almost certain that general apps need that too. If we make those more general changes, everyone wins.

I propose we do the following (somewhat of a repeat of the OP).

Roll-forward with a single gesture

When I run a .NET 7 app in a .NET 8 Preview environment, I have to do the following:

export DOTNET_ROLL_FORWARD=LatestMajor
export DOTNET_ROLL_FORWARD_TO_PRERELEASE=1

Note: There are some cases where an app with just LatestMajor will roll-forward to a preview release, I believe, if there is only that one preview release installed. I'm not certain what the algorithm is for that.

I find this requirement to be over the top and requires a lot of extra knowledge of users. I also find that the harm it is saving me from to not be all that compelling.

We have two choices here:

We want installing pre-release versions to be pretty safe, so the first option is a little too scary. That leaves that last option.

The following proposal isn't perfect, but is basically the same with a short suffix. Perhaps someone can come up with a better name.

export DOTNET_ROLL_FORWARD=TestLatest

The idea is select a name that describes the intent and is clearly not prod oriented.

Roll-forward on install

This should be similar for dotnet tool install as dotnet run

dotnet run sort of already has a story for this:

% cat hello-world.csproj | grep Target
    <TargetFramework>net6.0</TargetFramework>
% dotnet run --roll-forward LatestMajor --
Hello, World!
.NET 7.0.8
% cat ./bin/Debug/net6.0/hello-world.runtimeconfig.json
{
  "runtimeOptions": {
    "tfm": "net6.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "6.0.0"
    }
  }
}

The problem with this experience is that it is just affects that one launch, since it is a host function, but doesn't affect runtimeconfig.

The following also doesn't work.

dotnet run /p:RollForward=LatestMajor

However, that same approach works fine with dotnet build.

% dotnet build /p:RollForward=LatestMajor
% cat bin/Debug/net6.0/hello-world.runtimeconfig.json
{
  "runtimeOptions": {
    "tfm": "net6.0",
    "rollForward": "LatestMajor",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "6.0.0"
    }
  }
}
% dotnet build /p:RuntimeFrameworkVersion=8.0.0-preview.5.23280.8   
% cat bin/Debug/net6.0/hello-world.runtimeconfig.json
{
  "runtimeOptions": {
    "tfm": "net6.0",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "8.0.0-preview.5.23280.8"
    }
  }
}

It should work with dotnet run as well.

Clearly, adding <RollForward>LatestMajor</RollForward> to the project file works, but doesn't work as an after-the-fact solution, particularly for tools.

Ideally, the following would work for tools:

dotnet tool install --roll-forward=LatestMajor
dotnet tool install --roll-forward

Note: LatestMajorPre would also be an option here.

The two commands would be equivalent. The intent is that runtimeconfig would be rewritten to include the matching rollForward value.

We could also (additionally) consider shipping a tool for rewriting runtimeconfig as a general helper.

dotnet tool install UX

One of the key issues with dotnet tool install is that the diagnostic UX is pretty bad. This is also what leads people to set roll-forward by default or to multi-target.

This is what happens with a single target non-forward package.

$ dotnet tool install -g Umbraco.Tools.Packages
You can invoke the tool using the following command: UmbPack
Tool 'umbraco.tools.packages' (version '1.0.1') was successfully installed.
$ UmbPack
You must install or update .NET to run this application.

App: /root/.dotnet/tools/UmbPack
Architecture: arm64
Framework: 'Microsoft.NETCore.App', version '3.0.0' (arm64)
.NET location: /usr/share/dotnet

The following frameworks were found:
  7.0.8 at [/usr/share/dotnet/shared/Microsoft.NETCore.App]

Learn about framework resolution:
https://aka.ms/dotnet/app-launch-failed

To install missing framework, download:
https://aka.ms/dotnet-core-applaunch?framework=Microsoft.NETCore.App&framework_version=3.0.0&arch=arm64&rid=debian.11-arm64

The key issue is that there is no feedback at install that the tool will fail to launch. Instead, we should fail installation by default when an app will fail to launch. We probably need a hosting API that takes a runtimeconfig file and produces similar diagnostic information. In particular, we'd want something like the following:

Installation failed.

This app wasn't installed because it won't run on your machine. This can be resolved by one of the following:

1. Install with the `--roll-forward` flag, using with the following command.

dotnet tool install -g --roll-forward footool (this should be copy/pastable)

2. Install .NET 7 (which the tool requires) with the following link

https://aka.ms/dotnet/download/sdk/7

4. Install with `--force` and manually configure the tool to run, using with the following command.

dotnet tool install -g --force  footool

Closing

These proposals would make it easier to maintain our no auto roll-forward policy, while making it easiest for users to roll-forward on demand.

Developers could do the following:

vitek-karas commented 1 year ago

Random comments :-) (this should be a PR so that I can comment on specific sections)

DOTNET_ROLL_FORWARD=LatestMajor If there's only a pre-release version of runtime available, this will roll forward to it without setting the other variable. You only need to set the other variable if you want to use pre-release even though there's a matching release version.

I would be OK introducing DOTNET_ROLL_FORWARD=TestLatest as a shortcut effectively but I think it will have a limited value.

dotnet run /p:Property=Value Unfortunately there's a bug. /p: doesn't work, but -p: does. See for example How gets RunArguments property evaluated? · Issue #32551 · dotnet/sdk (github.com) for a discussion on the topic.

Problem: rollforward on install fixes the old tools on a new machine problem. But it doesn't fix a problem with tools on a new (typically) preview install of SDK. Asking people to reinstall the tools in that case feels wrong. Maybe the env variable is the solution then? Other than the scenario you already described above I think there are two interesting cases: