Runtime: Move appropriate parts of Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment into the shared framework

Created on 11 Jul 2018  路  97Comments  路  Source: dotnet/runtime

Rationale

It is often the case, when working with .NET Core that you want to do something based on the current platform/architecture. In many cases, this can include needing to interop with tools published using a particular "runtime identifier".

Today, the RuntimeIdentifier is only exposed through the Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.GetRuntimeIdentifier() API and is not exposed as part of CoreFX. This requires referencing an additional package, and prevents it from being used in certain scenarios (such as evaluation time in MSBuild).

As such, I propose that we expose a mechanism to get the RID for the current execution environment at runtime.

Proposed API

```C#

nullable enabled

namespace System.Runtime.InteropServices
{
public static class RuntimeInformation
{
// The current OS RID, e.g.: win7-x64, osx.10.11-x64, ubuntu.18.04-arm64, rhel.7-x64
public static string RuntimeIdentifier { get; }
}
}
```

The above API will return the value of a new AppContext variable RUNTIME_IDENTIFIER. This variable will be passed by the dotnet.exe host, and can be passed by any other native host when the runtime is loaded.

DISCUSSION: Should we still maintain a managed fallback code path for when this AppContext variable isn't present? For example if the app was loaded from a different native host that didn't set this variable.

Additional Notes

This would allow switching on the active RID from an MSBuild project/props/targets file, which would allow greater versatility in consuming native tools from a package during build time (or from a user program at runtime).

It may be worth reviewing the other APIs exposed via Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment and determining which others, if any, should also be exposed: https://github.com/dotnet/core-setup/blob/master/src/managed/Microsoft.DotNet.PlatformAbstractions/RuntimeEnvironment.cs

Updates

  • eerhardt: Add proposal for OperatingSystem and OperatingSystemVersion so Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment can be completely removed/deprecated/etc.

  • vihofer: Choosing _OS_ over _OperatingSystem_ as we already use _OS_ in IsOSPlatform. OperatingSystemPlatform is already accessible through IsOSPlatform(Platform). RuntimeArchitecture is already exposed as ProcessArchitecture. The RuntimeVersions proposal is already covered by dotnet/coreclr#22664 and doesn't require an api review.

  • eerhardt: Remove proposal for OSName and OSVersion, we will drop support for these APIs. Callers can instead call Ntdll.RtlGetVersionEx on Windows, or read /etc/os-release on Unix.
    Change RuntimeIdentifier from a property to a method call, since the proposed behavior will invoke a method: AppContext.GetData(), which isn't appropriate for a property.

api-approved area-System.Runtime

Most helpful comment

I don't think it is a great idea for code to take functionality dependency based on reported OS version at runtime either. But the fact of the matter is, it happens. The RID has the OS version inside of it. So anything you do with the RID will now mean you are dependent on the reported OS version.

We've been down the "do we need the OS Version public API?" path before, and it wasn't added as a "corefx" API previously. And that decision is what led to the creation of the PlatformAbstractions library in the first place. So whether we like it or not, we already have this public API. The issue is the API lives outside of corefx, and outside the "typical" place where people look for this type of information - the RuntimeInformation class. If all the existing callers of RuntimeEnvironment.OperatingSystemVersion can be convinced that they actually don't need to call it, then I think it can be removed.

Here are other places where people are asking for OS version:

https://github.com/dotnet/corefx/issues/16629
https://github.com/dotnet/corefx/issues/29395
https://github.com/dotnet/corefx/issues/8099
https://github.com/dotnet/corefx/issues/4741
https://github.com/dotnet/corefx/issues/12737
https://github.com/dotnet/corefx/issues/9011
https://github.com/dotnet/corefx/issues/1017
https://github.com/dotnet/cli/issues/2754

(note this isn't an exhaustive list, I stopped looking as I think the point is made)

The main usage of OperatingSystemVersion is logging/reporting types of scenarios. Here are the 2 places in the dotnet/cli that call RuntimeEnvironment.OperatingSystemVersion:

I know that we already have Environment.OSVersion. However, Environment.OSVersion is not implemented the same way as RuntimeEnvironment.OperatingSystemVersion. RuntimeEnvironment.OperatingSystemVersion will give you the version part of the RID, which comes from /etc/os-release and then also has some special logic to "normalize" the version on alpine and RHEL. Whereas Environment.OSVersion gets the version from calling uname and returning the .release portion directly.

Lastly, I think these methods/properties (GetRuntimeIdentifier(), OperatingSystem, OperatingSystemVersion) need to live together since they need to be consistent. If the RID says it is rhel.7-x64, then the OperatingSystemVersion needs to return 7. And getting the RID from corefx's RuntimeInformation, but needing to go to PlatformAbstraction's RuntimeEnvironment to get the OperatingSystemVersion doesn't make any sense. If we are going to move the API to get the current machine's RID into RuntimeInformation, then PlatformAbstraction's RuntimeEnvironment should be removed.

p.s.
On the topic of using reported versions in code, I think this comment from @terrajobst is relevant

Working around implementation level issues. Sometimes you need to work around specific behavior that is displayed by specific platform versions.

Version APIs are problematic. Believe me, we know very well how version checks are fragile. We've tried several times to remove version checks from our API surface. Be it in .NET Core or in Windows. But the reality is that some problems are just too hard to do reliably -- regardless of the approach. Giving developers the ability to perform version checks as the ultimate escape hatch is a sensible compromise. There is a reason why we don't want to make this API super discoverable and also turn the logic inside out, i.e. let the developer pass in the value they care about so we can do the version checking for them.

All 97 comments

FYI. @eerhardt

@nguerrera, @KathleenDollard, @livarcocc, @dsplaisted. I know there has been a similar request on the SDK/CLI team before

We already have other related issues like https://github.com/dotnet/corefx/issues/28620.

Is just getting the current RID helpful? Or do you usually also need to find a best match from a list of RIDs you have resources available for, using the RID graph?

Is just getting the current RID helpful? Or do you usually also need to find a best match from a list of RIDs you have resources available for, using the RID graph?

Yes, getting the best match from a list of RIDs is even better. Not sure how closely tied that is to NuGet, however.

We already have other related issues like dotnet/runtime#25681.

@weshaggard, right. But I didn't see anything that looked like the "recommended" API proposal layout and that covered the RuntimeIdentifier.

It may be worth reviewing the other APIs exposed via Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment and determining which others, if any, should also be exposed: https://github.com/dotnet/core-setup/blob/master/src/managed/Microsoft.DotNet.PlatformAbstractions/RuntimeEnvironment.cs

See my comments here: https://github.com/dotnet/corefx/issues/28620#issuecomment-377421773, copied below for ease in reading:

@eerhardt what is your thoughts on bring Microsoft.DotNet.PlatformAbstractions into corefx?

I would absolutely LOVE for this to happen.

However, I don't think it should go wholesale into corefx. I think pieces of it should be removed altogether (I'm looking at you HashCodeCombiner and Platform).

And some things should be folded into System.Runtime.InteropServices.RuntimeInformation, like RuntimeEnvironment.

ApplicationEnvironment can probably be deleted as well now that we have netstandard2.0. I'm not exactly sure what it's main purpose was - mostly for net45 and netstandard1.3 code to be able to call 1 method to get the BaseDirectory without having to #if themselves. With netstandard2.0, users can now call AppContext.BaseDirectory, or AppDomain.CurrentDomain.BaseDirectory for this.

So thinking about it more - probably move RuntimeEnvironment into RuntimeInformation, and delete the rest. Obviously the library will still live in servicing branches, if we ever need to service the already released versions of it. And users can continue to use the already released versions all they want.


The other public members of RuntimeEnvironment:

```C#
public static Platform OperatingSystemPlatform { get; } = PlatformApis.GetOSPlatform();

    public static string OperatingSystemVersion { get; } = PlatformApis.GetOSVersion();

    public static string OperatingSystem { get; } = PlatformApis.GetOSName();

    public static string RuntimeArchitecture { get; } = GetArch();

```

The following properties aren't already exposed in corefx:

  • OperatingSystem should be moved to RuntimeInformation.
  • OperatingSystemVersion should be moved to RuntimeInformation.

The following properties are dupes of corefx APIs:

  • OperatingSystemPlatform can be removed since we already have OSPlatform.
  • RuntimeArchitecture can be removed since we already have RuntimeInformation.ProcessArchitecture.

One question I have about the RID API:

Today, we have an environment variable that allows someone to set the RID:

C# private static readonly string OverrideEnvironmentVariableName = "DOTNET_RUNTIME_ID";

Do we plan on preserving this behavior with the proposed API? And if we do, would it make more sense for the API to be a method instead of a property? Since it would always need to check the env var.

Do we plan on preserving this behavior with the proposed API? And if we do, would it make more sense for the API to be a method instead of a property? Since it would always need to check the env var.

I think a method makes sense, if the behavior is preserved. Otherwise, a property seems the better choice.

In general I don't like the idea of applications taking functionality dependency based on reported OS version at runtime: there is a history of broken features because the OS can fake its answers, or later starts to support some feature and the app then requires recompilation. If possible testing/trying the feature itself is the preferred way. That said decisions based on broad categories (e.g.: Windows vs Unix) or more fine grained for tests, not production, are reasonable. Of course using RID info is totally fine in the context of building/packaging/etc.

Do we really need this as a public API? Can we instead bring it down to some place that provides the gains for building/packaging without exposing a public API?

I don't think it is a great idea for code to take functionality dependency based on reported OS version at runtime either. But the fact of the matter is, it happens. The RID has the OS version inside of it. So anything you do with the RID will now mean you are dependent on the reported OS version.

We've been down the "do we need the OS Version public API?" path before, and it wasn't added as a "corefx" API previously. And that decision is what led to the creation of the PlatformAbstractions library in the first place. So whether we like it or not, we already have this public API. The issue is the API lives outside of corefx, and outside the "typical" place where people look for this type of information - the RuntimeInformation class. If all the existing callers of RuntimeEnvironment.OperatingSystemVersion can be convinced that they actually don't need to call it, then I think it can be removed.

Here are other places where people are asking for OS version:

https://github.com/dotnet/corefx/issues/16629
https://github.com/dotnet/corefx/issues/29395
https://github.com/dotnet/corefx/issues/8099
https://github.com/dotnet/corefx/issues/4741
https://github.com/dotnet/corefx/issues/12737
https://github.com/dotnet/corefx/issues/9011
https://github.com/dotnet/corefx/issues/1017
https://github.com/dotnet/cli/issues/2754

(note this isn't an exhaustive list, I stopped looking as I think the point is made)

The main usage of OperatingSystemVersion is logging/reporting types of scenarios. Here are the 2 places in the dotnet/cli that call RuntimeEnvironment.OperatingSystemVersion:

I know that we already have Environment.OSVersion. However, Environment.OSVersion is not implemented the same way as RuntimeEnvironment.OperatingSystemVersion. RuntimeEnvironment.OperatingSystemVersion will give you the version part of the RID, which comes from /etc/os-release and then also has some special logic to "normalize" the version on alpine and RHEL. Whereas Environment.OSVersion gets the version from calling uname and returning the .release portion directly.

Lastly, I think these methods/properties (GetRuntimeIdentifier(), OperatingSystem, OperatingSystemVersion) need to live together since they need to be consistent. If the RID says it is rhel.7-x64, then the OperatingSystemVersion needs to return 7. And getting the RID from corefx's RuntimeInformation, but needing to go to PlatformAbstraction's RuntimeEnvironment to get the OperatingSystemVersion doesn't make any sense. If we are going to move the API to get the current machine's RID into RuntimeInformation, then PlatformAbstraction's RuntimeEnvironment should be removed.

p.s.
On the topic of using reported versions in code, I think this comment from @terrajobst is relevant

Working around implementation level issues. Sometimes you need to work around specific behavior that is displayed by specific platform versions.

Version APIs are problematic. Believe me, we know very well how version checks are fragile. We've tried several times to remove version checks from our API surface. Be it in .NET Core or in Windows. But the reality is that some problems are just too hard to do reliably -- regardless of the approach. Giving developers the ability to perform version checks as the ultimate escape hatch is a sensible compromise. There is a reason why we don't want to make this API super discoverable and also turn the logic inside out, i.e. let the developer pass in the value they care about so we can do the version checking for them.

Thanks for digging the history and showing that this was considered many times before :smile: you convinced me.

I've added to the proposal to include OperatingSystem, and OperatingSystemVersion strings. However, thinking more about this, would it make sense to create a new type that encapsulated the RID APIs? ex.

```C#
public struct RuntimeIdentifier
{
public string Id { get; }
public string OperatingSystem { get; }
public string OperatingSystemVersion { get; }
}

or potentially just a completely new `static class` that contained `RID` specific information:

```C#
public static class RuntimeIdentifier
{
    public static string GetCurrent();
    public static string OperatingSystem { get; }
    public static string OperatingSystemVersion { get; }
}

This would convey that the OperatingSystem and OperatingSystemVersion strings are directly tied to the RID's concept of what OperatingSystem and OperatingSystemVersion are. And it would be understandable why they are different than say Environment.OSVersion, or RuntimeInformation.IsOSPlatform (ex. RuntimeInformation.IsOSPlatform(OSPlatform.Create("ubuntu")) returns false even when running on ubuntu.)

I think it might make some since to have a type for RuntimeIdentifier if we plan to support other scenarios like checking compatibility with other RIDs, if we only care about getting the current RID then I'd stick with a static class.

I do think we need to be a little careful about making assumptions about the structure of a RID. While it is true it generally contains an OS and version it isn't always the case as a RID is just an string. It also commonly contains the architecture so if you are breaking it apart you may want to add that value as well.

ex. RuntimeInformation.IsOSPlatform(OSPlatform.Create("ubuntu")) returns false even when running on ubuntu

We should consider actually fixing that if we can someone embed the RID graph we could use that information to check these different hierarchies.

This is something that would be useful, especially when writing bindings for other native libraries. Has there been any update on this?

Updated the api proposal and changed the label to api-ready-for-review.

OperatingSystemPlatform is already accessible through IsOSPlatform(Platform)

Is there any API that returns the OSPlatform object representing current platform?

For example an API that could expose the value of s_osPlatformName from https://github.com/dotnet/corefx/blob/a10890f/src/System.Runtime.InteropServices.RuntimeInformation/src/System/Runtime/InteropServices/RuntimeInformation/RuntimeInformation.Unix.cs

public static string RuntimeIdentifier { get; }

What do we expect this API to be used for? Is this API useful without the library that allows you to reason about the RID graph?

The RIDs come with a lot of problems because of they form a graph. You cannot really do anything useful with the RID without having the library that is able to deal with the RID graph.

Is there any API that returns the OSPlatform object representing current platform?

@ViktorHofer Is that basically the proposed OSName API, or is OSName something else from OSPlatform? Could you please update the proposal with representative set of examples of values that each of the APIs return to make it clear what they do?

The move of Microsoft.DotNet.PlatformAbstractions out of dotnet/core-setup, which partly motivates this API, is tracked in https://github.com/dotnet/core-setup/issues/5213

The GetRuntimeIdentifier() in Microsoft.DotNet.PlatformAbstractions does not return what the calling code wants some of time, and it has a hack that lets you override to returned value via Environment variable to componsate for that. If we add this API to CoreFX, do we expect to keep this hack? (It comes down to what we expect this API to be actually used for... .)

Is that basically the proposed OSName API, or is OSName something else from OSPlatform?

No, that is not the proposed OSName.

The s_osPlatformName is something like Linux, OSX, FreeBSD.

The proposed OSName is something like ubuntu, rhel, centos, Mac OS X, FreeBSD. On Linux systems, this is the ID field in /etc/os-release (or /etc/readhat-release). Typically, it is the first part in the RuntimeIdentifier string for Linux systems.

public static string RuntimeIdentifier { get; }

What do we expect this API to be used for? Is this API useful without the library that allows you to reason about the RID graph?

We have it today in Microsoft.DotNet.PlatformAbstractions, and it is useful. There is a Rationale at the top of this issue. There are also numerous linked issues to this issue, here is one of them:

Any managed library (such as msbuild task) that loads native library needs to know RID. This need came up recently in libgit2sharp. We should add GetRuntimeId method.

Here is ASP.NET's build system calling it.

The CLI uses it to print it out during --info, so the user knows what .NET thinks is the current RID of the machine.

Today, you can reason about the RID graph by doing something similar to what the CLI does here.

We can add more "RID-graph reasoning" APIs in the future, if necessary. But starting with "what is the current machine's RID?" seems like a good starting point.

The GetRuntimeIdentifier() in Microsoft.DotNet.PlatformAbstractions does not return what the calling code wants some of time

It has to return exactly what the ".NET runtime" thinks is the RID of the machine. This API and the dotnet host must always be in sync, or else there will be problems.

it has a hack that lets you override to returned value via Environment variable to componsate for that

That "hack" has proven useful very often. Just recently, it was proven useful because we didn't have alpine 3.8 in the RID graph.

Here is ASP.NET's build system calling it.

This is old code in a private repo. I do not see it in live ASP.NET Core. Also, it is a great example that shows why the RID alone is useless. It has ad-hoc list of cases to compensate for when the RID is not what you want.

Should the API to give you best guess about the current RID be rather part of the RID graph reasoning library?

Sorry, that was a bad link to the ASP.NET build system. Here is the real link: https://github.com/aspnet/BuildTools/blob/b7b88d08d55abc8b71de9abf16e26fc713e332cd/src/ApiCheck.Console/NuGet/RuntimeGraph.cs#L93

Here is dotnet/arcade calling it: https://github.com/dotnet/arcade/blob/2d52dfb5fca30c0e64454ce01a1e9c81d7e75989/src/Microsoft.DotNet.Build.Tasks.Configuration/src/GenerateConfigurationProps.cs#L137

Should the API to give you best guess about the current RID be rather part of the RID graph reasoning library?

No, I believe it needs to be sync'd with what the runtime thinks is the current RID. Again, if these are different values (what the runtime thinks, and what this API produces) we are going to have problems.

sync'd with what the runtime thinks is the current RID
If the RID says it is rhel.7-x64, then the OperatingSystemVersion needs to return 7

Ok, so these APIs are really just tunnel to the host.

@eerhardt ?

cc @vitek-karas @jeffschwMSFT

Ok, so these APIs are really just tunnel to the host.

On .NET Core, yes. We would have to decide what to do on other places - throw PNSE, fallback to some C# implementation, etc. Today PlatformAbstractions has the logic in C# and is netstandard1.3....

Should the returned RID be filtered against the RID graph that the host happens to have, or should it be the raw first guess?

For use cases like the CLI showing it during --info, I think it needs to be the raw value. That is what is getting returned in PlatformAbstractions today.

We would have to decide what to do on other places

System.Runtime.InteropServices.RuntimeInformation is not a package anymore. This API would be .NET Core 3.0+ only (and maybe uap/uapaot - but that one is really just a flavor of .NET Core).

This API would be .NET Core 3.0+ only

How about Mono? I thought they used the libraries in corefx. Or any other netstandard2.1 platform?

Also note the breaking change here if we go through with https://github.com/dotnet/core-setup/issues/5213. Previously you could call this API on netstandard1.3. But that will be up to the core-setup issue to resolve. It shouldn't stop corefx from adding the API.

How about Mono? I thought they used the libraries in corefx.

Once Mono chooses to implement .NET Core App 3.0 API set, they will implement this API too.

Or any other netstandard2.1 platform?

They will not get this API.

That's sort of an issue IMO. RIDs aren't just .NET Core specific concepts. We use RuntimeIdentifier across all sorts of platforms.

/cc @marek-safar

@eerhardt I am not sure where you see the issue.

  • This is a new platform API. It will exist in new versions of the platforms only. We are not patching the existing versions of the platforms to add new platform APIs. (We tried to do that in the past, but we have made a conscious decision to not do that again.)

  • Mono has its own RID universe. There are discussions ongoing on how to integrate it better with .NET Core. Once we know what the plan is, I am sure we can find a good way to implement this API in new versions of Mono platforms.

I have added examples of the values returned by each of the proposed APIs. Do they look fine?

Thanks @jkotas. I hadn't had time for that yet.

I have added examples of the values returned by each of the proposed APIs. Do they look fine?

I thought we were going to mimic what we already have in PlatformAbstractions. In example the OSName is not returned in the RID format but as a readable version: https://github.com/dotnet/core-setup/blob/master/src/managed/Microsoft.DotNet.PlatformAbstractions/Native/PlatformApis.cs#L21-L36

In example the OSName is not returned in the RID format but as a readable version

That API is PlatformAbstractions looks confused. It returns all lower case ID some of the time that is not really human readable (e.g. rhel or ubuntu) and human readable string some of the time (e.g. Mac OS X).

What is this API meant to be used for?

Also, if this is meant to return human readable string, do we expect it to keep up with the OS branding changes (e.g. return macOS on Mac) ?

@eerhardt I am not sure where you see the issue.

This is a new platform API.

This API isn't new to our customers. The PlatformAbstractions API has been available since .NET Core 1.0, and it is available on netstandard1.3. If we are planning on removing the PlatformAbstractions API in favor of this new API, it is reasonable to discuss how it is going to impact our customers.

Mono has its own RID universe.

It's my understanding that the general RID design is similar/analogous to the TFM design. They are both pivots that can be taken when choosing which assets to use. They both have a "compatibility graph". etc. I thought we all played in the same "RID universe".

That API is PlatformAbstractions looks confused. It returns all lower case ID some of the time that is not really human readable (e.g. rhel or ubuntu) and human readable string some of the time (e.g. Mac OS X).
What is this API meant to be used for?

Agreed. We should probably fix this while we are making the change over. My thought is it should return the OS portion of the RID. ubuntu, rhel, alpine, freebsd, win, osx, etc. Just like OSVersion should return the version portion of the RID. 16.04, 7, 10, 3.8, 10.13.

My thought is it should return the OS portion of the RID

The OS portion of the RID is not what most folks would call OSName. Should the API be called something like RuntimeIdentifierOSName to make it clear that it is the cryptic name?

No, that is not the proposed OSName.
The s_osPlatformName is something like Linux, OSX, FreeBSD.

Would it require a separate API proposal for retrieval of OS family name and ancestries (that are used in RID graphs) something like:

  • WindowsNT
  • Unix

    • Linux

    • Debian



      • BackTrack


      • Ubuntu


      • Mint


      • LinuxBBQ



    • RPM



      • CentOS


      • Fedora


      • RHE



    • BSD

    • macOS

    • NetBSD

    • FreeBSD



      • GhostBSD



    • Solaris

    • SmartOS

aside: https://github.com/dotnet/corefx/issues/27417 and https://github.com/mono/mono/issues/13002 are also related discussions from perspective of interesting use-cases (e.g. WebAssembly in browser).

Mono and other platforms which implement netstandard would care only if this API would be in netstandard but it looks very .NET Core specific to me to end up there.

I don't know how the RID is constructed but it does not look like a simple form of a concatenation of existing properties exposed by RuntimeIdentifier and should live in different class.

What is also not clear to me from the proposal would the API return values of a host or target?

Should the API be called something like RuntimeIdentifierOSName to make it clear that it is the cryptic name?

What is also not clear to me from the proposal would the API return values of a host or target?

I proposed here to put all 3 new APIs into a new class/struct RuntimeIdentifier to make it more clear. With the struct version, you could imagine being able to construct a RuntimeIdentifier for a different computer than the one the current process is running on.

Mono and other platforms which implement netstandard would care only if this API would be in netstandard but it looks very .NET Core specific to me to end up there.

The concept of a RID/RuntimeIdentifier is not .NET Core specific. The concept, at its heart, is - how do you identify a type of computer? This is specifically important for native assets that only work on a certain type of computer. For example, a library that only works on a Ubuntu 16.04 x86-64 computer. Or a library that only works on a Windows 10 arm64 device. And then you can also imagine a hierarchy of RIDs - this library will work on any version of Windows x64 machine; this one will work on any Debian-based Linux machine; this one on any Linux machine with glibc v2.17 or greater. Etc.

This concept isn't .NET Core specific, and I'd argue it isn't even .NET specific - every computing platform that interops with C code has to consider this concept. Here is some documentation for the manylinux wheel policy in Python. This is the same concept - how do you distribute native assets and describe what kind of computers they work on? Well, to do that, you need to be able to identify types of computers, and specifically "what type of computer am I currently executing on?"

Here's an issue from a few years ago (that hasn't really gotten any traction) but the same exact concept being discussed w.r.t. Xamarin and SkiaSharp.

In .NET we have 2 main "pivots" to describe what assets will work where:

  1. "Which type of .NET?" - This is called the TargetFrameworkMoniker or TFM. ex. net45, netcoreapp2.1, netstandard2.0, xamarinios.
  2. "Which type of computer?" - This is called the RuntimeIdentifier or RID. ex. win10-x86, ubuntu.18.04-arm64, linux-x64.

Neither of those things are .NET Core specific in my opinion.

Neither of those things are .NET Core specific in my opinion.

These things are NuGet specific (or in more general - package manager specific, like in your Python example). NuGet is a built-time or deployment-time technology, not a execution-time technology. You should not really care about these things once your program is up and running.

For historic reasons, these things leaked quite a bit into execution-time in .NET Core. It is not the case with other .NET platforms, like Mono or Xamarin. I think that @marek-safar is saying that this API does not make a lot of sense on .NET platforms where the NuGetism have not leaked into execution-time.

These things are NuGet specific

Not true. We've had TargetFrameworkAttribute in the framework since .NET Framework 4.0, which predates NuGet by 6 months according to their Wikipedia release dates.

As for RID, like you state above, this concept is in the .NET Core runtime. It is definitely not NuGet specific because it is necessary for the runtime to work in portable applications.

You should not really care about these things once your program is up and running.
For historic reasons, these things leaked quite a bit into execution-time in .NET Core. It is not the case with other .NET platforms, like Mono or Xamarin.

