Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
241 changes: 165 additions & 76 deletions docs/core/tools/rid-specific-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,17 @@ ai-usage: ai-assisted

**This article applies to:** ✔️ .NET SDK 10 and later versions

Package .NET tools for specific platforms and architectures so you can distribute native, fast, and trimmed applications. This capability makes it easier to distribute native, fast, trimmed .NET applications for command-line tools like MCP servers or other platform-specific utilities.
Package .NET tools for specific platforms and architectures so you can distribute native, fast, and trimmed applications. This capability makes it easier to distribute optimized applications for command-line tools like MCP servers or other platform-specific utilities.

## Overview

Starting with .NET SDK 10, you can create .NET tools that target specific Runtime Identifiers (RIDs). These tools can be:
Starting with .NET SDK 10, you can create .NET tools that target specific operating system environments (represented by Runtime Identifiers (RIDs)). These tools can be:

- **RID-specific**: Compiled for particular operating systems and architectures.
- **Self-contained**: Include the .NET runtime and don't require a separate .NET installation.
- **Native AOT**: Use Ahead-of-Time compilation for faster startup and smaller memory footprint.

When users install a RID-specific tool, the .NET CLI automatically selects and installs the appropriate package for their platform.
Users don't notice a difference when they install the tool. The .NET CLI automatically selects and installs the best package for their platform.

## Opt in to RID-specific packaging

Expand Down Expand Up @@ -52,6 +52,7 @@ Alternatively, use `ToolPackageRuntimeIdentifiers` for tool-specific RID configu
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<PackAsTool>true</PackAsTool>
<PublishAot>true</PublishAot>
<ToolCommandName>mytool</ToolCommandName>
<ToolPackageRuntimeIdentifiers>win-x64;linux-x64;osx-arm64</ToolPackageRuntimeIdentifiers>
</PropertyGroup>
Expand All @@ -60,13 +61,36 @@ Alternatively, use `ToolPackageRuntimeIdentifiers` for tool-specific RID configu

Use a semicolon-delimited list of RID values. For a list of Runtime Identifiers, see the [RID catalog](../rid-catalog.md).

### When to use `RuntimeIdentifiers` vs `ToolPackageRuntimeIdentifiers`

Both `RuntimeIdentifiers` and `ToolPackageRuntimeIdentifiers` opt your tool into RID-specific packaging, but they serve slightly different purposes:

Use **`RuntimeIdentifiers`** when:

- You want the project to **build and publish RID-specific apps in general** (not just as a tool).
- You're primarily targeting **CoreCLR** (non-AOT) or you want the standard SDK behavior where a single `dotnet pack` produces multiple RID-specific packages.
- You may conditionalize `PublishAot` for a subset of RIDs, but you still want a CoreCLR-based package for every RID in `RuntimeIdentifiers`.

Use **`ToolPackageRuntimeIdentifiers`** when:

- You want to define **RID-specific behavior only for the tool packaging**, without changing how the project builds for other deployment scenarios.
- You're using **Native AOT** and plan to **manually build** AOT binaries per RID with `dotnet pack -r <RID>`.
- You want a **hybrid model** where some RIDs get Native AOT and others fall back to a portable CoreCLR implementation.

Notes:

- The top-level pointer package specifies the available RID-specific packages. If you specify `ToolPackageRuntimeIdentifiers`, it determines the tool RIDs; otherwise, `RuntimeIdentifiers` is used.
- `ToolPackageRuntimeIdentifiers` should be equal to or a subset of the RIDs in `RuntimeIdentifiers`
- When `PublishAot=true`, RID-specific packages are generated only when you pack for a specific RID (for example, `dotnet pack -r linux-x64`).
- Native AOT builds (`PublishAot=true`) is only supported when the build OS and target OS match.

## Package your tool

The packaging process differs depending on whether you're using AOT compilation. To build a NuGet package, or *.nupkg* file from the project, run the [dotnet pack](dotnet-pack.md) command.

### RID-specific and self-contained tools

For tools without AOT compilation, run `dotnet pack` once:
Run `dotnet pack` once:

```dotnetcli
dotnet pack
Expand All @@ -83,124 +107,189 @@ This command creates multiple NuGet packages:

### AOT tools

For tools with AOT compilation (`<PublishAot>true</PublishAot>`), you must pack separately for each platform:
For tools with AOT compilation (`<PublishAot>true</PublishAot>`), you must pack separately for each platform.

- Pack the top-level package once (on any platform):
#### Platform requirements for Native AOT

```dotnetcli
dotnet pack
Native AOT compilation requires the operating system (OS) part of the SDK RID to match the target RID's OS. The SDK can cross-compile for different architectures (for example, x64 to ARM64) but not across operating systems (for example, Windows to Linux).

