Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6701b89
Initial plan
Copilot Oct 20, 2025
7c0b33a
Implement automatic .NET SDK installation feature
Copilot Oct 20, 2025
a1a0549
Refactor RuntimesDirectory to use CliExecutionContext for better test…
Copilot Oct 20, 2025
b3ea029
Fix test compilation issues for RuntimesDirectory refactoring
Copilot Oct 20, 2025
3983f38
Add feature flag for SDK installation (dotnetSdkInstallationEnabled)
Copilot Oct 21, 2025
70cdfde
Add alwaysInstallSdk configuration for testing SDK installation
Copilot Oct 21, 2025
f902d94
Use GetRequiredService and expand IDotNetSdkInstaller interface
Copilot Oct 21, 2025
84aea89
Fix tests.
Oct 21, 2025
7713858
Prepend private SDK path to PATH environment variable
Copilot Oct 21, 2025
c2d1186
Add logging to DotNetSdkInstaller for installation output
Copilot Oct 21, 2025
fec0153
Set rollforward policy and prepend path to filename.
Oct 21, 2025
744eb29
Skip package prefetching when SDK is not installed
Copilot Oct 21, 2025
18d1dcd
Try pwsh before powershell for better cross-platform support
Copilot Oct 21, 2025
3ca0bf8
Add DOTNET_NOLOGO=1 environment variable to suppress welcome message
Copilot Oct 22, 2025
2fc0863
Check for private SDK installation before prompting
Copilot Oct 22, 2025
e23dc73
Verify private SDK by calling GetNuGetConfigPathsAsync
Copilot Oct 22, 2025
eaf74a0
Move SDK installation logic and change path from runtimes to sdks
Copilot Oct 22, 2025
38ea4a5
Change feature flag default to false and update SDK version to RC2
Copilot Oct 22, 2025
8fd320b
Apply suggestions from code review
davidfowl Oct 22, 2025
5cf5518
Use Path.PathSeparator instead of platform-specific logic
Copilot Oct 22, 2025
47d2b3d
Use File.SetUnixFileMode and wrap InstallAsync in ShowStatusAsync
Copilot Oct 22, 2025
b1b004c
Restore original message text for SDK installation
Copilot Oct 22, 2025
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/Aspire.Cli/CliExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@

namespace Aspire.Cli;

