diff --git a/docs/list-of-diagnostics.md b/docs/list-of-diagnostics.md index 6546bdbf744..a493239de02 100644 --- a/docs/list-of-diagnostics.md +++ b/docs/list-of-diagnostics.md @@ -16,8 +16,9 @@ | `ASPIRE011` | Error | '\[ProjectName\]' is configured to invoke the Aspire CLI through DNX, but the dnx command could not be found on PATH. | [src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets](../src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets) | | `ASPIRE012` | Warning | '\[ProjectName\]' uses the Aspire.AppHost.Sdk, which is obsolete but remains available for staged migration. Switch the project's Sdk attribute to Microsoft.NET.Sdk, add explicit PackageReference items for Aspire.Hosting.AppHost and Aspire.Hosting.Dotnet, and migrate C# resource ProjectReference items to AddDotnetProject calls. Set SuppressAspireAppHostSdkObsoleteWarning=true (or add ASPIRE012 to $(NoWarn)) to suppress temporarily during migration. See https://aka.ms/aspire/diagnostics/aspire012 for migration guidance. | [src/Aspire.AppHost.Sdk/SDK/Sdk.in.targets](../src/Aspire.AppHost.Sdk/SDK/Sdk.in.targets) | | `ASPIRE013` | Error | '\[ProjectName\]' requires the Aspire CLI bundle. Remove AspireUseCliBundle or set it to true, and install the matching Aspire CLI from https://get.aspire.dev. ASPIRE013 cannot be suppressed to disable the bundle. See https://aka.ms/aspire/diagnostics/aspire013 for more information. | [src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets](../src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets) | +| `ASPIRE014` | Warning | '\[ProjectName\]' is configured with an unsupported AspireCliInvocationMode value. Supported values are Path, Dnx, and DnxPinned. The default Path invocation mode will be used. | [src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets](../src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets) | -The CLI bundle is required by default for C# AppHosts and cannot be disabled (`ASPIRE013`). Non-C# AppHosts retain the suppressible `ASPIRE010` warning when the bundle is disabled. `AspireCliInvocationMode=Path` uses `aspire` from `PATH`, falling back to the `Aspire.Cli` version paired with the AppHost package through DNX when a compatible command is unavailable. `AspireCliInvocationMode=Dnx` invokes the unversioned `Aspire.Cli` package through DNX so an in-scope tool manifest is honored, or the latest package is used when no manifest applies. `AspireCliInvocationMode=DnxPinned` invokes the exact `Aspire.Cli` version paired with `Aspire.Hosting.AppHost`. +The CLI bundle is required by default for C# AppHosts and cannot be disabled (`ASPIRE013`). Non-C# AppHosts retain the suppressible `ASPIRE010` warning when the bundle is disabled. `AspireCliInvocationMode=Path` uses `aspire` from `PATH`, falling back to the `Aspire.Cli` version paired with the AppHost package through DNX when a compatible command is unavailable. `AspireCliInvocationMode=Dnx` invokes the unversioned `Aspire.Cli` package through DNX so an in-scope tool manifest is honored, or the latest package is used when no manifest applies. `AspireCliInvocationMode=DnxPinned` invokes the exact `Aspire.Cli` version paired with `Aspire.Hosting.AppHost`. Any other value produces the `ASPIRE014` warning and uses the default `Path` mode. When automatic CLI bundle setup fails, `ASPIRE009` includes the setup command's captured standard output and standard error, including DNX package acquisition errors. If a PATH CLI setup attempt falls back to DNX and neither produces a usable bundle, output from both attempts is included with the corresponding command. Successful or recovered setup attempts keep this output at diagnostic verbosity. diff --git a/src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets b/src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets index b8b23f2e441..81bad0a01be 100644 --- a/src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets +++ b/src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets @@ -226,6 +226,10 @@ namespace Projects%3B + + diff --git a/tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs b/tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs index fd977511171..7e6e7dd65a8 100644 --- a/tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs +++ b/tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs @@ -436,6 +436,84 @@ public async Task ComputeRunArgumentsFailsWhenDnxModeIsConfiguredAndDnxIsMissing Assert.Contains("Install or use the .NET SDK 10.0 or later", result.Output); } + [Theory] + [InlineData("DefinitelyInvalid")] + [InlineData("DnxPinnedd")] + public async Task ComputeRunArgumentsWarnsAndUsesPathModeWhenInvocationModeIsUnsupported(string invocationMode) + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var fakeCliDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "fake-cli")); + var aspireCliPath = await CreateFakeAspireCliAsync(fakeCliDirectory.FullName); + await CreateFakeDnxAsync(fakeCliDirectory.FullName); + var project = await CreateRunHookProjectAsync(workspace.Path, aspireUseCliBundle: true, + $$""" + + {{invocationMode}} + + """); + + var result = await RunComputeRunArgumentsAsync(project, CreatePathEnvironment(fakeCliDirectory.FullName)); + + Assert.True(result.ExitCode == 0, result.Output); + Assert.Contains("warning ASPIRE014", result.Output); + Assert.Contains($"AspireCliInvocationMode={invocationMode}, which is not a supported value", result.Output); + Assert.Contains("Supported values are Path, Dnx, and DnxPinned", result.Output); + + var properties = await GetComputeRunArgumentsPropertiesAsync( + project, + environment: CreatePathEnvironment(fakeCliDirectory.FullName)); + + Assert.Equal("Aspire", properties["_AspireResolvedCliInvocationMode"]); + Assert.Equal(GetExpectedAspireRunCommand(aspireCliPath), properties["RunCommand"]); + } + + [Theory] + [InlineData(null)] + [InlineData("Path")] + [InlineData("pAtH")] + [InlineData("Dnx")] + [InlineData("DnxPinned")] + public async Task ComputeRunArgumentsDoesNotWarnWhenInvocationModeIsSupported(string? invocationMode) + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var fakeCliDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "fake-cli")); + await CreateFakeAspireCliAsync(fakeCliDirectory.FullName); + await CreateFakeDnxAsync(fakeCliDirectory.FullName); + var extraProjectXml = invocationMode is null + ? null + : $$""" + + {{invocationMode}} + + """; + var project = await CreateRunHookProjectAsync(workspace.Path, aspireUseCliBundle: true, extraProjectXml); + + var result = await RunComputeRunArgumentsAsync(project, CreatePathEnvironment(fakeCliDirectory.FullName)); + + Assert.True(result.ExitCode == 0, result.Output); + Assert.DoesNotContain("ASPIRE014", result.Output); + } + + [Fact] + public async Task UnsupportedInvocationModeWarningCanBeSuppressed() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var fakeCliDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "fake-cli")); + await CreateFakeAspireCliAsync(fakeCliDirectory.FullName); + var project = await CreateRunHookProjectAsync(workspace.Path, aspireUseCliBundle: true, + """ + + DefinitelyInvalid + $(NoWarn);ASPIRE014 + + """); + + var result = await RunComputeRunArgumentsAsync(project, CreatePathEnvironment(fakeCliDirectory.FullName)); + + Assert.True(result.ExitCode == 0, result.Output); + Assert.DoesNotContain("ASPIRE014", result.Output); + } + [Fact] public async Task ComputeRunArgumentsSkipsNonExecutableDnxOnUnix() { @@ -1640,6 +1718,12 @@ private static async Task> GetComputeRunArgumentsProp environment); } + private static Task RunComputeRunArgumentsAsync(RunHookProject project, IDictionary environment) + => RunDotNetWithArgumentsAsync( + project.ProjectDirectory, + ["msbuild", "-nologo", "-restore", "-t:ComputeRunArguments", project.ProjectFile], + environment); + private static async Task> GetResolveAspireCliBundlePathPropertiesAsync( RunHookProject project, IDictionary environment,