Skip to content

Commit 32658e4

Browse files
Copilotrichlandergewarrenadegeo
authored
Clarify RID-specific .NET tools packaging and add hybrid pattern guidance (#51297)
* Initial plan * Update RID-specific tools documentation with hybrid packaging pattern and clarifications Co-authored-by: richlander <2608468+richlander@users.noreply.github.com> * Apply suggestion from @richlander * Clarify that RID-specific packaging is transparent to users Co-authored-by: richlander <2608468+richlander@users.noreply.github.com> * Add PublishAot to ToolPackageRuntimeIdentifiers example Co-authored-by: richlander <2608468+richlander@users.noreply.github.com> * Remove redundant AOT tool example section Co-authored-by: richlander <2608468+richlander@users.noreply.github.com> * Add example output for dotnet10-hybrid-tool Co-authored-by: richlander <2608468+richlander@users.noreply.github.com> * Apply markdown writing style improvements per review feedback Co-authored-by: richlander <2608468+richlander@users.noreply.github.com> * Apply suggestion from @richlander * Apply suggestion from @richlander * Apply suggestion from @richlander * Apply suggestion from @richlander * Apply suggestions from code review Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Andy (Steve) De George <67293991+adegeo@users.noreply.github.com> * Refine AOT tools packing instructions Updated the instructions for AOT tools to remove redundant commands and clarify RID-specific packing. * Clarify package structure, publishing order, and platform requirements for AOT Co-authored-by: richlander <2608468+richlander@users.noreply.github.com> * Add platform requirements and build options for Native AOT tools Co-authored-by: richlander <2608468+richlander@users.noreply.github.com> * Cleanup text --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: richlander <2608468+richlander@users.noreply.github.com> Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Co-authored-by: Andy (Steve) De George <67293991+adegeo@users.noreply.github.com>
1 parent 0afbd5c commit 32658e4

1 file changed

Lines changed: 165 additions & 76 deletions

File tree

docs/core/tools/rid-specific-tools.md

Lines changed: 165 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,17 @@ ai-usage: ai-assisted
1010

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

13-
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.
13+
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.
1414

1515
## Overview
1616

17-
Starting with .NET SDK 10, you can create .NET tools that target specific Runtime Identifiers (RIDs). These tools can be:
17+
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:
1818

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

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

2525
## Opt in to RID-specific packaging
2626

@@ -52,6 +52,7 @@ Alternatively, use `ToolPackageRuntimeIdentifiers` for tool-specific RID configu
5252
<OutputType>Exe</OutputType>
5353
<TargetFramework>net10.0</TargetFramework>
5454
<PackAsTool>true</PackAsTool>
55+
<PublishAot>true</PublishAot>
5556
<ToolCommandName>mytool</ToolCommandName>
5657
<ToolPackageRuntimeIdentifiers>win-x64;linux-x64;osx-arm64</ToolPackageRuntimeIdentifiers>
5758
</PropertyGroup>
@@ -60,13 +61,36 @@ Alternatively, use `ToolPackageRuntimeIdentifiers` for tool-specific RID configu
6061

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

64+
### When to use `RuntimeIdentifiers` vs `ToolPackageRuntimeIdentifiers`
65+
66+
Both `RuntimeIdentifiers` and `ToolPackageRuntimeIdentifiers` opt your tool into RID-specific packaging, but they serve slightly different purposes:
67+
68+
Use **`RuntimeIdentifiers`** when:
69+
70+
- You want the project to **build and publish RID-specific apps in general** (not just as a tool).
71+
- You're primarily targeting **CoreCLR** (non-AOT) or you want the standard SDK behavior where a single `dotnet pack` produces multiple RID-specific packages.
72+
- You may conditionalize `PublishAot` for a subset of RIDs, but you still want a CoreCLR-based package for every RID in `RuntimeIdentifiers`.
73+
74+
Use **`ToolPackageRuntimeIdentifiers`** when:
75+
76+
- You want to define **RID-specific behavior only for the tool packaging**, without changing how the project builds for other deployment scenarios.
77+
- You're using **Native AOT** and plan to **manually build** AOT binaries per RID with `dotnet pack -r <RID>`.
78+
- You want a **hybrid model** where some RIDs get Native AOT and others fall back to a portable CoreCLR implementation.
79+
80+
Notes:
81+
82+
- The top-level pointer package specifies the available RID-specific packages. If you specify `ToolPackageRuntimeIdentifiers`, it determines the tool RIDs; otherwise, `RuntimeIdentifiers` is used.
83+
- `ToolPackageRuntimeIdentifiers` should be equal to or a subset of the RIDs in `RuntimeIdentifiers`
84+
- When `PublishAot=true`, RID-specific packages are generated only when you pack for a specific RID (for example, `dotnet pack -r linux-x64`).
85+
- Native AOT builds (`PublishAot=true`) is only supported when the build OS and target OS match.
86+
6387
## Package your tool
6488

6589
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.
6690

6791
### RID-specific and self-contained tools
6892

69-
For tools without AOT compilation, run `dotnet pack` once:
93+
Run `dotnet pack` once:
7094

7195
```dotnetcli
7296
dotnet pack
@@ -83,124 +107,189 @@ This command creates multiple NuGet packages:
83107

84108
### AOT tools
85109

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

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

90-
```dotnetcli
91-
dotnet pack
114+
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).
115+
116+
This means you have several options for building Native AOT packages:
117+
118+
- **Build only for your development machine**: Support Native AOT only for the OS you're developing on.
119+
- **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.
120+
- **Federate your build across machines**: Use CI/CD systems like GitHub Actions or Azure DevOps Pipelines to build on different operating systems.
121+
122+
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.
123+
124+
#### Packing Native AOT tools
125+
126+
Pack the top-level package once (on any platform):
127+
128+
```dotnetcli
129+
dotnet pack
130+
```
131+
132+
Pack for each specific RID on the corresponding platform, for example:
133+
134+
```dotnetcli
135+
dotnet pack -r linux-x64
136+
```
137+
138+
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).
139+
140+
When you set `PublishAot` to `true`, the packing behavior changes:
141+
142+
- `dotnet pack` produces the **top-level pointer package** (package type `DotnetTool`).
143+
- 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`.
144+
145+
### Hybrid AOT + CoreCLR packaging pattern (example)
146+
147+
Some tools want the best of both worlds:
148+
149+
- **Native AOT** for a subset of high-priority platforms (depending on the tool).
150+
- A **portable CoreCLR fallback** that works on platforms not targeted by the Native AOT builds.
151+
152+
You can achieve this "hybrid" model with the following pattern:
153+
154+
1. **Configure the tool for Native AOT and tool-specific RIDs.**
155+
156+
In your project file, use `ToolPackageRuntimeIdentifiers` and enable `PublishAot`.
157+
158+
For example:
159+
160+
```xml
161+
<ToolPackageRuntimeIdentifiers>osx-arm64;linux-arm64;linux-x64;any</ToolPackageRuntimeIdentifiers>
162+
<PublishAot>true</PublishAot>
92163
```
93164

94-
- Pack for each specific RID on the corresponding platform:
165+
1. **Create the pointer package.**
166+
167+
Run `dotnet pack` once (on any platform) to build the top-level package that points to the RID-specific packages:
95168

96169
```dotnetcli
97-
dotnet pack -r win-x64
98-
dotnet pack -r linux-x64
99-
dotnet pack -r osx-arm64
170+
dotnet pack
100171
```
101172

102-
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).
173+
1. **Build Native AOT packages for selected RIDs.**
103174

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

106-
### Package types
177+
For example:
107178

108-
RID-specific tool packages use two package types:
179+
```
180+
dotnet pack -r linux-x64
181+
```
109182

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

113-
### Package metadata
185+
To provide a universal fallback, pack the `any` RID without AOT:
114186

115-
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.
187+
```dotnetcli
188+
dotnet pack -r any -p:PublishAot=false
189+
```
116190

117-
## Publish your tool
191+
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.
192+
193+
> [!NOTE]
194+
> 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:
195+
>
196+
> - `mcr.microsoft.com/dotnet/sdk:10.0-noble-aot`
197+
>
198+
> This is useful when your development machine isn't running Linux natively.
199+
200+
In this hybrid setup:
201+
202+
- The pointer package (`yourtool.<version>.nupkg`) references both:
203+
- RID-specific Native AOT packages (for example, `yourtool.osx-arm64`, `yourtool.linux-x64`).
204+
- The `any` CoreCLR package as a fallback.
205+
- The .NET CLI automatically picks the most appropriate package for the user's platform when they run `dotnet tool install` or `dnx`.
206+
207+
#### Example: `dotnet10-hybrid-tool`
208+
209+
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).
118210

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

121213
```dotnetcli
122-
dotnet nuget push path/to/package/root/*.nupkg
214+
dotnet tool install -g dotnet10-hybrid-tool
215+
dotnet10-hybrid-tool
123216
```
124217

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

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

129-
```dotnetcli
130-
dnx mytool
222+
```output
223+
Hi, I'm a 'DotNetCliTool v2' tool!
224+
Yes, I'm quite fancy.
225+
226+
Version: .NET 10.0.2
227+
RID: osx-arm64
228+
Mode: Native AOT
131229
```
132230

133-
The CLI automatically:
231+
Example output on a platform using the CoreCLR fallback:
134232

135-
1. Downloads the top-level package.
136-
1. Reads the RID-specific metadata.
137-
1. Identifies the most appropriate package for the current platform.
138-
1. Downloads and runs the RID-specific package.
233+
```output
234+
Hi, I'm a 'DotNetCliTool v2' tool!
235+
Yes, I'm quite fancy.
139236
140-
## Example: Create an AOT tool
237+
Version: .NET 10.0.2
238+
RID: win-x64
239+
Mode: CoreCLR
240+
```
141241

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

144-
1. Create a new console application:
244+
## Publish your tool
145245

146-
```dotnetcli
147-
dotnet new console -n MyFastTool
148-
cd MyFastTool
149-
```
246+
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:
150247

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

153-
```xml
154-
<Project Sdk="Microsoft.NET.Sdk">
155-
<PropertyGroup>
156-
<OutputType>Exe</OutputType>
157-
<TargetFramework>net10.0</TargetFramework>
158-
<PackAsTool>true</PackAsTool>
159-
<ToolCommandName>myfasttool</ToolCommandName>
160-
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
161-
<PublishAot>true</PublishAot>
162-
<PackageId>MyFastTool</PackageId>
163-
<Version>1.0.0</Version>
164-
<Authors>Your Name</Authors>
165-
<Description>A fast AOT-compiled tool</Description>
166-
</PropertyGroup>
167-
</Project>
168-
```
251+
To ensure a smooth publishing process:
169252

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

172-
```csharp
173-
Console.WriteLine("Hello from MyFastTool!");
174-
Console.WriteLine($"Running on {Environment.OSVersion}");
255+
```dotnetcli
256+
dotnet nuget push yourtool.win-x64.1.0.0.nupkg
257+
dotnet nuget push yourtool.linux-x64.1.0.0.nupkg
258+
dotnet nuget push yourtool.osx-arm64.1.0.0.nupkg
259+
dotnet nuget push yourtool.any.1.0.0.nupkg
175260
```
176261

177-
1. Pack the top-level package:
262+
1. Publish the top-level package last:
178263

179264
```dotnetcli
180-
dotnet pack
265+
dotnet nuget push yourtool.1.0.0.nupkg
181266
```
182267

183-
1. Pack for each specific RID (on the corresponding platform):
268+
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.
184269

185-
On Windows:
270+
## Install and run tools
186271

187-
```dotnetcli
188-
dotnet pack -r win-x64
189-
```
272+
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.
190273

191-
On Linux:
274+
To install a tool globally:
192275

193-
```dotnetcli
194-
dotnet pack -r linux-x64
195-
```
276+
```dotnetcli
277+
dotnet tool install -g mytool
278+
```
196279

197-
On macOS:
280+
Once installed, you can invoke it directly:
198281

199-
```dotnetcli
200-
dotnet pack -r osx-arm64
201-
```
282+
```dotnetcli
283+
mytool
284+
```
285+
286+
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:
287+
288+
```dotnetcli
289+
dnx mytool
290+
```
202291

203-
1. Publish all packages to NuGet.org by using the [dotnet nuget push](dotnet-nuget-push.md) command.
292+
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.
204293

205294
## See also
206295

0 commit comments

Comments
 (0)