Skip to content

Add Native AOT support for tool list --local, tool run, tool uninstall --local, and tool search - #54827

Merged
NikolaMilosavljevic merged 3 commits into
dotnet:baronfel/tool-resolution-aotfrom
NikolaMilosavljevic:aot.tool
Jun 18, 2026
Merged

NikolaMilosavljevic merged 3 commits into
dotnet:baronfel/tool-resolution-aotfrom
NikolaMilosavljevic:aot.tool

Conversation

@NikolaMilosavljevic

@NikolaMilosavljevic NikolaMilosavljevic commented Jun 17, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #54810 (baronfel/tool-resolution-aot). This PR targets that branch so the diff under review is just the dotnet tool subcommand wiring. #54810 already makes tool-command resolution/invocation AOT-able (and links the shared manifest/package + command-resolution sources, grants InternalsVisibleTo for IFileSystem, and gates IProject/MSBuild off the AOT path); this PR builds on that to run the subcommands in-process. Once #54810 merges, this can be retargeted to main.

Summary

Enables Native AOT support for the AOT-capable subset of dotnet tool subcommands: local tool list, local tool uninstall, tool run, and tool search.

Following the unified-parser model (#54653), ToolCommandParser.ConfigureCommand is shared between the managed and AOT paths via inline #if CLI_AOT regions, the AOT CLI links the real command implementations, and commands/options that cannot run under AOT throw CommandNotAvailableInAotException so NativeEntryPoint re-hosts the managed CLI.

What runs under AOT vs. falls back

dotnet tool invocation AOT behavior
tool list / tool list --local Runs in AOT (ToolListLocalCommand)
tool uninstall <pkg> (local) Runs in AOT (ToolUninstallLocalCommand)
tool run <cmd> Runs in AOT (ToolRunCommand)
tool search <term> Runs in AOT (HttpClient + STJ source-gen)
tool list --global / --tool-path Falls back to managed
tool uninstall --global / --tool-path Falls back to managed
bare tool, install, update, restore, execute Falls back to managed

Changes

  • ToolCommandParser.cs — AOT-aware ConfigureCommand. Under #if CLI_AOT it only overrides the AOT-capable paths: local list/uninstall guard on LocationOptions.IsGlobalOrToolPath (global/tool-path throw fallback) and run/search call the real commands. Bare tool and install/update/restore/execute keep the recursive default fallback already set by ConfigureAotActions. The managed #else branch is unchanged from before.
  • NugetToolSearchApiRequest.cs — under #if CLI_AOT, resolve the NuGet service index with HttpClient + a System.Text.Json source-generated context instead of NuGet.Protocol. The rest of the search pipeline (STJ deserializer, PrintableTable) was already AOT-friendly.
  • Parser.cs — wire ToolCommandParser.ConfigureCommand into ConfigureAotActions.
  • AotSourceFiles.props — link only the sources unique to running these subcommands in-process (the parser, the four command implementations, PrintableTable, and the NuGet search sources); the shared manifest/package and command-resolution infrastructure is already linked by Make tool-command resolution and invocation AOT-able #54810.
  • AotParserTests.cs — parse coverage for the AOT-capable tool commands plus fallback assertions for the managed-only variants.

Validation

  • dotnet-aot.csproj and managed dotnet.csproj: build with 0 warnings / 0 errors.
  • Full Native AOT publish (-r win-x64): ILC generates native code with zero AOT/trim warnings.
  • AotParserTests: 38/38 pass (parse + managed-fallback theories, plus an invoke-level assertion that tool list executes on the AOT path without falling back).

Performance

Measured with eng/gather-otel.ps1 against the locally-built redist dotnet.exe, toggling DOTNET_CLI_ENABLEAOT (Debug, win-x64). End-to-end wall-clock per invocation:

dotnet tool list — fully handled on the AOT fast path (no network):

mean median min max
managed 378.2 ms 374.9 ms 363.8 ms 417.2 ms
AOT 68.4 ms 67.6 ms 64.4 ms 72.8 ms

→ −309.8 ms, 5.5× faster, 81.9% lower latency (12 timed runs/path, 2 warmups discarded).

dotnet tool search exercises the new HttpClient + STJ service-index path and returns identical results to the managed path. Its end-to-end time is dominated by NuGet network I/O (~700 ms of service-index + query round-trips), so the ~300 ms startup win is present but not the dominant cost (managed ≈ 875 ms vs AOT ≈ 922 ms mean, within network variance).

Copilot AI review requested due to automatic review settings June 17, 2026 17:35
@baronfel baronfel added the Area-dotnet AOT Items that are part of the dotnet CLI AOT-ification effort label Jun 17, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the Native AOT CLI “fast-path” to cover an AOT-capable subset of dotnet tool by wiring the real command implementations into the shared unified parser and falling back to the managed CLI for unsupported variants.

Changes:

  • Wire ToolCommandParser.ConfigureCommand into the AOT action configuration so local tool list/tool uninstall, tool run, and tool search execute natively while other tool flows throw CommandNotAvailableInAotException for managed fallback.
  • Make dotnet tool search service-index resolution AOT-friendly by replacing NuGet.Protocol usage with HttpClient + System.Text.Json source generation under #if CLI_AOT.
  • Trim resolver/env-var helper code paths under #if !CLI_AOT, and link the required tool/manifest/search sources into dotnet-aot via AotSourceFiles.props, plus add parser coverage in dotnet-aot.Tests.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/dotnet-aot.Tests/AotParserTests.cs Adds parse + fallback assertions for the AOT-capable tool subcommands and managed-only variants.
src/Cli/Microsoft.DotNet.InternalAbstractions/Properties/Properties.cs Adds InternalsVisibleTo for dotnet-aot and dotnet-aot.Tests.
src/Cli/dotnet/Parser.cs Wires ToolCommandParser.ConfigureCommand into ConfigureAotActions.
src/Cli/dotnet/NugetSearch/NugetToolSearchApiRequest.cs Adds AOT-safe NuGet service-index resolution via HttpClient + STJ source-gen.
src/Cli/dotnet/Commands/Tool/ToolCommandParser.cs Implements AOT-aware tool action wiring with selective managed fallback.
src/Cli/dotnet/CommandFactory/CommandSpec.cs Excludes project-based env-var helper under CLI_AOT.
src/Cli/dotnet/CommandFactory/CommandFactoryUsingResolver.cs Excludes resolver-heavy overloads under CLI_AOT, keeping Create(CommandSpec).
src/Cli/dotnet-aot/AotSourceFiles.props Links the tool-manifest/tool-package infrastructure and AOT-capable tool command sources into the AOT build.

Comment thread src/Cli/dotnet/NugetSearch/NugetToolSearchApiRequest.cs
Comment on lines +249 to +261
[Theory]
[InlineData("tool list")]
[InlineData("tool list --local")]
[InlineData("tool run mytool")]
[InlineData("tool uninstall mypackage")]
[InlineData("tool search mysearchterm")]
public void ParseAotToolCommand_HasNoErrors(string commandLine)
{
// The AOT-capable `tool` subcommands (local list/uninstall, run, search) parse cleanly
// because their real implementations are linked into the AOT CLI.
var result = Parser.Parse(commandLine.Split(' '));
Assert.Empty(result.Errors);
}

@NikolaMilosavljevic NikolaMilosavljevic Jun 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in fc928d6 — added InvokeAotToolListCommand_ExecutesWithoutManagedFallback which invokes tool list / tool list --local and asserts exit code 0 (no CommandNotAvailableInAotException), so a mis-wired action would now be caught. tool list succeeds with empty output when no manifest is present (ToolManifestFinder.Inspect returns empty rather than throwing).

@NikolaMilosavljevic
NikolaMilosavljevic requested review from a team as code owners June 17, 2026 17:50
@NikolaMilosavljevic
NikolaMilosavljevic changed the base branch from main to baronfel/tool-resolution-aot June 17, 2026 17:51
@Nigusu-Allehu
Nigusu-Allehu self-requested a review June 17, 2026 17:52
…al, and tool search

Align the AOT-capable `dotnet tool` subcommands with the unified parser model
(PR dotnet#54653) used by the `sln` and `sdk check` commands: link the real command
implementations into the AOT CLI instead of maintaining a separate self-contained
handler, and wire them via an AOT-aware ToolCommandParser.ConfigureCommand.

- ToolCommandParser: under #if CLI_AOT, the local `list`/`uninstall`, `run`,
  and `search` paths call the real commands. The bare `tool` command, the
  `--global`/`--tool-path` variants, and `install`/`update`/`restore`/
  `execute` throw CommandNotAvailableInAotException so NativeEntryPoint falls
  back to the managed CLI.
- NugetToolSearchApiRequest: swap the NuGet.Protocol service-index lookup for an
  HttpClient + System.Text.Json source-gen path under #if CLI_AOT; the rest of the
  search pipeline (STJ deserializer, PrintableTable) is already AOT-friendly.
- CommandFactoryUsingResolver / CommandSpec: guard the resolver-policy overloads
  and the project-based env-var helper under #if !CLI_AOT, keeping only the
  CommandSpec path that `tool run` needs.
- AotSourceFiles.props: link the tool manifest/package infrastructure, the local
  list/uninstall/run/search implementations, and the NuGet search sources.
- Grant InternalsVisibleTo to dotnet-aot(.Tests) from InternalAbstractions for
  IFileSystem.
- AotParserTests: cover parsing of the AOT-capable tool commands and assert the
  managed-only variants fall back via CommandNotAvailableInAotException.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

@baronfel baronfel left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one nit, and we can come back and fully light this up when the AOT NuGEt APIs are available.

One thing that would be great is if you were able to take before/after stats similar to how I did in my PR using the otel spans + dashboard. Helps me have easily-accessible data for product announcements later on.

Comment thread src/Cli/dotnet/Commands/Tool/ToolCommandParser.cs Outdated
The recursive default fallback in ConfigureAotActions already sets every
tool subcommand (and the bare tool command) to throw
CommandNotAvailableInAotException, so re-asserting it for the bare command
and install/update/restore/execute was redundant. Keep only the actions
that differ from the default (local list/uninstall guards, run, search).

Addresses PR review feedback from @baronfel.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NikolaMilosavljevic

Copy link
Copy Markdown
Contributor Author

LGTM with one nit, and we can come back and fully light this up when the AOT NuGEt APIs are available.

One thing that would be great is if you were able to take before/after stats similar to how I did in my PR using the otel spans + dashboard. Helps me have easily-accessible data for product announcements later on.

Will provide data soon.

CliStrings.NonRetriableNugetSearchFailure,
serviceIndexUrl, $"{searchQueryServiceType} not found in service index", response.StatusCode));

