Description
[breaking] dotnet build/publish produces RID-specific apps by default
Proposal: dotnet build
(and friends) produce RID-specific apps by default. This change results in apps that are smaller, simpler, faster to startup, and more reliable.
Proposal: any
is the new RID for portable apps, since they will no longer be the default.
Example:
PS C:\Users\rich\app3> dotnet add package SkiaSharp
PS C:\Users\rich\app3> dotnet build
PS C:\Users\rich\app3> Get-ChildItem .\bin\Debug\ -Recurse | Measure-Object -Sum Length | Select-Object Sum
Sum
---
31754654
PS C:\Users\rich\app3> Get-ChildItem .\bin\Debug\*Skia* -Recurse | Select-Object Name, Length, Directory
Name Length Directory
---- ------ ---------
libSkiaSharp.dylib 8495552 C:\Users\rich\app3\bin\Debug\net6.0\runtimes\osx\native
libSkiaSharp.dll 7177104 C:\Users\rich\app3\bin\Debug\net6.0\runtimes\win-arm64\native
libSkiaSharp.dll 8413072 C:\Users\rich\app3\bin\Debug\net6.0\runtimes\win-x64\native
libSkiaSharp.dll 7081872 C:\Users\rich\app3\bin\Debug\net6.0\runtimes\win-x86\native
SkiaSharp.dll 414096 C:\Users\rich\app3\bin\Debug\net6.0
PS C:\Users\rich\app3> Remove-Item -Recurse bin
PS C:\Users\rich\app3> dotnet build -a x64
PS C:\Users\rich\app3> Get-ChildItem .\bin\Debug\ -Recurse | Measure-Object -Sum Length | Select-Object Sum
Sum
---
8998050
PS C:\Users\rich\app3> Get-ChildItem .\bin\Debug\*Skia* -Recurse | Select-Object Name, Length, Directory
Name Length Directory
---- ------ ---------
libSkiaSharp.dll 8413072 C:\Users\rich\app3\bin\Debug\net6.0\win-x64
SkiaSharp.dll 414096 C:\Users\rich\app3\bin\Debug\net6.0\win-x64
This app, with a SkiaSharp dependency, is 31MB by default (with the existing behavior) and ~9MB with the proposed behavior. That's a significant benefit. If you have multiple RID-specific dependencies, then the benefit could be higher.
Related: #23539
Context
.NET Core 1.0 started with the concept of both portable and RID-specific apps. We thought of portable apps as being matched with framework-dependent deployment and RID-specific apps being matched with self-contained deployment. That made good sense at the time. The idea is that portable apps could run on any runtime you paired the app with, and self-contained apps could only run in the specific environment that they were published for. Also, self-contained apps were the only apps with executables. Framework dependent. It wasn't until .NET Core 3.0 that framework dependent apps got native executables by default. We now have a platform that has a diverse set of application offerings, which bias to portable or RID-specific apps being preferred. It's worth re-considering which of the two models should be the default.
Portable apps only make sense if the following characteristics are all true:
- App is deployed as framework-dependent.
- App doesn't require a native executable for the desired UX, and can be launched with the
dotnet MyApp.dll
pattern. - Size isn't a primary performance metric.
Client apps don't satisfy those characteristics:
- Desktop client apps require an executable.
- Mobile apps are self-contained.
- Wasm apps are self-contained.
Server apps are mixed:
- For container apps, RID-specific is best since size is a primary performance metric, and because the app will only ever be launched one way. The flexibility offered by portable apps doesn't apply.
- Apps that are developed and built on one environment (like Windows x64) and uploaded as binaries to a cloud service and then run in another environment (like Linux Arm64) are the poster-child for portable apps.
There is also the scenario where a dependency may have a security issue (CVE) on one platform and not another. It would be nice if you didn't have to service a Linux binary on Windows (or vice versa) just to satisfy compliance rules.
In looking at the landscape, it becomes obvious that most scenarios benefit significantly from RID-specific deployment (either framework-dependent or self-contained) and that only niche scenarios benefit from portable apps. As a result, we should switch to RID-specific builds as the new SDK default.
Note: It turns out we already did this in very early .NET 7, but it wasn't quite complete: