Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/Cli/dn/dn-native-debug.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@
<LocalDebuggerCommand>$(_DnExePath)</LocalDebuggerCommand>
<LocalDebuggerCommandArguments>--info</LocalDebuggerCommandArguments>
<LocalDebuggerWorkingDirectory>$(_DnPublishDir)</LocalDebuggerWorkingDirectory>
<LocalDebuggerEnvironment>DOTNET_ROOT=$(_DotnetRootPath)</LocalDebuggerEnvironment>
<LocalDebuggerEnvironment>DOTNET_ROOT=$(_DotnetRootPath)
DOTNET_CLI_ENABLEAOT=true</LocalDebuggerEnvironment>
<DebuggerFlavor>WindowsLocalDebugger</DebuggerFlavor>
</PropertyGroup>

Expand Down
6 changes: 4 additions & 2 deletions src/Cli/dn/dn.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@
<_LaunchSettingsLines Include=" %22commandLineArgs%22: %22--info%22," />
<_LaunchSettingsLines Include=" %22workingDirectory%22: %22$(_DnCwdPath.Replace('\','\\'))%22," />
<_LaunchSettingsLines Include=" %22environmentVariables%22: {" />
<_LaunchSettingsLines Include=" %22DOTNET_ROOT%22: %22$(_DotnetRootPath.Replace('\','\\'))%22" />
<_LaunchSettingsLines Include=" %22DOTNET_ROOT%22: %22$(_DotnetRootPath.Replace('\','\\'))%22," />
<_LaunchSettingsLines Include=" %22DOTNET_CLI_ENABLEAOT%22: %22true%22" />
<_LaunchSettingsLines Include=" }," />
<_LaunchSettingsLines Include=" %22nativeDebugging%22: true" />
<_LaunchSettingsLines Include=" }" />
Expand All @@ -93,7 +94,7 @@
<_VcxUserLines Include=" &lt;LocalDebuggerCommand&gt;$(_DnExePath)&lt;/LocalDebuggerCommand&gt;" />
<_VcxUserLines Include=" &lt;LocalDebuggerCommandArguments&gt;--info&lt;/LocalDebuggerCommandArguments&gt;" />
<_VcxUserLines Include=" &lt;LocalDebuggerWorkingDirectory&gt;$(_DnCwdPath)&lt;/LocalDebuggerWorkingDirectory&gt;" />
<_VcxUserLines Include=" &lt;LocalDebuggerEnvironment&gt;DOTNET_ROOT=$(_DotnetRootPath)&lt;/LocalDebuggerEnvironment&gt;" />
<_VcxUserLines Include=" &lt;LocalDebuggerEnvironment&gt;DOTNET_ROOT=$(_DotnetRootPath)&#xA;DOTNET_CLI_ENABLEAOT=true&lt;/LocalDebuggerEnvironment&gt;" />
<_VcxUserLines Include=" &lt;DebuggerFlavor&gt;WindowsLocalDebugger&lt;/DebuggerFlavor&gt;" />
<_VcxUserLines Include=" &lt;/PropertyGroup&gt;" />
<_VcxUserLines Include="&lt;/Project&gt;" />
Expand Down Expand Up @@ -128,6 +129,7 @@
<_DebugScriptLines Include="REM (Debug > Windows > Breakpoints > New > Function, Language = All Languages)." />
<_DebugScriptLines Include="REM" />
<_DebugScriptLines Include="set DOTNET_ROOT=$(_DotnetRootPath)" />
<_DebugScriptLines Include="set DOTNET_CLI_ENABLEAOT=true" />
<_DebugScriptLines Include="devenv /debugexe %22$(_DnExePath)%22 --info" />
</ItemGroup>
<WriteLinesToFile File="$(MSBuildThisFileDirectory)debug-dn.cmd" Lines="@(_DebugScriptLines)" Overwrite="true" WriteOnlyWhenDifferent="true" />
Expand Down
66 changes: 37 additions & 29 deletions src/Cli/dotnet-aot/DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ to the .NET SDK CLI. The goal is to achieve near-instant startup for common
commands while preserving full functionality through the managed CLI.

