sdk icon indicating copy to clipboard operation
sdk copied to clipboard

Missing Linux `TargetFrameworkMoniker`

Open hansmbakker opened this issue 1 year ago • 1 comments

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

I am trying to create a cross-platform project with platform-specific implementations of several classes.

For most platforms, it would be possible to use the TargetFrameworkMoniker to conditionally include files like MAUI does (see e.g. https://github.com/dotnet/maui/blob/main/src/Essentials/src/Essentials.csproj). However, no TargetFrameworkMoniker is available for Linux.

This currently being unavailable also has the consequence that APIs like https://github.com/dotnet/runtime/pull/69980/files need an [UnsupportedPlatform("windows")] attribute rather than only making the APIs available on Unix OSes.

Describe the solution you'd like

  • A net7.0-linux TargetFrameworkMoniker

  • alternative considered: OperatingSystem.IsLinux() - this relies on runtime analysis and makes methods larger or requires a wrapper that returns a platform-specific implementation at runtime.

  • alternative considered: using inverse #if to assume that

    if it is not Windows, iOS, Android, MacOS - it must be Linux

    Pseudocode: #if !(WINDOWS || MACOS || IOS || ANDROID). This seems brittle.

Additional context

These pages have no pointers about workarounds for this case:

  • https://docs.microsoft.com/en-us/dotnet/standard/frameworks
  • https://docs.microsoft.com/en-us/dotnet/core/tutorials/libraries#how-to-multitarget

hansmbakker avatar Aug 09 '22 21:08 hansmbakker

Here's the reasoning behind that from the original design:

Why is there no TFM for Linux?

The primary reason for OS specific TFMs is to vary API surface, not for varying behavior. RIDs allow varying behavior and have support for various Linux flavors. Specifically, TFMs aren't (primarily) meant to allow calling P/Invokes under #if, most of the time that should be done by doing runtime checks or by using RIDs. The primary reason for a TFM is to exclude large amounts of managed representations for OS technologies (WinForms, WPF, Apple's NS APIs, Android etc).

Also, Android, iOS, macOS, and Windows share that they offer a stable ABI so that exchanging binaries makes sense. Linux is too generic of a concept for that, it's basically just the kernel, which again boils down to the only thing you can do is calling P/Invokes.

@terrajobst @mhutch Should we reconsider having a linux TargetFramework since we're adding some Linux-specific APIs? How many Linux-specific APIs like this do we have or do we expect to add?

dsplaisted avatar Sep 21 '22 21:09 dsplaisted

@terrajobst @mhutch could you please share your thoughts on this one? Having this functionality would help make my code a lot cleaner.

hansmbakker avatar Nov 19 '22 21:11 hansmbakker

I think what's actually missing here is the ability to compile for multiple RIDs.

TFMs are about the APIs that are available for your code to compile against, RIDs are about what platform/architecture your code runs on. If your implementation is specific to the platform that you run on, then should be a RID-specific implementation. However, we currently have no tooling for compiling a project for multiple RIDs, and packages that contain multiple RID-specific implementations currently must be constructed from the output of multiple projects. The lack of multi-RID support has les to folks using TFMs for this purpose instead.

We have two mechanisms for platform-specific APIs. We can put them in a platform-specific TFM, which is a good solution when exposing a large feature "area" such as an app model that is tightly bound to a single platform. Alternatively, we can make the API available on all platforms but only provide implementations on a subset of platforms. In this case, we use annotations and analyzers to indicate to the developer when they may need runtime checks in their code before calling the API. This is a lighter weight solution as it does not force consuming code to have platform-specific builds, and is great for smaller scale platform-specific feature such as a individual method to set a platform-specific option on a cross-platform feature. There is of course a continuum between these extremes and there's not necessarily a "right" answer to which solution should be used. However, we do not yet have any Linux-specific APIs where a TFM would clearly be the better of the two options, which is why we don't have the TFM (yet).

mhutch avatar Nov 23 '22 02:11 mhutch

@mhutch What would help me a lot is guidance at https://learn.microsoft.com/en-us/dotnet/core/tutorials/libraries#how-to-multitarget or https://learn.microsoft.com/en-us/dotnet/standard/library-guidance/cross-platform-targeting?source=recommendations where it is explained how to create multitargeted code that also has a condition for Linux. If tooling support for that is missing then please consider this issue as a request for that.

My idea was a TFM since it fits the idea of "platforms" and automatically handles dependencies but I see now that the concept of RIDs is more correct.

The current alternative of requiring separate Linux and non-Linux NuGet packages is a pain as there is no tooling/compile time-supported metadata I can add to my library.

hansmbakker avatar Dec 02 '22 15:12 hansmbakker

