Unify the managed and NativeAOT CLI parsers into one shared implementation - #54653
Conversation
There was a problem hiding this comment.
Pull request overview
This PR unifies the managed dotnet.dll and NativeAOT bridge dotnet-aot.dll CLI parsing so both entrypoints build the same full DotNetCommandDefinition command tree, enabling consistent parsing/help output and allowing the AOT path to execute a limited set of commands while falling back to the managed CLI for everything else.
Changes:
- Collapses the previous split managed/AOT parser into one shared
Parserwith small#if CLI_AOTaction-wiring differences. - Enables AOT to render
--helpacross the full command surface (including external-tool forwarders) and to emit--cli-schema. - Updates AOT tests and AOT build inputs to cover full-tree parsing/help/schema plus managed-fallback behavior; removes the now-dead
CommandLineInfoduplicate.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| test/dotnet-aot.Tests/dotnet-aot.Tests.csproj | Updates import comment for shared AOT sources. |
| test/dotnet-aot.Tests/AotParserTests.cs | Expands test coverage to full-tree parsing, help rendering, cli-schema, and fallback behavior. |
| src/Common/EnvironmentVariableNames.cs | Adds XML doc for DOTNET_CLI_TELEMETRY_OPTOUT. |
| src/Cli/Microsoft.DotNet.Cli.Utils/MSBuildForwardingAppWithoutLogging.cs | Adds forceOutOfProc support and extensive XML docs around MSBuild forwarding behavior. |
| src/Cli/Microsoft.DotNet.Cli.Definitions/Commands/DotNetCommandDefinition.cs | Moves to a shared root Command definition, adds recursive --help, and adjusts directive handling. |
| src/Cli/dotnet/ParserOptionActions.cs | Shares option actions across managed/AOT, adds AOT-friendly option-presence checks, and AOT-safe --info/--cli-schema behavior. |
| src/Cli/dotnet/Parser.cs | Centralizes parser creation and adds AOT fallback wiring, shared help builder, and shared exception handling. |
| src/Cli/dotnet/NuGetSignatureVerificationEnabler.cs | Excludes MSBuild-specific overload from AOT compilation. |
| src/Cli/dotnet/Extensions/ParseResultExtensions.cs | Moves parsing helpers needed by AOT out of #if !CLI_AOT and refactors shared helpers. |
| src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs | Excludes in-proc MSBuild logger creation from AOT while keeping LLM adjustments shared. |
| src/Cli/dotnet/Commands/NuGet/NuGetCommand.cs | Forces out-of-proc NuGet forwarding under AOT. |
| src/Cli/dotnet/Commands/MSBuild/MSBuildForwardingApp.cs | Forces out-of-proc MSBuild forwarding under AOT and adjusts distributed logger wiring for AOT constraints. |
| src/Cli/dotnet/CommandLineInfo.cs | Removes obsolete AOT-only version/info implementation. |
| src/Cli/dotnet/CliSchema.cs | Switches to AOT-safe schema export overload and improves enum default rendering. |
| src/Cli/dotnet-aot/NativeEntryPoint.cs | Uses shared parser + shared exception handling; adds parse-time fallback safety and structured activities. |
| src/Cli/dotnet-aot/DESIGN.md | Updates design doc to reflect full-tree AOT parsing + fallback model and shared option actions. |
| src/Cli/dotnet-aot/AotSourceFiles.props | Updates linked shared sources and adds forwarding-app sources needed for AOT help. |
| src/Cli/dn/dn-native-debug.vcxproj | Removes now-deleted CommandLineInfo.cs from native debug project items. |
|
I used sizoscope-cli to diff the impact of this PR on the generated NAOT library: > dnx sizoscope-cli \
E:\code\dotnet-sdk\artifacts\obj\dotnet-aot\Release\net11.0\win-x64\native\dotnet-aot.mstat \
E:\code\copilot-worktrees\sdk\baronfel-studious-eureka\artifacts\obj\dotnet-aot\Release\net11.0\win-x64\native\dotnet-aot.mstat \
--output dotnet-aot-sizoscope-diff.mdNet result is +781kB, mostly from the Microsoft.DotNet.Cli.Definitions library and embedded resource strings for the many languages we support. |
… to managed CLI Extracts the file-based app detection condition from Program.TryRunFileBasedApp into a shared ParseResultExtensions.GetFileBasedAppEntryPointToken helper (built on the AOT-safe VirtualProjectBuilder.IsValidEntryPointPath). The managed CLI reuses it to re-dispatch 'dotnet app.cs' as 'dotnet run --file', and the AOT NativeEntryPoint uses it to defer such invocations to the managed CLI rather than printing root usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…or MSBuild cleanup - AOT NativeEntryPoint now defers any unrecognized top-level token to the managed CLI, not just file-based apps: 'dotnet <external>' (e.g. dotnet ef) and 'dotnet app.cs' both land on the root's hidden subcommand argument and require managed external-command / run-pipeline resolution. Generalized the detection into ParseResultExtensions.RequiresManagedCommandResolution. - Removed an unused using, fixed an unindented comment, filled in an empty XML doc summary, and fixed a grammar typo in the MSBuild forwarding apps. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NormalizeRootOptions matched the help option with a name-based check (Option<bool> whose Name equals --help), a leftover from when DotNetCommandDefinition briefly carried an explicit help option. With DotNetCommandDefinition back to a RootCommand base, the System.CommandLine-injected HelpOption's Name is not --help, so the match failed and the option kept the default S.CL help action. That caused every command's --help/-h to render via the default builder (wrong description, default-value display, arg-name display) and broke the help-output snapshot tests across dotnet.Tests and dotnet-new.IntegrationTests. Restore the canonical 'option is HelpOption' type check (matching main) so help is served by DotnetHelpBuilder again. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
11891d4 to
41badfe
Compare
|
/ba-g test failures aren't related to this change, known flaky tests |
Per review feedback, the unified parser (dotnet#54653) renders the full command tree and help from AOT, so falling back to the managed CLI just to print a missing-command error + usage is unnecessary overhead. Bare 'dotnet sdk' and 'dotnet sln' now render from AOT; only 'sln add' still falls back since it requires MSBuild. Moved HandleMissingCommand out of the !CLI_AOT block so it compiles into AOT. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Running the AOT CLI tests in CI surfaced real failures from the new tests added in dotnet#54653: * GetFileBasedAppEntryPointToken pulls in Microsoft.Build, which cannot be linked into a NativeAOT image (ILC reports the method 'will always throw'). Skip the three affected AotParserTests under NativeAOT (guarded by RuntimeFeature.IsDynamicCodeSupported, so the managed test run still exercises them). Tracked by dotnet#54806. * InvokeCliSchema_RendersSchemaJsonFromAot evaluates a shell-completion default that requires SHELL to be set; the bare AzDO script task does not export it on Linux. Set SHELL on the AOT test step. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…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>
…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>
Goal
Unify the two
dotnetCLI parser entrypoints — the managed CLI (dotnet.dll) and the NativeAOT bridge (dotnet-aot.dll) — into a single sharedParserimplementation, and push as much parsing work as possible into the native ("AOT bubble") path.Previously
src/Cli/dotnet/Parser.cswas split by one giant top-level#if CLI_AOT … #else … #endifthat produced two almost-entirely-differentParserclasses: the AOT side hand-rolled a minimal command tree (only--version/--info/sln), while the managed side built the full tree. That duplicated all the shared plumbing and meant the AOT entrypoint could not parse or serve help for the real command surface.Now both binaries build the same full
DotNetCommandDefinitiontree and share the same parsing, help, version/info, cli-schema, and exception-handling code. The only differences are small, inline#if CLI_AOTregions where a code path genuinely cannot run under AOT yet — and in those cases the command transparently falls back to the managed CLI.How the AOT fast-path works
When
DOTNET_CLI_ENABLEAOT=true,NativeEntryPointparses with the shared parser and:--version,--info,--cli-schema,--helpfor every command, and the AOT-capableslnsubcommands) execute natively and return.CommandNotAvailableInAotException; the bridge catches it (and any unexpected parse-time failure) and hosts the manageddotnet.dllto run the command — so behavior is identical, just slower on first use.Highlights
One shared
Parser.cs#ifinto a singleParserclass. Shared members (response-file handling, parser/invocation config,Parse/Invoke/InvokeAsync,GetBuiltInCommand,ExceptionHandler,DotnetHelpBuilder) are written once.#if CLI_AOTblocks (ConfigureManagedActionsvsConfigureAotActions), matching the patterns already used elsewhere in the repo (SolutionCommandParser,Program.cs).Full command surface parses + helps from AOT
DotNetCommandDefinition, so parsing and--helpmatch the managed CLI exactly.--helpis served entirely from the native path for every command — including the external-tool commands (msbuild/nuget/vstest/format/fsi). Rather than special-casing help, the forwarding apps themselves were made AOT-friendly so the help writer (DotnetHelpBuilder) has zero conditional compilation.Forwarding apps made AOT-safe (out-of-process)
MSBuildForwardingAppforces out-of-process forwarding under AOT (addedforceOutOfProctoMSBuildForwardingAppWithoutLogging) and skips the in-proc engine; the LLM-environment terminal-logger adjustment fromCommonRunHelpers.AdjustMSBuildForLLMsnow applies in AOT too (only the in-procGetConsoleLoggeris excluded).NuGetCommandalways uses the shelling-outNuGetForwardingApprunner under AOT (never the in-procNuGet.CommandLine.XPlatrunner).fsi/format/vstestforwarders (already process-spawn) plus their dependencies were added to the AOT compile set.Shared option actions
--versionand--infouse the samePrintVersionAction/PrintInfoActionas the managed CLI;--infoonly#if !CLI_AOT-omits the workload/MSBuild version lines that aren't AOT-compatible yet.--cli-schemaruns in AOT via the source-generatedJsonSerializerContext. As part of this,CliSchema.HumanizeValuenow renders all enum option/argument defaults as their name (previously onlyVerbosityOptions), which both reads better and is required for source-gen serialization;GetJsonSchemawas switched to the AOT-safeJsonTypeInfooverload ofGetJsonSchemaAsNode.CliSchema.csnow has no AOT conditional compilation.ExceptionHandleris now shared and invoked from the AOT path for non-fallback exceptions, keeping error reporting at parity with the managed CLI.Cleanup
CommandLineInfo.cs— it was an AOT-only duplicate of the version/info output that became dead once the realPrintVersionAction/PrintInfoActionwere made AOT-able.Validation
dotnet.csprojbuilds clean; behavior is unchanged (logic reorganized, not altered).dotnet-aot.csprojAOT-publishes forwin-x64with 0 ILC/trim warnings (the full command tree + all included forwarders + cli-schema serialization are trim/AOT-clean).dotnet-aot.Testspass, including new coverage fordotnet --help,build --help, external-tool help rendering from AOT,--cli-schemaJSON output, and the managed-fallback contract.