Skip to content

Enable 'dotnet sdk check' command in AOT native binary - #54391

Merged
baronfel merged 31 commits into
dotnet:mainfrom
NikolaMilosavljevic:aot3
Jun 29, 2026
Merged

baronfel merged 31 commits into
dotnet:mainfrom
NikolaMilosavljevic:aot3

Conversation

@NikolaMilosavljevic

Copy link
Copy Markdown
Contributor

Summary

Enables the dotnet sdk check command to run in the AOT-compiled native CLI binary, improving cold-start performance for this command.

Changes

  • Parser.cs: Register sdk check command in the #if CLI_AOT section with SdkCheckCommand.Run action
  • dotnet-aot.csproj: Add compile items (CommandBase, PrintableTable, 6 sdk-check files), embedded resources (CliStrings.resx, CliCommandStrings.resx), and Microsoft.Deployment.DotNet.Releases package reference
  • NativeEntryPoint.cs: Extract ExecuteCore method so the entire [UnmanagedCallersOnly] entry point body is wrapped in a top-level try/catch — no managed exception can cross the unmanaged boundary. Also sets Program.DotnetRoot and adds inner exception handling for AOT invoke path.
  • Program.cs: Add Program.DotnetRoot static property (AOT-only) to thread host-provided dotnet root to commands; add try/catch in Main
  • CommandBase.cs: Guard ParseResultExtensions import and ShowHelpOrErrorIfAppropriate call with #if !CLI_AOT (these depend on types not available in AOT build)
  • SdkCheckCommand.cs: Conditional extern alias, conditional EnvironmentProvider using, thread Program.DotnetRoot in AOT path

Design Decisions

  • sdk check was chosen as the first new AOT command because it has zero MSBuild/NuGet dependencies and does all work in-process (HTTP fetch + JSON parsing + table formatting)
  • Microsoft.Deployment.DotNet.Releases uses JsonDocument DOM API (no reflection) — verified AOT-safe
  • Full CliStrings.resx and CliCommandStrings.resx are embedded (size tradeoff for simplicity over splitting ~20 needed strings)
  • NativeEntryPoint.Execute wraps the entire method body in try/catch since [UnmanagedCallersOnly] methods must never let managed exceptions escape

Known Compromises

  • dotnet sdk (bare) shows minimal System.CommandLine error vs full help in non-AOT
  • Localization may be English-only in AOT binary (satellite assembly availability depends on publish layout)
  • No AOT-specific smoke tests added yet (no existing test infra for AOT binary)

Testing

Copilot AI review requested due to automatic review settings May 20, 2026 20:06

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 enables the dotnet sdk check command to run via the NativeAOT-compiled CLI fast-path, threading the host-provided DOTNET_ROOT into the command and hardening the unmanaged entry point so managed exceptions cannot escape across the boundary.

Changes:

  • Registers sdk check in the AOT-only parser and wires it to SdkCheckCommand.Run.
  • Extends the dotnet-aot project to compile/link the sdk check implementation and embed required .resx resources plus the Microsoft.Deployment.DotNet.Releases dependency.
  • Refactors the AOT native entry point to wrap the full [UnmanagedCallersOnly] method body in a top-level try/catch, and introduces Program.DotnetRoot (AOT-only) for passing the dotnet root into commands.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/Cli/dotnet/Program.cs Adds AOT-only DotnetRoot and wraps AOT Main in error handling.
src/Cli/dotnet/Parser.cs Registers sdk check under the AOT parser.
src/Cli/dotnet/Commands/Sdk/Check/SdkCheckCommand.cs Adds AOT conditional wiring to pass Program.DotnetRoot and adjusts aliasing for AOT builds.
src/Cli/dotnet/CommandBase.cs Guards help/error extension usage for AOT builds.
src/Cli/dotnet-aot/NativeEntryPoint.cs Extracts ExecuteCore and wraps the unmanaged entry point body with a top-level try/catch; sets Program.DotnetRoot for the AOT fast-path.
src/Cli/dotnet-aot/dotnet-aot.csproj Includes sdk check sources/resources and adds Microsoft.Deployment.DotNet.Releases package reference.
Comments suppressed due to low confidence (1)

src/Cli/dotnet/Parser.cs:37

  • This error message is hard-coded even though a localized resource exists (CliStrings.RequiredCommandNotPassed). Please use the resource string here to keep messages consistent and localizable (and since CliStrings.resx is embedded for AOT in this PR).
        sdkCommand.SetAction(parseResult =>
        {
            parseResult.InvocationConfiguration.Error.WriteLine("Required command was not provided.");
            return 1;

Comment thread src/Cli/dotnet/Parser.cs Outdated
Add the sdk check command to the AOT-compiled CLI binary (dotnet-aot.csproj).
This command lists installed SDKs and runtimes and checks for updates,
performing all work in-process without MSBuild or NuGet dependencies.

Changes:
- Register 'sdk check' command in the AOT Parser.cs section
- Add exception handling in both Program.Main and NativeEntryPoint for
  GracefulException and unexpected errors
- Thread dotnetRoot from native host into SdkCheckCommand to ensure
  the correct .NET installation is inspected
- Guard CommandBase.ShowHelpOrErrorIfAppropriate with #if !CLI_AOT
  (extension method references types unavailable in AOT)
- Handle extern alias for EnvironmentProvider conditionally in
  SdkCheckCommand (#if CLI_AOT uses direct reference)
- Add Compile items, EmbeddedResource items (CliStrings.resx,
  CliCommandStrings.resx), and Microsoft.Deployment.DotNet.Releases
  package reference to dotnet-aot.csproj

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NikolaMilosavljevic and others added 6 commits May 21, 2026 13:22
On macOS, NativeAOT statically links libSystem.Security.Cryptography.Native.Apple.a
into every shared library output. The Swift binding classes in this archive conflict
with the host process's copy, causing duplicate ObjC class warnings on stderr.
Since the AOT CLI does not use cryptographic APIs, safely remove this library.

Port of fix from PR dotnet#54384.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NikolaMilosavljevic and others added 6 commits May 29, 2026 08:15
The merge with main (PR dotnet#54297) introduced AotSourceFiles.props, a shared
source list imported by both dotnet-aot.csproj and the new dotnet-aot.Tests.csproj.
The merge resolution left duplicate Compile items in dotnet-aot.csproj for files
that are now provided by the props import (CommandLineInfo.cs, Parser.cs,
EnvironmentVariableNames.cs), which would cause duplicate-item build failures.

Since the shared Parser.cs references the 'sdk check' command, the test project
(which compiles Parser.cs via the props) also needs the command's sources,
resources, and package dependency. Move those into AotSourceFiles.props so both
consumers stay in sync, and remove the now-duplicated entries from dotnet-aot.csproj.

Also move the host-provided DotnetRoot state from Program.cs into the shared
NativeEntryPoint.cs, because NativeEntryPoint.cs is compiled by the test project
but Program.cs is not. This keeps the test project building without pulling in
Program.cs (which defines a Main entry point).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reconcile the aot3 branch merge with main's PR dotnet#54384 (sln AOT support):

- AotSourceFiles.props: remove duplicate CommandBase.cs and duplicate
  CliStrings/CliCommandStrings EmbeddedResource items left by the merge;
  adopt main's grouped layout with a dedicated 'sdk check' command group.
- NativeEntryPoint.cs: fix garbled ExecuteCore (out-of-scope parseResult)
  by using main's CommandNotAvailableInAotException fall-through structure
  plus the DotnetRoot assignment needed by sdk check.
- Resolve CS0433 EnvironmentProvider ambiguity (now that the sln command
  references Microsoft.DotNet.Cli.Definitions, which also compiles
  EnvironmentProvider.cs): add Aliases=DotNetNativeWrapper,global to the
  NativeWrapper references in dotnet-aot.csproj and dotnet-aot.Tests.csproj,
  and make SdkCheckCommand.cs use the extern alias unconditionally.

All three projects build clean (0 warnings/0 errors); 44 AOT tests pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
NikolaMilosavljevic and others added 3 commits June 4, 2026 14:20
Merging main's dotnet#54544 (OTel for AOT) renamed the dotnet-aot NativeWrapper
ProjectReference alias to 'global,NativeWrapper' (for ManagedHost.cs) and
the conflict resolution dropped the 'Aliases' metadata from the test
project's NativeWrapper reference entirely.

SdkCheckCommand.cs (shared by dotnet.csproj, dotnet-aot.csproj and
dotnet-aot.Tests.csproj) uses 'extern alias DotNetNativeWrapper', so both
AOT projects failed to compile it (CS0430) and the test project also hit
the EnvironmentProvider CS0433 ambiguity again.

Expose both alias names on the AOT projects' NativeWrapper reference
(global,NativeWrapper,DotNetNativeWrapper) so ManagedHost.cs keeps using
NativeWrapper while SdkCheckCommand.cs keeps using DotNetNativeWrapper,
matching the non-AOT dotnet.csproj convention without touching shared code.

dotnet-aot.csproj and dotnet-aot.Tests.csproj build clean; 46 AOT tests pass.

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

Copy link
Copy Markdown
Contributor Author

@JeremyKuhne @baronfel this is now passing all checks and is ready for review. I think I'll need to adjust the implementation if we merge #54653 first.

@NikolaMilosavljevic

Copy link
Copy Markdown
Contributor Author

Updated the changes to account for Parser PR that was merged yesterday: #54653

@NikolaMilosavljevic

Copy link
Copy Markdown
Contributor Author

All checks are passing with new changes to address Parser code to align with changes made by @baronfel

This is now ready for review - @marcpopMSFT @JeremyKuhne @baronfel

@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.

I have a question about the bare dotnet SDK command, but the rest looks great 👍

{
public static void ConfigureCommand(SdkCommandDefinition command)
{
#if CLI_AOT

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.

Is this still true? The entire command tree should be usable from AOT now so we may not need this special case anymore.

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.

Let me check - thanks.

Comment thread src/Cli/dotnet/Parser.cs Outdated
// for `sln list`/`migrate`/`remove` and falls back for `sln` and `sln add`.
SolutionCommandParser.ConfigureCommand(rootCommand.SolutionCommand);

// SdkCommandParser is AOT-aware: `sdk check` runs natively; bare `dotnet sdk` falls back.

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.

I think bare 'dotnet sdk' should be served from the AOT side too - there's no reason to startup a coreclr just to display help IMO.

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.

Looking into this - thanks.

@JeremyKuhne JeremyKuhne 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.

Nothing to add over @baronfel

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>
@NikolaMilosavljevic

Copy link
Copy Markdown
Contributor Author

@baronfel good catch, thanks! You're right — since #54653 the full command tree and help render from AOT, so falling back to coreclr just to print the missing-command error + usage was wasteful. I verified empirically that bare dotnet sdk now renders its error + help entirely from AOT (HandleMissingCommand -> ShowHelp), matching the managed CLI.

I applied the same reasoning to bare dotnet sln, which had the identical fallback pattern — it now also renders from AOT. Only sln add still falls back, since it genuinely requires MSBuild. Added test coverage for both (InvokeBareSdk_RendersHelpFromAot, InvokeBareSln_RendersHelpFromAot).

Pushed in a4378b3.

@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.

🚢

@baronfel
baronfel enabled auto-merge (squash) June 16, 2026 16:45
NikolaMilosavljevic and others added 4 commits June 16, 2026 11:37
# Conflicts:
#	src/Cli/dotnet/Extensions/ParseResultExtensions.cs
#	src/Cli/dotnet/Parser.cs
#	test/dotnet-aot.Tests/AotParserTests.cs
…STest

- Remove duplicate 'using Microsoft.DotNet.Cli.Commands.Sdk' and 'using Microsoft.DotNet.Cli.Commands.Tool' in Parser.cs

- Convert TransientSdkResolutionErrorDetectorTests from xUnit ([Fact]) to MSTest ([TestMethod])

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
# Conflicts:
#	src/Cli/dotnet/Extensions/ParseResultExtensions.cs
NikolaMilosavljevic and others added 5 commits June 22, 2026 14:34
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dotnet-aot.Tests compiles the entire shared CLI parser into its test assembly via AotSourceFiles.props. The method-based Helix scheduler counts those hundreds of non-test types and over-shards the assembly, producing shards that contain only non-test types (e.g. CliSchema's nested records). Such shards run zero tests and fail under MTP with exit code 8. Add a MethodLimitMultiplier so the assembly runs as a single work item.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@baronfel
baronfel merged commit a1a07a8 into dotnet:main Jun 29, 2026
25 checks passed
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview7 milestone Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants