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
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
{
"appHostPath": "../apphost.ts",
"language": "typescript/nodejs",
"channel": "pr-13970",
"sdkVersion": "13.2.0-pr.13970.g9fb24263",
"packages": {
"Aspire.Hosting.Azure.Storage": "13.2.0-pr.13970.g9fb24263"
"Aspire.Hosting.Azure.Storage": ""
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
{
"appHostPath": "../apphost.ts",
"language": "typescript/nodejs",
"channel": "local",
"sdkVersion": "13.2.0-preview.1.26081.1",
"packages": {
"Aspire.Hosting.RabbitMQ": "13.2.0-preview.1.26081.1"
"Aspire.Hosting.RabbitMQ": ""
}
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
{
"appHostPath": "../apphost.ts",
"language": "typescript/nodejs",
"channel": "pr-13970",
"sdkVersion": "13.1.0",
"packages": {
"Aspire.Hosting.SqlServer": "13.2.0-pr.13970.g0575147c"
"Aspire.Hosting.SqlServer": ""
}
}
}
9 changes: 7 additions & 2 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,13 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
string? configuredChannel = null;
if (project.LanguageId != KnownLanguageId.CSharp)
{
var settings = AspireJsonConfiguration.Load(effectiveAppHostProjectFile.Directory!.FullName);
configuredChannel = settings?.Channel;
var appHostDirectory = effectiveAppHostProjectFile.Directory!.FullName;
var isProjectReferenceMode = AspireRepositoryDetector.DetectRepositoryRoot(appHostDirectory) is not null;
if (!isProjectReferenceMode)
{
var settings = AspireJsonConfiguration.Load(appHostDirectory);
configuredChannel = settings?.Channel;
}
}

var packagesWithChannels = await InteractionService.ShowStatusAsync(
Expand Down
13 changes: 10 additions & 3 deletions src/Aspire.Cli/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,26 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
return ExitCodeConstants.FailedToFindProject;
}

var allChannels = await _packagingService.GetChannelsAsync(cancellationToken);
var project = _projectFactory.GetProject(projectFile);
var isProjectReferenceMode = project.IsUsingProjectReferences(projectFile);

// Check if channel or quality option was provided (channel takes precedence)
var channelName = parseResult.GetValue(_channelOption) ?? parseResult.GetValue(_qualityOption);
PackageChannel channel;

var allChannels = await _packagingService.GetChannelsAsync(cancellationToken);

if (!string.IsNullOrEmpty(channelName))
{
// Try to find a channel matching the provided channel/quality
channel = allChannels.FirstOrDefault(c => string.Equals(c.Name, channelName, StringComparison.OrdinalIgnoreCase))
?? throw new ChannelNotFoundException($"No channel found matching '{channelName}'. Valid options are: {string.Join(", ", allChannels.Select(c => c.Name))}");
}
else if (isProjectReferenceMode)
{
channel = allChannels.FirstOrDefault(c => c.Type is PackageChannelType.Implicit)
?? allChannels.First();
}
else
{
// If there are hives (PR build directories), prompt for channel selection.
Expand All @@ -181,8 +189,7 @@ protected override async Task<int> ExecuteAsync(ParseResult parseResult, Cancell
}
}

// Get the appropriate project handler and update packages
var project = _projectFactory.GetProject(projectFile);
// Update packages using the appropriate project handler
var updateContext = new UpdatePackagesContext
{
AppHostFile = projectFile,
Expand Down
58 changes: 41 additions & 17 deletions src/Aspire.Cli/Configuration/AspireJsonConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,32 +163,56 @@ public bool RemovePackage(string packageId)
}

/// <summary>
/// Gets all package references including the base Aspire.Hosting packages.
/// Uses the SdkVersion for base packages.
/// Note: Aspire.Hosting.AppHost is an SDK package (not a runtime DLL) and is excluded.
/// Gets the effective SDK version for package-based AppHost preparation.
/// Falls back to <paramref name="defaultSdkVersion"/> when no SDK version is configured.
/// </summary>
public string GetEffectiveSdkVersion(string defaultSdkVersion)
{
return string.IsNullOrWhiteSpace(SdkVersion) ? defaultSdkVersion : SdkVersion;
}

/// <summary>
/// Gets all package references including the base Aspire.Hosting package.
/// Empty package versions in settings are resolved to the effective SDK version.
/// </summary>
/// <param name="defaultSdkVersion">Default SDK version to use when not configured.</param>
/// <returns>Enumerable of (PackageName, Version) tuples.</returns>
public IEnumerable<(string Name, string Version)> GetAllPackages()
public IEnumerable<(string Name, string Version)> GetAllPackages(string defaultSdkVersion)
{
var sdkVersion = SdkVersion ?? throw new InvalidOperationException("SdkVersion must be set before calling GetAllPackages. Use LoadOrCreate to ensure it's set.");
var sdkVersion = GetEffectiveSdkVersion(defaultSdkVersion);

// Base packages always included (Aspire.Hosting.AppHost is an SDK, not a runtime DLL)
// Base package always included (Aspire.Hosting.AppHost is an SDK, not a runtime DLL)
yield return ("Aspire.Hosting", sdkVersion);

// Additional packages from settings
if (Packages is not null)
if (Packages is null)
{
yield break;
}

foreach (var (packageName, version) in Packages)
{
foreach (var (packageName, version) in Packages)
// Skip base packages and SDK-only packages
if (string.Equals(packageName, "Aspire.Hosting", StringComparison.OrdinalIgnoreCase) ||
string.Equals(packageName, "Aspire.Hosting.AppHost", StringComparison.OrdinalIgnoreCase))
{
// Skip base packages and SDK-only packages
if (string.Equals(packageName, "Aspire.Hosting", StringComparison.OrdinalIgnoreCase) ||
string.Equals(packageName, "Aspire.Hosting.AppHost", StringComparison.OrdinalIgnoreCase))
{
continue;
}

yield return (packageName, version);
continue;
}

yield return (packageName, string.IsNullOrWhiteSpace(version) ? sdkVersion : version);
}
}

/// <summary>
/// Gets all package references including the base Aspire.Hosting packages.
/// Uses the SdkVersion for base packages.
/// Note: Aspire.Hosting.AppHost is an SDK package (not a runtime DLL) and is excluded.
/// </summary>
/// <returns>Enumerable of (PackageName, Version) tuples.</returns>
public IEnumerable<(string Name, string Version)> GetAllPackages()
{
var sdkVersion = !string.IsNullOrWhiteSpace(SdkVersion)
? SdkVersion
: throw new InvalidOperationException("SdkVersion must be set to a non-empty value before calling GetAllPackages. Use LoadOrCreate to ensure it's set.");
return GetAllPackages(sdkVersion);
}
}
39 changes: 2 additions & 37 deletions src/Aspire.Cli/Projects/AppHostServerProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Aspire.Cli.DotNet;
using Aspire.Cli.NuGet;
using Aspire.Cli.Packaging;
using Aspire.Cli.Utils;
using Microsoft.Extensions.Logging;

namespace Aspire.Cli.Projects;
Expand Down Expand Up @@ -58,7 +59,7 @@ public async Task<IAppHostServerProject> CreateAsync(string appPath, Cancellatio
}

// Priority 1: Check for dev mode (ASPIRE_REPO_ROOT or running from Aspire source repo)
var repoRoot = DetectAspireRepoRoot();
var repoRoot = AspireRepositoryDetector.DetectRepositoryRoot(appPath);
if (repoRoot is not null)
{
return new DotNetBasedAppHostServerProject(
Expand Down Expand Up @@ -91,40 +92,4 @@ public async Task<IAppHostServerProject> CreateAsync(string appPath, Cancellatio
"No Aspire AppHost server is available. Ensure the Aspire CLI is installed " +
"with a valid bundle layout, or reinstall using 'aspire setup --force'.");
}

/// <summary>
/// Detects the Aspire repository root for dev mode.
/// Checks ASPIRE_REPO_ROOT env var first, then walks up from the CLI executable
/// looking for a git repo containing Aspire.slnx.
/// </summary>
private static string? DetectAspireRepoRoot()
{
// Check explicit environment variable
var envRoot = Environment.GetEnvironmentVariable("ASPIRE_REPO_ROOT");
if (!string.IsNullOrEmpty(envRoot) && Directory.Exists(envRoot))
{
return envRoot;
}

// Auto-detect: walk up from the CLI executable looking for .git + Aspire.slnx
var cliPath = Environment.ProcessPath;
if (string.IsNullOrEmpty(cliPath))
{
return null;
}

var dir = Path.GetDirectoryName(cliPath);
while (dir is not null)
{
if (Directory.Exists(Path.Combine(dir, ".git")) &&
File.Exists(Path.Combine(dir, "Aspire.slnx")))
{
return dir;
}

dir = Path.GetDirectoryName(dir);
}

return null;
}
}
6 changes: 6 additions & 0 deletions src/Aspire.Cli/Projects/DotNetAppHostProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ private static bool IsValidSingleFileAppHost(FileInfo candidateFile)
/// <inheritdoc />
public string? AppHostFileName => "apphost.cs";

/// <inheritdoc />
public bool IsUsingProjectReferences(FileInfo appHostFile)
{
return false;
}

// ═══════════════════════════════════════════════════════════════
// EXECUTION
// ═══════════════════════════════════════════════════════════════
Expand Down
58 changes: 37 additions & 21 deletions src/Aspire.Cli/Projects/GuestAppHostProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,22 +128,41 @@ public bool CanHandle(FileInfo appHostFile)
/// <inheritdoc />
public string? AppHostFileName => _resolvedLanguage.DetectionPatterns.FirstOrDefault();

/// <inheritdoc />
public bool IsUsingProjectReferences(FileInfo appHostFile)
{
return AspireRepositoryDetector.DetectRepositoryRoot(appHostFile.Directory?.FullName) is not null;
}

/// <summary>
/// Gets all packages including the code generation package for the current language.
/// </summary>
private async Task<List<(string Name, string Version)>> GetAllPackagesAsync(
AspireJsonConfiguration config,
CancellationToken cancellationToken)
{
var packages = config.GetAllPackages().ToList();
var defaultSdkVersion = GetEffectiveSdkVersion();
var packages = config.GetAllPackages(defaultSdkVersion).ToList();
var codeGenPackage = await _languageDiscovery.GetPackageForLanguageAsync(_resolvedLanguage.LanguageId, cancellationToken);
if (codeGenPackage is not null)
{
packages.Add((codeGenPackage, config.SdkVersion!));
var codeGenVersion = config.GetEffectiveSdkVersion(defaultSdkVersion);
packages.Add((codeGenPackage, codeGenVersion));
}
return packages;
}

private AspireJsonConfiguration LoadConfiguration(DirectoryInfo directory)
{
var effectiveSdkVersion = GetEffectiveSdkVersion();
return AspireJsonConfiguration.LoadOrCreate(directory.FullName, effectiveSdkVersion);
}

private string GetPrepareSdkVersion(AspireJsonConfiguration config)
{
return config.GetEffectiveSdkVersion(GetEffectiveSdkVersion());
}

/// <summary>
/// Prepares the AppHost server (creates files and builds for dev mode, restores packages for prebuilt mode).
/// </summary>
Expand All @@ -162,14 +181,14 @@ public bool CanHandle(FileInfo appHostFile)
/// </summary>
private async Task BuildAndGenerateSdkAsync(DirectoryInfo directory, CancellationToken cancellationToken)
{
var appHostServerProject = await _appHostServerProjectFactory.CreateAsync(directory.FullName, cancellationToken);

// Step 1: Load config - source of truth for SDK version and packages
var effectiveSdkVersion = GetEffectiveSdkVersion();
var config = AspireJsonConfiguration.LoadOrCreate(directory.FullName, effectiveSdkVersion);
var config = LoadConfiguration(directory);
var packages = await GetAllPackagesAsync(config, cancellationToken);
var sdkVersion = GetPrepareSdkVersion(config);

var appHostServerProject = await _appHostServerProjectFactory.CreateAsync(directory.FullName, cancellationToken);

var (buildSuccess, buildOutput, _, _) = await PrepareAppHostServerAsync(appHostServerProject, config.SdkVersion!, packages, cancellationToken);
var (buildSuccess, buildOutput, _, _) = await PrepareAppHostServerAsync(appHostServerProject, sdkVersion, packages, cancellationToken);
if (!buildSuccess)
{
if (buildOutput is not null)
Expand Down Expand Up @@ -269,19 +288,19 @@ public async Task<int> RunAsync(AppHostProjectContext context, CancellationToken
}

// Build phase: build AppHost server (dependency install happens after server starts)
var appHostServerProject = await _appHostServerProjectFactory.CreateAsync(directory.FullName, cancellationToken);

// Load config - source of truth for SDK version and packages
var effectiveSdkVersion = GetEffectiveSdkVersion();
var config = AspireJsonConfiguration.LoadOrCreate(directory.FullName, effectiveSdkVersion);
var config = LoadConfiguration(directory);
var packages = await GetAllPackagesAsync(config, cancellationToken);

var appHostServerProject = await _appHostServerProjectFactory.CreateAsync(directory.FullName, cancellationToken);
var sdkVersion = GetPrepareSdkVersion(config);

var buildResult = await _interactionService.ShowStatusAsync(
":gear: Preparing Aspire server...",
async () =>
{
// Prepare the AppHost server (build for dev mode, restore for prebuilt)
var (prepareSuccess, prepareOutput, channelName, needsCodeGen) = await PrepareAppHostServerAsync(appHostServerProject, config.SdkVersion!, packages, cancellationToken);
var (prepareSuccess, prepareOutput, channelName, needsCodeGen) = await PrepareAppHostServerAsync(appHostServerProject, sdkVersion, packages, cancellationToken);
if (!prepareSuccess)
{
return (Success: false, Output: prepareOutput, Error: "Failed to prepare app host.", ChannelName: (string?)null, NeedsCodeGen: false);
Expand Down Expand Up @@ -557,14 +576,13 @@ public async Task<int> PublishAsync(PublishContext context, CancellationToken ca
try
{
// Step 1: Load config - source of truth for SDK version and packages
var effectiveSdkVersion = GetEffectiveSdkVersion();
var config = AspireJsonConfiguration.LoadOrCreate(directory.FullName, effectiveSdkVersion);
var packages = await GetAllPackagesAsync(config, cancellationToken);

var appHostServerProject = await _appHostServerProjectFactory.CreateAsync(directory.FullName, cancellationToken);
var config = LoadConfiguration(directory);
var packages = await GetAllPackagesAsync(config, cancellationToken);
var sdkVersion = GetPrepareSdkVersion(config);

// Prepare the AppHost server (build for dev mode, restore for prebuilt)
var (prepareSuccess, prepareOutput, _, needsCodeGen) = await PrepareAppHostServerAsync(appHostServerProject, config.SdkVersion!, packages, cancellationToken);
var (prepareSuccess, prepareOutput, _, needsCodeGen) = await PrepareAppHostServerAsync(appHostServerProject, sdkVersion, packages, cancellationToken);
if (!prepareSuccess)
{
// Set OutputCollector so PipelineCommandBase can display errors
Expand Down Expand Up @@ -802,8 +820,7 @@ public async Task<bool> AddPackageAsync(AddPackageContext context, CancellationT
}

// Load config - source of truth for SDK version and packages
var effectiveSdkVersion = GetEffectiveSdkVersion();
var config = AspireJsonConfiguration.LoadOrCreate(directory.FullName, effectiveSdkVersion);
var config = LoadConfiguration(directory);

// Update .aspire/settings.json with the new package
config.AddOrUpdatePackage(context.PackageId, context.PackageVersion);
Expand All @@ -825,8 +842,7 @@ public async Task<UpdatePackagesResult> UpdatePackagesAsync(UpdatePackagesContex
}

// Load config - source of truth for SDK version and packages
var effectiveSdkVersion = GetEffectiveSdkVersion();
var config = AspireJsonConfiguration.LoadOrCreate(directory.FullName, effectiveSdkVersion);
var config = LoadConfiguration(directory);

// Find updates for SDK version and packages
string? newSdkVersion = null;
Expand Down
7 changes: 7 additions & 0 deletions src/Aspire.Cli/Projects/IAppHostProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,13 @@ internal interface IAppHostProject
/// </summary>
string? AppHostFileName { get; }

/// <summary>
/// Determines whether this AppHost should use project references instead of package references.
/// </summary>
/// <param name="appHostFile">The AppHost file being operated on.</param>
/// <returns><see langword="true"/> when project-reference mode should be used; otherwise <see langword="false"/>.</returns>
bool IsUsingProjectReferences(FileInfo appHostFile);

/// <summary>
/// Runs the AppHost project.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Cli/Projects/ProjectLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ private async Task CreateSettingsFileAsync(FileInfo projectFile, CancellationTok
if (language is not null && !language.LanguageId.Value.Equals(KnownLanguageId.CSharp, StringComparison.OrdinalIgnoreCase))
{
await configurationService.SetConfigurationAsync("language", language.LanguageId.Value, isGlobal: false, cancellationToken);

// Inherit SDK version from parent/global config if available
var inheritedSdkVersion = await configurationService.GetConfigurationAsync("sdkVersion", cancellationToken);
if (!string.IsNullOrEmpty(inheritedSdkVersion))
Expand Down
Loading
Loading