The current implementation uses a standalone `dn.exe` host that lives alongside
the existing `dotnet` CLI. `dn.exe` emulates functionality that will eventually
be integrated into the dotnet host (muxer) itself — see
[dotnet/runtime#126171](https://github.com/dotnet/runtime/issues/126171). Once
the muxer gains native SDK resolution support, the AOT entry point will be
called directly by the standard `dotnet` executable and `dn.exe` will remain as
a local development and testing entry point. At that point, AOT-handled command
implementations will likely be removed from the managed `dotnet.dll` package as
well.
the existing `dotnet` CLI. `dn.exe` emulates the muxer's `try_invoke_aot_sdk`
function — see
[dotnet/runtime#126171](https://github.com/dotnet/runtime/issues/126171). The
muxer looks for `dotnet-aot` in the resolved SDK directory and, when found,
calls `dotnet_execute` directly. `dn.exe` follows the same contract and serves
as a local development and testing entry point. The AOT fast path is gated
behind `DOTNET_CLI_ENABLEAOT=true`; when the variable is unset or false, the
bridge falls through to the managed CLI immediately.

## Motivation

Expand Down Expand Up @@ -48,14 +48,16 @@ graph TD

subgraph L2["Layer 2 · dotnet-aot.dll (Native AOT Shared Library)"]
Entry["NativeEntryPoint.Execute()"]
AotCheck{"DOTNET_CLI_ENABLEAOT<br/>enabled?"}
Parse["Parser.Parse(args)"]
Fast{"Command handled<br/>by AOT path?"}
Invoke["Parser.Invoke()"]
DebugCheck["Check for native debugger"]
HostInit["ManagedHost.RunApp()"]
Entry --> Parse --> Fast
Entry --> AotCheck
AotCheck -- "Yes" --> Parse --> Fast
AotCheck -- "No" --> HostInit
Fast -- "Yes" --> Invoke
Fast -- "No" --> DebugCheck --> HostInit
Fast -- "No" --> HostInit
end

Invoke --> Done["Return exit code"]
Expand Down Expand Up @@ -92,15 +94,18 @@ A NativeAOT shared library (`NativeLib=Shared`) that exports a single
`[UnmanagedCallersOnly]` entry point: `dotnet_execute`. This layer contains
the dual-path dispatch logic.

**Fast path** — The AOT bridge compiles a minimal `Parser` (guarded by
`#if CLI_AOT`) that handles simple commands (`--version`, `--info`) entirely in
native code. If the parser recognizes the command, it executes immediately and
returns.
**Fast path** — When `DOTNET_CLI_ENABLEAOT=true`, the AOT bridge compiles a
minimal `Parser` (guarded by `#if CLI_AOT`) that handles simple commands
(`--version`, `--info`) entirely in native code. If the parser recognizes the
command, it executes immediately and returns.

**Slow path** — For any command not handled by the AOT parser, the bridge calls
`ManagedHost.RunApp()`, which uses the hostfxr native hosting APIs
(`hostfxr_initialize_for_dotnet_command_line` / `hostfxr_run_app`) to bootstrap
CoreCLR and run `dotnet.dll`.
**Slow path** — When `DOTNET_CLI_ENABLEAOT` is not set or the AOT parser does
not handle the command, the bridge calls `ManagedHost.RunApp()`, which uses the
hostfxr native hosting APIs (`hostfxr_initialize_for_dotnet_command_line` /
`hostfxr_set_runtime_property_value` / `hostfxr_run_app`) to bootstrap CoreCLR
and run `dotnet.dll`. The bridge passes through the `host_path`, `dotnet_root`,
and `hostfxr_path` received from the caller so that the runtime is configured
exactly as the muxer would configure it for an SDK command.

```mermaid
sequenceDiagram
Expand All @@ -113,13 +118,14 @@ sequenceDiagram
dn->>aot: dotnet_execute(hostPath, dotnetRoot, sdkDir, hostfxrPath, argc, argv)
aot->>aot: Parser.Parse(args)

alt Command handled by AOT
alt DOTNET_CLI_ENABLEAOT=true and command handled by AOT
aot->>aot: Parser.Invoke(parseResult)
aot-->>dn: exit code
else Command not handled
aot->>hfxr: hostfxr_initialize_for_dotnet_command_line(args)
hfxr->>clr: Load CoreCLR runtime
else Command not handled or AOT disabled
aot->>hfxr: hostfxr_initialize_for_dotnet_command_line(args, host_path, dotnet_root)
aot->>hfxr: hostfxr_set_runtime_property_value(handle, "HOSTFXR_PATH", hostfxrPath)
aot->>hfxr: hostfxr_run_app(handle)
hfxr->>clr: Load CoreCLR runtime
hfxr->>cli: Program.Main(args)
cli-->>hfxr: exit code
hfxr-->>aot: exit code
Expand Down Expand Up @@ -156,8 +162,9 @@ In the shared files:
- **`Program.cs`** — Under `#if CLI_AOT`, provides a simple `Main` that
delegates to the AOT parser. Under `#else`, provides the full CLI entry point
with telemetry, signal handlers, and workload checks.
- **`CommandLineInfo.cs`** — Shared without conditional compilation; prints
version and environment information.
- **`CommandLineInfo.cs`** — Uses `#if CLI_AOT` to substitute lightweight
implementations for workload info, localized strings, and OS detection that
would otherwise pull in dependencies incompatible with AOT.

```mermaid
graph LR
Expand Down Expand Up @@ -442,10 +449,11 @@ dialog (or auto-attaches in configured environments).

## Future Work

- **Muxer integration** — The `dn.exe` host emulates muxer behavior locally.
Once [dotnet/runtime#126171](https://github.com/dotnet/runtime/issues/126171)
lands, the dotnet muxer will call `dotnet_execute` directly. `dn.exe` will
continue to serve as a local development and testing entry point.
- **Muxer integration** — The muxer's `try_invoke_aot_sdk` function
([dotnet/runtime#126171](https://github.com/dotnet/runtime/issues/126171))
already calls `dotnet_execute` from the resolved SDK directory, passing
`host_path`, `dotnet_root`, `sdk_dir`, and `hostfxr_path`. `dn.exe`
emulates this same contract for local development and testing.
- **Remove AOT commands from managed package** — After the AOT path is
validated and shipping, the `#if CLI_AOT` implementations in `Parser.cs`
and `Program.cs` can be removed from the managed `dotnet.dll` build.
Expand Down
24 changes: 20 additions & 4 deletions src/Cli/dotnet-aot/ManagedHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,17 @@ public int InvokeMethod(string assemblyPath, string typeName, string methodName)
/// Runs the managed application using the hostfxr command-line hosting path.
/// This is the simplest way to invoke <c>dotnet.dll Program.Main(args)</c>.
/// </summary>
/// <param name="hostPath">Path to the host executable (e.g., dotnet.exe).</param>
/// <param name="dotnetRoot">Path to the .NET installation root.</param>
/// <param name="hostfxrPath">Path to the hostfxr library.</param>
/// <param name="args">Command-line arguments (first element should be the app path).</param>
/// <returns>The application exit code.</returns>
public static int RunApp(string dotnetRoot, string[] args)
public static int RunApp(string hostPath, string dotnetRoot, string hostfxrPath, string[] args)
{
nint handle;

var parameters = new Interop.hostfxr_initialize_parameters
{
size = sizeof(Interop.hostfxr_initialize_parameters),
host_path = PlatformStringMarshaller.ConvertToUnmanaged(hostPath),
dotnet_root = PlatformStringMarshaller.ConvertToUnmanaged(dotnetRoot),
};

Expand All @@ -125,7 +126,7 @@ public static int RunApp(string dotnetRoot, string[] args)
args.Length,
args,
in parameters,
out handle);
out nint handle);

if (result != StatusCode.Success && handle == 0)
{
Expand All @@ -134,6 +135,20 @@ public static int RunApp(string dotnetRoot, string[] args)

try
{
// Set HOSTFXR_PATH property to match the muxer's behavior for SDK commands.
// The muxer sets this when is_sdk_command=true so the SDK can load hostfxr
// without relying on dlopen/LoadLibrary to find it.
if (!string.IsNullOrEmpty(hostfxrPath))
{
StatusCode propertyResult = Interop.hostfxr_set_runtime_property_value(
handle, Constants.RuntimeProperty.HostFxrPath, hostfxrPath);
if (propertyResult != StatusCode.Success)
{
throw new InvalidOperationException(
$"hostfxr_set_runtime_property_value failed for {Constants.RuntimeProperty.HostFxrPath}. Status: {propertyResult} (0x{(uint)propertyResult:X8})");
}
}

StatusCode appResult = Interop.hostfxr_run_app(handle);
return (int)appResult;
}
Expand All @@ -144,6 +159,7 @@ public static int RunApp(string dotnetRoot, string[] args)
}
finally
{
PlatformStringMarshaller.Free(parameters.host_path);
PlatformStringMarshaller.Free(parameters.dotnet_root);
}
}
Expand Down
19 changes: 10 additions & 9 deletions src/Cli/dotnet-aot/NativeEntryPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,26 +34,27 @@ static int Execute(
args[i] = PlatformStringMarshaller.ConvertToManaged(argv[i]) ?? string.Empty;
}