return new Uri(resource.Id);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I see we are trusting the nuget server to return a valid uri. If we want to be more defensive, we should catch an exception that could be thrown if the uri is malformed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2c4c1a8. new Uri(resource.Id) is now wrapped in a try/catch for UriFormatException/ArgumentException and translated into a NugetSearchApiRequestException (NonRetriableNugetSearchFailure), so a malformed server-supplied URL stays a GracefulException like the rest of the method.

Comment thread src/Cli/dotnet/NugetSearch/NugetToolSearchApiRequest.cs
… URL

Address review nits from @Nigusu-Allehu on the CLI_AOT DomainAndPath():
- Dispose the HttpResponseMessage via a using block.
- Defensively translate a malformed server-supplied service-index URL into a
  NugetSearchApiRequestException (GracefulException) instead of letting
  UriFormatException/ArgumentException escape unhandled.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@NikolaMilosavljevic

Copy link
Copy Markdown
Contributor Author

Perf data added to the description under Performance (captured with your eng/gather-otel.ps1 + Aspire dashboard). Headline: dotnet tool list goes 378.2 ms → 68.4 ms mean — 5.5× faster, 81.9% lower latency. dotnet tool search is network-bound (NuGet service-index + query), so the ~300 ms startup win is dwarfed by I/O there, but the new STJ/HTTP path returns identical results.

@NikolaMilosavljevic

Copy link
Copy Markdown
Contributor Author

/azl run dotnet-sdk-public-ci

@NikolaMilosavljevic
NikolaMilosavljevic merged commit e45383d into dotnet:baronfel/tool-resolution-aot Jun 18, 2026
5 of 7 checks passed
baronfel pushed a commit that referenced this pull request Jun 18, 2026
…l --local, and tool search (#54827)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area-dotnet AOT Items that are part of the dotnet CLI AOT-ification effort

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants