.NET 10 shipped dnx
(dotnet tool exec): one-shot nuget tools, npx-style. You don’t install
them. You just run dnx stop and it downloads into the nuget cache and
starts.
NOTE: I’m using
stopas the sample Native AOT tool. It sends SIGINT (Ctrl+C) to a process in a cross-platform way — same as hitting Ctrl+C, notStop-Process/kill -9. The RID-specific packages are native binaries, so they run with no .NET installed at all.
The catch: dnx is an SDK command. There is even an
issue to ship it with the runtime.
Until that happens (if it happens), you pay for the whole SDK just to
start a tool. And even when the nupkg is already in the cache, a cached
dnx stop@2.1.0 is still ~500ms. That’s the SDK tax.
This gets more annoying once tools are RID-specific Native AOT. Starting a native exe through the SDK is kinda the opposite of the point.
So I built ndnx. Same CLI. Native AOT itself. No SDK.
TL;DR;
macOS / Linux:
curl -fsSL https://github.com/devlooped/ndnx/releases/latest/download/install.sh | sh
Windows (PowerShell):
irm https://github.com/devlooped/ndnx/releases/latest/download/install.ps1 | iex
Then:
ndnx stop --help
If you can dnx stop, you can ndnx stop:
dnx stop --help
ndnx stop --help
dnx stop@2.1.0 --help
ndnx stop@2.1.0 --help
dnx dotnetsay@1.0.0 Hello
ndnx dotnetsay@1.0.0 Hello
You don’t need -- before the tool args. Things Just Work.
stop is Native AOT. So is winget
(Scott’s winget TUI, Windows-only). dotnetsay is a classic
framework-dependent tool. All of them are just nuget packages. ndnx picks
the host RID and starts Runner=executable binaries directly, or
dotnet exec for Runner=dotnet.
A first run downloads into the nuget cache. A later ndnx tool@version
starts the cached binary. No SDK. No restore.
It is not a nuget global tool on purpose. Installing the thing that lets
you skip the SDK with dnx would be a pretty stupid chicken-and-egg.
Same cache as dnx
ndnx uses the same nuget cache as dnx and dotnet restore. If you
already ran dnx stop@2.1.0, ndnx stop@2.1.0 just starts it. The
other way around too.
Pin an exact version and a second run does not hit the feed. You also
don’t pass a RID: ndnx stop picks your OS/arch, same as dnx.
The flags are the dnx ones: --source, --add-source, --configfile,
--version, --prerelease, --yes/-y, --allow-roll-forward,
--verbosity, --disable-parallel, --ignore-failed-sources,
--no-http-cache, --interactive.
Startup time
Cold is empty-cache download → start. Cached is start only (tool@version
so neither runner resolves latest). linux-x64 (WSL2) is SDK 10.0.110;
win-x64 is SDK 10.0.303. winget is win-x64 SDK 11.0.100-preview.
| Tool | Runner | Cold linux-x64 | Cold win-x64 | Cached linux-x64 | Cached win-x64 |
|---|---|---|---|---|---|
stop@2.1.0 (native AOT) |
dnx |
2.2 s | 2.1 s | 580 ms | 520 ms |
ndnx |
1.2 s | 1.5 s | 10 ms | 34 ms | |
dotnetsay@1.0.0 (framework-dependent) |
dnx |
1.6 s | 1.8 s | 570 ms | 545 ms |
ndnx |
849 ms | 1.1 s | 12 ms | 50 ms | |
winget (native AOT TUI, Windows) |
dnx |
— | 2.7 s | — | 820 ms |
ndnx |
— | 1.2 s | — | 275 ms |
Cold is the median of 3 empty-cache runs. Cached is the median of 10 runs
after one seed. Isolated NUGET_PACKAGES. stop / dotnetsay from a
private feed; winget from nuget.org. dnx is the SDK script
(dotnet dnx).
winget is a TUI so it does not exit on its own. Cold is winget
(latest), cached is winget@0.13.2. Each run waits until winget-tui
is up, then ndnx stop@2.1.0 signals that PID. The extra ~250ms vs
stop is the TUI starting, not ndnx.
10ms vs 580ms to start a native tool that is already on disk. That’s the number that made me write this.
Framework-dependent tools still need a runtime (dotnet on PATH or
DOTNET_ROOT). Native ones don’t. ndnx preflights Runner=dotnet against
the muxer’s Microsoft.NETCore.App so you get a sane error instead of a
host crash.
Floating versions stay current
I already wanted this in 2021 for long-running tools, which is how
dotnet-evergreen
happened. dnx doesn’t do it. ndnx does, for free, when you don’t pin.
A floating version — unspecified, @*, @*-*, or a nuget range — stays
current. ndnx starts the latest match, watches the feed, downloads a
newer matching version before stopping the child, then sends SIGINT /
Ctrl+C (and WM_CLOSE if the tool is a GUI) and restarts. Short tools
still exit as soon as the child exits.
Pin an exact version (tool@2.1.0) to disable that loop.
The poll interval defaults to 5 seconds and can be set in
.netconfig:
[ndnx]
interval = 5
ndnx walks from the working directory up, then ~/.netconfig.
--verbosity quiet hides the Updating … line.
This is the interesting path for anything that stays up: MCP servers, agent harnesses, bots. Long-running, want the latest build, don’t want to bounce them yourself.
Update ndnx itself
Self-update from GitHub Releases (optional version, including downgrades):
ndnx --update
ndnx --update 0.1.0
Learn more in the project repository.
Enjoy!
/kzu dev↻d