internal sealed class CliExecutionContext(DirectoryInfo workingDirectory, DirectoryInfo hivesDirectory, DirectoryInfo cacheDirectory, bool debugMode = false)
internal sealed class CliExecutionContext(DirectoryInfo workingDirectory, DirectoryInfo hivesDirectory, DirectoryInfo cacheDirectory, DirectoryInfo sdksDirectory, bool debugMode = false)
{
public DirectoryInfo WorkingDirectory { get; } = workingDirectory;
public DirectoryInfo HivesDirectory { get; } = hivesDirectory;
public DirectoryInfo CacheDirectory { get; } = cacheDirectory;
public DirectoryInfo SdksDirectory { get; } = sdksDirectory;
public bool DebugMode { get; } = debugMode;

private BaseCommand? _command;
Expand Down
10 changes: 8 additions & 2 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ internal sealed class AddCommand : BaseCommand
private readonly IAddCommandPrompter _prompter;
private readonly AspireCliTelemetry _telemetry;
private readonly IDotNetSdkInstaller _sdkInstaller;
private readonly ICliHostEnvironment _hostEnvironment;
private readonly IFeatures _features;

public AddCommand(IDotNetCliRunner runner, IPackagingService packagingService, IInteractionService interactionService, IProjectLocator projectLocator, IAddCommandPrompter prompter, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext)
public AddCommand(IDotNetCliRunner runner, IPackagingService packagingService, IInteractionService interactionService, IProjectLocator projectLocator, IAddCommandPrompter prompter, AspireCliTelemetry telemetry, IDotNetSdkInstaller sdkInstaller, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, ICliHostEnvironment hostEnvironment)
: base("add", AddCommandStrings.Description, features, updateNotifier, executionContext, interactionService)
{
ArgumentNullException.ThrowIfNull(runner);
Expand All @@ -35,13 +37,17 @@ public AddCommand(IDotNetCliRunner runner, IPackagingService packagingService, I
ArgumentNullException.ThrowIfNull(prompter);
ArgumentNullException.ThrowIfNull(telemetry);
ArgumentNullException.ThrowIfNull(sdkInstaller);
ArgumentNullException.ThrowIfNull(hostEnvironment);
ArgumentNullException.ThrowIfNull(features);

_runner = runner;
_packagingService = packagingService;
_projectLocator = projectLocator;
_prompter = prompter;
_telemetry = telemetry;
_sdkInstaller = sdkInstaller;
_hostEnvironment = hostEnvironment;
_features = features;

var integrationArgument = new Argument<string>("integration");
integrationArgument.Description = AddCommandStrings.IntegrationArgumentDescription;
Expand All @@ -64,7 +70,7 @@ public AddCommand(IDotNetCliRunner runner, IPackagingService packagingService, I
protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
// Check if the .NET SDK is available
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, cancellationToken))
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, _features, _hostEnvironment, cancellationToken))
{
return ExitCodeConstants.SdkNotInstalled;
}
Expand Down
68 changes: 48 additions & 20 deletions src/Aspire.Cli/Commands/CacheCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,39 +45,67 @@ protected override Task<int> ExecuteAsync(ParseResult parseResult, CancellationT
try
{
var cacheDirectory = ExecutionContext.CacheDirectory;

if (!cacheDirectory.Exists)
{
InteractionService.DisplayMessage("information", CacheCommandStrings.CacheAlreadyEmpty);
return Task.FromResult(ExitCodeConstants.Success);
}

var filesDeleted = 0;

// Delete all cache files and subdirectories
foreach (var file in cacheDirectory.GetFiles("*", SearchOption.AllDirectories))
// Delete cache files and subdirectories
if (cacheDirectory.Exists)
{
try
// Delete all cache files and subdirectories
foreach (var file in cacheDirectory.GetFiles("*", SearchOption.AllDirectories))
{
file.Delete();
filesDeleted++;
try
{
file.Delete();
filesDeleted++;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException)
{
// Continue deleting other files even if some fail
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException)

// Delete subdirectories
foreach (var directory in cacheDirectory.GetDirectories())
{
// Continue deleting other files even if some fail
try
{
directory.Delete(recursive: true);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException)
{
// Continue deleting other directories even if some fail
}
}
}

// Delete subdirectories
foreach (var directory in cacheDirectory.GetDirectories())
// Also clear the sdks directory
var sdksDirectory = ExecutionContext.SdksDirectory;
if (sdksDirectory.Exists)
{
try
foreach (var file in sdksDirectory.GetFiles("*", SearchOption.AllDirectories))
{
directory.Delete(recursive: true);
try
{
file.Delete();
filesDeleted++;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException)
{
// Continue deleting other files even if some fail
}
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException)

// Delete subdirectories
foreach (var directory in sdksDirectory.GetDirectories())
{
// Continue deleting other directories even if some fail
try
{
directory.Delete(recursive: true);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException)
{
// Continue deleting other directories even if some fail
}
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/Aspire.Cli/Commands/ExecCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ internal class ExecCommand : BaseCommand
private readonly IAnsiConsole _ansiConsole;
private readonly AspireCliTelemetry _telemetry;
private readonly IDotNetSdkInstaller _sdkInstaller;
private readonly ICliHostEnvironment _hostEnvironment;
private readonly IFeatures _features;

public ExecCommand(
IDotNetCliRunner runner,
Expand All @@ -36,7 +38,7 @@ public ExecCommand(
IDotNetSdkInstaller sdkInstaller,
IFeatures features,
ICliUpdateNotifier updateNotifier,
CliExecutionContext executionContext)
CliExecutionContext executionContext, ICliHostEnvironment hostEnvironment)
: base("exec", ExecCommandStrings.Description, features, updateNotifier, executionContext, interactionService)
{
ArgumentNullException.ThrowIfNull(runner);
Expand All @@ -46,13 +48,17 @@ public ExecCommand(
ArgumentNullException.ThrowIfNull(ansiConsole);
ArgumentNullException.ThrowIfNull(telemetry);
ArgumentNullException.ThrowIfNull(sdkInstaller);
ArgumentNullException.ThrowIfNull(hostEnvironment);
ArgumentNullException.ThrowIfNull(features);

_runner = runner;
_certificateService = certificateService;
_projectLocator = projectLocator;
_ansiConsole = ansiConsole;
_telemetry = telemetry;
_sdkInstaller = sdkInstaller;
_hostEnvironment = hostEnvironment;
_features = features;

var projectOption = new Option<FileInfo?>("--project");
projectOption.Description = ExecCommandStrings.ProjectArgumentDescription;
Expand Down Expand Up @@ -81,7 +87,7 @@ public ExecCommand(
protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
// Check if the .NET SDK is available
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, cancellationToken))
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, _features, _hostEnvironment, cancellationToken))
{
return ExitCodeConstants.SdkNotInstalled;
}
Expand Down
7 changes: 5 additions & 2 deletions src/Aspire.Cli/Commands/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ internal sealed class InitCommand : BaseCommand, IPackageMetaPrefetchingCommand
private readonly ISolutionLocator _solutionLocator;
private readonly AspireCliTelemetry _telemetry;
private readonly IDotNetSdkInstaller _sdkInstaller;
private readonly ICliHostEnvironment _hostEnvironment;
private readonly IFeatures _features;
private readonly ICliUpdateNotifier _updateNotifier;
private readonly CliExecutionContext _executionContext;
Expand All @@ -55,7 +56,7 @@ public InitCommand(
IDotNetSdkInstaller sdkInstaller,
IFeatures features,
ICliUpdateNotifier updateNotifier,
CliExecutionContext executionContext,
CliExecutionContext executionContext, ICliHostEnvironment hostEnvironment,
IInteractionService interactionService)
: base("init", InitCommandStrings.Description, features, updateNotifier, executionContext, interactionService)
{
Expand All @@ -67,6 +68,7 @@ public InitCommand(
ArgumentNullException.ThrowIfNull(solutionLocator);
ArgumentNullException.ThrowIfNull(telemetry);
ArgumentNullException.ThrowIfNull(sdkInstaller);
ArgumentNullException.ThrowIfNull(hostEnvironment);

_runner = runner;
_certificateService = certificateService;
Expand All @@ -76,6 +78,7 @@ public InitCommand(
_solutionLocator = solutionLocator;
_telemetry = telemetry;
_sdkInstaller = sdkInstaller;
_hostEnvironment = hostEnvironment;
_features = features;
_updateNotifier = updateNotifier;
_executionContext = executionContext;
Expand All @@ -94,7 +97,7 @@ public InitCommand(
protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
// Check if the .NET SDK is available
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, cancellationToken))
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, _features, _hostEnvironment, cancellationToken))
{
return ExitCodeConstants.SdkNotInstalled;
}
Expand Down
7 changes: 5 additions & 2 deletions src/Aspire.Cli/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal sealed class NewCommand : BaseCommand, IPackageMetaPrefetchingCommand
private readonly IEnumerable<ITemplate> _templates;
private readonly AspireCliTelemetry _telemetry;
private readonly IDotNetSdkInstaller _sdkInstaller;
private readonly ICliHostEnvironment _hostEnvironment;
private readonly IFeatures _features;
private readonly ICliUpdateNotifier _updateNotifier;
private readonly CliExecutionContext _executionContext;
Expand All @@ -52,7 +53,7 @@ public NewCommand(
IDotNetSdkInstaller sdkInstaller,
IFeatures features,
ICliUpdateNotifier updateNotifier,
CliExecutionContext executionContext)
CliExecutionContext executionContext, ICliHostEnvironment hostEnvironment)
: base("new", NewCommandStrings.Description, features, updateNotifier, executionContext, interactionService)
{
ArgumentNullException.ThrowIfNull(runner);
Expand All @@ -62,13 +63,15 @@ public NewCommand(
ArgumentNullException.ThrowIfNull(templateProvider);
ArgumentNullException.ThrowIfNull(telemetry);
ArgumentNullException.ThrowIfNull(sdkInstaller);
ArgumentNullException.ThrowIfNull(hostEnvironment);

_runner = runner;
_nuGetPackageCache = nuGetPackageCache;
_certificateService = certificateService;
_prompter = prompter;
_telemetry = telemetry;
_sdkInstaller = sdkInstaller;
_hostEnvironment = hostEnvironment;
_features = features;
_updateNotifier = updateNotifier;
_executionContext = executionContext;
Expand Down Expand Up @@ -121,7 +124,7 @@ private async Task<ITemplate> GetProjectTemplateAsync(ParseResult parseResult, C
protected override async Task<int> ExecuteAsync(ParseResult parseResult, CancellationToken cancellationToken)
{
// Check if the .NET SDK is available
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, cancellationToken))
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, _features, _hostEnvironment, cancellationToken))
{
return ExitCodeConstants.SdkNotInstalled;
}
Expand Down
6 changes: 3 additions & 3 deletions src/Aspire.Cli/Commands/PublishCommandBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ protected PublishCommandBase(string name, string description, IDotNetCliRunner r
ArgumentNullException.ThrowIfNull(projectLocator);
ArgumentNullException.ThrowIfNull(telemetry);
ArgumentNullException.ThrowIfNull(sdkInstaller);
ArgumentNullException.ThrowIfNull(features);
ArgumentNullException.ThrowIfNull(hostEnvironment);
ArgumentNullException.ThrowIfNull(features);

_runner = runner;
_projectLocator = projectLocator;
_telemetry = telemetry;
_sdkInstaller = sdkInstaller;
_features = features;
_hostEnvironment = hostEnvironment;
_features = features;

var projectOption = new Option<FileInfo?>("--project")
{
Expand Down Expand Up @@ -86,7 +86,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
StartTerminalProgressBar();

// Check if the .NET SDK is available
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, cancellationToken))
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, _features, _hostEnvironment, cancellationToken))
{
// Send terminal progress bar stop sequence
StopTerminalProgressBar();
Expand Down
8 changes: 6 additions & 2 deletions src/Aspire.Cli/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ internal sealed class RunCommand : BaseCommand
private readonly IDotNetSdkInstaller _sdkInstaller;
private readonly IServiceProvider _serviceProvider;
private readonly IFeatures _features;
private readonly ICliHostEnvironment _hostEnvironment;

public RunCommand(
IDotNetCliRunner runner,
Expand All @@ -45,7 +46,8 @@ public RunCommand(
IFeatures features,
ICliUpdateNotifier updateNotifier,
IServiceProvider serviceProvider,
CliExecutionContext executionContext)
CliExecutionContext executionContext,
ICliHostEnvironment hostEnvironment)
: base("run", RunCommandStrings.Description, features, updateNotifier, executionContext, interactionService)
{
ArgumentNullException.ThrowIfNull(runner);
Expand All @@ -56,6 +58,7 @@ public RunCommand(
ArgumentNullException.ThrowIfNull(telemetry);
ArgumentNullException.ThrowIfNull(configuration);
ArgumentNullException.ThrowIfNull(sdkInstaller);
ArgumentNullException.ThrowIfNull(hostEnvironment);

_runner = runner;
_interactionService = interactionService;
Expand All @@ -67,6 +70,7 @@ public RunCommand(
_serviceProvider = serviceProvider;
_sdkInstaller = sdkInstaller;
_features = features;
_hostEnvironment = hostEnvironment;

var projectOption = new Option<FileInfo?>("--project");
projectOption.Description = RunCommandStrings.ProjectArgumentDescription;
Expand Down Expand Up @@ -99,7 +103,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
}

// Check if the .NET SDK is available
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, cancellationToken))
if (!await SdkInstallHelper.EnsureSdkInstalledAsync(_sdkInstaller, InteractionService, _features, _hostEnvironment, cancellationToken))
{
return ExitCodeConstants.SdkNotInstalled;
}
Expand Down
Loading
Loading