From d8aeab45de6153060e3c8c9149f5b0dccd55a70c Mon Sep 17 00:00:00 2001 From: Glenn Watson Date: Sat, 8 Jun 2019 21:03:37 +1000 Subject: [PATCH] Add nuget package reference version range support --- .../Extractors/NuGetExtractor.cs | 17 +++- .../PlatformExtractors/NetFrameworkBase.cs | 2 +- .../NuGet/NuGetPackageHelper.cs | 84 +++++++++++++++++-- .../ObservablesForEventGenerator.cs | 16 ++++ src/Pharmacist.Core/Pharmacist.Core.csproj | 3 +- .../ReferenceLocators/ReferenceLocator.cs | 2 +- src/Pharmacist.MsBuild/PharmacistNuGetTask.cs | 7 +- src/Pharmacist.Tests/IntegrationTests.cs | 2 +- .../NuGetPackageHelperTests.cs | 4 +- 9 files changed, 118 insertions(+), 19 deletions(-) diff --git a/src/Pharmacist.Core/Extractors/NuGetExtractor.cs b/src/Pharmacist.Core/Extractors/NuGetExtractor.cs index 61c47b5..2be751b 100644 --- a/src/Pharmacist.Core/Extractors/NuGetExtractor.cs +++ b/src/Pharmacist.Core/Extractors/NuGetExtractor.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using NuGet.Frameworks; +using NuGet.LibraryModel; using NuGet.Packaging.Core; using Pharmacist.Core.NuGet; @@ -34,7 +35,21 @@ public class NuGetExtractor : IExtractor /// A task to monitor the progress. public async Task Extract(IReadOnlyCollection targetFrameworks, IReadOnlyCollection packages) { - var results = await NuGetPackageHelper.DownloadPackageAndFilesAndFolder(packages, targetFrameworks).ConfigureAwait(false); + var results = await NuGetPackageHelper.DownloadPackageFilesAndFolder(packages, targetFrameworks).ConfigureAwait(false); + + Assemblies = new List(results.SelectMany(x => x.files).Where(x => x.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase))); + SearchDirectories = new List(results.Select(x => x.folder)); + } + + /// + /// Extracts the data using the specified target framework. + /// + /// The target framework to extract in order of priority. + /// The packages to extract the information from. + /// A task to monitor the progress. + public async Task Extract(IReadOnlyCollection targetFrameworks, IReadOnlyCollection packages) + { + var results = await NuGetPackageHelper.DownloadPackageFilesAndFolder(packages, targetFrameworks).ConfigureAwait(false); Assemblies = new List(results.SelectMany(x => x.files).Where(x => x.EndsWith(".dll", StringComparison.InvariantCultureIgnoreCase))); SearchDirectories = new List(results.Select(x => x.folder)); diff --git a/src/Pharmacist.Core/Extractors/PlatformExtractors/NetFrameworkBase.cs b/src/Pharmacist.Core/Extractors/PlatformExtractors/NetFrameworkBase.cs index fb43fd3..2a6cad9 100644 --- a/src/Pharmacist.Core/Extractors/PlatformExtractors/NetFrameworkBase.cs +++ b/src/Pharmacist.Core/Extractors/PlatformExtractors/NetFrameworkBase.cs @@ -30,7 +30,7 @@ internal abstract class NetFrameworkBase : BasePlatform /// Building events for WPF on Mac is not implemented. 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(results.Select(x => x.folder)); diff --git a/src/Pharmacist.Core/NuGet/NuGetPackageHelper.cs b/src/Pharmacist.Core/NuGet/NuGetPackageHelper.cs index c71550f..6f00bae 100644 --- a/src/Pharmacist.Core/NuGet/NuGetPackageHelper.cs +++ b/src/Pharmacist.Core/NuGet/NuGetPackageHelper.cs @@ -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 { @@ -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> _providers; - private static readonly IFrameworkNameProvider _frameworkNameProvider = DefaultFrameworkNameProvider.Instance; static NuGetPackageHelper() @@ -50,7 +55,36 @@ static NuGetPackageHelper() /// /// Gets the directory where the packages will be stored. /// - public static string PackageDirectory { get; } = Path.Combine(Path.GetTempPath(), "EventBuilder.NuGet"); + public static string PackageDirectory { get; } = Path.Combine(Path.GetTempPath(), "ReactiveUI.Pharmacist"); + + /// + /// Downloads the specified packages and returns the files and directories where the package NuGet package lives. + /// + /// Library identities we want to match. + /// Optional framework parameter which will force NuGet to evaluate as the specified Framework. If null it will use .NET Standard 2.0. + /// Optional v3 nuget source. Will default to default nuget.org servers. + /// If we should get the dependencies. + /// Directories to package folders. Will be lib/build/ref if not defined. + /// A cancellation token. + /// The directory where the NuGet packages are unzipped to. Also the files contained within the requested package only. + public static async Task files)>> DownloadPackageFilesAndFolder( + IReadOnlyCollection libraryIdentities, + IReadOnlyCollection frameworks = null, + PackageSource nugetSource = null, + bool getDependencies = true, + IReadOnlyCollection 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); + } /// /// Downloads the specified packages and returns the files and directories where the package NuGet package lives. @@ -62,7 +96,7 @@ static NuGetPackageHelper() /// Directories to package folders. Will be lib/build/ref if not defined. /// A cancellation token. /// The directory where the NuGet packages are unzipped to. Also the files contained within the requested package only. - public static async Task files)>> DownloadPackageAndFilesAndFolder( + public static Task files)>> DownloadPackageFilesAndFolder( IReadOnlyCollection packageIdentities, IReadOnlyCollection frameworks = null, PackageSource nugetSource = null, @@ -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); + } + + /// + /// Downloads the specified packages and returns the files and directories where the package NuGet package lives. + /// + /// The identity of the packages to find. + /// Framework parameter which will force NuGet to evaluate as the specified Framework. If null it will use .NET Standard 2.0. + /// Nuget source repository. Will default to default nuget.org servers. + /// If we should get the dependencies. + /// Directories to package folders. Will be lib/build/ref if not defined. + /// A cancellation token. + /// The directory where the NuGet packages are unzipped to. Also the files contained within the requested package only. + private static async Task files)>> DownloadPackageFilesAndFolder( + IReadOnlyCollection packageIdentities, + IReadOnlyCollection frameworks, + SourceRepository sourceRepository, + bool getDependencies = true, + IReadOnlyCollection 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> GetPackagesToCopy( IReadOnlyCollection 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(token); + var downloadResource = await sourceRepository.GetResourceAsync(token).ConfigureAwait(false); var packagesToCopy = new Dictionary(StringComparer.InvariantCultureIgnoreCase); @@ -221,6 +276,17 @@ private static bool EqualToOrLessThan(this NuGetFramework firstFramework, NuGetF return firstFramework.Version <= secondFramework.Version; } + private static async Task GetBestMatch(LibraryRange identity, SourceRepository sourceRepository, CancellationToken token) + { + var findPackageResource = await sourceRepository.GetResourceAsync(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 GetFileGroups(this PackageReaderBase reader, string folder) { var groups = new Dictionary>(new NuGetFrameworkFullComparer()); @@ -245,7 +311,7 @@ private static IEnumerable 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]; diff --git a/src/Pharmacist.Core/ObservablesForEventGenerator.cs b/src/Pharmacist.Core/ObservablesForEventGenerator.cs index c2f8378..aaf49f9 100644 --- a/src/Pharmacist.Core/ObservablesForEventGenerator.cs +++ b/src/Pharmacist.Core/ObservablesForEventGenerator.cs @@ -16,6 +16,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using NuGet.Frameworks; +using NuGet.LibraryModel; using NuGet.Packaging.Core; using Pharmacist.Core.Extractors; @@ -91,6 +92,21 @@ public static async Task ExtractEventsFromNuGetPackages(Stream outputStream, IRe await ExtractEventsFromAssemblies(outputStream, extractor.Assemblies, extractor.SearchDirectories).ConfigureAwait(false); } + /// + /// Extracts the events and delegates from the specified platform. + /// + /// Stream that we should output to. + /// The packages to process. + /// The framework to generate for in order of priority. + /// A task to monitor the progress. + public static async Task ExtractEventsFromNuGetPackages(Stream outputStream, IReadOnlyCollection packages, IReadOnlyCollection frameworks) + { + var extractor = new NuGetExtractor(); + await extractor.Extract(frameworks, packages).ConfigureAwait(false); + + await ExtractEventsFromAssemblies(outputStream, extractor.Assemblies, extractor.SearchDirectories).ConfigureAwait(false); + } + /// /// Extracts the events and delegates from the specified platform. /// diff --git a/src/Pharmacist.Core/Pharmacist.Core.csproj b/src/Pharmacist.Core/Pharmacist.Core.csproj index 311eb0a..a967e09 100644 --- a/src/Pharmacist.Core/Pharmacist.Core.csproj +++ b/src/Pharmacist.Core/Pharmacist.Core.csproj @@ -13,7 +13,8 @@ - + + diff --git a/src/Pharmacist.Core/ReferenceLocators/ReferenceLocator.cs b/src/Pharmacist.Core/ReferenceLocators/ReferenceLocator.cs index d042d19..6a9ad29 100644 --- a/src/Pharmacist.Core/ReferenceLocators/ReferenceLocator.cs +++ b/src/Pharmacist.Core/ReferenceLocators/ReferenceLocator.cs @@ -50,7 +50,7 @@ public static Task GetReferenceLocation(bool includePreRelease = true) private static async Task GetWindowsInstallationDirectory(bool includePreRelease) { - var results = await NuGetPackageHelper.DownloadPackageAndFilesAndFolder( + var results = await NuGetPackageHelper.DownloadPackageFilesAndFolder( new[] { VSWherePackageIdentity }, new[] { new NuGetFramework("Any") }, packageFolders: new[] { PackagingConstants.Folders.Tools }, diff --git a/src/Pharmacist.MsBuild/PharmacistNuGetTask.cs b/src/Pharmacist.MsBuild/PharmacistNuGetTask.cs index c127edc..e39c163 100644 --- a/src/Pharmacist.MsBuild/PharmacistNuGetTask.cs +++ b/src/Pharmacist.MsBuild/PharmacistNuGetTask.cs @@ -11,6 +11,7 @@ using Microsoft.Build.Framework; using Microsoft.Build.Utilities; +using NuGet.LibraryModel; using NuGet.Packaging.Core; using NuGet.Versioning; @@ -74,20 +75,20 @@ public override bool Execute() var packageReferences = PackageReferences.Where(x => !ExclusionPackageReferenceSet.Contains(x.ItemSpec)); - var packages = new List(); + var packages = new List(); // 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); } diff --git a/src/Pharmacist.Tests/IntegrationTests.cs b/src/Pharmacist.Tests/IntegrationTests.cs index 2061a4b..d12b975 100644 --- a/src/Pharmacist.Tests/IntegrationTests.cs +++ b/src/Pharmacist.Tests/IntegrationTests.cs @@ -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(); diff --git a/src/Pharmacist.Tests/NuGetPackageHelperTests.cs b/src/Pharmacist.Tests/NuGetPackageHelperTests.cs index 1add63c..71af77c 100644 --- a/src/Pharmacist.Tests/NuGetPackageHelperTests.cs +++ b/src/Pharmacist.Tests/NuGetPackageHelperTests.cs @@ -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(); @@ -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();