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: 1 addition & 3 deletions src/Aspire.Cli/Packaging/PackageSourceOverrideMappings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ namespace Aspire.Cli.Packaging;

internal static class PackageSourceOverrideMappings
{
internal const string NuGetOrgSource = "https://api.nuget.org/v3/index.json";

public static PackageMapping[] Create(string packageSourceOverride, PackageChannel? requestedChannel)
{
ArgumentException.ThrowIfNullOrWhiteSpace(packageSourceOverride);
Expand Down Expand Up @@ -35,7 +33,7 @@ public static PackageMapping[] Create(string packageSourceOverride, PackageChann

if (!mappings.Any(static mapping => mapping.PackageFilter == PackageMapping.AllPackages))
{
mappings.Add(new PackageMapping(PackageMapping.AllPackages, NuGetOrgSource));
mappings.Add(new PackageMapping(PackageMapping.AllPackages, PackageSources.NuGetOrg));
}

return [.. mappings.DistinctBy(static mapping => $"{mapping.PackageFilter}\0{mapping.Source}")];
Expand Down
9 changes: 9 additions & 0 deletions src/Aspire.Cli/Packaging/PackageSources.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Cli.Packaging;

internal static class PackageSources
{
internal const string NuGetOrg = "https://api.nuget.org/v3/index.json";
}
107 changes: 90 additions & 17 deletions src/Aspire.Cli/Packaging/PackagingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Semver;
using System.Globalization;
using System.Reflection;
using System.Security;

namespace Aspire.Cli.Packaging;

Expand Down Expand Up @@ -47,6 +48,7 @@ internal class PackagingService : IPackagingService
private readonly IFeatures _features;
private readonly IConfiguration _configuration;
private readonly ILogger<PackagingService> _logger;
private readonly Func<string?> _processPathProvider;

// Cached result of the staging-channel availability check. The inputs (CLI identity,
// overrideStagingFeed, StagingChannelEnabled feature) are effectively static for the
Expand All @@ -55,13 +57,20 @@ internal class PackagingService : IPackagingService
// UpdateCommand, IntegrationPackageSearchService, NuGetPackagePrefetcher, etc.).
private readonly Lazy<string?> _stagingUnavailableReasonCache;

public PackagingService(CliExecutionContext executionContext, INuGetPackageCache nuGetPackageCache, IFeatures features, IConfiguration configuration, ILogger<PackagingService> logger)
public PackagingService(
CliExecutionContext executionContext,
INuGetPackageCache nuGetPackageCache,
IFeatures features,
IConfiguration configuration,
ILogger<PackagingService> logger,
Func<string?>? processPathProvider = null)
{
_executionContext = executionContext;
_nuGetPackageCache = nuGetPackageCache;
_features = features;
_configuration = configuration;
_logger = logger;
_processPathProvider = processPathProvider ?? (() => Environment.ProcessPath);
_stagingUnavailableReasonCache = new Lazy<string?>(ComputeStagingChannelUnavailableReason);
}

Expand All @@ -79,13 +88,13 @@ public Task<IEnumerable<PackageChannel>> GetChannelsAsync(CancellationToken canc

var stableChannel = PackageChannel.CreateExplicitChannel(PackageChannelNames.Stable, PackageChannelQuality.Stable, new[]
{
new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index.json")
new PackageMapping(PackageMapping.AllPackages, PackageSources.NuGetOrg)
}, _nuGetPackageCache, cliDownloadBaseUrl: "https://aka.ms/dotnet/9/aspire/ga/daily", logger: _logger);

var dailyChannel = PackageChannel.CreateExplicitChannel(PackageChannelNames.Daily, PackageChannelQuality.Prerelease, new[]
{
new PackageMapping("Aspire*", "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet9/nuget/v3/index.json"),
new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index.json")
new PackageMapping(PackageMapping.AllPackages, PackageSources.NuGetOrg)
}, _nuGetPackageCache, cliDownloadBaseUrl: "https://aka.ms/dotnet/9/aspire/daily", logger: _logger);

var prPackageChannels = new List<PackageChannel>();
Expand All @@ -99,22 +108,18 @@ public Task<IEnumerable<PackageChannel>> GetChannelsAsync(CancellationToken canc
var prHives = _executionContext.HivesDirectory.GetDirectories();
foreach (var prHive in prHives)
{
// The packages subdirectory contains the actual .nupkg files
var packagesDirectory = new DirectoryInfo(Path.Combine(prHive.FullName, "packages"));
var pinnedVersion = GetLocalHivePinnedVersion(packagesDirectory);

// Use forward slashes for cross-platform NuGet config compatibility
var packagesPath = packagesDirectory.FullName.Replace('\\', '/');
var prChannel = PackageChannel.CreateExplicitChannel(prHive.Name, PackageChannelQuality.Both, new[]
{
new PackageMapping("Aspire*", packagesPath),
new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index.json")
}, _nuGetPackageCache, pinnedVersion: pinnedVersion, logger: _logger);

prPackageChannels.Add(prChannel);
prPackageChannels.Add(CreateLocalHiveChannel(prHive.Name, new DirectoryInfo(Path.Combine(prHive.FullName, "packages"))));
}
}

if (TryResolvePrInstallPackagesDirectory(TryGetProcessPathForPrInstallDiscovery(), _executionContext.IdentityChannel) is { } prInstallPackagesDirectory)
{
// The install-prefix hive belongs to the running PR CLI. Prefer it over a same-named
// default hive so a stale ~/.aspire/hives/pr-<N> cannot mask the co-installed packages.
prPackageChannels.RemoveAll(c => string.Equals(c.Name, _executionContext.IdentityChannel, StringComparisons.ChannelName));
prPackageChannels.Add(CreateLocalHiveChannel(_executionContext.IdentityChannel, prInstallPackagesDirectory));
}

var channels = new List<PackageChannel>([defaultChannel, stableChannel]);

// Add staging channel after stable and before daily. Staging CLI builds should
Expand Down Expand Up @@ -142,6 +147,74 @@ public Task<IEnumerable<PackageChannel>> GetChannelsAsync(CancellationToken canc
return Task.FromResult<IEnumerable<PackageChannel>>(channels);
}

private string? TryGetProcessPathForPrInstallDiscovery()
{
try
{
return _processPathProvider();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to get process path while discovering PR package hive.");
return null;
}
}

private PackageChannel CreateLocalHiveChannel(string name, DirectoryInfo packagesDirectory)
{
var pinnedVersion = GetLocalHivePinnedVersion(packagesDirectory);

// Use forward slashes for cross-platform NuGet config compatibility
var packagesPath = packagesDirectory.FullName.Replace('\\', '/');
return PackageChannel.CreateExplicitChannel(name, PackageChannelQuality.Both, new[]
{
new PackageMapping("Aspire*", packagesPath),
new PackageMapping(PackageMapping.AllPackages, PackageSources.NuGetOrg)
}, _nuGetPackageCache, pinnedVersion: pinnedVersion, logger: _logger);
}

internal static DirectoryInfo? TryResolvePrInstallPackagesDirectory(string? processPath, string identityChannel)
{
if (!identityChannel.StartsWith("pr-", StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrEmpty(processPath))
{
return null;
}

DirectoryInfo binaryDirectory;
try
{
var binaryDirectoryPath = Path.GetDirectoryName(Path.GetFullPath(processPath));
if (string.IsNullOrEmpty(binaryDirectoryPath))
{
return null;
}

binaryDirectory = new DirectoryInfo(binaryDirectoryPath);
}
catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException or SecurityException)
{
return null;
}

// Archive installs from get-aspire-cli-pr place the binary at:
// <prefix>/dogfood/pr-<N>/bin/aspire
// and the matching packages at:
// <prefix>/hives/pr-<N>/packages
if (!string.Equals(binaryDirectory.Name, "bin", StringComparison.OrdinalIgnoreCase) ||
binaryDirectory.Parent is not { } prDirectory ||
!string.Equals(prDirectory.Name, identityChannel, StringComparisons.ChannelName) ||
prDirectory.Parent is not { } dogfoodDirectory ||
!string.Equals(dogfoodDirectory.Name, "dogfood", StringComparison.OrdinalIgnoreCase) ||
dogfoodDirectory.Parent is not { } installPrefix)
{
return null;
}

var packagesDirectory = new DirectoryInfo(Path.Combine(installPrefix.FullName, "hives", identityChannel, "packages"));
return packagesDirectory.Exists ? packagesDirectory : null;
}

private PackageChannel? CreateStagingChannel(PackageChannelQuality defaultQuality)
{
// Refuse to synthesize a staging channel on CLI identities that cannot produce a real
Expand Down Expand Up @@ -181,7 +254,7 @@ public Task<IEnumerable<PackageChannel>> GetChannelsAsync(CancellationToken canc
var stagingChannel = PackageChannel.CreateExplicitChannel(PackageChannelNames.Staging, stagingQuality, new[]
{
new PackageMapping("Aspire*", stagingFeedUrl),
new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index.json")
new PackageMapping(PackageMapping.AllPackages, PackageSources.NuGetOrg)
}, _nuGetPackageCache, configureGlobalPackagesFolder: !useSharedFeed, cliDownloadBaseUrl: "https://aka.ms/dotnet/9/aspire/rc/daily", pinnedVersion: pinnedVersion, logger: _logger);

// Surface the resolved staging routing so users can see what `--channel staging` actually
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -632,9 +632,9 @@ private void ThrowIfStagingUnavailable(string? requestedChannel)
// --source argument list must agree so non-Aspire transitives have the same
// catch-all source in both views.
if (hasOverride && !matchedChannelHasAllPackagesMapping &&
!sources.Contains(PackageSourceOverrideMappings.NuGetOrgSource, StringComparer.OrdinalIgnoreCase))
!sources.Contains(PackageSources.NuGetOrg, StringComparer.OrdinalIgnoreCase))
{
sources.Add(PackageSourceOverrideMappings.NuGetOrgSource);
sources.Add(PackageSources.NuGetOrg);
}
}
catch (Exception ex)
Expand Down
16 changes: 15 additions & 1 deletion src/Aspire.Cli/Templating/TemplateNuGetConfigService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,11 @@ public async Task<TemplatePackageSelection> ResolveTemplatePackageAsync(Template

// Honor PR hives only when the caller opts in. Init suppresses this so a developer
// with stale ~/.aspire/hives/* doesn't get a different template than on a clean machine.
var hasPrHives = query.IncludePrHives && executionContext.GetHiveCount() > 0;
// PR dogfood installs can discover a matching local-build channel outside the default
// hives directory, so also treat an installed local-build source as a hive signal.
var hasPrHives = query.IncludePrHives &&
(executionContext.GetHiveCount() > 0 ||
allChannels.Any(static c => c.Type is PackageChannelType.Explicit && HasInstalledLocalBuildPackageSource(c)));

IEnumerable<PackageChannel> channels;
if (!string.IsNullOrEmpty(query.RequestedChannel))
Expand Down Expand Up @@ -292,6 +296,16 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
return new TemplatePackageSelection(prompted.Package, prompted.Channel);
}

private static bool HasInstalledLocalBuildPackageSource(PackageChannel channel)
{
return VersionHelper.IsLocalBuildChannel(channel.Name) &&
channel.Mappings?.Any(static mapping =>
mapping.PackageFilter.StartsWith("Aspire", StringComparison.OrdinalIgnoreCase) &&
mapping.PackageFilter != PackageMapping.AllPackages &&
!UrlHelper.IsHttpUrl(mapping.Source) &&
Directory.Exists(mapping.Source)) == true;
}

/// <summary>
/// Installs the resolved Aspire project templates package, generating a temporary NuGet.config from the channel mappings when the channel is explicit.
/// </summary>
Expand Down
Loading
Loading