How else are you going to decide which asset to load at execution-time without the concept of "what type of computer?"

Mono solved this problem with a dllmap config files. They just did it another way with os, cpu and wordsize separate properties. But in the end.... it's the same concept, just a different implementation.

I don't know how Xamarin tackles the problem. My guess is there is no concept of "portable" apps in Xamarin - you publish for only "one type of computer". But maybe @marek-safar can shed some light on how Xamarin can pick different native assets if the same app is running on iOS 11 vs. 12. I know there are different TFMs for each OS version (ex. MonoAndroid81, MonoAndroid90), so that might be one way it is "solved", but it definitely isn't a scalable solution.

The .NET Framework has this problem, but doesn't solve it because (a) historically it is Windows-only (which helps) and (b) for processor architecture differences it is "left up to the reader" (i.e. user) to figure out how to deal with. (For an example here, look at how much fun DiaSymReader gets to have trying to figure out how to load its native assembly.)

Having a way to identify a type of computer seems valuable to me across all .NETs.

It is definitely not NuGet specific because it is necessary for the runtime to work in portable applications.

Right, this is what I mean by "these things leaked quite a bit into execution-time in .NET Core". I see the support for portable apps built into the .NET Core host as extension of NuGet, not really part of the core runtime. In the early days of .NET Core, we called it "NuGet-aware host" (vs. corerun that was the simple host).

FYI, we're considering moving away from NuGet handling RIDs: https://github.com/dotnet/cli/issues/10528

Though from the runtime's perspective, it probably doesn't make much difference whether it's NuGet or the SDK tasks/targets that handle build-time RID asset selection. As stated, the runtime/host still need to handle RIDs at runtime for portable apps.

Also - we're considering using RIDs at runtime (well in the host) to support runtime selection of VMs (CoreCLR vs Mono).

I don't know how Xamarin tackles the problem.

The output app/binary it's very much tailored to the specific platform version so there is no need for anything portable. For the libraries it's different but what the developers mostly do is that they target the lowest OS version they need and bundle that into their libraries. The idea of building X versions of the same library for different OS versions is not what anyone wants to do.

dllmap solves a similar issue but I think it's conceptually very different. There is no catalogue of predefined rules and values which controls everything. Yes, you cannot automatically extend the mapping for new OS version but that's not big deal.

Could you share an example of where this API will be used and how?

Could you share an example of where this API will be used and how?

We have the API today - RuntimeEnvironment.GetRuntimeIdentifier(). So searching for that will show you all the places that use it today. https://github.com/search?q=RuntimeEnvironment+GetRuntimeIdentifier&type=Code

Here is one example:

https://github.com/dotnet/sourcelink/blob/7a486bfb10e0ccba9d72774c32ffbfeb67886c9b/src/Microsoft.Build.Tasks.Git/GitLoaderContext.cs#L57

That is how the "source link" code decides which libgit2sharp native assembly to load.

https://github.com/search?q=RuntimeEnvironment+GetRuntimeIdentifier&type=Code

Lot of the links in this query point to what look like legacy workarounds that should go away, e.g. this is to support some legacy stuff. What are the good examples where we expect this API to be used in non-legacy code going forward?

The one I know about is when folks run NuGet at runtime (ie reason about the RID graph), like what ASP.NET does here: https://github.com/aspnet/BuildTools/blob/master/src/ApiCheck.Console/NuGet/RuntimeGraph.cs

That is how the "source link" code decides which libgit2sharp native assembly to load.

This was a workaround for lack of proper native library loading APIs. Notice that it comes with a hardcoded RID graph that will be constantnly outdated.

The proper way to fix this going forward is to use the native library loading APIs introduced in netcoreapp3.0, and not worry about the RIDs at all.

The proper way to fix this going forward is to use the native library loading APIs introduced in netcoreapp3.0, and not worry about the RIDs at all.

Can you share an example of how to use these APIs to decide which library to load?

EF Core's scenario for this also involves native library loading. It's pretty advanced, but here's the gist of it:

We call into the native SQLite library which in turn tries to load another native library (a SQLite extension). Since it's native code loading a native library, it bypasses all of .NET Core's logic to find native libraries in NuGet package assets. Instead, before calling into SQLite, we scan the NuGet package assets for a version of the extension library compatible with the current RID. If we find it, we add it to PATH/LD_LIBRARY_PATH/DYLD_LIBRARY_PATH.

Alternatively, this scenario could also be solved by handling native NuGet assets differently so that they could also be loaded by native libraries.

@bricelam Could you please share the link to the code? How do you decide that the extension library is compatible with the current RID?

Thanks for the link. This supports the hypothesis that the RID returning API is only useful with the library that lets you reason about the RID graph.

https://github.com/dotnet/sdk/pull/2997 is adding a use of this API. The use case is related to reasoning about the RID graph. It is trying to figure out if the host build machine is compatible with the target RID because we can't do "cross-crossgen".

Because this code is running in the build, it needs to target .NET Framework and .NET Core.

cc @fadimounir

If this API is not going to be supported on .NET Framework when moved to corefx, then we can't add this dependency to a build task in Microsoft.NET.Sdk.

It occured to me that we stamp the sdk with a $(NETCoreSdkRuntimeIdentifier) that is available in the build.

So we probably have a solution.

I would like to get some clarity on the way forward here in order to complete https://github.com/dotnet/core-setup/issues/5213 for 3.0

cc @ericstj for his thoughts.

I looked into a similar set of problems a back in the 2.0 timeframe and here's what I came up with:
capabilities.pdf (internal link)

I believe it covers a lot of the concerns folks are raising here. @jkotas what are your thoughts on adding an API like this?

If we did so I think it'd have the same effect that @eerhardt suggested of dead-ending PlatformAbstractions.

@jkotas what are your thoughts on adding an API like this?

The set of APIs proposed in your doc are the RID graph walking APIs: RuntimeInformation.FrameworkDescription, RuntimeInformation.IsCompatibleWithRuntimeIdentifier, RuntimeInformation.ChooseBestRuntimeIdentifier, RuntimeInformation.ChooseBestRuntimeIdentifierAndTargetFramework. I agree that these RID graph walking APIs are useful if you want to implement (subset of) what NuGet policy does.

I think the open questions are:

  • Should the RID graph walking APIs be together with the get current RID API? Your proposal has them together. I agree that it makes sense to have them together. The current RID is useless for all practical purposed without also having the RID walking APIs.

  • Should all RID APIs be inbox and part of netcoreapp (ie fold both Microsoft.DotNet.PlatformAbstractions and Microsoft.Extensions.DependencyModel into netcoreapp)? My opionion is that these APIs should not be inbox and part of netcoreapp.

The benefit to putting things inbox is that it makes it consistent with the runtime. If we leave the implementation out of box it will inevitably be only as good as what we can do today (unless we design some lower-level contract that the framework itself exposes). If a new run-time comes along that requires a new way to calculate RIDs the out-of-box implementation will not know how to do that and will disagree with the host. Same goes for a new TFM that an out-of band lib doesn't know how to discover.

In other words, the only way this can be done correctly is to make it out of box for existing frameworks/runtimes and inbox for all future frameworks/runtimes. I don't think we need to make DependencyModel inbox, that can continue to be an out-of-band thing since that is tied to the format of the deps file which ships with the app. Contrast that to RID/TFM that represent the framework which don't ship in the app.

a new run-time comes along that requires a new way to calculate RIDs the out-of-box implementation

I often hear PMs and other folks who are more involved in end-to-end experiences say that RIDs work very poorly and we need something else. I think there is a equal or even higher change that a new run-time comes along with the existing RID concept deprecated.

Even if that did happen we'd still have an ecosystem of packages to carry forward. I also agree that RID needs improvement, but I'm more pragmatic about exposing it. The APIs I propose don't preclude us cracking the strings, changing the algo, precedence, etc.

What are the next steps here? Do we take the capabilities.pdf document above, turn it into an official API proposal at the top of this issue, and then bring it forward to an API review?

I would be in favor of doing that.

I think before taking to API review we need to think about the home for these APIs: type, assembly, and how that assembly ships. We need to have data and a recommendation. I guarantee that will be a large portion of the discussion.

we need to think about the home for these APIs: type, assembly, and how that assembly ships

... and how it is implemented (ie what is the implementation going to depend on)

Do we take the capabilities.pdf document above, turn it into an official API proposal at the top of this issue

I would call this the ambitious plan.

What is the minimum required for 3.0 here? I believe it is just about getting rid of Microsoft.DotNet.PlatformAbstractions. The rest is optional for 3.0. The minimum work required for get rid of Microsoft.DotNet.PlatformAbstractions can look like this:

  • Add API that returns current RID to Microsoft.Extensions.DependencyModel. Microsoft.Extensions.DependencyModel is where the RID graph walking APIs live today. It does not make sense to separate RID walking and current RID because of one cannot be used without the other.
  • Add unmanaged entrypoint that returns the current RID to the donet host. Try to PInvoke this entrypoint in the above API before falling back to guessing to ensure consistency with newer hosts.
  • No need to add any new APIs to netcoreapp itself

I think that plan has merits, but I have some concerns with it that I'd like to hash out.

What is the minimum required for 3.0 here? I believe it is just about getting rid of Microsoft.DotNet.PlatformAbstractions

I'm not 100% positive the only goal here is to get rid of PlatformAbstractions.

From reading the original proposal above:

Today, the RuntimeIdentifier is only exposed through the Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.GetRuntimeIdentifier() API and is not exposed as part of CoreFX. This requires referencing an additional package, and prevents it from being used in certain scenarios (such as evaluation time in MSBuild).

This plan doesn't address the original concern. However, I'm not sure it must be addressed.


The minimum work required for get rid of Microsoft.DotNet.PlatformAbstractions can look like this...

It feels like this plan is just collapsing Microsoft.DotNet.PlatformAbstractions and Microsoft.Extensions.DependencyModel into a single library. But I'm not sure how that improves the situation. The only improvement in the above plan is Add unmanaged entrypoint that returns the current RID to the donet host., which can be done without collapsing these two libraries together.

It does not make sense to separate RID walking and current RID because of one cannot be used without the other.

Technically Microsoft.Extensions.DependencyModel doesn't really have support for RID walking. The DependencyModel library's goal is to support reading and writing .deps.json files. It only supports the RIDs that are is in a .deps.json file that a user can point it to. It just so happens one .deps.json file (Microsoft.NETCore.App.deps.json in the shared fx) has the RID graph for the current machine. This situation is less than ideal because:

  1. You cannot query for RIDs that aren't in the current machine's tree. (i.e. you can't inspect any linux RIDs from a Windows machine.)
  2. If you are a self-contained application, there is no shared fx, and thus no Microsoft.NETCore.App.deps.json file. Therefore - no RID walking is supported.

In summary, what I am saying is that the current RID APIs of Microsoft.Extensions.DependencyModel are not really cut out to be "THE" RID walking APIs.

However, that doesn't mean we can't add new capabilities to Microsoft.Extensions.DependencyModel now or in the future to address these issues.

But it does muddy the story for "What is the DependencyModel library?". Right now the answer is - "The DependencyModel library reads and writes deps.json files". With this plan, the answer becomes - "The DependencyModel library reads and writes deps.json files. And it has OS / Runtime detection features.". The next obvious question will be - "But RuntimeInformation has OS / Runtime detection features - why do we have two?"

You cannot query for RIDs that aren't in the current machine's tree. (i.e. you can't inspect any linux RIDs from a Windows machine.)

That sounds perfectly fine to me. It does not make sense to ship Linux specific stuff in Windows runtime, or vice versa. If you want the current global RID graph, you should get it from the latest SDK, not from the runtime.

My assumption was the runtime APIs proposed here would only understand RIDs relevant to current machine, not every RID that ever existed.

the current RID APIs of Microsoft.Extensions.DependencyModel are not really cut out to be "THE" RID walking APIs.

They seem to work fine for folks replicating NuGet at runtime. In any case, we should make sure that any better RID walking APIs are able to satisfy the current use cases, e.g. https://github.com/aspnet/EntityFrameworkCore/blob/2.2.0/src/EFCore.Sqlite.Core/Infrastructure/SpatialiteLoader.cs#L109-L135 can be rewritten using them.

Have a look at the Swift @available attribute here.

If we go forward with the plan of moving all "RID"-related APIs into the DependencyModel library, would we be able to close the issue https://github.com/dotnet/corefx/issues/27417? And tell anyone who wanted to do this broad of platform detection to use the DependencyModel library? (After we added platform detection into DependencyModel for all the platforms listed in that issue.)

cc @migueldeicaza

Moving this to Future as there is no clear consensus on what the long term strategy for a RID API should look like. This won't be able to be completed in the 3.0 time frame.

After discussing with @jkotas and @ericstj, I have modified this API proposal to just include an API for getting the current RID of the machine. I have updated the original issue comment as appropriate.

The proposed RuntimeInformation.GetRuntimeIdentifier() method will return a new AppContext variable RUNTIME_IDENTIFIER. This variable will be passed by the dotnet.exe host, and can be passed by any other native host when the runtime is loaded.

DISCUSSION: Should we still maintain a managed fallback code path for when this AppContext variable isn't present? For example if the app was loaded from a different native host that didn't set RUNTIME_IDENTIFIER.

We decided to drop OSName and OSVersion. The current API for this information is RuntimeInformation.OSDescription. It would be confusing to have all 3 of these APIs, and each returning different information. The only known use-case for OSName and OSVersion APIs are for diagnostic or telemetry types of situations, which is solved by RuntimeInformation.OSDescription.

Note, that the following callsites in dotnet/sdk will need to be updated since those APIs will be going away - /cc @nguerrera @dsplaisted.

https://github.com/dotnet/sdk/blob/b1223209644d900702287faea8e9b71f95ec49f8/src/Layout/toolset-tasks/CurrentPlatform.cs#L159
https://github.com/dotnet/sdk/blob/b1223209644d900702287faea8e9b71f95ec49f8/src/Cli/dotnet/Telemetry/TelemetryCommonProperties.cs#L66
https://github.com/dotnet/sdk/blob/dc3dc1499ed2ce922c6c5fa2ad4a6c8166436049/src/Cli/dotnet/Program.cs#L313

Marking this as api-ready-for-review to get the GetRuntimeIdentifier() method moved from PlatformAbstractions.RuntimeEnvironment to RuntimeInformation.

CURRENT_RUNTIME_IDENTIFIER

Nit: Why not just RUNTIME_IDENTIFIER? I do not think that the "CURRENT" in the name is necessary.

Should we still maintain a managed fallback code path for when this AppContext variable isn't present?

I do not think so. The API should be allowed to return null when the RID is not known.

@eerhardt Can you leave the old APIs in for a while (I'd suggest at least a different preview release than we make the SDK change in)? Otherwise we may have trouble doing the change "atomically" due to how dependencies flow, etc.

@jkotas -

Why not just RUNTIME_IDENTIFIER?

Good point. I've updated the proposal.

@dsplaisted -

Can you leave the old APIs in for a while

For sure. We have separate issues for adding the new API to RuntimeInformation (this issue). And the issue to remove the old PlatformAbstractions library (#3470). The latter can't happen until the new API is in place, and it won't happen at the same time. Separate preview releases would probably make sense, but I'd like to do it as early as possible to get feedback on the takeback here. We _could_ start weaning the SDK off PlatformAbstractions now. There are a few places that use OperatingSystemPlatform and RuntimeArchitecture that could switch to RuntimeInformation now.

Ex:
https://github.com/dotnet/sdk/blob/b1223209644d900702287faea8e9b71f95ec49f8/src/Cli/Microsoft.DotNet.Cli.Utils/ProcessReaper.cs#L40
https://github.com/dotnet/sdk/blob/b1223209644d900702287faea8e9b71f95ec49f8/src/Cli/Microsoft.DotNet.Cli.Utils/RuntimeEnvironmentRidExtensions.cs#L28-L29

Video

  • Looks good

    • We like that we don't duplicate code between the native and the managed implementation

    • However, we should make sure that other hosts can access the logic in the hosting APIs, so it seems they should expose an easy API to compute the current RID and set the AppContext value (or make sure they are automatically set)

  • We should make it a property
  • It shouldn't return a nullable string, rather it should do a sensible fallback (either in the host or the managed side)

```C#

nullable enabled

namespace System.Runtime.InteropServices
{
public static class RuntimeInformation
{
// The current OS RID, e.g.: win7-x64, osx.10.11-x64, ubuntu.18.04-arm64, rhel.7-x64
public static string RuntimeIdentifier { get; }
}
}
```

It shouldn't return a nullable string, rather it should do a sensible fallback

There is no sensible fallback in number of advanced cases. That's why it was proposed as potentially returning null.

I guess we can make it return any RID as the fallback, but is it really better than null?

The proposed options for what happens if AppContext.GetData("RUNTIME_IDENTIFIER") returns null were:

  • Throw an exception
  • Return any
  • Do a super simple managed fallback:

    • win-{arch}

    • osx-{arch}

    • linux-{arch}

    • ...

    • potentially any at the end if all the known IsOSPlatform checks return false.

I guess we can make it return any RID as the fallback, but is it really better than null?

The case where it is better than null is that any is a valid RID, and if you were passing that into some other API (or were inspecting the RID graph), it would be a treated as a valid RID.

The fallback really means "don't know". If you pass any to other APIs that expect actual meaningful RID, they are likely going to crash anyway.

The issue in the API design meeting regarding this API being defined as:

```C#
public static string? RuntimeIdentifier { get; }

Is that it is an exceptional case when the AppContext variable isn't set. When an app is launched by our app host, the `RUNTIME_IDENTIFIER` AppContext variable will be set. If an app is using the new [preferred native Hosting APIs](https://docs.microsoft.com/en-us/dotnet/core/tutorials/netcore-hosting#hosting-apis), it will be set. The only time `RUNTIME_IDENTIFIER` won't be set is if someone is using the `CoreClrHost.h` or `mscoree.h` native hosting, which shouldn't be a lot of places. Forcing callers of this method to have to handle `null` when it is an exceptional situation, and there is a possible non-null fallback, wasn't desired.

> If you pass any to other APIs that expect actual meaningful RID, they are likely going to crash anyway.

I would think that depends on the API. Since `any` is a valid RID, I don't see why it would crash. If it can't find `any` in its graph, it would treat it the same as any invalid RID it couldn't find.

However, the other API is probably defined as 

nullable enbale

void MyRidApi(string rid)
```

And we couldn't say MyRidApi(RuntimeInformation.RuntimeIdentifier), without the compiler complaining for everyone.


The current RuntimeEnvironment API returns unknown-{arch} when it can't figure it out. Perhaps keeping that same behavior is the best approach for this new API.

The only time RUNTIME_IDENTIFIER won't be set is if someone is using the CoreClrHost.h or mscoree.h native hosting

That's not the only case. The broader .NET ecosystem includes e.g. Unity that has its own runtime and number of Unity target platforms do not even have well-defined RIDs by design.

I am fine with this being implemented as `GetData("RUNTIME_IDENTIFIER") ?? "any"`` (or "unknown" if that works better).

I think the primary conclusion is that we'd consider such hosts as having a bug. Frankly, I'd even go so far and say we should crash on startup when a RID isn't specified. Or said differently, for this API to be reliable, the host must set it meaningfully. That's why we should force the change there, rather than asking everyone to guard for null.

We like that we don't duplicate code between the native and the managed implementation

If by native we also consider the control flow stemming from https://github.com/dotnet/runtime/blob/3d8073daba7a84fe2692816763ada490b6353359/src/installer/corehost/cli/hostmisc/pal.h#L67 as a problem, then +1. For operating systems, that are not officially supported, such as BSD lane or even other SystemV likes such as Illumous, these kind of hardcoded constraint unnecessary puts another barrier on already complicated system for porting work, which is suffering for a long time due to:

  • over-engineered RID barriers everywhere: starting from post- CoreCLR native compilation to the package delivery via NuGet to build publish dotnet apps.
  • heavy (mandatory?) dependency on CI: so if AzDevOps (since it has replaced its more capable counterpart Jenkins in 2019) does not support the operating system (i.e. anything beyond Windows, macOS, Linux), we have no dice to get dotnet(1) running and will have our fingers crossed forever since dotnet/source-build also relies on AzDevOps and is locked down to those three systems.

By the time we get any version working on other OS, it is already too late in lifetime of that version, due to breaking changes in infra for the next version.

IMO, keep RID story super simple; restrictions wise and otherwise so it can scale better in what it was originally designed to do. This is mitigate some pain points in porting work for vast range of targets.

@terrajobst As @am11 said, the RIDs are ever-engineered, do not scale and major barrier for entry for non-Microsoft driven ports. It is why I believe that it is important that this API is allowed to return "don't know answer". If we dislike null, it is ok for this don't know answer to be "any", "unknown" or even empty string.

@terrajobst As @am11 said, the RIDs are ever-engineered, do not scale and major barrier for entry for non-Microsoft driven ports. It is why I believe that it is important that this API is allowed to return "don't know answer". If we dislike null, it is ok for this don't know answer to be "any", "unknown" or even empty string.

The host doesn't need to validate the string against the graph; it just needs to return a string that is unique and can be used to identity the runtime. However, for libraries with native dependencies it's important that "sensible" answers are returned. As you said, returning "any" is unlikely to be a good answer.

However, for libraries with native dependencies it's important that "sensible" answers are returned

I would like to see programs and libraries that try to do their own native library resolution based on RIDs to assume that the native library is supplied by other means for unknown RID. Nobody really needs to their own native library resolution in .NET Core, this is very niche case - often just a left over from .NET Framework. I know that there are a few libraries that do that for "reasons", and I would not feel bad that the potential null is in their face.

I would like to avoid the situation we have with Assembly.Location today where every other call to this API does not handle the case of empty path correctly that results in a lot of breaks with single-file publishing.

@eerhardt the value returned for RuntimeIdentifier depends on the distributions known to .NET Core?
For example, if I have installed Microsoft linux-x64 .NET Core build on Ubuntu 18.04, this returns ubuntu.18.04-x64?
If I install that same version on Ubuntu 20.04, this will return linux-x64 (because ubuntu.20.04-x64 is not a known rid)?

From a use-case perspective, it may be interesting to be able to query whether the platform is compatible with an rid. Both above are compatible with linux-x64.

This API is not doing any filtering against any known lists. It is going to return ubuntu.20.04-x64 in your example.

@jkotas Ah, that's good! The processing is limited to some normalization of /etc/os-release/ (like stripping the minor for rhel7).

fwiw, since this returns an rid for the OS, OSIdentifier would be a more indicative name.

From a use-case perspective, it may be interesting to be able to query whether the platform is compatible with an rid.

Today, you can use the framework's .deps.json file, or the app's .deps.json file if it is a self-contained app (the RID graph was added for self-contained apps with https://github.com/dotnet/sdk/issues/3361), to get the RID graph. .deps.json files can be loaded with the DependencyModel library. See the RuntimeGraph property on DependencyContext. You can use that to see what RID is compatible with what for the current platform.

If you need a full RID graph (ex. win information on unix), we don't have an API to get this information. You would have to ship and update the runtime.json file yourself.

The processing is limited to some normalization of /etc/os-release/ (like stripping the minor for rhel7).

Yes, it still does that normalization for rhel and alpine.

since this returns an rid for the OS, OSIdentifier would be a more indicative name.

I believe that would be confusing since everywhere else in the system (CLI options, SDK properties, Documentation, etc) calls this RID or RuntimeIdentifier.

Was this page helpful?
0 / 5 - 0 ratings