Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable workload search for users to find latest set versions #42652

Merged
merged 9 commits into from
Aug 29, 2024
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
Expand Up @@ -29,6 +29,11 @@ Task<NuGetVersion> GetLatestPackageVersion(PackageId packageId,
PackageSourceLocation packageSourceLocation = null,
bool includePreview = false);

Task<IEnumerable<NuGetVersion>> GetLatestPackageVersions(PackageId packageId,
int numberOfResults,
PackageSourceLocation packageSourceLocation = null,
bool includePreview = false);

Task<NuGetVersion> GetBestPackageVersionAsync(PackageId packageId,
VersionRange versionRange,
PackageSourceLocation packageSourceLocation = null);
Expand Down
34 changes: 24 additions & 10 deletions src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,12 @@ public async Task<IEnumerable<string>> ExtractPackageAsync(string packagePath, D
return allFilesInPackage;
}

public async Task<IEnumerable<IPackageSearchMetadata>> GetLatestVersionsOfPackage(string packageId, bool includePreview, int numberOfResults)
joeloff marked this conversation as resolved.
Show resolved Hide resolved
{
IEnumerable<PackageSource> packageSources = LoadNuGetSources(new PackageId(packageId), null, null);
return (await GetLatestVersionsInternalAsync(packageId, packageSources, includePreview, CancellationToken.None, numberOfResults)).Select(result => result.Item2);
}

private async Task<(PackageSource, NuGetVersion)> GetPackageSourceAndVersion(PackageId packageId,
NuGetVersion packageVersion = null,
PackageSourceLocation packageSourceLocation = null,
Expand Down Expand Up @@ -489,8 +495,13 @@ private static string GenerateVersionRangeErrorDescription(string packageIdentif
string packageIdentifier, IEnumerable<PackageSource> packageSources, bool includePreview,
CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(packageSources);
return (await GetLatestVersionsInternalAsync(packageIdentifier, packageSources, includePreview, cancellationToken, 1)).First();
}

private async Task<IEnumerable<(PackageSource, IPackageSearchMetadata)>> GetLatestVersionsInternalAsync(
string packageIdentifier, IEnumerable<PackageSource> packageSources, bool includePreview, CancellationToken cancellationToken, int numberOfResults)
{
ArgumentNullException.ThrowIfNull(packageSources);
if (string.IsNullOrWhiteSpace(packageIdentifier))
{
throw new ArgumentException($"{nameof(packageIdentifier)} cannot be null or empty",
Expand Down Expand Up @@ -540,13 +551,13 @@ await Task.WhenAll(

if (stableVersions.Any())
{
return stableVersions.MaxBy(r => r.package.Identity.Version);
return stableVersions.OrderByDescending(r => r.package.Identity.Version).Take(numberOfResults);
}
}

(PackageSource, IPackageSearchMetadata) latestVersion = accumulativeSearchResults
.MaxBy(r => r.package.Identity.Version);
return latestVersion;
IEnumerable<(PackageSource, IPackageSearchMetadata)> latestVersions = accumulativeSearchResults
.OrderByDescending(r => r.package.Identity.Version);
return latestVersions.Take(numberOfResults);
}

public async Task<NuGetVersion> GetBestPackageVersionAsync(PackageId packageId,
Expand Down Expand Up @@ -691,15 +702,18 @@ bool TryGetPackageMetadata(
public async Task<NuGetVersion> GetLatestPackageVersion(PackageId packageId,
PackageSourceLocation packageSourceLocation = null,
bool includePreview = false)
{
return (await GetLatestPackageVersions(packageId, numberOfResults: 1, packageSourceLocation, includePreview)).First();
}

public async Task<IEnumerable<NuGetVersion>> GetLatestPackageVersions(PackageId packageId, int numberOfResults, PackageSourceLocation packageSourceLocation = null, bool includePreview = false)
{
CancellationToken cancellationToken = CancellationToken.None;
IPackageSearchMetadata packageMetadata;
IEnumerable<PackageSource> packagesSources = LoadNuGetSources(packageId, packageSourceLocation);

(_, packageMetadata) = await GetLatestVersionInternalAsync(packageId.ToString(), packagesSources,
includePreview, cancellationToken).ConfigureAwait(false);

return packageMetadata.Identity.Version;
return (await GetLatestVersionsInternalAsync(packageId.ToString(), packagesSources,
includePreview, cancellationToken, numberOfResults).ConfigureAwait(false)).Select(result =>
result.Item2.Identity.Version);
}

public async Task<IEnumerable<string>> GetPackageIdsAsync(string idStem, bool allowPrerelease, PackageSourceLocation packageSourceLocation = null, CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,14 @@
<data name="PlatformColumnName" xml:space="preserve">
<value>Platforms</value>
</data>
<data name="PrintSetVersionsDescription" xml:space="preserve">
<value>Output a list of the latest released workload versions. Takes the --take option to specify how many to provide and --format to alter the format.</value>
<comment>Do not localize --take or --format</comment>
</data>
<data name="FormatOptionDescription" xml:space="preserve">
<value>Changes the format of outputted workload versions. Can take 'json' or 'list'</value>
</data>
<data name="TakeOptionMustBePositive" xml:space="preserve">
<value>The --take option must be positive.</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using Microsoft.DotNet.Workloads.Workload.Search;
using LocalizableStrings = Microsoft.DotNet.Workloads.Workload.Search.LocalizableStrings;

namespace Microsoft.DotNet.Cli
{
internal static class SearchWorkloadSetsParser
{
public static readonly CliOption<int> TakeOption = new("--take") { DefaultValueFactory = (_) => 5 };

public static readonly CliOption<string> FormatOption = new("--format")
{
Description = LocalizableStrings.FormatOptionDescription
};

private static readonly CliCommand Command = ConstructCommand();

public static CliCommand GetCommand()
{
return Command;
}

private static CliCommand ConstructCommand()
{
var command = new CliCommand("version", LocalizableStrings.PrintSetVersionsDescription);
command.Options.Add(FormatOption);
command.Options.Add(TakeOption);

TakeOption.Validators.Add(optionResult =>
{
if (optionResult.GetValueOrDefault<int>() <= 0)
{
throw new ArgumentException(LocalizableStrings.TakeOptionMustBePositive);
}
});

command.SetAction(parseResult => new WorkloadSearchCommand(parseResult)
{
ListWorkloadSetVersions = true
}.Execute());

return command;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.CommandLine;
using System.Text.Json;
using Microsoft.Deployment.DotNet.Releases;
using Microsoft.DotNet.Cli;
using Microsoft.DotNet.Cli.NuGetPackageDownloader;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.DotNet.Workloads.Workload.Install;
using Microsoft.NET.Sdk.WorkloadManifestReader;
using Microsoft.TemplateEngine.Cli.Commands;

namespace Microsoft.DotNet.Workloads.Workload.Search
{
Expand All @@ -15,6 +18,10 @@ internal class WorkloadSearchCommand : WorkloadCommandBase
private readonly IWorkloadResolver _workloadResolver;
private readonly ReleaseVersion _sdkVersion;
private readonly string _workloadIdStub;
private readonly int _numberOfWorkloadSetsToTake;
private readonly string _workloadSetOutputFormat;
private readonly IWorkloadManifestInstaller _installer;
internal bool ListWorkloadSetVersions { get; set; } = false;

public WorkloadSearchCommand(
ParseResult result,
Expand All @@ -34,10 +41,43 @@ public WorkloadSearchCommand(

_sdkVersion = creationResult.SdkVersion;
_workloadResolver = creationResult.WorkloadResolver;

_numberOfWorkloadSetsToTake = result.GetValue(SearchWorkloadSetsParser.TakeOption);
_workloadSetOutputFormat = result.GetValue(SearchWorkloadSetsParser.FormatOption);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we don't do any validation that this is a valid value. So if you have jsno or something as a typo it will just output it as CSV.

I don't think CSV with no spaces is great as a default. If the default is supposed to be human-readable, I would suggest outputting it in text format where each version is on a separate line. Eventually this might change to be a table.

We should think about what information we might want to include in the future and try to make sure the output formats can support that without breaking changes. So for the json output we may want each element to be a dictionary that has a workloadVersion key/value, so that we can add additional data (such as a description of what's updated, or what version of something like XCode is supported).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked @baronfel what format he'd want for json a couple weeks ago, and he said he just wanted a JSON array of version strings.

I do think there are other things users could potentially want to know, but the key things (from my perspective) are things we'd expect the user to ask for more precisely in a future command. As an example, it's very natural for the user to want to know what workload versions are part of this random version we just gave them, but then they'd run dotnet workload search version 9.0.103 or whatever it is, and they'd get that information.

I do think having it be parseable as a csv is useful, but since both you and joeloff commented on that, I can make that an option like json and have the default (or typos) put versions on their own lines.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - for this command which is just answering the question "which workload set versions are available", a simple list/json array makes sense.

IMO @dsplaisted's idea is much more aligned with the other commands we have been discussing - "what's in workload set X?" and "what components are supported in X?".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do agree with

I don't think CSV with no spaces is great as a default. If the default is supposed to be human-readable, I would suggest outputting it in text format where each version is on a separate line. Eventually this might change to be a table.

though - @Forgind please capture screenshots/examples and put them in the PR description to help discussions here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put a few examples of what it should look like in the description. (Note that I made csv its own thing.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks - this was very helpful for me to see!


_installer = WorkloadInstallerFactory.GetWorkloadInstaller(
reporter,
new SdkFeatureBand(_sdkVersion),
_workloadResolver,
Verbosity,
creationResult.UserProfileDir,
!SignCheck.IsDotNetSigned(),
restoreActionConfig: new RestoreActionConfig(result.HasOption(SharedOptions.InteractiveOption)),
elevationRequired: false,
shouldLog: false);
}

public override int Execute()
{
if (ListWorkloadSetVersions)
{
var featureBand = new SdkFeatureBand(_sdkVersion);
var packageId = _installer.GetManifestPackageId(new ManifestId("Microsoft.NET.Workloads"), featureBand);
var versions = PackageDownloader.GetLatestPackageVersions(packageId, _numberOfWorkloadSetsToTake, packageSourceLocation: null, includePreview: !string.IsNullOrWhiteSpace(_sdkVersion.Prerelease))
.GetAwaiter().GetResult()
.Select(version => WorkloadManifestUpdater.WorkloadSetPackageVersionToWorkloadSetVersion(featureBand, version.Version.ToString()));
if (_workloadSetOutputFormat?.Equals("json", StringComparison.OrdinalIgnoreCase) == true)
{
Reporter.WriteLine(JsonSerializer.Serialize(versions.Select(version => version.ToDictionary(_ => "workloadVersion", v => v))));
}
else
{
Reporter.WriteLine(string.Join('\n', versions));
}

return 0;
}

IEnumerable<WorkloadResolver.WorkloadInfo> availableWorkloads = _workloadResolver.GetAvailableWorkloads()
.OrderBy(workload => workload.Id);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static CliCommand GetCommand()
private static CliCommand ConstructCommand()
{
var command = new CliCommand("search", LocalizableStrings.CommandDescription);
command.Subcommands.Add(SearchWorkloadSetsParser.GetCommand());
command.Arguments.Add(WorkloadIdStubArgument);
command.Options.Add(CommonOptions.HiddenVerbosityOption);
command.Options.Add(VersionOption);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading