diff --git a/.vsts-ci.yml b/.vsts-ci.yml
index fbf1f7ba5316..9e3d3039d470 100644
--- a/.vsts-ci.yml
+++ b/.vsts-ci.yml
@@ -178,7 +178,7 @@ extends:
# arm64 linker and the RID-specific ILCompiler package handle the cross-link; no sysroot is
# required). This leg therefore BUILDS on the existing win-x64 pool with /p:CrossBuild=true and
# produces the dotnet-aot .dll plus its mstat/dgml size-analysis artifacts (that step is gated
- # on runAoTTests). It is build-only (runTests: false): there is no wired windows arm64 Helix
+ # on the AoT category). It is build-only (runTests: false): there is no wired windows arm64 Helix
# queue yet, so running the *.AoT.Tests on arm64 hardware is a follow-up pending a
# windows.11.arm64 queue. Runs for internal PRs and test builds.
- ${{ if or(eq(parameters.runTestBuild, true), eq(variables['Build.Reason'], 'PullRequest')) }}:
@@ -199,10 +199,9 @@ extends:
# 'LNK1322: cannot avoid potential ARM hazard (Cortex-A53 MPCore processor bug #843419)'
# because the ILCompiler-produced object isn't split into per-function sections, so the
# MSVC arm64 linker cannot apply the erratum fixup. This leg has never passed. Re-enable
- # once the ILCompiler fix lands.
+ # once the ILCompiler fix lands. Tracked by dotnet/sdk#55298.
- categoryName: AoT
disableJob: true
- runAoTTests: true
targetArchitecture: arm64
runtimeIdentifier: win-arm64
osProperties: /p:CrossBuild=true
@@ -316,7 +315,6 @@ extends:
linuxJobParameterSets:
- categoryName: AoT
container: azureLinuxCrossArm64
- runAoTTests: true
targetArchitecture: arm64
runtimeIdentifier: linux-arm64
osProperties: $(linuxOsglibcProperties) /p:CrossBuild=true
diff --git a/.vsts-pr.yml b/.vsts-pr.yml
index 3f9d1e900e19..073ae03ce00d 100644
--- a/.vsts-pr.yml
+++ b/.vsts-pr.yml
@@ -69,13 +69,14 @@ stages:
# dotnet-aot is built with NativeAOT, which can cross-compile win-x64 -> win-arm64 (the MSVC arm64
# linker and the RID-specific ILCompiler package handle the cross-link; no sysroot is required).
# This leg BUILDS on the win-x64 pool with /p:CrossBuild=true and produces the dotnet-aot .dll plus
- # its mstat/dgml size-analysis artifacts (that step is gated on runAoTTests). It is build-only
+ # its mstat/dgml size-analysis artifacts (that step is gated on the AoT category). It is build-only
# (runTests: false): there is no wired windows arm64 Helix queue yet, so running the *.AoT.Tests on
# arm64 hardware is a follow-up pending a windows.11.arm64 queue.
# TEMPORARILY DISABLED (disableJob: true): the win-arm64 NativeAOT cross-link fails with
# 'LNK1322: cannot avoid potential ARM hazard (Cortex-A53 MPCore processor bug #843419)' because
# the ILCompiler-produced object isn't split into per-function sections, so the MSVC arm64 linker
# cannot apply the erratum fixup. This leg has never passed. Re-enable once the ILCompiler fix lands.
+ # Tracked by dotnet/sdk#55298.
- template: /eng/pipelines/templates/jobs/sdk-job-matrix.yml@self
parameters:
pool:
@@ -86,7 +87,6 @@ stages:
windowsJobParameterSets:
- categoryName: AoT
disableJob: true
- runAoTTests: true
targetArchitecture: arm64
runtimeIdentifier: win-arm64
osProperties: /p:CrossBuild=true
@@ -119,7 +119,6 @@ stages:
linuxJobParameterSets:
- categoryName: AoT
container: azureLinuxCrossArm64
- runAoTTests: true
targetArchitecture: arm64
runtimeIdentifier: linux-arm64
osProperties: $(linuxOsglibcProperties) /p:CrossBuild=true
diff --git a/eng/pipelines/templates/jobs/sdk-build.yml b/eng/pipelines/templates/jobs/sdk-build.yml
index da5be2c2f120..116a03fe0ee3 100644
--- a/eng/pipelines/templates/jobs/sdk-build.yml
+++ b/eng/pipelines/templates/jobs/sdk-build.yml
@@ -18,11 +18,14 @@ parameters:
timeoutInMinutes: ''
### ENV VARS ###
testFullMSBuild: false
+ # Enables the Blazor WebAssembly AoT tests (test/Microsoft.NET.Sdk.BlazorWebAssembly.AoT.Tests)
+ # on Helix via the RunAoTTests env var.
runAoTTests: false
# Runs the NativeAOT CLI tests (test/dotnet-aot.Tests) on the build agent after the build.
# These publish the test assembly as a NativeAOT binary and run it on the same architecture,
- # validating the AOT CLI entry-point code under NativeAOT compilation. This is distinct from
- # runAoTTests, which runs the Blazor WebAssembly AoT tests on Helix.
+ # validating the AOT CLI entry-point code under NativeAOT compilation. Only enabled where the
+ # agent architecture matches the target (the produced native binary must run on the agent);
+ # cross-arch AoT legs leave it false.
runNativeAotCliTests: false
### MSBUILD ###
targetArchitecture: x64
@@ -273,10 +276,11 @@ jobs:
continueOnError: true
condition: always()
- # Only the AoT legs publish the dotnet-aot NativeAOT library, so only they produce
- # the size-analysis files. Gate these steps so they don't run (and upload empty
- # artifacts) on non-AoT legs like the TestBuild/FullFramework jobs.
- - ${{ if eq(parameters.runAoTTests, true) }}:
+ # Only the AoT legs build the dotnet-aot NativeAOT library, so only they produce the
+ # size-analysis files. Gate on the AoT category so these steps run on every AoT leg (including
+ # the build-only cross-arch legs) and never upload empty artifacts on non-AoT legs like the
+ # TestBuild/FullFramework jobs.
+ - ${{ if eq(parameters.categoryName, 'AoT') }}:
- task: CopyFiles@2
displayName: ๐ฃ Copy NativeAOT Size Analysis Files
inputs:
diff --git a/src/Cli/dotnet-aot/DESIGN.md b/src/Cli/dotnet-aot/DESIGN.md
index 924b19e676b8..e0d310c7ba01 100644
--- a/src/Cli/dotnet-aot/DESIGN.md
+++ b/src/Cli/dotnet-aot/DESIGN.md
@@ -10,9 +10,27 @@ 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.
+as a local development and testing entry point. The AOT fast path is enabled by
+default on all platforms (see [Opting out](#opting-out-dotnet_cli_enableaot));
+setting `DOTNET_CLI_ENABLEAOT=false` (or `0`/`no`/`off`) opts out, and the bridge
+falls through to the managed CLI immediately.
+
+## Opting out (`DOTNET_CLI_ENABLEAOT`)
+
+The AOT command-handling fast path is **enabled by default on all platforms**. The
+fast path is designed to be behaviorally identical to the managed CLI, transparently
+deferring to it for anything it cannot handle, so it should require no action from users.
+
+If you need to bypass the AOT path entirely โ for example to diagnose a suspected parity
+issue โ set the `DOTNET_CLI_ENABLEAOT` environment variable to a falsy value before
+invoking `dotnet`:
+
+- Disable: `false`, `0`, `no`, or `off`
+- Enable: `true`, `1`, `yes`, or `on`
+- Unset (default): enabled
+
+When disabled, every invocation is routed straight to the managed CLI, exactly as it
+behaved before the AOT fast path was enabled by default.
## Motivation
@@ -48,7 +66,7 @@ graph TD
subgraph L2["Layer 2 ยท dotnet-aot.dll (Native AOT Shared Library)"]
Entry["NativeEntryPoint.Execute()"]
- AotCheck{"DOTNET_CLI_ENABLEAOT
enabled?"}
+ AotCheck{"DOTNET_CLI_ENABLEAOT
not disabled?"}
Parse["Parser.Parse(args)"]
Fast{"Command handled
by AOT path?"}
Invoke["Parser.Invoke()"]
@@ -94,7 +112,8 @@ 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** โ When `DOTNET_CLI_ENABLEAOT=true`, the AOT bridge builds the
+**Fast path** โ Unless `DOTNET_CLI_ENABLEAOT` is explicitly disabled, the AOT
+bridge builds the
**full** command tree (the same `DotNetCommandDefinition` used by the managed
CLI) so that parsing and `--help` match the managed CLI exactly. Commands that
can run entirely in AOT (`--version`, `--info`, and the AOT-capable `sln`
@@ -120,7 +139,8 @@ its full resolver set, deferral produces identical user-facing behavior. Out-of-
process invocation only happens after a non-null spec, so a command is never
executed twice.
-**Slow path** โ When `DOTNET_CLI_ENABLEAOT` is not set or the AOT bridge does
+**Slow path** โ When `DOTNET_CLI_ENABLEAOT` is disabled (`false`/`0`/`no`/`off`)
+or the AOT bridge 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
@@ -138,17 +158,23 @@ sequenceDiagram
participant cli as dotnet.dll (Layer 3)
dn->>aot: dotnet_execute(hostPath, dotnetRoot, sdkDir, hostfxrPath, argc, argv)
- aot->>aot: Parser.Parse(args)
- alt DOTNET_CLI_ENABLEAOT=true and built-in command handled by AOT
- aot->>aot: Parser.Invoke(parseResult)
- aot-->>dn: exit code
- else External command that resolves in AOT (tool / PATH / app-base)
- aot->>aot: TryInvokeExternalCommand โ TryResolveCommandSpec
- aot->>tool: Command.Execute() (out of process)
- tool-->>aot: exit code
- aot-->>dn: exit code
- else Command not handled, unresolved, file-based app, or AOT disabled
+ alt DOTNET_CLI_ENABLEAOT enabled (default)
+ aot->>aot: Parser.Parse(args)
+ alt Built-in command handled by AOT
+ aot->>aot: Parser.Invoke(parseResult)
+ aot-->>dn: exit code
+ else External command that resolves in AOT (tool / PATH / app-base)
+ aot->>aot: TryInvokeExternalCommand โ TryResolveCommandSpec
+ aot->>tool: Command.Execute() (out of process)
+ tool-->>aot: exit code
+ aot-->>dn: exit code
+ else Command not handled, unresolved, or file-based app
+ aot->>cli: ManagedHost.RunApp(args) โ managed fallback (see below)
+ cli-->>aot: exit code
+ aot-->>dn: exit code
+ end
+ else DOTNET_CLI_ENABLEAOT disabled (opt-out)
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)
diff --git a/src/Cli/dotnet-aot/NativeEntryPoint.cs b/src/Cli/dotnet-aot/NativeEntryPoint.cs
index 74d7d8f53f3f..6df92ba2f5bd 100644
--- a/src/Cli/dotnet-aot/NativeEntryPoint.cs
+++ b/src/Cli/dotnet-aot/NativeEntryPoint.cs
@@ -160,6 +160,10 @@ internal static int ExecuteCore(
int exitCode = 1;
bool success = false;
+ // True once a command has been handled entirely in AOT (no managed fallback). Gates the
+ // terminal telemetry emission below: the managed fallback child writes its own disk log, so
+ // we must only write ours when we did not fall back.
+ bool aotHandledInProcess = false;
try
{
@@ -173,8 +177,7 @@ internal static int ExecuteCore(
// can use it instead of re-probing PATH / environment for the dotnet installation.
DotnetRoot = string.IsNullOrEmpty(dotnetRoot) ? null : dotnetRoot;
- // Try the AOT-compiled path for supported commands (if enabled)
- if (EnvironmentVariableParser.ParseBool(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_ENABLEAOT), defaultValue: false))
+ if (EnvironmentVariableParser.ParseBool(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_ENABLEAOT), defaultValue: true))
{
ParseResult? parseResult = null;
using (var parse = Activities.Source.StartActivity("parse"))
@@ -205,22 +208,22 @@ internal static int ExecuteCore(
{
if (parseResult.CanBeInvoked())
{
- try
+ // Parse errors here usually mean the command is contributed dynamically by the managed
+ // CLI (e.g. NuGet's `package update`/`why`) and absent from the static AOT tree, so defer.
+ if (parseResult.Errors.Count == 0)
{
- // Invoke the built-in command in-process using the shared CommandInvocation
- // helper, identical to the managed CLI: same exit-code handling, including the
- // "new" command's 127 adjustment and Parser.ExceptionHandler. This keeps the
- // two entry points in parity.
- exitCode = CommandInvocation.ExecuteInternalCommand(parseResult);
- success = true;
- // The built-in command ran in-process (no managed fallback), so emit the same
- // top-level parser telemetry the managed CLI sends from Program.ProcessArgsAndExecute.
- SendAotParserTelemetry(parseResult, globalJsonState);
- return exitCode;
- }
- catch (CommandNotAvailableInAotException)
- {
- // The parsed command requires the managed CLI โ fall through to the managed fallback below.
+ try
+ {
+ exitCode = CommandInvocation.ExecuteInternalCommand(parseResult);
+ success = true;
+ aotHandledInProcess = true;
+ SendAotParserTelemetry(parseResult, globalJsonState);
+ return exitCode;
+ }
+ catch (CommandNotAvailableInAotException)
+ {
+ // The parsed command requires the managed CLI โ fall through to the managed fallback below.
+ }
}
}
// An unrecognized top-level token is either an external command (`dotnet ef`, a global
@@ -229,6 +232,7 @@ internal static int ExecuteCore(
// project tools, and anything that does not resolve to the managed CLI.
else if (parseResult is not null && TryInvokeExternalCommand(parseResult, args, sdkDirectory, mainActivity, globalJsonState, out exitCode, out success))
{
+ aotHandledInProcess = true;
return exitCode;
}
}
@@ -236,7 +240,7 @@ internal static int ExecuteCore(
// Fall back to the fully managed dotnet CLI by hosting .NET.
// Set a best-effort display name from args when we have not done a full parse
- // (i.e. DOTNET_CLI_ENABLEAOT was not set or the command fell through without calling SetDisplayName).
+ // (i.e. DOTNET_CLI_ENABLEAOT was explicitly disabled or the command fell through without calling SetDisplayName).
if (mainActivity is not null && mainActivity.DisplayName == "main")
{
var fallbackName = args.Length > 0 ? $"dotnet {args[0]}" : "dotnet";
@@ -264,10 +268,23 @@ internal static int ExecuteCore(
}
finally
{
+ if (aotHandledInProcess && TelemetryClient is Telemetry.TelemetryClient telemetryClient)
+ {
+ // Mirror the managed CLI behavior for compat (Program.cs finally)
+ telemetryClient.ThreadBlockingTrackEvent("command/finish", new Dictionary { { "exitCode", exitCode.ToString() } });
+ }
+
mainActivity?.AddTag("process.exit.code", exitCode);
mainActivity?.SetStatus(success ? ActivityStatusCode.Ok : ActivityStatusCode.Error);
mainActivity?.Stop();
Telemetry.TelemetryClient.FlushProviders();
+
+ if (aotHandledInProcess)
+ {
+ // The command ran entirely in AOT, so there is no managed fallback child process to persist
+ // the telemetry disk log. Write it here, mirroring Program.cs's TelemetryClient.WriteLogIfNecessary().
+ Telemetry.TelemetryClient.WriteLogIfNecessary();
+ }
}
}
diff --git a/src/Cli/dotnet-aot/dotnet-aot.csproj b/src/Cli/dotnet-aot/dotnet-aot.csproj
index 21cb0eb26f17..e43693142573 100644
--- a/src/Cli/dotnet-aot/dotnet-aot.csproj
+++ b/src/Cli/dotnet-aot/dotnet-aot.csproj
@@ -5,6 +5,16 @@
$(SdkTargetFramework)
dotnet-aot
+
+ <_SystemCommandLineExecutableName>dotnet
Library
Shared
true
@@ -96,31 +106,6 @@
Trim="true" />
-
-
-
-
-
-
-
-
diff --git a/src/Cli/dotnet/Parser.cs b/src/Cli/dotnet/Parser.cs
index 73d752c7a78b..06cc63bc2efa 100644
--- a/src/Cli/dotnet/Parser.cs
+++ b/src/Cli/dotnet/Parser.cs
@@ -123,7 +123,19 @@ private static void NormalizeRootOptions(DotNetCommandDefinition rootCommand)
}
else if (option is HelpOption helpOption)
{
+#if CLI_AOT
+ // On the AOT path some commands keep their static definition, but the managed CLI produces
+ // their help dynamically with content that has no static equivalent:
+ // * `new` is replaced with a template-engine-backed command that adds the template
+ // short-name/args usage line, the Arguments section, and per-template options.
+ // * `test` (Microsoft.Testing.Platform mode) builds and forwards `--help` to the test
+ // application, which contributes the "Extension Options:" section and per-extension options.
+ // Rendering the static definition's help here would omit all of that, so defer help for those
+ // subtrees to the managed CLI to keep the output in parity.
+ helpOption.Action = new AotPrintHelpAction(helpOption, DotnetHelpBuilder.Instance.Value, rootCommand.NewCommand, rootCommand.TestCommand);
+#else
helpOption.Action = new PrintHelpAction(helpOption, DotnetHelpBuilder.Instance.Value);
+#endif
helpOption.Description = CliStrings.ShowHelpDescription;
}
}
@@ -247,6 +259,39 @@ public override int Invoke(ParseResult parseResult)
return 0;
}
}
+
+ ///
+ /// Help action for the AOT CLI. It renders help entirely from the shared command tree (like the
+ /// managed CLI) except for commands whose managed help is produced dynamically and therefore has
+ /// no static equivalent in the AOT definition. For those it throws
+ /// so NativeEntryPoint defers to the managed
+ /// CLI, whose help output the snapshot tests expect. Such commands include new (the managed
+ /// CLI replaces it with a template-engine-backed command that adds the template short-name/args usage
+ /// line, the Arguments section, and per-template options) and test (Microsoft.Testing.Platform
+ /// mode builds and forwards --help to the test application, which contributes the
+ /// "Extension Options:" section and per-extension options).
+ ///
+ private sealed class AotPrintHelpAction(Option option, HelpBuilder builder, params Command[] managedHelpCommands)
+ : PrintHelpAction(option, builder)
+ {
+ private readonly Command[] _managedHelpCommands = managedHelpCommands;
+
+ public override int Invoke(ParseResult parseResult)
+ {
+ // Walk from the innermost parsed command up to the root; if any command whose help the
+ // managed CLI generates dynamically is anywhere in that chain, defer to the managed CLI.
+ for (System.CommandLine.Parsing.SymbolResult? result = parseResult.CommandResult; result is not null; result = result.Parent)
+ {
+ if (result is System.CommandLine.Parsing.CommandResult commandResult
+ && Array.Exists(_managedHelpCommands, c => ReferenceEquals(c, commandResult.Command)))
+ {
+ throw new CommandNotAvailableInAotException();
+ }
+ }
+
+ return base.Invoke(parseResult);
+ }
+ }
#endif
public static Command? GetBuiltInCommand(string commandName) =>
diff --git a/test/dotnet-aot.Tests/AotIntegrationTests.cs b/test/dotnet-aot.Tests/AotIntegrationTests.cs
index eb39331bee94..904a34b88aba 100644
--- a/test/dotnet-aot.Tests/AotIntegrationTests.cs
+++ b/test/dotnet-aot.Tests/AotIntegrationTests.cs
@@ -13,7 +13,7 @@ namespace Microsoft.DotNet.Cli.Tests;
///
[TestCategory("AOT")]
[TestClass]
-public class AotIntegrationTests
+public partial class AotIntegrationTests
{
public TestContext TestContext { get; set; } = null!;
@@ -72,7 +72,9 @@ public class AotIntegrationTests
}
else
{
- psi.Environment.Remove("DOTNET_CLI_ENABLEAOT");
+ // The AOT fast path is enabled by default, so explicitly disable it (rather than just
+ // removing the variable) to exercise the managed-fallback behavior.
+ psi.Environment["DOTNET_CLI_ENABLEAOT"] = "false";
}
if (extraEnv is not null)
@@ -237,11 +239,11 @@ public void AotBuild_WithEnableAot_FallsBackToManaged()
}
[TestMethod]
- public void Version_WithoutEnableAot_StillWorks()
+ public void Version_WithAotDisabled_StillWorks()
{
SkipIfDnUnavailable();
- // Without DOTNET_CLI_ENABLEAOT, everything goes through managed fallback
+ // With DOTNET_CLI_ENABLEAOT disabled, everything goes through managed fallback
var (exitCode, stdout, stderr) = RunDn(["--version"], enableAot: false);
// Managed fallback requires dotnet.dll + all dependencies in the layout.
@@ -257,7 +259,7 @@ public void Version_WithoutEnableAot_StillWorks()
}
[TestMethod]
- public void Info_WithoutEnableAot_ShowsFullInfo()
+ public void Info_WithAotDisabled_ShowsFullInfo()
{
SkipIfDnUnavailable();
diff --git a/test/dotnet-aot.Tests/AotParserTests.cs b/test/dotnet-aot.Tests/AotParserTests.cs
index 17020f95ffe8..301e9174b4d7 100644
--- a/test/dotnet-aot.Tests/AotParserTests.cs
+++ b/test/dotnet-aot.Tests/AotParserTests.cs
@@ -17,7 +17,7 @@ namespace Microsoft.DotNet.Cli.Tests;
/// so the bridge can fall back.
///
[TestClass]
-public class AotParserTests
+public partial class AotParserTests
{
// File-based app detection (GetFileBasedAppEntryPointToken -> VirtualProjectBuilder.IsValidEntryPointPath)
// pulls in the Microsoft.Build assembly, which cannot be loaded into a NativeAOT image, so the call
@@ -233,6 +233,35 @@ public void InvokeCommandHelp_RendersFromAotWithoutFallback()
Assert.IsNull(exception);
}
+ [TestMethod]
+ [DataRow("new")]
+ [DataRow("new --help")]
+ [DataRow("new console --help")]
+ [DataRow("new list --help")]
+ [DataRow("new install --help")]
+ public void InvokeNewHelp_FallsBackToManaged(string commandLine)
+ {
+ // The managed CLI replaces `new` with a template-engine-backed command whose help is
+ // generated dynamically (template short-name/args usage line, Arguments section, per-template
+ // options). The AOT definition has no static equivalent, so help for the `new` subtree must
+ // defer to the managed CLI rather than render the incomplete static help.
+ var result = Parser.Parse(commandLine.Split(' '));
+ Assert.ThrowsExactly(() => Parser.Invoke(result));
+ }
+
+ [TestMethod]
+ [DataRow("test")]
+ [DataRow("test --help")]
+ public void InvokeTestHelp_FallsBackToManaged(string commandLine)
+ {
+ // In Microsoft.Testing.Platform mode the managed CLI builds the test project and forwards
+ // `--help` to the test application, which contributes the "Extension Options:" section and
+ // per-extension options. The AOT definition cannot reproduce that, so help for the `test`
+ // subtree must defer to the managed CLI. (Bare `test` also requires MSBuild and falls back.)
+ var result = Parser.Parse(commandLine.Split(' '));
+ Assert.ThrowsExactly(() => Parser.Invoke(result));
+ }
+
[TestMethod]
public void InvokeExternalToolHelp_RendersFromAotWithoutFallback()
{
diff --git a/test/dotnet-aot.Tests/DotnetRootResolverTests.cs b/test/dotnet-aot.Tests/DotnetRootResolverTests.cs
index 0474dae866e6..1c8306d493fe 100644
--- a/test/dotnet-aot.Tests/DotnetRootResolverTests.cs
+++ b/test/dotnet-aot.Tests/DotnetRootResolverTests.cs
@@ -13,7 +13,7 @@ namespace Microsoft.DotNet.Cli.Tests;
/// Path construction uses Path.Combine for cross-platform compatibility.
///
[TestClass]
-public class DotnetRootResolverTests
+public partial class DotnetRootResolverTests
{
// Helper to build platform-appropriate paths for test inputs/outputs.
// When isWindows=true, uses a Windows-style root; otherwise Unix-style.
diff --git a/test/dotnet-aot.Tests/NativeEntryPointTests.cs b/test/dotnet-aot.Tests/NativeEntryPointTests.cs
index e81b98938f0e..f7a5e82886c7 100644
--- a/test/dotnet-aot.Tests/NativeEntryPointTests.cs
+++ b/test/dotnet-aot.Tests/NativeEntryPointTests.cs
@@ -15,7 +15,7 @@ namespace Microsoft.DotNet.Cli.Tests;
/// "managed fallback not found" error because test env doesn't have dotnet.dll in sdkDir.
///
[TestClass]
-public class NativeEntryPointTests
+public partial class NativeEntryPointTests
{
///
/// Runs a test action with relevant environment variables restored afterward.
@@ -243,7 +243,7 @@ public void ExecuteCore_AotDisabled_VersionCommand_FallsBack()
}
[TestMethod]
- public void ExecuteCore_AotNotSet_VersionCommand_FallsBack()
+ public void ExecuteCore_AotNotSet_VersionCommand_UsesAotByDefault()
{
WithEnvRestore(() =>
{
@@ -256,8 +256,9 @@ public void ExecuteCore_AotNotSet_VersionCommand_FallsBack()
hostfxrPath: "",
args: ["--version"]);
- // Default is false โ managed fallback โ files missing โ returns 1
- Assert.AreEqual(1, exitCode);
+ // Default is enabled on all platforms โ AOT path handles --version in-process โ exit 0
+ // (without falling back to the missing managed dotnet.dll).
+ Assert.AreEqual(0, exitCode);
});
}
@@ -286,12 +287,39 @@ public void ExecuteCore_AotEnabledVariousFormats_TakesAotPath(string enableValue
});
}
+ [TestMethod]
+ [DataRow("false")]
+ [DataRow("False")]
+ [DataRow("FALSE")]
+ [DataRow("0")]
+ [DataRow("no")]
+ [DataRow("off")]
+ public void ExecuteCore_AotDisabledVariousFormats_FallsBack(string disableValue)
+ {
+ WithEnvRestore(() =>
+ {
+ Environment.SetEnvironmentVariable("DOTNET_CLI_ENABLEAOT", disableValue);
+
+ int exitCode = NativeEntryPoint.ExecuteCore(
+ hostPath: "test-host",
+ dotnetRoot: "test-root",
+ sdkDir: "nonexistent-sdk-dir",
+ hostfxrPath: "",
+ args: ["--version"]);
+
+ // All these formats opt out of AOT โ managed fallback โ files missing โ returns 1
+ Assert.AreEqual(1, exitCode);
+ });
+ }
+
[TestMethod]
public void ExecuteCore_MissingFallbackFiles_ReturnsOneAndWritesError()
{
WithEnvRestore(() =>
{
- Environment.SetEnvironmentVariable("DOTNET_CLI_ENABLEAOT", null);
+ // Disable the AOT fast path so the invocation is routed to the managed fallback,
+ // which is missing in the test layout and should surface the fallback error.
+ Environment.SetEnvironmentVariable("DOTNET_CLI_ENABLEAOT", "false");
var originalErr = Console.Error;
var stderrWriter = new StringWriter();
diff --git a/test/dotnet-aot.Tests/WorkloadInstallDetectorTests.cs b/test/dotnet-aot.Tests/WorkloadInstallDetectorTests.cs
index bd8d1c930193..56713992ed1c 100644
--- a/test/dotnet-aot.Tests/WorkloadInstallDetectorTests.cs
+++ b/test/dotnet-aot.Tests/WorkloadInstallDetectorTests.cs
@@ -15,7 +15,7 @@ namespace Microsoft.DotNet.Cli.Tests;
/// detection runs without NuGet, MSBuild, or installer-IPC dependencies.
///
[TestClass]
-public class WorkloadInstallDetectorTests
+public partial class WorkloadInstallDetectorTests
{
private static string CurrentFeatureBand => new SdkFeatureBand(Product.Version).ToString();