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

Proposal for platform-specific apps for .NET 8 #29950

Open
richlander opened this issue Jan 15, 2023 · 11 comments
Open

Proposal for platform-specific apps for .NET 8 #29950

richlander opened this issue Jan 15, 2023 · 11 comments
Labels
Area-NetSDK untriaged Request triage from a team member

Comments

@richlander
Copy link
Member

richlander commented Jan 15, 2023

Proposal for platform-specific apps for .NET 8

We've been on a journey to rationalize dotnet build and dotnet publish in terms of producing platform-specific apps and how the CLI exposes a natural progression of app specialization options. The overall product started out without sufficient design on this topic and we've been trying to remedy that ever since. We believe we can finally correct these design challenges with .NET 8.

Notes:

  • build is used a short-hand for build and publish unless specifically called out otherwise.
  • "platform-specific" is used as a synonym for "RID-specific".
  • Intended as a concrete plan for much of Improve RID and publish UX #26446.

Context

Framework-dependent apps have been the default build output type since .NET Core 1.0. That remains the best default in our view. The primary deployment model for .NET is a globally installed runtime, and framework-dependent apps align with that. We should retain this behavior.

In many scenarios, an app will only ever run in one platform environment (like Linux + x64), and developers know that a priori (for example for containers or client apps). There can be performance benefits by taking advantage of that. In addition, apps built for a single environment are simpler (fewer assets and flat directory structure). We should make it easier to produce platform-specific apps, ideally making it the default.

In contrast, in other scenarios, developers benefit from apps being portable across operating system and architectures. That model has been the default to date and is arguably an advanced pseudo-magic behavior. This behavior should be opt-in since it comes with important trade-offs that developers should understand (or that publishing workflows provide as a requirement).

Platform specialization

Developers can specialize an app for a given platform by specifying a Runtime ID (RID) when building an app, with the -r argument, like with:

dotnet build -r linux-x64

Specializing an app with a RID produces a self-contained app. This design choice has always been unfortunate, since RID specialization equally applies to framework-dependent and self-contained apps.

The SDK provides a warning when a RID is specified without any additional arguments related to output type. This warning was added with .NET 6.

root@efbeccd6a45b:/app# dotnet build -r linux-arm64
MSBuild version 17.4.1+9a89d02ff for .NET
  Determining projects to restore...
  Restored /app/app.csproj (in 5.16 sec).
/usr/share/dotnet/sdk/7.0.102/Sdks/Microsoft.NET.Sdk/targets/Microsoft.NET.Sdk.targets(1142,5): warning NETSDK1179: One of '--self-contained' or '--no-self-contained' options are required when '--runtime' is used. [/app/app.csproj]

The intent of this warning was enabling an eventual change in the default behavior when specifying a RID. Given that the warning has been in place for two versions, including an LTS version, we can reasonably now make the change.

Starting with apps that target .NET 8:

  • Building with a RID will produce a framework-dependent app.
  • If --self-contained or --self-contained true is specified, then a self-contained app will be produced. Same when the SelfContained=true.
  • Warning NETSDK1179 will no longer be produced.
  • For earlier versions, We should consider changing the wording of the warning to make clear that the behavior has changed for .NET 8+ apps.

As the current warning suggests, developers should specify --self-contained in response for this change, if they haven't already.

Platform specializing by default

We can further refine this proposal by specializing apps by default. We also need to consider compatibility, and an opt-out to enable producing portable apps.

In fact, there is already a CLI argument that provides this behavior:

root@efbeccd6a45b:/app# dotnet build --help | grep runtime
  --use-current-runtime                Use current runtime as the target 
                                       runtime.

We could add a property with that name, but we can probably do better.

Instead, we propose a TargetRuntime property that pairs with TargetFramework. It will be a convenience wrapper over RuntimeIdentifier, meaning that it will always produce a valid value (including empty string) for that property.

TargetRuntime will allow the following values:

  • portable -- same as specifying no RID and pro
  • current -- same as specifying --use-current-runtime
  • any valid RID

.NET 8 templates will be updated to include this property, set to current, making .NET 8 apps RID-specific by default.

The follow project file demonstrates the change for the console template.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <TargetRuntime>current</TargetRuntime>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

We are aware that there are two variants of "current RID", one short, one long.

The --use-current-runtime flag results in an OS-specific RID, like osx-arm64, while dotnet --info prints a RID with a version, as follows.

% dotnet --info | grep RID
 RID:         osx.13-arm64

That seems odd and asymmetric. Ideally, both variants would be accessible from within a project file and the CLI. More generally, we don't have good names for these different styles of RIDs. We don't need to resolve that for this feature, but it is something that we should separately look into.

Portable apps and apphost

Portable apps include the apphost by default. That doesn't really make sense since the apphost is platform-specific, while the rest of the app is intended to work anywhere. However, it's possible that developers appreciate having an apphost for local dev.

There is no harm to shipping an apphost with a portable app. It is very small and very unlikely to be the source of a vulnerability. However, the platform it supports is arbitrary with respect to the platforms it might run on. As a result, it is hard to describe a general use case (beyond development).

We could adopt the following model:

  • Portable apps do not include an apphost, the same behavior as early .NET Core versions.
  • Developers that want an apphost (the existing behavior) can set UseAppHost=true.
  • Developer that want an apphost only for development can set UseDebugAppHost=true (which would be a new property). An alternative would be UseAppHost=Debug.

As an aside, we notice many Dockerfile examples where apps are launched with the dotnet foo.dll pattern, even those they don't need to. Lauching an app with the dotnet launcher is a fine pattern and should certainly be embraced by portable apps, since it's the only correct cross-platform pattern.

@nagilson @DamianEdwards @baronfel @dsplaisted @elinor-fung

@dotnet-issue-labeler dotnet-issue-labeler bot added Area-NetSDK untriaged Request triage from a team member labels Jan 15, 2023
@nagilson
Copy link
Member

Thanks for scoping this out. We should definitely spend time thinking about the defaults we've set. In the past, you've shown with Publish that the defaults chosen back in the day can certainly be wrong. A few comments regarding this:

For earlier versions, We should consider changing the wording of the warning to make clear that the behavior has changed for .NET 8+ apps.

By this, I assume you mean when building/publishing for TFM below 8 on the 8.0 SDK. Correct me if I'm wrong.
If we do this RID -> No SC change, we should limit the scope of this to NET 8.0+ TFMs IMO.
Before this work is done, I expect us to still need to talk to partners as there are some that likely still depend on RID -> SC. But the warning makes it easier to suggest.

We could add a property with that name

The UseCurrentRuntimeIdentifier property currently exists and does roughly what is spec'd here. Maybe we could make another alias for this.

AppOutputType

I assume this would be undefined if the Output is not Exe. We would have to consider edge cases: certain apps marked as Exe aren't actually Exe and certain apps not marked as Exe are actually Exe.

RE: AppHost Change:

IMO I would want to keep apphost. I think space is less of a premium nowadays, as a developer / user of .NET I would prefer ease of use over saving some MB. It depends on how big the apphost is, which I am unsure of?

RE: Platform specializing default:

I think we should only do this for templates if we do it as you suggested. But personally, I still think I would prefer my app to be portable unless defined otherwise.

@richlander
Copy link
Member Author

For earlier versions, We should consider changing the wording of the warning to make clear that the behavior has changed for .NET 8+ apps.

I'm making the point that the warning message for pre .NET 8 TFM apps can link to a breaking change notice, for example. We would have more information than we did when we first wrote the warning and could share that.

AppOutputType

I'm proposing to replace OutputType for this scenario. In fact, we could make it an error for AppOutputType and OutputType to co-exist.

AppHost change

I'm raising that I don't think that there is an important scenario for the apphost with portable apps, with the exception of development. Do you have scenarios in mind?

But personally, I still think I would prefer my app to be portable unless defined otherwise.

Why is that? I think you are making a value judgement on one set of scenarios over others. Can you elaborate on that?

@nagilson
Copy link
Member

I'm making the point that the warning message for pre .NET 8 TFM apps can link to a breaking change notice, for example.

100%. I also interpret this as the breaking change being TargetFramework specific, which I agree with. 😉

I'm proposing to replace OutputType for this scenario. In fact, we could make it an error for AppOutputType and OutputType to co-exist.

We have made progress on a new spec for this, so dropping this discussion. Excited with the final outcome of what we came up with!

I'm raising that I don't think that there is an important scenario for the apphost with portable apps, with the exception of development. Do you have scenarios in mind?

Nope, I better understand what you mean now. It's unfortunate we didn't get to discuss this today, but it's something we should definitely look at. But this isn't in a state where it's super actionable for us until it's been discussed further. If there are customers who want it / data on how much space it would save, etc, this would be good to add.

