Last active
September 26, 2021 02:54
-
-
Save tintoy/3d9467d59b2547ad227ba5d1560ee838 to your computer and use it in GitHub Desktop.
NuGet feed lister
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<Project Sdk="Microsoft.NET.Sdk"> | |
<PropertyGroup> | |
<OutputType>Exe</OutputType> | |
<TargetFramework>net5.0</TargetFramework> | |
</PropertyGroup> | |
<ItemGroup> | |
<PackageReference Include="NuGet.Client" Version="4.2.0" /> | |
<PackageReference Include="NuGet.Configuration" Version="5.8.1" /> | |
<PackageReference Include="NuGet.Credentials" Version="5.8.1" /> | |
<PackageReference Include="NuGet.PackageManagement" Version="5.8.1" NoWarn="NU1701" /> | |
<PackageReference Include="NuGet.Packaging" Version="5.8.1" /> | |
<PackageReference Include="NuGet.Versioning" Version="5.8.1" /> | |
<PackageReference Include="Serilog" Version="2.5.0" /> | |
<PackageReference Include="Serilog.Enrichers.Demystify" Version="0.1.0-dev-00016" /> | |
<PackageReference Include="Serilog.Extensions.Logging" Version="2.0.2" /> | |
<PackageReference Include="Serilog.Sinks.Console" Version="3.0.0" /> | |
</ItemGroup> | |
</Project> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using NuGet.Common; | |
using NuGet.Configuration; | |
using NuGet.Protocol.Core.Types; | |
using NuGet.Versioning; | |
using System; | |
using System.Collections.Generic; | |
using System.Threading.Tasks; | |
using System.Linq; | |
using System.Threading; | |
namespace NuGetFeedLister | |
{ | |
/// <summary> | |
/// Helper methods for interacting with the NuGet API. | |
/// </summary> | |
public static class NuGetHelper | |
{ | |
/// <summary> | |
/// Get all package sources configured for the specified workspace. | |
/// </summary> | |
/// <param name="workspaceRootDirectory"> | |
/// The workspace's root directory. | |
/// </param> | |
/// <returns> | |
/// A list of configured package sources. | |
/// </returns> | |
public static List<PackageSource> GetWorkspacePackageSources(string workspaceRootDirectory) | |
{ | |
if (String.IsNullOrWhiteSpace(workspaceRootDirectory)) | |
throw new ArgumentException("Argument cannot be null, empty, or entirely composed of whitespace: 'workspaceRootDirectory'.", nameof(workspaceRootDirectory)); | |
return new List<PackageSource>( | |
new PackageSourceProvider( | |
Settings.LoadDefaultSettings(workspaceRootDirectory) | |
) | |
.LoadPackageSources() | |
); | |
} | |
/// <summary> | |
/// Get NuGet AutoComplete APIs for the specified package source URLs. | |
/// </summary> | |
/// <param name="packageSourceUrls"> | |
/// The package source URLs. | |
/// </param> | |
/// <returns> | |
/// A task that resolves to a list of <see cref="AutoCompleteResource"/>s. | |
/// </returns> | |
public static Task<List<AutoCompleteResource>> GetAutoCompleteResources(params string[] packageSourceUrls) | |
{ | |
return GetAutoCompleteResources( | |
packageSourceUrls.Select(packageSourceUrl => new PackageSource(packageSourceUrl)) | |
); | |
} | |
/// <summary> | |
/// Get NuGet AutoComplete APIs for the specified package sources. | |
/// </summary> | |
/// <param name="packageSources"> | |
/// The package sources. | |
/// </param> | |
/// <returns> | |
/// A task that resolves to a list of <see cref="AutoCompleteResource"/>s. | |
/// </returns> | |
public static Task<List<AutoCompleteResource>> GetAutoCompleteResources(params PackageSource[] packageSources) | |
{ | |
return GetAutoCompleteResources( | |
(IEnumerable<PackageSource>)packageSources | |
); | |
} | |
/// <summary> | |
/// Get NuGet AutoComplete APIs for the specified package sources. | |
/// </summary> | |
/// <param name="packageSources"> | |
/// The package sources. | |
/// </param> | |
/// <param name="cancellationToken"> | |
/// An optional <see cref="CancellationToken"/> that can be used to cancel the operation. | |
/// </param> | |
/// <returns> | |
/// A task that resolves to a list of <see cref="AutoCompleteResource"/>s. | |
/// </returns> | |
public static async Task<List<AutoCompleteResource>> GetAutoCompleteResources(IEnumerable<PackageSource> packageSources, CancellationToken cancellationToken = default(CancellationToken)) | |
{ | |
if (packageSources == null) | |
throw new ArgumentNullException(nameof(packageSources)); | |
List<AutoCompleteResource> autoCompleteResources = new List<AutoCompleteResource>(); | |
var providers = new List<Lazy<INuGetResourceProvider>>(); | |
// Add v3 API support | |
providers.AddRange(Repository.Provider.GetCoreV3()); | |
foreach (PackageSource packageSource in packageSources) | |
{ | |
SourceRepository sourceRepository = new SourceRepository(packageSource, providers); | |
AutoCompleteResource autoCompleteResource = await sourceRepository.GetResourceAsync<AutoCompleteResource>(cancellationToken); | |
if (autoCompleteResource != null) | |
autoCompleteResources.Add(autoCompleteResource); | |
} | |
return autoCompleteResources; | |
} | |
/// <summary> | |
/// Suggest package Ids based on a prefix. | |
/// </summary> | |
/// <param name="autoCompleteResources"> | |
/// The <see cref="AutoCompleteResource"/>s used to retrieve suggestions. | |
/// </param> | |
/// <param name="packageIdPrefix"> | |
/// The package Id prefix to match. | |
/// </param> | |
/// <param name="includePrerelease"> | |
/// Include packages with only pre-release versions available? | |
/// </param> | |
/// <param name="logger"> | |
/// An optional NuGet logger to be used for reporting errors / progress (etc). | |
/// </param> | |
/// <param name="cancellationToken"> | |
/// An optional cancellation token that can be used to cancel the request. | |
/// </param> | |
/// <returns> | |
/// A sorted set of suggested package Ids. | |
/// </returns> | |
public static async Task<SortedSet<string>> SuggestPackageIds(this IEnumerable<AutoCompleteResource> autoCompleteResources, string packageIdPrefix, bool includePrerelease = false, ILogger logger = null, CancellationToken cancellationToken = default(CancellationToken)) | |
{ | |
if (autoCompleteResources == null) | |
throw new ArgumentNullException(nameof(autoCompleteResources)); | |
if (packageIdPrefix == null) | |
throw new ArgumentNullException(nameof(packageIdPrefix)); | |
IEnumerable<string>[] results = await Task.WhenAll( | |
autoCompleteResources.Select( | |
autoCompleteResource => autoCompleteResource.IdStartsWith(packageIdPrefix, includePrerelease, logger ?? NullLogger.Instance, cancellationToken) | |
) | |
); | |
return new SortedSet<string>( | |
results.Flatten() | |
); | |
} | |
/// <summary> | |
/// Suggest versions for the specified package. | |
/// </summary> | |
/// <param name="autoCompleteResources"> | |
/// The <see cref="AutoCompleteResource"/>s used to retrieve suggestions. | |
/// </param> | |
/// <param name="versionPrefix"> | |
/// An optional version prefix to match. | |
/// </param> | |
/// <param name="packageId"> | |
/// The package Id to match. | |
/// </param> | |
/// <param name="includePrerelease"> | |
/// Include pre-release versions? | |
/// </param> | |
/// <param name="logger"> | |
/// An optional NuGet logger to be used for reporting progress (etc). | |
/// </param> | |
/// <param name="cancellationToken"> | |
/// An optional cancellation token that can be used to cancel the request. | |
/// </param> | |
/// <returns> | |
/// A sorted set of suggested package versions. | |
/// </returns> | |
public static async Task<SortedSet<NuGetVersion>> SuggestPackageVersions(this IEnumerable<AutoCompleteResource> autoCompleteResources, string packageId, bool includePrerelease = false, string versionPrefix = "", ILogger logger = null, CancellationToken cancellationToken = default(CancellationToken)) | |
{ | |
if (autoCompleteResources == null) | |
throw new ArgumentNullException(nameof(autoCompleteResources)); | |
if (packageId == null) | |
throw new ArgumentNullException(nameof(packageId)); | |
IEnumerable<NuGetVersion>[] results = await Task.WhenAll( | |
autoCompleteResources.Select( | |
autoCompleteResource => autoCompleteResource.VersionStartsWith(packageId, versionPrefix, includePrerelease, null, logger ?? NullLogger.Instance, cancellationToken) | |
) | |
); | |
return new SortedSet<NuGetVersion>( | |
results.Flatten(), | |
VersionComparer.VersionReleaseMetadata | |
); | |
} | |
/// <summary> | |
/// Flatten the sequence, enumerating nested sequences. | |
/// </summary> | |
/// <typeparam name="TSource"> | |
/// The source element type. | |
/// </typeparam> | |
/// <param name="source"> | |
/// The source sequence of sequences. | |
/// </param> | |
/// <returns> | |
/// The flattened sequence. | |
/// </returns> | |
static IEnumerable<TSource> Flatten<TSource>(this IEnumerable<IEnumerable<TSource>> source) | |
{ | |
if (source == null) | |
throw new ArgumentNullException(nameof(source)); | |
return source.SelectMany(items => items); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Serilog; | |
using Serilog.Events; | |
using System; | |
using System.Threading.Tasks; | |
using NC = NuGet.Common; | |
namespace NuGetFeedLister | |
{ | |
/// <summary> | |
/// Serilog logging adapter for NuGet. | |
/// </summary> | |
class NuGetSerilogAdapter | |
: NC.ILogger | |
{ | |
public NuGetSerilogAdapter(ILogger parentLogger) | |
{ | |
if (parentLogger == null) | |
throw new ArgumentNullException(nameof(parentLogger)); | |
Logger = parentLogger.ForContext(propertyName: "SourceContext", value: "NuGet"); | |
} | |
ILogger Logger { get; } | |
LogEventLevel ConvertLogLevel(NC.LogLevel level) | |
{ | |
switch (level) | |
{ | |
case NC.LogLevel.Error: | |
{ | |
return LogEventLevel.Error; | |
} | |
case NC.LogLevel.Warning: | |
{ | |
return LogEventLevel.Warning; | |
} | |
case NC.LogLevel.Information: | |
{ | |
return LogEventLevel.Information; | |
} | |
case NC.LogLevel.Debug: | |
{ | |
return LogEventLevel.Verbose; // NuGet vs Serilog - Debug and Verbose are swapped | |
} | |
case NC.LogLevel.Verbose: | |
{ | |
return LogEventLevel.Debug; // NuGet vs Serilog - Debug and Verbose are swapped | |
} | |
default: | |
{ | |
return LogEventLevel.Information; | |
} | |
} | |
} | |
void NC.ILogger.Log(NC.LogLevel level, string data) | |
{ | |
LogEventLevel logEventLevel = ConvertLogLevel(level); | |
Logger.Write(logEventLevel, data); | |
} | |
void NC.ILogger.Log(NC.ILogMessage message) | |
{ | |
LogEventLevel logEventLevel = ConvertLogLevel(message.Level); | |
Logger.Write(logEventLevel, "{NuGetLogCode:l}: {NuGetLogMessage}", | |
message.Code, | |
message.Message | |
); | |
} | |
Task NC.ILogger.LogAsync(NC.LogLevel level, string data) | |
{ | |
LogEventLevel logEventLevel = ConvertLogLevel(level); | |
Logger.Write(logEventLevel, data); | |
return Task.CompletedTask; | |
} | |
Task NC.ILogger.LogAsync(NC.ILogMessage message) | |
{ | |
LogEventLevel logEventLevel = ConvertLogLevel(message.Level); | |
Logger.Write(logEventLevel, "{NuGetLogCode:l}: {NuGetLogMessage}", | |
message.Code, | |
message.Message | |
); | |
return Task.CompletedTask; | |
} | |
void NC.ILogger.LogError(string data) | |
{ | |
Logger.Error(data); | |
} | |
void NC.ILogger.LogWarning(string data) | |
{ | |
Logger.Warning(data); | |
} | |
void NC.ILogger.LogInformation(string data) | |
{ | |
Logger.Information(data); | |
} | |
void NC.ILogger.LogInformationSummary(string data) | |
{ | |
Logger.Information(data); | |
} | |
void NC.ILogger.LogMinimal(string data) | |
{ | |
Logger.Information(data); | |
} | |
void NC.ILogger.LogDebug(string data) | |
{ | |
Logger.Verbose(data); | |
} | |
void NC.ILogger.LogVerbose(string data) | |
{ | |
Logger.Debug(data); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using NuGet.Configuration; | |
using NuGet.Credentials; | |
using NuGet.Protocol; | |
using NuGet.Protocol.Core.Types; | |
using Serilog; | |
using Serilog.Sinks.SystemConsole.Themes; | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Reflection; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace NuGetFeedLister | |
{ | |
/// <summary> | |
/// A quick-and-dirty test for listing packages from all configured package sources. | |
/// </summary> | |
static class Program | |
{ | |
/// <summary> | |
/// The package Id prefix to search for. | |
/// </summary> | |
static readonly string SearchForPackagePrefix = "Microsoft.AspNetCore"; | |
/// <summary> | |
/// The logger used by NuGet (forwards log entries to Serilog). | |
/// </summary> | |
static NuGetSerilogAdapter NuGetLogger { get; set; } | |
/// <summary> | |
/// The main program entry-point. | |
/// </summary> | |
static async Task Main() | |
{ | |
ConfigureLogging(); | |
CancellationTokenSource cancellationSource = new CancellationTokenSource(); | |
cancellationSource.CancelAfter( | |
TimeSpan.FromSeconds(10) | |
); | |
try | |
{ | |
ConfigureNuGetCredentialProviders(); | |
List<PackageSource> packageSources = NuGetHelper.GetWorkspacePackageSources( | |
workspaceRootDirectory: Directory.GetCurrentDirectory() | |
); | |
Log.Information("Found {PackageSourceCount} configured package sources.", packageSources.Count); | |
List<AutoCompleteResource> autoCompleteResources = await NuGetHelper.GetAutoCompleteResources(packageSources, cancellationSource.Token); | |
for (int packageSourceIndex = 0; packageSourceIndex < packageSources.Count; packageSourceIndex++) | |
{ | |
Log.Information("Service index for packagesource {PackageSourceName} ({PackageSourceUri}):", | |
packageSources[packageSourceIndex].Name ?? "<no name>", | |
packageSources[packageSourceIndex].SourceUri | |
); | |
ServiceIndexResourceV3 serviceIndex = (ServiceIndexResourceV3) | |
autoCompleteResources[packageSourceIndex] | |
.GetType() | |
.GetField("_serviceIndex", BindingFlags.Instance | BindingFlags.NonPublic) | |
.GetValue(autoCompleteResources[packageSourceIndex]); | |
foreach (ServiceIndexEntry indexEntry in serviceIndex.Entries) | |
{ | |
Log.Information(" {ServiceType} (v{ClientVersion}) => {ServiceUri}", | |
indexEntry.Type, | |
indexEntry.ClientVersion, | |
indexEntry.Uri | |
); | |
} | |
Log.Information("Requesting suggestions for package prefix {PackagePrefix} from package source {PackageSourceName} ({PackageSourceUri})...", | |
SearchForPackagePrefix, | |
packageSources[packageSourceIndex].Name ?? "<no name>", | |
packageSources[packageSourceIndex].SourceUri | |
); | |
IEnumerable<string> suggestions = await autoCompleteResources[packageSourceIndex].IdStartsWith( | |
packageIdPrefix: SearchForPackagePrefix, | |
includePrerelease: true, | |
log: NuGetLogger, | |
cancellationSource.Token | |
); | |
Log.Information("Received {SuggestionCount} suggestions for package prefix {PackagePrefix} from package source {PackageSourceName} ({PackageSourceUri}).", | |
suggestions.Count(), | |
SearchForPackagePrefix, | |
packageSources[packageSourceIndex].Name ?? "<no name>", | |
packageSources[packageSourceIndex].SourceUri | |
); | |
foreach (string suggestion in suggestions) | |
Log.Information("\tSuggested package: {PackageId}", suggestion); | |
} | |
} | |
catch (Exception unexpectedError) | |
{ | |
Log.Error(unexpectedError, "Unexpected error."); | |
} | |
} | |
/// <summary> | |
/// Configure Serilog and NuGet logging. | |
/// </summary> | |
static void ConfigureLogging() | |
{ | |
ILogger rootLogger = new LoggerConfiguration() | |
.MinimumLevel.Verbose() | |
.Enrich.WithDemystifiedStackTraces() | |
.Enrich.FromLogContext() | |
.WriteTo.Console( | |
outputTemplate: "[{SourceContext}/{Level:u3}] {Message:lj}{NewLine}{Exception}", | |
theme: SystemConsoleTheme.Literate | |
) | |
.CreateLogger(); | |
Log.Logger = rootLogger.ForContext(propertyName: "SourceContext", value: nameof(Program)); | |
NuGetLogger = new NuGetSerilogAdapter(rootLogger); | |
// Hackily override NuGet's default logger. | |
typeof(NuGet.Common.NullLogger) | |
.GetField("_instance", BindingFlags.Static | BindingFlags.NonPublic) | |
.SetValue(obj: null /* static */, value: NuGetLogger); | |
} | |
/// <summary> | |
/// Configure NuGet's credential providers (i.e. support for authenticated package feeds). | |
/// </summary> | |
static void ConfigureNuGetCredentialProviders() | |
{ | |
DefaultCredentialServiceUtility.SetupDefaultCredentialService( | |
logger: NuGetLogger, | |
nonInteractive: true | |
); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment