Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
101 changes: 87 additions & 14 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 Down Expand Up @@ -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, "https://api.nuget.org/v3/index.json")
Comment thread
radical marked this conversation as resolved.
Outdated
}, _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
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