// Try the AOT-compiled path first for supported commands
var parseResult = Parser.Parse(args);
bool handled = parseResult.Errors.Count == 0;

if (handled)
// Try the AOT-compiled path for supported commands (if enabled)
if (EnvironmentVariableParser.ParseBool(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_ENABLEAOT), defaultValue: false))
{
return Parser.Invoke(parseResult);
var parseResult = Parser.Parse(args);
if (parseResult.Errors.Count == 0)
{
return Parser.Invoke(parseResult);
}
}

// Fall back to the fully managed dotnet CLI by hosting .NET
string dotnetDll = Path.Combine(sdkDir, "dotnet.dll");
string runtimeConfig = Path.Combine(sdkDir, "dotnet.runtimeconfig.json");
string dotnetDll = Path.Join(sdkDir, "dotnet.dll");
string runtimeConfig = Path.Join(sdkDir, "dotnet.runtimeconfig.json");
Comment on lines -47 to +49
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.

Just keep in mind that these do have differences between them:

Image


if (File.Exists(dotnetDll) && File.Exists(runtimeConfig))
{
// Use the command-line hosting path to run dotnet.dll
string[] appArgs = new string[args.Length + 1];
appArgs[0] = dotnetDll;
Array.Copy(args, 0, appArgs, 1, args.Length);
return ManagedHost.RunApp(dotnetRoot, appArgs);
return ManagedHost.RunApp(hostPath, dotnetRoot, hostfxrPath, appArgs);
}

// No managed fallback available
Expand Down
1 change: 1 addition & 0 deletions src/Cli/dotnet-aot/dotnet-aot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
<Compile Include="..\dotnet\Program.cs" Link="Program.cs" />
<Compile Include="..\dotnet\CommandLineInfo.cs" Link="CommandLineInfo.cs" />
<Compile Include="..\dotnet\Parser.cs" Link="Parser.cs" />
<Compile Include="$(RepoRoot)src\Common\EnvironmentVariableNames.cs" LinkBase="Common" />
</ItemGroup>

<ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/Common/EnvironmentVariableNames.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ internal static class EnvironmentVariableNames
public static readonly string DOTNET_ADD_GLOBAL_TOOLS_TO_PATH = nameof(DOTNET_ADD_GLOBAL_TOOLS_TO_PATH);
public static readonly string DOTNET_NOLOGO = nameof(DOTNET_NOLOGO);
public static readonly string DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK = nameof(DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK);
public static readonly string DOTNET_CLI_ENABLEAOT = nameof(DOTNET_CLI_ENABLEAOT);
public static readonly string DOTNET_CLI_TELEMETRY_SESSIONID = nameof(DOTNET_CLI_TELEMETRY_SESSIONID);
public static readonly string DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING = nameof(DOTNET_CLI_CONSOLE_USE_DEFAULT_ENCODING);
// Telemetry logging/debug/testing.
Expand Down
3 changes: 2 additions & 1 deletion tasks.code-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"args": ["--info"],
"cwd": "${workspaceFolder}${/}artifacts${/}bin${/}dn${/}Debug${/}net11.0${/}win-x64${/}publish",
"environment": [
{ "name": "DOTNET_ROOT", "value": "${workspaceFolder}/.dotnet" }
{ "name": "DOTNET_ROOT", "value": "${workspaceFolder}/.dotnet" },
{ "name": "DOTNET_CLI_ENABLEAOT", "value": "true" }
],
"stopAtEntry": false,
"console": "integratedTerminal"
Expand Down
Loading