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();