This means you have several options for building Native AOT packages:

- **Build only for your development machine**: Support Native AOT only for the OS you're developing on.
- **Use containers for Linux builds**: If you're on macOS or Windows, use containers to cross-compile for Linux. For example, use `mcr.microsoft.com/dotnet/sdk:10.0-noble-aot` container images.
- **Federate your build across machines**: Use CI/CD systems like GitHub Actions or Azure DevOps Pipelines to build on different operating systems.

You don't need to build all RID-specific packages on the same machine or at the same time. You just need to build and publish them before you publish the top-level package.

#### Packing Native AOT tools

Pack the top-level package once (on any platform):

```dotnetcli
dotnet pack
```

Pack for each specific RID on the corresponding platform, for example:

```dotnetcli
dotnet pack -r linux-x64
```

You must run each RID-specific pack command on a platform where the OS matches the target RID's OS. For more information about the prerequisites for Native AOT compilation, see [Native AOT deployment](../deploying/native-aot/index.md).

When you set `PublishAot` to `true`, the packing behavior changes:

- `dotnet pack` produces the **top-level pointer package** (package type `DotnetTool`).
- RID-specific AOT packages are produced only when you explicitly pass `-r <RID>`, for example, `dotnet pack -r linux-x64` or `dotnet pack -r osx-arm64`.

### Hybrid AOT + CoreCLR packaging pattern (example)

Some tools want the best of both worlds:

- **Native AOT** for a subset of high-priority platforms (depending on the tool).
- A **portable CoreCLR fallback** that works on platforms not targeted by the Native AOT builds.

You can achieve this "hybrid" model with the following pattern:

1. **Configure the tool for Native AOT and tool-specific RIDs.**

In your project file, use `ToolPackageRuntimeIdentifiers` and enable `PublishAot`.

For example:

```xml
<ToolPackageRuntimeIdentifiers>osx-arm64;linux-arm64;linux-x64;any</ToolPackageRuntimeIdentifiers>
<PublishAot>true</PublishAot>
```

- Pack for each specific RID on the corresponding platform:
1. **Create the pointer package.**

Run `dotnet pack` once (on any platform) to build the top-level package that points to the RID-specific packages:

```dotnetcli
dotnet pack -r win-x64
dotnet pack -r linux-x64
dotnet pack -r osx-arm64
dotnet pack
```

You must run each RID-specific pack command on the matching platform because AOT compilation produces native binaries. For more information about the prerequisites for Native AOT compilation, see [Native AOT deployment](../deploying/native-aot/index.md).
1. **Build Native AOT packages for selected RIDs.**

## Package structure
Native AOT compilation requires building on the target platform. Build each AOT-enabled RID package on the matching platform using `dotnet pack -r <RID>`.

### Package types
For example:

RID-specific tool packages use two package types:
```
dotnet pack -r linux-x64
```

- **DotnetTool**: The top-level package that contains metadata.
- **DotnetToolRidPackage**: The RID-specific packages that contain the actual tool binaries.
1. **Build a CoreCLR fallback package.**

### Package metadata
To provide a universal fallback, pack the `any` RID without AOT:

The top-level package includes metadata that signals it's a RID-specific tool and lists the RID-specific packages. When you run `dotnet tool install`, the CLI reads this metadata to determine which RID-specific package to install for the current platform.
```dotnetcli
dotnet pack -r any -p:PublishAot=false
```

## Publish your tool
This produces a portable CoreCLR package (for example, `yourtool.any.<version>.nupkg`) that can run on platforms that don't have a dedicated AOT build.

> [!NOTE]
> You can also use the `.NET SDK 10.0-noble-aot` container images to build and package Linux Native AOT tools from any host that supports Linux containers. For example:
>
> - `mcr.microsoft.com/dotnet/sdk:10.0-noble-aot`
>
> This is useful when your development machine isn't running Linux natively.

In this hybrid setup:

- The pointer package (`yourtool.<version>.nupkg`) references both:
- RID-specific Native AOT packages (for example, `yourtool.osx-arm64`, `yourtool.linux-x64`).
- The `any` CoreCLR package as a fallback.
- The .NET CLI automatically picks the most appropriate package for the user's platform when they run `dotnet tool install` or `dnx`.

#### Example: `dotnet10-hybrid-tool`

The [`dotnet10-hybrid-tool` repository](https://github.com/richlander/dotnet10-hybrid-tool) demonstrates this hybrid packaging pattern with Native AOT packages for `osx-arm64`, `linux-arm64`, and `linux-x64`, plus a CoreCLR fallback package for the `any` RID (used, for example, on Windows when no AOT build is available).

Publish all packages to NuGet.org or your package feed by using [dotnet nuget push](dotnet-nuget-push.md):
You can install and try the tool yourself:

```dotnetcli
dotnet nuget push path/to/package/root/*.nupkg
dotnet tool install -g dotnet10-hybrid-tool
dotnet10-hybrid-tool
```

## Run a RID-specific tool
The tool reports its runtime framework description, runtime identifier (RID), and compilation mode (Native AOT or CoreCLR).

Users run RID-specific tools the same way as platform-agnostic tools:
Example output on a platform with Native AOT:

```dotnetcli
dnx mytool
```output
Hi, I'm a 'DotNetCliTool v2' tool!
Yes, I'm quite fancy.

Version: .NET 10.0.2
RID: osx-arm64
Mode: Native AOT
```

The CLI automatically:
Example output on a platform using the CoreCLR fallback:

1. Downloads the top-level package.
1. Reads the RID-specific metadata.
1. Identifies the most appropriate package for the current platform.
1. Downloads and runs the RID-specific package.
```output
Hi, I'm a 'DotNetCliTool v2' tool!
Yes, I'm quite fancy.

## Example: Create an AOT tool
Version: .NET 10.0.2
RID: win-x64
Mode: CoreCLR
```

Here's a complete example of creating an AOT-compiled RID-specific tool:
This makes it a useful way to experiment with RID-specific, AOT-compiled tools and the CoreCLR fallback behavior.

1. Create a new console application:
## Publish your tool

```dotnetcli
dotnet new console -n MyFastTool
cd MyFastTool
```
When publishing RID-specific tool packages, the .NET CLI uses the version number of the top-level package to select the matching RID-specific packages. This means:

1. Update the project file to enable AOT and RID-specific packaging:
- All RID-specific packages must have the exact same version as the top-level package.
- All packages must be published to your feed before the top-level package becomes available.

```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<PackAsTool>true</PackAsTool>
<ToolCommandName>myfasttool</ToolCommandName>
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
<PublishAot>true</PublishAot>
<PackageId>MyFastTool</PackageId>
<Version>1.0.0</Version>
<Authors>Your Name</Authors>
<Description>A fast AOT-compiled tool</Description>
</PropertyGroup>
</Project>
```
To ensure a smooth publishing process:

1. Add your application code in `Program.cs`:
1. Publish all RID-specific packages first:

```csharp
Console.WriteLine("Hello from MyFastTool!");
Console.WriteLine($"Running on {Environment.OSVersion}");
```dotnetcli
dotnet nuget push yourtool.win-x64.1.0.0.nupkg
dotnet nuget push yourtool.linux-x64.1.0.0.nupkg
dotnet nuget push yourtool.osx-arm64.1.0.0.nupkg
dotnet nuget push yourtool.any.1.0.0.nupkg
```

1. Pack the top-level package:
1. Publish the top-level package last:

```dotnetcli
dotnet pack
dotnet nuget push yourtool.1.0.0.nupkg
```

1. Pack for each specific RID (on the corresponding platform):
Publishing the top-level package last ensures that all referenced RID-specific packages are available when users install your tool. If a user installs your tool before all RID packages are published, the installation will fail.

On Windows:
## Install and run tools

```dotnetcli
dotnet pack -r win-x64
```
Whether a tool uses RID-specific packaging is an implementation detail that's transparent to users. You install and run tools the same way, regardless of whether the tool developer opted into RID-specific packaging.

On Linux:
To install a tool globally:

```dotnetcli
dotnet pack -r linux-x64
```
```dotnetcli
dotnet tool install -g mytool
```

On macOS:
Once installed, you can invoke it directly:

```dotnetcli
dotnet pack -r osx-arm64
```
```dotnetcli
mytool
```

You can also use the `dnx` helper, which behaves similarly to `npx` in the Node.js ecosystem: it downloads and launches a tool in a single gesture if it isn't already present:

```dotnetcli
dnx mytool
```

1. Publish all packages to NuGet.org by using the [dotnet nuget push](dotnet-nuget-push.md) command.
When a tool uses RID-specific packaging, the .NET CLI automatically selects the correct package for your platform. You don't need to specify a RID—the CLI infers it from your system and downloads the appropriate RID-specific package.

## See also

Expand Down