And I found MSBuild.Sdk.Extras which might help with building for RIDs but dotnet build does not support it, and it seems not to be maintained anymore.

Better support from Microsoft for multi-targeting with Linux, built-in into the dotnet sdk, would be great!

hansmbakker avatar Dec 15 '22 19:12 hansmbakker

This feature definitely needs more attention, as it is making it unreasonable difficult to work on multi-target libraries and applications.

Note, this issue and my comment is not about having platform specific API build-in, but about creating cross-platform projects that achieve these APIs by themselves or referencing platform specific packages.

While in theory I would agree with @mhutch and the original design idea, it doesn't seem to reflect how Target Frameworks are used in reality.

For instance, MAUI supports multiple target frameworks - Windows, Mac Catalyst, Android, iOS, and Tizen. If a developer needs to create a library for MAUI and use native APIs, they need to target these frameworks. If a developer needs to create a single-project that will work on all of these frameworks - they can use multitargeting and include all of these targets in the same project.

It works pretty well until you want to extend the list of available frameworks. While it's not a problem in MAUI (yet), it is already a problem in frameworks like Avalonia and Uno. We support Linux and browser targets as well. Which means we already cannot treat plain "net6.0" as Linux, because it can be a browser as well. As a reference, this PR is blocked because of missing TFM support.

In the end, we have a mix of:

  • Android and iOS which can only be differentiated by TFM. And you can't easily run an application without such TFM.
  • Linux and Browser which can only be differentiated by RID as there is no TFM
  • Windows and Mac which has optional TFM with a set of platform-specific APIs like Android/iOS, but it also works and runs without any and has decent IDE support. Primarily because .NET Core was working fine on these platforms before ".NET 5" TFMs were introduced.

In order to support multiple platforms, developers now need to have a mix of TFM with #if/else directives for the well-supported platforms and OperatingSystem.IsLinux/IsBrowser runtime calls for "net6.0" (i.e. "other"). Not to mention, it gets complicated with NuGet packages that need to be somehow referenced from the "net6.0" for both Linux and browser (and without browser AOT compiled rightfully complaining about it).

If the .NET ecosystem switches from TFM to RID - that's fine, but it seems to require way more changes including availability of platform-specific APIs.

As an example, there is an interesting project that has to build their own workload with custom TFM in order to achieve expected behavior.

maxkatz6 avatar Jan 25 '23 07:01 maxkatz6

@dsplaisted we now have a target framework for Browser platform, which I am glad to see. It is now possible to at least dedicate a plain "net8.0" target (without any platform moniker) to Linux. But other than that, are there any plans to introduce net9.0-linux?

While one can argue, that it's hard to define "Linux" as a platform, .NET ecosystem is already built around "Linux" keyword defining a specific sub-class of Operating Systems at least.

maxkatz6 avatar Dec 11 '23 04:12 maxkatz6

No, as far as I know we still don't have any plans to add a linux platform to the TargetFramework.

We are hoping to eventually add support to allow you to multi-target a project over things that are not the target frameworks that we define: https://github.com/NuGet/Home/pull/12124. This would allow you to build a project twice for .NET 6 via multi-targeting over custom TargetFrameworks that you would define. That would probably help with building projects like this, but this wouldn't flow through NuGet packages so it might not help much with the ecosystem side.

@terrajobst FYI

dsplaisted avatar Dec 11 '23 22:12 dsplaisted

@dsplaisted thank you for sharing this spec. Looks promissing.

The latest use case I found where we need "net8.0-linux", is MAUI Essentials project, Which has compile-time implementations for different essential APIs like FilePicker. And atm if somebody decided to add Linux support there, they would have to do runtime checks, combined with compile-time checks of other platforms.

From what I understood from the spec you mentioned, it should be possible to create "net9.0-linux" as an alias for "net9.0", but with a correct RID and put compiled lib to the runtimes/linux/net9.0 folder of the nuget package. So it should be picked up properly when user builds the app with "-r linux-x64".

maxkatz6 avatar Dec 12 '23 02:12 maxkatz6

From what I understood from the spec you mentioned, it should be possible to create "net9.0-linux" as an alias for "net9.0", but with a correct RID and put compiled lib to the runtimes/linux/net9.0 folder of the nuget package. So it should be picked up properly when user builds the app with "-r linux-x64".

Yes, I think this will be possible. Note that we're not initially planning any built-in support for creating NuGet packages in a situation like this. Likely you would have to set some properties in the project file to tell the pack package to ignore the default handling for net9.0-linux, and add custom items/metadata to tell it where to put those files.

Also note that we are making some progress but this isn't currently committed for a specific release. It's possible it could end up in .NET 9 but we don't know yet.

dsplaisted avatar Dec 12 '23 14:12 dsplaisted