Why is that? I think you are making a value judgement on one set of scenarios over others. Can you elaborate on that?

That is true. This feedback was mainly targeted toward the concept of making dotnet RID specific by default, which after discussion we decided against. I am quite the advocate for the TargetRuntime change though!

@richlander
Copy link
Member Author

richlander commented Jan 20, 2023

The portable apphost change isn't about space saving. It's about defining the scenario for the apphost (for portable apps). I claim that it is very narrow and that having it confuses things. If it wasn't there, it would help people better understand what portable apps are for.

Perhaps it isn't obvious, but the apphost is NOT portable.

@elinor-fung
Copy link
Member

To make sure I'm understanding this correctly, these would all be distinct breaking changes for apps targeting .NET 8 here, right?

  1. Specifying a RID no longer implies self-contained when self-contained is not specified (yay!)
  2. Applications are platform-specific by default
  3. Portable / non-platform-specific apps don't produce an apphost

Instead, we propose a TargetRuntime property that pairs with TargetFramework. It will be a convenience wrapper over RuntimeIdentifier, meaning that it will always produce a valid value (including empty string) for that property.

Can we not let RuntimeIdentifier understand new values? Runtime is pretty overloaded and my initial interpretation of TargetRuntime is mono vs coreclr rather than the RID, especially we have a UseMonoRuntime property. I also think the interactions between similar properties tends to be confusing - for example, if I'm upgrading to 8.0, should I be changing RuntimeIdentifier -> TargetRuntime, which one wins / do we block/validate setting both, is there a RuntimeIdentifiers equivalent.

I'm raising that I don't think that there is an important scenario for the apphost with portable apps, with the exception of development.

I think development is a large one. If we do this, it might make sense to default to keeping it for development scenarios rather than requiring users to se a new property.

@richlander
Copy link
Member Author

Runtime is definitely overloaded. We were unable to come up with a better name or really a better name that didn't also contribute to more confusion.

We're not planning on changing the underlying default (at least not now) so we're reliant on a new property in .NET 8 templates. Many users don't real with these low-level concepts today.

There were two points against RuntimeIdentifier:

  • The new values are not RIDs, making our new default use of the property a bit odd.
  • The property name is scary.

We chose TargetRuntime since it matches TargetFramework. People understand that they need to set that. Runtime and Framework so close together is admittedly odd. I wish we'd taken a bit more time with some of these naming choices in v1.0.

A lot of this is "least bad design". We concluded that the proposed plan is "least bad."

The rules we had in mind:

  • TargetRuntime and RuntimeIdentifier cannot co-exist.
  • TargetRuntime and RuntimeIdentifiers cannot co-exist. RuntimeIdentifier and RuntimeIdentifiers can co-exist today. There is one scenario where that is useful. However, we should probably not carry that forward with a new property.

UseMonoRuntime is important, however, there is likely limited overlap between these two scenarios. AFAIK, we use Mono exclusively for RID-specific scenarios.

Yes, the development time scenario for an apphost for portable apps is compelling. It's called out and I think enabling that some way is a good idea.

@nagilson
Copy link
Member

nagilson commented May 8, 2023

The platform specialization part of this has been completed in #30038 :)

@reflectronic
Copy link

Hey, it's not really clear to me what the plan is anymore, could someone clarify? Is RID-specific going to become the default for dotnet build?

@DamianEdwards
Copy link
Member

@reflectronic my understanding was that it was not, at least in .NET 8. IIRC last time this was discussed it was suggested that this be revisited very early in .NET 9 (e.g. preview 1) to ensure any associated impact can be better assessed and mitigated.

@nagilson
Copy link
Member

Damian's recollection is correct! Though the idea of doing it in .NET 9 is very up in the air.

@Nirmal4G
Copy link
Contributor

Nirmal4G commented Aug 1, 2024

I don't want to say, I told you so! yet here I say anyways!!

This is exactly what I proposed when we moved to .NET 5 with a new TF! Here's my comment and examples using TargetRuntime in the proposal.

We need multiple frameworks either platform-specific (Windows/Mac/Linux) or runtime-specific (Android/iOS) or workload-specific (Desktop/Server/Web) and a standard library with a common API surface across them. We already had it but it was poorly implemented and eventually abandoned.

I see that we are going down the same path once again but this time we should learn from past transgressions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-NetSDK untriaged Request triage from a team member
Projects
None yet
Development

No branches or pull requests

6 participants