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
4 changes: 2 additions & 2 deletions docs/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
| `ASPIRE008` | Error | '\[ProjectName\]' project requires GenerateAssemblyInfo to be enabled. The Aspire AppHost relies on assembly metadata attributes to locate required dependencies. Please remove <GenerateAssemblyInfo>false</GenerateAssemblyInfo> from your project file or set it to true. | [src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets](../src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets) |
| `ASPIRE009` | Error | '\[ProjectName\]' is configured to use the Aspire CLI bundle, but the bundle could not be resolved. | [src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets](../src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets) |
| `ASPIRE010` | Warning | '\[ProjectName\]' is configured with AspireUseCliBundle=false. Some Aspire features require the Aspire CLI bundle. Set AspireUseCliBundle=true to enable those features, or suppress ASPIRE010 to continue without the bundle. | [src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets](../src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets) |
| `ASPIRE011` | Error | '\[ProjectName\]' is configured with AspireCliInvocationMode=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) |
| `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) |

Setting `AspireUseCliBundle=true` enables CLI delegation. When `aspire` is not available on `PATH` and `dnx` is available, the AppHost uses DNX noninteractively to restore and execute the exact `Aspire.Cli` version paired with its `Aspire.AppHost.Sdk` from the configured NuGet sources.
Setting `AspireUseCliBundle=true` enables CLI delegation. `AspireCliInvocationMode=Path` uses `aspire` from `PATH`, falling back to the `Aspire.Cli` version paired with the AppHost SDK 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.AppHost.Sdk`.

## Analyzer Warnings

Expand Down
19 changes: 11 additions & 8 deletions src/Aspire.Hosting.AppHost/build/Aspire.Hosting.AppHost.in.targets
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,15 @@ namespace Projects%3B
<PropertyGroup>
<_AspireCliInvocationMode Condition="'$(AspireCliInvocationMode)' == ''">Path</_AspireCliInvocationMode>
<_AspireCliInvocationMode Condition="'$(AspireCliInvocationMode)' != ''">$(AspireCliInvocationMode)</_AspireCliInvocationMode>
<_AspireUseDnxCliRunHook Condition="'$(_AspireCliInvocationMode.ToLowerInvariant())' == 'dnx'">true</_AspireUseDnxCliRunHook>
<_AspireUseDnxCliRunHook Condition="'$(_AspireCliInvocationMode.ToLowerInvariant())' == 'dnx' or '$(_AspireCliInvocationMode.ToLowerInvariant())' == 'dnxpinned'">true</_AspireUseDnxCliRunHook>
<_AspireCliDnxPackageId>aspire.cli</_AspireCliDnxPackageId>
<!-- Project Resource v2 is expected to remove Aspire.AppHost.Sdk. When that happens,
replace AspireHostingSDKVersion as the paired CLI version source.
<!-- An explicit Dnx mode leaves the package unversioned so DNX can honor an in-scope tool manifest,
or select the latest package when no manifest applies. Path fallback and DnxPinned use the CLI
version paired with the AppHost SDK. Project Resource v2 is expected to remove Aspire.AppHost.Sdk;
when that happens, replace AspireHostingSDKVersion as the paired CLI version source.
See https://github.com/microsoft/aspire/issues/16386. -->
<_AspireCliDnxPackageReference>$(_AspireCliDnxPackageId)@$(AspireHostingSDKVersion)</_AspireCliDnxPackageReference>
<_AspireCliDnxPackageReference Condition="'$(_AspireCliInvocationMode.ToLowerInvariant())' == 'dnx'">$(_AspireCliDnxPackageId)</_AspireCliDnxPackageReference>
Comment thread
DamianEdwards marked this conversation as resolved.
<_AspireCliDnxPackageReference Condition="'$(_AspireCliInvocationMode.ToLowerInvariant())' != 'dnx'">$(_AspireCliDnxPackageId)@$(AspireHostingSDKVersion)</_AspireCliDnxPackageReference>
</PropertyGroup>

<Target Name="_ResolveAspireCliInvocation"
Expand Down Expand Up @@ -422,9 +425,9 @@ namespace Projects%3B
<!-- dotnet run appends application arguments after ComputeRunArguments, so the double-dash separator
must be present even when RunArguments is initially empty. -->
<RunArguments Condition="'$(AspireCliPath)' != '' or '$(_AspireResolvedCliInvocationMode)' == 'Aspire'">run --project "$(_AspireRunProject)" --no-build --</RunArguments>
<!-- Bundle opt-in authorizes DNX to restore the Aspire CLI version paired with this AppHost SDK
from the user's configured NuGet sources. dotnet run is noninteractive, so DNX cannot ask
for confirmation during the launch. -->
<!-- Bundle opt-in authorizes DNX to restore the selected Aspire CLI package from the user's
configured NuGet sources. dotnet run is noninteractive, so DNX cannot ask for confirmation
during the launch. -->
<RunArguments Condition="'$(AspireCliPath)' == '' and '$(_AspireResolvedCliInvocationMode)' == 'Dnx'">$(_AspireResolvedDnxHostArgumentsPrefix)--yes $(_AspireCliDnxPackageReference) -- run --project "$(_AspireRunProject)" --no-build --</RunArguments>
<!-- The explicit-shim command line is emitted as:
cmd /D /V:OFF /C C:\path^ with^ spaces\aspire.cmd run [separator] user-arg
Expand All @@ -446,7 +449,7 @@ namespace Projects%3B
Condition="'$(IsAspireHost)' == 'true' and '$(AspireUseCliBundle)' == 'true' and '$(_AspireUseDnxCliRunHook)' == 'true' and '$(AspireCliPath)' == ''">
<Error Code="ASPIRE011"
Condition="'$(_AspireResolvedDnxPath)' == ''"
Text="$(MSBuildProjectName) is configured with AspireCliInvocationMode=Dnx, but the dnx command could not be found on PATH. Install or use the .NET SDK 10.0 or later, set AspireCliInvocationMode=Path to use the global aspire command, or set AspireCliPath to an explicit Aspire CLI executable." />
Text="$(MSBuildProjectName) is configured with AspireCliInvocationMode=$(_AspireCliInvocationMode), but the dnx command could not be found on PATH. Install or use the .NET SDK 10.0 or later, set AspireCliInvocationMode=Path to use the global aspire command, or set AspireCliPath to an explicit Aspire CLI executable." />
</Target>

<!-- This target registers the location of the Aspire orchestration dependencies -->
Expand Down
4 changes: 3 additions & 1 deletion src/Aspire.Hosting.Tasks/ResolveAspireCliInvocation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ public override bool Execute()
return true;
}

var forceDnx = string.Equals(AspireCliInvocationMode, "Dnx", StringComparison.OrdinalIgnoreCase);
var forceDnx =
string.Equals(AspireCliInvocationMode, "Dnx", StringComparison.OrdinalIgnoreCase) ||
string.Equals(AspireCliInvocationMode, "DnxPinned", StringComparison.OrdinalIgnoreCase);
if (!forceDnx)
{
ResolvedAspireCliPath = CommandPathResolver.ResolveFromPath("aspire", PathEnvironmentVariable);
Expand Down
69 changes: 54 additions & 15 deletions tests/Aspire.Hosting.Sdk.Tests/AppHostSdkTargetsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@ public async Task ComputeRunArgumentsUsesAspireCliWhenCliBundleIsEnabled()

[Theory]
[InlineData(null)]
[InlineData("Dnx")]
[InlineData("dNx")]
public async Task ComputeRunArgumentsUsesDnxAspireCliWhenSelected(string? invocationMode)
[InlineData("DnxPinned")]
[InlineData("dNxPiNnEd")]
public async Task ComputeRunArgumentsUsesPinnedDnxAspireCliWhenSelected(string? invocationMode)
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var fakeCliDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "fake-cli"));
Expand Down Expand Up @@ -154,15 +154,47 @@ public async Task ComputeRunArgumentsUsesDnxAspireCliWhenSelected(string? invoca
Assert.Equal(project.ProjectDirectory, properties["RunWorkingDirectory"]);
}

[Fact]
public async Task ComputeRunArgumentsFailsWhenDnxModeIsConfiguredAndDnxIsMissing()
[Theory]
[InlineData("Dnx")]
[InlineData("dNx")]
public async Task ComputeRunArgumentsUsesManifestAwareDnxAspireCliWhenSelected(string invocationMode)
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var fakeCliDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "fake-cli"));
var dnxPath = await CreateFakeDnxAsync(fakeCliDirectory.FullName);
await CreateFakeAspireCliAsync(fakeCliDirectory.FullName);
var project = await CreateRunHookProjectAsync(workspace.Path, aspireUseCliBundle: true,
$$"""
<PropertyGroup>
<AspireCliInvocationMode>{{invocationMode}}</AspireCliInvocationMode>
</PropertyGroup>
""");

var properties = await GetComputeRunArgumentsPropertiesAsync(
project,
["-p:RunArguments=--custom foo"],
CreatePathEnvironment(fakeCliDirectory.FullName));

Assert.Equal("Dnx", properties["_AspireResolvedCliInvocationMode"]);
Assert.Equal("13.5.0", properties["_AspireResolvedCliVersion"]);
Assert.Equal("true", properties["_AspireCliVersionSupportsRunHook"]);
Assert.Equal(GetExpectedDnxRunCommand(dnxPath), properties["RunCommand"]);
Assert.Equal(GetExpectedDnxRunArguments(dnxPath, project, "--custom foo", pinned: false), properties["RunArguments"]);
Assert.Contains("--yes aspire.cli -- --version", properties["_AspireCliVersionCommand"]);
Assert.Equal(project.ProjectDirectory, properties["RunWorkingDirectory"]);
}

[Theory]
[InlineData("Dnx")]
[InlineData("DnxPinned")]
public async Task ComputeRunArgumentsFailsWhenDnxModeIsConfiguredAndDnxIsMissing(string invocationMode)
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var emptyPathDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "empty-path"));
var project = await CreateRunHookProjectAsync(workspace.Path, aspireUseCliBundle: true,
"""
$$"""
<PropertyGroup>
<AspireCliInvocationMode>Dnx</AspireCliInvocationMode>
<AspireCliInvocationMode>{{invocationMode}}</AspireCliInvocationMode>
</PropertyGroup>
""");

Expand All @@ -173,7 +205,7 @@ public async Task ComputeRunArgumentsFailsWhenDnxModeIsConfiguredAndDnxIsMissing

Assert.NotEqual(0, result.ExitCode);
Assert.Contains("ASPIRE011", result.Output);
Assert.Contains("AspireCliInvocationMode=Dnx", result.Output);
Assert.Contains($"AspireCliInvocationMode={invocationMode}", result.Output);
Assert.Contains("dnx command could not be found on PATH", result.Output);
Assert.Contains("Install or use the .NET SDK 10.0 or later", result.Output);
}
Expand Down Expand Up @@ -280,15 +312,17 @@ public async Task ComputeRunArgumentsWrapsConfiguredAspireCommandShimOnWindows(s
Assert.Equal(GetExpectedWindowsCommandShimRunArguments(project, aspireCliPath, "--custom foo"), properties["RunArguments"]);
}

[Fact]
public async Task ComputeRunArgumentsUsesConfiguredAspireCliPathWhenDnxModeIsConfigured()
[Theory]
[InlineData("Dnx")]
[InlineData("DnxPinned")]
public async Task ComputeRunArgumentsUsesConfiguredAspireCliPathWhenDnxModeIsConfigured(string invocationMode)
{
using var workspace = TemporaryWorkspace.Create(outputHelper);
var emptyPathDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "empty-path"));
var project = await CreateRunHookProjectAsync(workspace.Path, aspireUseCliBundle: true,
"""
$$"""
<PropertyGroup>
<AspireCliInvocationMode>Dnx</AspireCliInvocationMode>
<AspireCliInvocationMode>{{invocationMode}}</AspireCliInvocationMode>
</PropertyGroup>
""");
var fakeCliDirectory = Directory.CreateDirectory(Path.Combine(workspace.Path, "fake-cli"));
Expand Down Expand Up @@ -899,6 +933,10 @@ @echo off
echo 13.5.0
exit /b 0
)
if "%~1"=="--yes" if "%~2"=="aspire.cli" if "%~3"=="--" if "%~4"=="--version" (
echo 13.5.0
exit /b 0
)
type nul > "%ASPIRE_TEST_CAPTURE_PATH%"
:loop
if "%~1"=="" exit /b 0
Expand All @@ -908,7 +946,7 @@ goto loop
"""
: ("""
#!/bin/sh
if [ "$1" = "--yes" ] && [ "$2" = "aspire.cli@13.5.0" ] && [ "$3" = "--" ] && [ "$4" = "--version" ]; then
if [ "$1" = "--yes" ] && { [ "$2" = "aspire.cli@13.5.0" ] || [ "$2" = "aspire.cli" ]; } && [ "$3" = "--" ] && [ "$4" = "--version" ]; then
echo "13.5.0"
exit 0
fi
Expand Down Expand Up @@ -1189,12 +1227,13 @@ private static string GetExpectedExplicitAspireRunArguments(RunHookProject proje
return string.IsNullOrEmpty(extraArguments) ? arguments : $"{arguments} {extraArguments}";
}

private static string GetExpectedDnxRunArguments(string dnxPath, RunHookProject project, string? extraArguments = null)
private static string GetExpectedDnxRunArguments(string dnxPath, RunHookProject project, string? extraArguments = null, bool pinned = true)
{
var prefix = OperatingSystem.IsWindows()
? $"exec \"{Path.Combine(Path.GetDirectoryName(dnxPath)!, "sdk", AspireCliVersion, "dotnet.dll")}\" dnx "
: string.Empty;
var arguments = $"{prefix}--yes aspire.cli@{AspireCliVersion} -- run --project \"{project.ProjectFile}\" --no-build --";
var packageReference = pinned ? $"aspire.cli@{AspireCliVersion}" : "aspire.cli";
var arguments = $"{prefix}--yes {packageReference} -- run --project \"{project.ProjectFile}\" --no-build --";

return string.IsNullOrEmpty(extraArguments) ? arguments : $"{arguments} {extraArguments}";
}
Expand Down
Loading