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: 4 additions & 0 deletions extension/build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) {
}

Write-Host "All prerequisites satisfied."

# Ensure we run from the extension directory
Set-Location $PSScriptRoot

Write-Host ""
Write-Host "Running yarn install..."
yarn install
Expand Down
5 changes: 5 additions & 0 deletions extension/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ if ! command -v dotnet &> /dev/null; then
fi

echo "All prerequisites satisfied."

# Ensure we run from the extension directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

echo ""
echo "Running yarn install..."
yarn install
Expand Down
65 changes: 50 additions & 15 deletions src/Aspire.Cli/Commands/AddCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,6 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
throw new EmptyChoicesException(AddCommandStrings.NoIntegrationPackagesFound);
}

var version = parseResult.GetValue(s_versionOption);

var packagesWithShortName = packagesWithChannels.Select(GenerateFriendlyName).OrderBy(p => p.FriendlyName, new CommunityToolkitFirstComparer());

if (!packagesWithShortName.Any())
Expand All @@ -172,7 +170,15 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
return ExitCodeConstants.FailedToAddPackage;
}

var filteredPackagesWithShortName = packagesWithShortName.Where(p => p.FriendlyName == integrationName || p.Package.Id == integrationName);
var filteredPackagesWithShortName = packagesWithShortName
.Where(p => p.FriendlyName == integrationName || p.Package.Id == integrationName);

var version = parseResult.GetValue(s_versionOption);

if (!filteredPackagesWithShortName.Any() && integrationName is not null && version is not null && !_hostEnvironment.SupportsInteractiveInput)
{
throw new EmptyChoicesException(string.Format(CultureInfo.CurrentCulture, AddCommandStrings.SpecifiedVersionRequiresExactPackageMatch, integrationName));
}

if (!filteredPackagesWithShortName.Any() && integrationName is not null)
{
Expand All @@ -199,12 +205,10 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
// the version. If there is more than one match then we prompt.
var selectedNuGetPackage = filteredPackagesWithShortName.Count() switch
{
0 => await GetPackageByInteractiveFlowWithNoMatchesMessage(packagesWithShortName, integrationName, cancellationToken),
1 => filteredPackagesWithShortName.First().Package.Version == version
? filteredPackagesWithShortName.First()
: await GetPackageByInteractiveFlow(filteredPackagesWithShortName, null, cancellationToken),
> 1 => await GetPackageByInteractiveFlow(filteredPackagesWithShortName, version, cancellationToken),
_ => throw new InvalidOperationException(AddCommandStrings.UnexpectedNumberOfPackagesFound)
0 => await GetPackageByInteractiveFlowWithNoMatchesMessage(effectiveAppHostProjectFile.Directory!, packagesWithShortName, integrationName, version, cancellationToken),
1 when filteredPackagesWithShortName.First().Package.Version == version
=> filteredPackagesWithShortName.First(),
_ => await GetPackageByInteractiveFlow(effectiveAppHostProjectFile.Directory!, filteredPackagesWithShortName, version, cancellationToken)
};

// When installing from a PR channel, ensure the project has access to
Expand Down Expand Up @@ -313,7 +317,24 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
}
}

private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlow(IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? preferredVersion, CancellationToken cancellationToken)
private static async Task<IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)>> GetAllPackageVersions(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, CancellationToken cancellationToken)
{
var distinctPackageIds = possiblePackages.DistinctBy(package => package.Package.Id);
var channels = possiblePackages.Select(package => package.Channel).Distinct();

var versions = new List<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)>();
foreach (var channel in channels)
{
foreach (var package in distinctPackageIds)
{
var packages = await channel.GetPackageVersionsAsync(package.Package.Id, workingDirectory, cancellationToken);
versions.AddRange(packages.Select(p => (FriendlyName: package.FriendlyName, Package: p, Channel: channel)));
}
}
return versions;
}

private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlow(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? preferredVersion, CancellationToken cancellationToken)
{
var distinctPackages = possiblePackages.DistinctBy(p => p.Package.Id);

Expand All @@ -331,10 +352,24 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>

// If any of the package versions are an exact match for the preferred version
// then we can skip the version prompt and just use that version.
if (packageVersions.Any(p => p.Package.Version == preferredVersion))
if (!string.IsNullOrEmpty(preferredVersion))
{
var preferredVersionPackage = packageVersions.First(p => p.Package.Version == preferredVersion);
return preferredVersionPackage;
if (packageVersions.Any(p => p.Package.Version == preferredVersion))
{
var preferredVersionPackage = packageVersions.First(p => p.Package.Version == preferredVersion);
return preferredVersionPackage;
}

var allVersions = await InteractionService.ShowStatusAsync(
string.Format(CultureInfo.CurrentCulture, AddCommandStrings.SearchingForSpecifiedPackageVersion, selectedPackage.Package.Id, preferredVersion),
async () => await GetAllPackageVersions(workingDirectory, packageVersions, cancellationToken));
var matchedPreferredVersionPackage = allVersions.FirstOrDefault(packageVersion => packageVersion.Package.Version == preferredVersion);
if (matchedPreferredVersionPackage.Package is not null)
{
return matchedPreferredVersionPackage;
}

throw new EmptyChoicesException(string.Format(CultureInfo.CurrentCulture, AddCommandStrings.SpecifiedVersionNotFoundForPackage, selectedPackage.Package.Id, preferredVersion));
}

// When PR hives are present, prefer the package that exactly matches the installed
Expand Down Expand Up @@ -370,14 +405,14 @@ await Parallel.ForEachAsync(channels, cancellationToken, async (channel, ct) =>
return version;
}

private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlowWithNoMatchesMessage(IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? searchTerm, CancellationToken cancellationToken)
private async Task<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> GetPackageByInteractiveFlowWithNoMatchesMessage(DirectoryInfo workingDirectory, IEnumerable<(string FriendlyName, NuGetPackage Package, PackageChannel Channel)> possiblePackages, string? searchTerm, string? preferredVersion, CancellationToken cancellationToken)
{
if (searchTerm is not null)
{
InteractionService.DisplaySubtleMessage(string.Format(CultureInfo.CurrentCulture, AddCommandStrings.NoPackagesMatchedSearchTerm, searchTerm));
}

return await GetPackageByInteractiveFlow(possiblePackages, null, cancellationToken);
return await GetPackageByInteractiveFlow(workingDirectory, possiblePackages, preferredVersion, cancellationToken);
}

internal static (string FriendlyName, NuGetPackage Package, PackageChannel Channel) GenerateFriendlyName((NuGetPackage Package, PackageChannel Channel) packageWithChannel)
Expand Down
2 changes: 0 additions & 2 deletions src/Aspire.Cli/Commands/BaseCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,6 @@ protected BaseCommand(string name, string description, IFeatures features, ICliU
}
}

InteractionService.DisplayEmptyLine();

return exitCode;
});
}
Expand Down
28 changes: 19 additions & 9 deletions src/Aspire.Cli/DotNet/DotNetCliRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal interface IDotNetCliRunner
Task<int> BuildAsync(FileInfo projectFilePath, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken);
Task<int> AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, bool noRestore, ProcessInvocationOptions options, CancellationToken cancellationToken);
Task<int> AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, ProcessInvocationOptions options, CancellationToken cancellationToken);
Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, ProcessInvocationOptions options, CancellationToken cancellationToken);
Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool exactMatch, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, ProcessInvocationOptions options, CancellationToken cancellationToken);
Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, ProcessInvocationOptions options, CancellationToken cancellationToken);
Task<(int ExitCode, IReadOnlyList<FileInfo> Projects)> GetSolutionProjectsAsync(FileInfo solutionFile, ProcessInvocationOptions options, CancellationToken cancellationToken);
Task<int> AddProjectReferenceAsync(FileInfo projectFile, FileInfo referencedProject, ProcessInvocationOptions options, CancellationToken cancellationToken);
Expand Down Expand Up @@ -394,7 +394,7 @@ private async Task StartBackchannelAsync(IProcessExecution? execution, string so
using var activity = telemetry.StartDiagnosticActivity();

var isSingleFileAppHost = projectFile.Name.Equals("apphost.cs", StringComparison.OrdinalIgnoreCase);

// If we are a single file app host then we use the build command instead of msbuild command.
var cliArgsList = new List<string> { isSingleFileAppHost ? "build" : "msbuild" };

Expand Down Expand Up @@ -981,7 +981,7 @@ public async Task<string> ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
return result;
}

public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, ProcessInvocationOptions options, CancellationToken cancellationToken)
public async Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool exactMatch, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, ProcessInvocationOptions options, CancellationToken cancellationToken)
{
using var activity = telemetry.StartDiagnosticActivity();

Expand All @@ -1006,7 +1006,7 @@ public async Task<string> ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w

// Build a cache key using the main discriminators, including CLI version.
var cliVersion = VersionHelper.GetDefaultTemplateVersion();
rawKey = $"query={query}|prerelease={prerelease}|take={take}|skip={skip}|nugetConfigHash={nugetConfigHash}|cliVersion={cliVersion}";
rawKey = $"query={query}|exactMatch={exactMatch}|prerelease={prerelease}|take={take}|skip={skip}|nugetConfigHash={nugetConfigHash}|cliVersion={cliVersion}";
var cached = await _diskCache.GetAsync(rawKey, cancellationToken).ConfigureAwait(false);
if (cached is not null)
{
Expand All @@ -1033,14 +1033,24 @@ public async Task<string> ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
"package",
"search",
query,
"--take",
take.ToString(CultureInfo.InvariantCulture),
"--skip",
skip.ToString(CultureInfo.InvariantCulture),
"--format",
"json"
];

if (exactMatch) // search for all versions that match the query exactly
{
cliArgs.Add("--exact-match");
}
else // 'exact-match' flag causes the take and skip arguments to be ignored
{
cliArgs.AddRange([
"--take",
take.ToString(CultureInfo.InvariantCulture),
"--skip",
skip.ToString(CultureInfo.InvariantCulture),
]);
}

if (nugetConfigFile is not null)
{
cliArgs.Add("--configfile");
Expand Down Expand Up @@ -1228,7 +1238,7 @@ public async Task<string> ComputeNuGetConfigHierarchySha256Async(DirectoryInfo w
// Parse output - skip header lines (Project(s) and ----------)
var projects = new List<FileInfo>();
var startParsing = false;

foreach (var line in stdoutLines)
{
if (string.IsNullOrWhiteSpace(line))
Expand Down
61 changes: 52 additions & 9 deletions src/Aspire.Cli/NuGet/BundleNuGetPackageCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public async Task<IEnumerable<NuGetPackage>> GetTemplatePackagesAsync(
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
"Aspire.ProjectTemplates",
query: "Aspire.ProjectTemplates",
exactMatch: false,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);
Expand All @@ -58,7 +59,8 @@ public async Task<IEnumerable<NuGetPackage>> GetIntegrationPackagesAsync(
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
"Aspire.Hosting",
query: "Aspire.Hosting",
exactMatch: false,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);
Expand All @@ -74,7 +76,8 @@ public async Task<IEnumerable<NuGetPackage>> GetCliPackagesAsync(
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
"Aspire.Cli",
query: "Aspire.Cli",
exactMatch: false,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);
Expand All @@ -93,17 +96,39 @@ public async Task<IEnumerable<NuGetPackage>> GetPackagesAsync(
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
packageId,
query: packageId,
exactMatch: false,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);

return FilterPackages(packages, filter);
}

public async Task<IEnumerable<NuGetPackage>> GetPackageVersionsAsync(
DirectoryInfo workingDirectory,
string exactPackageId,
bool prerelease,
FileInfo? nugetConfigFile,
bool useCache,
CancellationToken cancellationToken)
{
var packages = await SearchPackagesInternalAsync(
workingDirectory,
query: exactPackageId,
exactMatch: true,
prerelease,
nugetConfigFile,
cancellationToken).ConfigureAwait(false);

bool FilterExactIdMatch(string? id) => string.Equals(id, exactPackageId, StringComparison.Ordinal);
return FilterPackages(packages, FilterExactIdMatch);
}

private async Task<IEnumerable<NuGetPackage>> SearchPackagesInternalAsync(
DirectoryInfo workingDirectory,
string query,
bool exactMatch,
bool prerelease,
FileInfo? nugetConfigFile,
CancellationToken cancellationToken)
Expand Down Expand Up @@ -195,12 +220,30 @@ private async Task<IEnumerable<NuGetPackage>> SearchPackagesInternalAsync(
}

// Convert to NuGetPackage format
return result.Packages.Select(p => new NuGetPackage
if (!exactMatch)
{
return result.Packages.Select(p => new NuGetPackage
{
Id = p.Id,
Version = p.Version,
Source = p.Source ?? string.Empty
}).ToList();
}
else
{
Id = p.Id,
Version = p.Version,
Source = p.Source ?? string.Empty
}).ToList();
var exactMatchResultPackage = result.Packages
.FirstOrDefault(p => p.Id.Equals(query, StringComparison.Ordinal));
if (exactMatchResultPackage is null || exactMatchResultPackage.AllVersions is null)
{
return [];
}
return exactMatchResultPackage.AllVersions.Select(packageVersion => new NuGetPackage
{
Id = exactMatchResultPackage.Id,
Version = packageVersion,
Source = exactMatchResultPackage.Source ?? string.Empty
}).ToList();
}
}
catch (JsonException ex)
{
Expand Down
Loading
Loading