Skip to content

[.NET 9] RuntimeInformation.RuntimeIdentifier includes unexpected OS details on Linux repo installs #114156

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

Open
ptr727 opened this issue Apr 2, 2025 · 14 comments
Labels
area-Host untriaged New issue has not been triaged by the area owner

Comments

@ptr727
Copy link

ptr727 commented Apr 2, 2025

Description

Per docs it is expected that RuntimeInformation.RuntimeIdentifier returns OS agnostic RID's.

In my testing the RID's returned on some Linux installs includes the OS name, this is not expected, and breaks usage of the reported RID. (I use the RID to download single file dotnet tools)

Note that the issue may be related to how the SDK/runtime was built and installed, as the behavior on Windows (.NET sourced rom Msft, and Debian (.NET sourced from install script) appear to work, while the SDK/runtime installed using the OS native repository does not produce the correct results.

Summary:
Win11 Msft download : Ok
Debian https://dot.net/v1/dotnet-install.sh : Ok
Ubuntu dotnet-runtime-9.0 : Fail
Alpine dotnet9-runtime : Fail

Reproduction Steps

Windows 11 x64, .NET 9 SDK installed: (Ok)
dotnet --info -> RID: win-x64
Console.WriteLine(RuntimeInformation.RuntimeIdentifier); -> win-x64

Alpine, docker alpine:latest, apk add dotnet9-runtime (Fail)
dotnet --info -> RID: alpine.3.21-x64 (expected linux-musl-x64)
Console.WriteLine(RuntimeInformation.RuntimeIdentifier); -> alpine.3.21-x64 (expected linux-musl-x64)

Debian, docker debian:stable-slim, https://dot.net/v1/dotnet-install.sh (Ok)
dotnet --info -> RID: linux-x64
Console.WriteLine(RuntimeInformation.RuntimeIdentifier); -> linux-x64

Ubuntu, docker ubuntu:rolling, dotnet-runtime-9.0 (Fail)
dotnet --info -> RID: ubuntu.24.10-x64 (expected linux-x64)
Console.WriteLine(RuntimeInformation.RuntimeIdentifier); -> ubuntu.24.10-x64 (expected linux-x64)

Expected behavior

RID returned in dotnet --info and RuntimeInformation.RuntimeIdentifier is consistent with documentation.

Actual behavior

See reproduction steps, it appears that .NET installed via Linux native repos do not return the correct RID.

Regression?

I last tested this behavior in .NET v7 and it worked then, but that was a long time ago.

Known Workarounds

Always install using Msft helper script, but that is not recommended for production use.

Configuration

.NET 9.0.3

See description for install steps on Docker.

Other information

No response

@dotnet-issue-labeler dotnet-issue-labeler bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Apr 2, 2025
@dotnet-policy-service dotnet-policy-service bot added the untriaged New issue has not been triaged by the area owner label Apr 2, 2025
@pra2892
Copy link
Contributor

pra2892 commented Apr 2, 2025

This issue with RuntimeInformation.RuntimeIdentifier returning OS-specific details on Linux when installed from native repositories can be problematic when you need consistent RIDs.

Here's a possible solution that normalizes the RID to the expected format:

using System;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;

class Program
{
    static void Main()
    {
        // Get the raw RID as reported by the runtime
        string rawRid = RuntimeInformation.RuntimeIdentifier;
        Console.WriteLine($"Raw RID: {rawRid}");
        
        // Get the normalized RID
        string normalizedRid = GetNormalizedRuntimeIdentifier();
        Console.WriteLine($"Normalized RID: {normalizedRid}");
    }
    
    /// <summary>
    /// Gets a normalized runtime identifier that's consistent across different installation methods.
    /// </summary>
    public static string GetNormalizedRuntimeIdentifier()
    {
        string rid = RuntimeInformation.RuntimeIdentifier;
        
        // If already in the expected format, return as is
        if (rid == "win-x64" || rid == "win-arm64" || 
            rid == "linux-x64" || rid == "linux-arm64" || 
            rid == "linux-musl-x64" || rid == "linux-musl-arm64" ||
            rid == "osx-x64" || rid == "osx-arm64")
        {
            return rid;
        }
        
        // Handle OS-specific RIDs from native repositories
        
        // Extract architecture (should be the part after the last dash)
        string architecture = "x64"; // Default
        if (rid.Contains("-"))
        {
            architecture = rid.Substring(rid.LastIndexOf('-') + 1);
        }
        
        // Determine OS and variant
        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
        {
            return $"win-{architecture}";
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
        {
            // Check if it's Alpine Linux (musl-based)
            if (rid.Contains("alpine") || IsAlpineLinux())
            {
                return $"linux-musl-{architecture}";
            }
            return $"linux-{architecture}";
        }
        else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
        {
            return $"osx-{architecture}";
        }
        
        // Fallback to the original RID if we can't normalize it
        return rid;
    }
    
    /// <summary>
    /// Checks if the current Linux distribution is Alpine (musl-based).
    /// </summary>
    private static bool IsAlpineLinux()
    {
        try
        {
            // Check for /etc/os-release file which contains distribution info
            if (File.Exists("/etc/os-release"))
            {
                string content = File.ReadAllText("/etc/os-release");
                return content.Contains("ID=alpine") || content.Contains("ID=\"alpine\"");
            }
            
            // Alternative check for /etc/alpine-release
            return File.Exists("/etc/alpine-release");
        }
        catch
        {
            return false;
        }
    }
}

This solution:

  1. Creates a GetNormalizedRuntimeIdentifier() method that returns a consistent RID format regardless of installation method
  2. Handles the specific cases mentioned in your issue (Windows, standard Linux, and Alpine Linux)
  3. Includes a helper method to detect Alpine Linux specifically, which needs the "linux-musl-x64" RID

You can use this normalized RID for your tool downloads instead of directly using RuntimeInformation.RuntimeIdentifier. This should work across all installation methods, including native repository installations.

For production use, you might want to add more comprehensive detection for other Linux distributions or edge cases, but this covers the main scenarios you described in the issue.

Thank You,

Prashant Yadav

@ptr727
Copy link
Author

ptr727 commented Apr 2, 2025

For production use, you might want to add more comprehensive detection for other Linux distributions or edge cases, but this covers the main scenarios you described in the issue.

Thank you, this is a temporary workaround, I do believe the repo based installers should report the correct information, and workarounds should not be required.

I'd like to hear from dotnet if this is in fact a repo installer issue, how they interact with maintainers to get it fixed, and maybe add tests to assure correct RID behavior?

@jkotas jkotas removed the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Apr 2, 2025
Copy link
Contributor

Tagging subscribers to this area: @vitek-karas, @agocke, @VSadov
See info in area-owners.md if you want to be subscribed.

@jkotas
Copy link
Member

jkotas commented Apr 2, 2025

This is expected. The distro package managers install distro-specific .NET runtime. The distro-specific .NET runtime has RuntimeIdentifier specific to that distro.

What are you trying to do? If you just need to check whether you are running on Linux, there is OperatingSystem.IsLinux() API for that.

cc @dotnet/distro-maintainers

@ptr727
Copy link
Author

ptr727 commented Apr 2, 2025

I am trying to get the RID so that I can download the correct single file test tool.

The method of using dotnet --info is very widely used to get the RID. So making the result of dotnet --info and RuntimeInformation.RuntimeIdentifier non-deterministic is very troublesome, and it is contrary to the documentation.

I expect some form of method that returns the RID in a deterministic way, that matches the documentation, regardless of how the runtime was installed.

@agocke
Copy link
Member

agocke commented Apr 17, 2025

@elinor-fung Looks like the documentation isn't quite right. It says that the RID on ubuntu will be linux-x64, but that's dependent on whether they're using a portable or non-portable build, right?

@am11
Copy link
Member

am11 commented Apr 17, 2025

I guess more problematic is the behavior of published apps, which differ from dotnet run:

$ dotnet run
ubuntu.25.04-arm64

# dotnet publish -p:PublishSingleFile=true -o publish-singlefile
$ publish-singlefile/app1 
linux-arm64

# dotnet publish -p:PublishAot=true -o publish-aot
$ publish-aot/app1        
linux-arm64

At minimum, we should make it consistently return the same RID, either portable or non-portable. This is informational API for telemetry purposes etc. For other use-cases, OperatingSystem.IsXx APIs are preferred as @jkotas has pointed out. However, that does not cover cases like linux-musl-{arch} for which we have to detect libc flavor by shelling out to environment or P/Invoking. Ideally, both pieces of information are useful for non-portable builds: effective RID (for telemetry etc.) and portable RIDs (for native assets / runtime.json NuGet/Home#10571 etc.).

@ptr727
Copy link
Author

ptr727 commented Apr 17, 2025

Thank you for continuing to investigate.

I do want to call out that the .NET diagnostic tooling expects normalized RID's, so I'd still prefer some solution where dotnet --info (as I mentioned before this is used in grep all over github to get the RID) or some other CLI accessible command returns the normalized RID as expected by .NET tooling, or that all .NET tooling support whatever RID is returned from other .NET CLI tools.

@elinor-fung
Copy link
Member

@elinor-fung Looks like the documentation isn't quite right. It says that the RID on ubuntu will be linux-x64, but that's dependent on whether they're using a portable or non-portable build, right?

It should probably call that out explicitly. The example is technically correct (version of Ubuntu from when they didn't produce their own packages), but confusing. Tried to clarify in: dotnet/dotnet-api-docs#11203

@jkoritzinsky
Copy link
Member

I guess more problematic is the behavior of published apps, which differ from dotnet run:

$ dotnet run
ubuntu.25.04-arm64

dotnet publish -p:PublishSingleFile=true -o publish-singlefile

$ publish-singlefile/app1
linux-arm64

dotnet publish -p:PublishAot=true -o publish-aot

$ publish-aot/app1
linux-arm64
At minimum, we should make it consistently return the same RID, either portable or non-portable. This is informational API for telemetry purposes etc. For other use-cases, OperatingSystem.IsXx APIs are preferred as @jkotas has pointed out. However, that does not cover cases like linux-musl-{arch} for which we have to detect libc flavor by shelling out to environment or P/Invoking. Ideally, both pieces of information are useful for non-portable builds: effective RID (for telemetry etc.) and portable RIDs (for native assets / runtime.json NuGet/Home#10571 etc.).

I think this is actually a different case: The SDK specifically uses a portable RID as the default RID when you publish, even if you're on a non-portable SD (see the behavior of UseCurrentRuntimeIdentifier). So using dotnet run uses the current SDK, whereas PublishSingleFile will use the corresponding singlefilehost for the portable RID. So you are actually getting a different host.

This behavior is a reasonable default IMO because otherwise the default publish experience would mean that a project built on a SourceBuild-based SDK (like RHEL) would publish non-portable, whereas building on a Microsoft-build would publish portable.

@am11
Copy link
Member

am11 commented Apr 18, 2025

I think this is actually a different case: The SDK specifically uses a portable RID as the default RID when you publish, even if you're on a non-portable SD (see the behavior of UseCurrentRuntimeIdentifier). So using dotnet run uses the current SDK, whereas PublishSingleFile will use the corresponding singlefilehost for the portable RID. So you are actually getting a different host.

Yes I saw the behavior, rather wrote it myself dotnet/sdk@347f78f and it was equally messy back then.

Do we want RuntimeInformation.RuntimeIdentifier API to change the value based on --runtime and -p:UseRidGraph etc. or do we want this API to report consistent result? The current behavior is not very intuitive from UX perspective and docs are still not capturing what contributes to its output. The output seems to be leaking build-time concerns; how SDK and hosts were built, and publish options used.

@tmds
Copy link
Member

tmds commented Apr 18, 2025

I don't think we should be changing the value of RuntimeInformation.RuntimeIdentifier as we still like a way to return the non-portable rid.

Perhaps an additional property, like RuntimeInformation.PortableRuntimeIdentifier can be considered.

If you are doing something on the SDK side, there is the NETCoreSdkPortableRuntimeIdentifier MSBuild property.

The broader use-case is to find a best match among a set of target rids. e.g. I have installers for linux-x64 and osx-x64, what is the best match on my fedora.41-x64.

@am11
Copy link
Member

am11 commented Apr 18, 2025

I don't think we should be changing the value of RuntimeInformation.RuntimeIdentifier as we still like a way to return the non-portable rid.

RuntimeInformation.RuntimeIdentifier is changing the value today based on multiple factors, as seen above. Do you mean current behavior is correct or that it shouldn't be changing the values on those factors?

In addition to how user has installed the SDK (from MSFT or the distro), these are some differences:

$ apt install -y dotnet-sdk-9.0
$ dotnet new console -n app1 && cd app1
$ cat >Program.cs<<EOF
using System.Runtime.InteropServices;
Console.WriteLine(RuntimeInformation.RuntimeIdentifier);
EOF

$ dotnet run
ubuntu.25.04-arm64

$ dotnet publish --self-contained -o out && out/app1
linux-arm64

$ dotnet publish --self-contained -o sc --ucr && out/app1
ubuntu.25.04-arm64

$ dotnet publish --self-contained -r ubuntu.25.04-arm64 -p:UseRidGraph=true -o out && out/app1
ubuntu.25.04-arm64

$ dotnet publish --self-contained -r ubuntu.22.04-arm64 -p:UseRidGraph=true -o out && out/app1
linux-arm64

$ dotnet publish --self-contained -r fedora.39-arm64 -p:UseRidGraph=true -o out && out/app1
linux-arm64

By reading the remarks section in docs as an API consumer, these output should be explainable without needing to learn the internal infrastructure details of dotnet org.

@tmds
Copy link
Member

tmds commented Apr 18, 2025

RuntimeInformation.RuntimeIdentifier is changing the value today based on multiple factor

In all cases it should be returning the rid the native assets are compatible with (that is: the vmr build TargetRid property).

fedora.41-x64 assets are not compatible to run on all linux-x64.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-Host untriaged New issue has not been triaged by the area owner
Projects
Status: No status
Development

No branches or pull requests

8 participants