Skip to content

Commit

Permalink
Add nuget package reference version range support
Browse files Browse the repository at this point in the history
  • Loading branch information
glennawatson committed Jun 8, 2019
1 parent 59ea451 commit d8aeab4
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 19 deletions.
17 changes: 16 additions & 1 deletion src/Pharmacist.Core/Extractors/NuGetExtractor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Threading.Tasks;

using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Packaging.Core;

using Pharmacist.Core.NuGet;
Expand All @@ -34,7 +35,21 @@ public class NuGetExtractor : IExtractor
/// <returns>A task to monitor the progress.</returns>
public async Task Extract(IReadOnlyCollection<NuGetFramework> targetFrameworks, IReadOnlyCollection<PackageIdentity> packages)
{
var results = await NuGetPackageHelper.DownloadPackageAndFilesAndFolder(packages, targetFrameworks).ConfigureAwait(false);
var results = await NuGetPackageHelper.DownloadPackageFilesAndFolder(packages, targetFrameworks).ConfigureAwait(false);

Assemblies = new List<string>(results.SelectMany(x => x.files).Where(x => x.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)));
SearchDirectories = new List<string>(results.Select(x => x.folder));
}

/// <summary>
/// Extracts the data using the specified target framework.
/// </summary>
/// <param name="targetFrameworks">The target framework to extract in order of priority.</param>
/// <param name="packages">The packages to extract the information from.</param>
/// <returns>A task to monitor the progress.</returns>
public async Task Extract(IReadOnlyCollection<NuGetFramework> targetFrameworks, IReadOnlyCollection<LibraryRange> packages)
{
var results = await NuGetPackageHelper.DownloadPackageFilesAndFolder(packages, targetFrameworks).ConfigureAwait(false);

Assemblies = new List<string>(results.SelectMany(x => x.files).Where(x => x.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)));
SearchDirectories = new List<string>(results.Select(x => x.folder));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal abstract class NetFrameworkBase : BasePlatform
/// <exception cref="NotSupportedException">Building events for WPF on Mac is not implemented.</exception>
public override async Task Extract(string referenceAssembliesLocation)
{
var results = await NuGetPackageHelper.DownloadPackageAndFilesAndFolder(new[] { ReferenceNuGet }, new[] { ReferenceFramework }).ConfigureAwait(false);
var results = await NuGetPackageHelper.DownloadPackageFilesAndFolder(new[] { ReferenceNuGet }, new[] { ReferenceFramework }).ConfigureAwait(false);
var files = results.SelectMany(x => x.files).Where(x => x.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)).ToArray();
SetFiles(files);
SearchDirectories = new List<string>(results.Select(x => x.folder));
Expand Down
84 changes: 75 additions & 9 deletions src/Pharmacist.Core/NuGet/NuGetPackageHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@

using NuGet.Configuration;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Packaging;
using NuGet.Packaging.Core;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;

namespace Pharmacist.Core.NuGet
{
Expand All @@ -28,14 +30,17 @@ namespace Pharmacist.Core.NuGet
public static class NuGetPackageHelper
{
private const int ProcessingCount = 32;

private const string DefaultNuGetSource = "https://api.nuget.org/v3/index.json";

private static readonly string[] DefaultFoldersToGrab = { PackagingConstants.Folders.Lib, PackagingConstants.Folders.Build, PackagingConstants.Folders.Ref };

// Bunch of NuGet based objects we can cache and only create once.
private static readonly string _globalPackagesPath;
private static readonly NuGetLogger _logger = new NuGetLogger();
private static readonly PackageDownloadContext _downloadContext = new PackageDownloadContext(NullSourceCacheContext.Instance);
private static readonly SourceCacheContext _sourceCacheContext = NullSourceCacheContext.Instance;
private static readonly PackageDownloadContext _downloadContext = new PackageDownloadContext(_sourceCacheContext);
private static readonly List<Lazy<INuGetResourceProvider>> _providers;

private static readonly IFrameworkNameProvider _frameworkNameProvider = DefaultFrameworkNameProvider.Instance;

static NuGetPackageHelper()
Expand All @@ -50,7 +55,36 @@ static NuGetPackageHelper()
/// <summary>
/// Gets the directory where the packages will be stored.
/// </summary>
public static string PackageDirectory { get; } = Path.Combine(Path.GetTempPath(), "EventBuilder.NuGet");
public static string PackageDirectory { get; } = Path.Combine(Path.GetTempPath(), "ReactiveUI.Pharmacist");

/// <summary>
/// Downloads the specified packages and returns the files and directories where the package NuGet package lives.
/// </summary>
/// <param name="libraryIdentities">Library identities we want to match.</param>
/// <param name="frameworks">Optional framework parameter which will force NuGet to evaluate as the specified Framework. If null it will use .NET Standard 2.0.</param>
/// <param name="nugetSource">Optional v3 nuget source. Will default to default nuget.org servers.</param>
/// <param name="getDependencies">If we should get the dependencies.</param>
/// <param name="packageFolders">Directories to package folders. Will be lib/build/ref if not defined.</param>
/// <param name="token">A cancellation token.</param>
/// <returns>The directory where the NuGet packages are unzipped to. Also the files contained within the requested package only.</returns>
public static async Task<IReadOnlyCollection<(string folder, IReadOnlyCollection<string> files)>> DownloadPackageFilesAndFolder(
IReadOnlyCollection<LibraryRange> libraryIdentities,
IReadOnlyCollection<NuGetFramework> frameworks = null,
PackageSource nugetSource = null,
bool getDependencies = true,
IReadOnlyCollection<string> packageFolders = null,
CancellationToken token = default)
{
// If the user hasn't selected a default framework to extract, select .NET Standard 2.0
frameworks = frameworks ?? new[] { FrameworkConstants.CommonFrameworks.NetStandard20 };

// Use the provided nuget package source, or use nuget.org
var sourceRepository = new SourceRepository(nugetSource ?? new PackageSource(DefaultNuGetSource), _providers);

var packages = await Task.WhenAll(libraryIdentities.Select(x => GetBestMatch(x, sourceRepository, token))).ConfigureAwait(false);

return await DownloadPackageFilesAndFolder(packages, frameworks, sourceRepository, getDependencies, packageFolders, token).ConfigureAwait(false);
}

/// <summary>
/// Downloads the specified packages and returns the files and directories where the package NuGet package lives.
Expand All @@ -62,7 +96,7 @@ static NuGetPackageHelper()
/// <param name="packageFolders">Directories to package folders. Will be lib/build/ref if not defined.</param>
/// <param name="token">A cancellation token.</param>
/// <returns>The directory where the NuGet packages are unzipped to. Also the files contained within the requested package only.</returns>
public static async Task<IReadOnlyCollection<(string folder, IReadOnlyCollection<string> files)>> DownloadPackageAndFilesAndFolder(
public static Task<IReadOnlyCollection<(string folder, IReadOnlyCollection<string> files)>> DownloadPackageFilesAndFolder(
IReadOnlyCollection<PackageIdentity> packageIdentities,
IReadOnlyCollection<NuGetFramework> frameworks = null,
PackageSource nugetSource = null,
Expand All @@ -74,22 +108,43 @@ static NuGetPackageHelper()
frameworks = frameworks ?? new[] { FrameworkConstants.CommonFrameworks.NetStandard20 };

// Use the provided nuget package source, or use nuget.org
var source = new SourceRepository(nugetSource ?? new PackageSource("https://api.nuget.org/v3/index.json"), _providers);
var sourceRepository = new SourceRepository(nugetSource ?? new PackageSource(DefaultNuGetSource), _providers);

var librariesToCopy = await GetPackagesToCopy(packageIdentities, source, frameworks.First(), getDependencies, token).ConfigureAwait(false);
return DownloadPackageFilesAndFolder(packageIdentities, frameworks, sourceRepository, getDependencies, packageFolders, token);
}

/// <summary>
/// Downloads the specified packages and returns the files and directories where the package NuGet package lives.
/// </summary>
/// <param name="packageIdentities">The identity of the packages to find.</param>
/// <param name="frameworks">Framework parameter which will force NuGet to evaluate as the specified Framework. If null it will use .NET Standard 2.0.</param>
/// <param name="sourceRepository">Nuget source repository. Will default to default nuget.org servers.</param>
/// <param name="getDependencies">If we should get the dependencies.</param>
/// <param name="packageFolders">Directories to package folders. Will be lib/build/ref if not defined.</param>
/// <param name="token">A cancellation token.</param>
/// <returns>The directory where the NuGet packages are unzipped to. Also the files contained within the requested package only.</returns>
private static async Task<IReadOnlyCollection<(string folder, IReadOnlyCollection<string> files)>> DownloadPackageFilesAndFolder(
IReadOnlyCollection<PackageIdentity> packageIdentities,
IReadOnlyCollection<NuGetFramework> frameworks,
SourceRepository sourceRepository,
bool getDependencies = true,
IReadOnlyCollection<string> packageFolders = null,
CancellationToken token = default)
{
var librariesToCopy = await GetPackagesToCopy(packageIdentities, sourceRepository, frameworks.First(), getDependencies, token).ConfigureAwait(false);

return CopyPackageFiles(librariesToCopy, frameworks, packageFolders ?? DefaultFoldersToGrab, token);
}

private static async Task<IEnumerable<(PackageIdentity packageIdentity, DownloadResourceResult downloadResourceResult, bool includeFilesInOutput)>> GetPackagesToCopy(
IReadOnlyCollection<PackageIdentity> startingPackages,
SourceRepository source,
SourceRepository sourceRepository,
NuGetFramework framework,
bool getDependencies,
CancellationToken token)
{
// Get the download resource from the nuget client API. This is basically a DI locator.
var downloadResource = source.GetResource<DownloadResource>(token);
var downloadResource = await sourceRepository.GetResourceAsync<DownloadResource>(token).ConfigureAwait(false);

var packagesToCopy = new Dictionary<string, (PackageIdentity packageIdentity, DownloadResourceResult downloadResourceResult, bool includeFilesInOutput)>(StringComparer.InvariantCultureIgnoreCase);

Expand Down Expand Up @@ -221,6 +276,17 @@ private static bool EqualToOrLessThan(this NuGetFramework firstFramework, NuGetF
return firstFramework.Version <= secondFramework.Version;
}

private static async Task<PackageIdentity> GetBestMatch(LibraryRange identity, SourceRepository sourceRepository, CancellationToken token)
{
var findPackageResource = await sourceRepository.GetResourceAsync<FindPackageByIdResource>(token).ConfigureAwait(false);

var versions = await findPackageResource.GetAllVersionsAsync(identity.Name, _sourceCacheContext, _logger, token).ConfigureAwait(false);

var bestPackageVersion = versions?.FindBestMatch(identity.VersionRange, version => version);

return new PackageIdentity(identity.Name, bestPackageVersion);
}

private static IEnumerable<FrameworkSpecificGroup> GetFileGroups(this PackageReaderBase reader, string folder)
{
var groups = new Dictionary<NuGetFramework, List<string>>(new NuGetFrameworkFullComparer());
Expand All @@ -245,7 +311,7 @@ private static IEnumerable<FrameworkSpecificGroup> GetFileGroups(this PackageRea
private static NuGetFramework GetFrameworkFromPath(this IPackageCoreReader reader, string path, bool allowSubFolders = false)
{
var nuGetFramework = NuGetFramework.AnyFramework;
var strArray = path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
var strArray = path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
if ((strArray.Length == 3 || strArray.Length > 3) && allowSubFolders)
{
string folderName = strArray[1];
Expand Down
16 changes: 16 additions & 0 deletions src/Pharmacist.Core/ObservablesForEventGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.CodeAnalysis.CSharp.Syntax;

using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.Packaging.Core;

using Pharmacist.Core.Extractors;
Expand Down Expand Up @@ -91,6 +92,21 @@ public static async Task ExtractEventsFromNuGetPackages(Stream outputStream, IRe
await ExtractEventsFromAssemblies(outputStream, extractor.Assemblies, extractor.SearchDirectories).ConfigureAwait(false);
}

/// <summary>
/// Extracts the events and delegates from the specified platform.
/// </summary>
/// <param name="outputStream">Stream that we should output to.</param>
/// <param name="packages">The packages to process.</param>
/// <param name="frameworks">The framework to generate for in order of priority.</param>
/// <returns>A task to monitor the progress.</returns>
public static async Task ExtractEventsFromNuGetPackages(Stream outputStream, IReadOnlyCollection<LibraryRange> packages, IReadOnlyCollection<NuGetFramework> frameworks)
{
var extractor = new NuGetExtractor();
await extractor.Extract(frameworks, packages).ConfigureAwait(false);

await ExtractEventsFromAssemblies(outputStream, extractor.Assemblies, extractor.SearchDirectories).ConfigureAwait(false);
}

/// <summary>
/// Extracts the events and delegates from the specified platform.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion src/Pharmacist.Core/Pharmacist.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
<ItemGroup>
<PackageReference Include="ICSharpCode.Decompiler" Version="4.0.0.4521" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.0.0" />
<PackageReference Include="nuget.protocol" Version="4.8.2" />
<PackageReference Include="NuGet.Protocol" Version="4.8.2" />
<PackageReference Include="NuGet.LibraryModel" Version="4.8.2" />
<PackageReference Include="polly" Version="7.1.0" />
<PackageReference Include="splat" Version="7.2.1" />
<PackageReference Include="system.collections.immutable" Version="1.5.0" />
Expand Down
2 changes: 1 addition & 1 deletion src/Pharmacist.Core/ReferenceLocators/ReferenceLocator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static Task<string> GetReferenceLocation(bool includePreRelease = true)

private static async Task<string> GetWindowsInstallationDirectory(bool includePreRelease)
{
var results = await NuGetPackageHelper.DownloadPackageAndFilesAndFolder(
var results = await NuGetPackageHelper.DownloadPackageFilesAndFolder(
new[] { VSWherePackageIdentity },
new[] { new NuGetFramework("Any") },
packageFolders: new[] { PackagingConstants.Folders.Tools },
Expand Down
7 changes: 4 additions & 3 deletions src/Pharmacist.MsBuild/PharmacistNuGetTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;

using NuGet.LibraryModel;
using NuGet.Packaging.Core;
using NuGet.Versioning;

Expand Down Expand Up @@ -74,20 +75,20 @@ public override bool Execute()

var packageReferences = PackageReferences.Where(x => !ExclusionPackageReferenceSet.Contains(x.ItemSpec));

var packages = new List<PackageIdentity>();
var packages = new List<LibraryRange>();

// Include all package references that aren't ourselves.
foreach (var packageReference in packageReferences)
{
var include = packageReference.ItemSpec;

if (!NuGetVersion.TryParse(packageReference.GetMetadata("Version"), out var nuGetVersion))
if (!VersionRange.TryParse(packageReference.GetMetadata("Version"), out var nuGetVersion))
{
this.Log().Error($"Package {include} does not have a valid Version.");
continue;
}

var packageIdentity = new PackageIdentity(include, nuGetVersion);
var packageIdentity = new LibraryRange(include, nuGetVersion, LibraryDependencyTarget.Package);
packages.Add(packageIdentity);
}

Expand Down
2 changes: 1 addition & 1 deletion src/Pharmacist.Tests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ public async Task IntegrationTestAssemblyTest()
{
using (var memoryStream = new MemoryStream())
{
var folders = (await NuGetPackageHelper.DownloadPackageAndFilesAndFolder(new[] { new PackageIdentity("NETStandard.Library", new NuGetVersion("2.0.0")) }).ConfigureAwait(false)).Select(x => x.folder);
var folders = (await NuGetPackageHelper.DownloadPackageFilesAndFolder(new[] { new PackageIdentity("NETStandard.Library", new NuGetVersion("2.0.0")) }).ConfigureAwait(false)).Select(x => x.folder);

await ObservablesForEventGenerator.ExtractEventsFromAssemblies(memoryStream, new[] { typeof(InstanceClass).Assembly.Location }, folders).ConfigureAwait(false);
memoryStream.Flush();
Expand Down
4 changes: 2 additions & 2 deletions src/Pharmacist.Tests/NuGetPackageHelperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public async Task CanGetNuGetProtocolAndDependencies()
var frameworks = new[] { FrameworkConstants.CommonFrameworks.NetStandard20 };

var result = (await NuGetPackageHelper
.DownloadPackageAndFilesAndFolder(package, frameworks: frameworks)
.DownloadPackageFilesAndFolder(package, frameworks: frameworks)
.ConfigureAwait(false)).ToList();

result.ShouldNotBeEmpty();
Expand All @@ -169,7 +169,7 @@ private static async Task GetAndCheckTizenPackage()
var frameworks = new[] { FrameworkConstants.CommonFrameworks.NetStandard20 };

var result = (await NuGetPackageHelper
.DownloadPackageAndFilesAndFolder(package, frameworks: frameworks)
.DownloadPackageFilesAndFolder(package, frameworks: frameworks)
.ConfigureAwait(false)).ToList();

var actualFiles = result.SelectMany(x => x.files).Where(x => x.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase)).ToList();
Expand Down

0 comments on commit d8aeab4

Please sign in to comment.