Created
April 17, 2020 11:31
-
-
Save alistairjevans/4de1dccfb7288e0460b7b04f9a700a04 to your computer and use it in GitHub Desktop.
Complete Example Package Loader
This file contains 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 AutoStep.Extensions; | |
using Microsoft.Extensions.DependencyModel; | |
using NuGet.Common; | |
using NuGet.Configuration; | |
using NuGet.Frameworks; | |
using NuGet.Packaging; | |
using NuGet.Packaging.Core; | |
using NuGet.Packaging.Signing; | |
using NuGet.Protocol.Core.Types; | |
using NuGet.Resolver; | |
using NuGet.Versioning; | |
using System; | |
using System.Collections.Generic; | |
using System.IO; | |
using System.Linq; | |
using System.Text; | |
using System.Threading; | |
using System.Threading.Tasks; | |
namespace NugetConsole | |
{ | |
public class Loader | |
{ | |
/// <summary> | |
/// Represents the configuration for a single extension to install. | |
/// </summary> | |
public class ExtensionConfiguration | |
{ | |
public string Package { get; set; } | |
public string Version { get; set; } | |
public bool PreRelease { get; set; } | |
} | |
public async Task LoadExtensions() | |
{ | |
// Define a source provider, with nuget, plus my own feed. | |
var sourceProvider = new PackageSourceProvider(NullSettings.Instance, new[] | |
{ | |
new PackageSource("https://api.nuget.org/v3/index.json"), | |
new PackageSource("https://f.feedz.io/autostep/ci/nuget/index.json") | |
}); | |
// Establish the source repository provider; the available providers come from our custom settings. | |
var sourceRepositoryProvider = new SourceRepositoryProvider(sourceProvider, Repository.Provider.GetCoreV3()); | |
// Get the list of repositories. | |
var repositories = sourceRepositoryProvider.GetRepositories(); | |
// Disposable source cache. | |
using var sourceCacheContext = new SourceCacheContext(); | |
// You should use an actual logger here, this is a NuGet ILogger instance. | |
var logger = new NullLogger(); | |
// My extension configuration: | |
var extensions = new[] | |
{ | |
new ExtensionConfiguration | |
{ | |
Package = "AutoStep.Web", | |
PreRelease = true // Allow pre-release versions. | |
} | |
}; | |
// Replace this with a proper cancellation token. | |
var cancellationToken = CancellationToken.None; | |
// The framework we're using. | |
var targetFramework = NuGetFramework.ParseFolder("netcoreapp3.1"); | |
var allPackages = new HashSet<SourcePackageDependencyInfo>(); | |
var dependencyContext = DependencyContext.Default; | |
foreach (var ext in extensions) | |
{ | |
var packageIdentity = await GetPackageIdentity(ext, sourceCacheContext, logger, repositories, cancellationToken); | |
if (packageIdentity is null) | |
{ | |
throw new InvalidOperationException($"Cannot find package {ext.Package}."); | |
} | |
await GetPackageDependencies(packageIdentity, sourceCacheContext, targetFramework, logger, repositories, dependencyContext, allPackages, cancellationToken); | |
} | |
var packagesToInstall = GetPackagesToInstall(sourceRepositoryProvider, logger, extensions, allPackages); | |
// Where do we want to install our packages? | |
var packageDirectory = Path.Combine(Environment.CurrentDirectory, ".extensions"); | |
var nugetSettings = Settings.LoadDefaultSettings(packageDirectory); | |
await InstallPackages(sourceCacheContext, logger, packagesToInstall, packageDirectory, nugetSettings, cancellationToken); | |
} | |
private async Task InstallPackages(SourceCacheContext sourceCacheContext, ILogger logger, | |
IEnumerable<SourcePackageDependencyInfo> packagesToInstall, string rootPackagesDirectory, | |
ISettings nugetSettings, CancellationToken cancellationToken) | |
{ | |
var packagePathResolver = new PackagePathResolver(rootPackagesDirectory, true); | |
var packageExtractionContext = new PackageExtractionContext( | |
PackageSaveMode.Defaultv3, | |
XmlDocFileSaveMode.Skip, | |
ClientPolicyContext.GetClientPolicy(nugetSettings, logger), | |
logger); | |
foreach (var package in packagesToInstall) | |
{ | |
var downloadResource = await package.Source.GetResourceAsync<DownloadResource>(cancellationToken); | |
// Download the package (might come from the shared package cache). | |
var downloadResult = await downloadResource.GetDownloadResourceResultAsync( | |
package, | |
new PackageDownloadContext(sourceCacheContext), | |
SettingsUtility.GetGlobalPackagesFolder(nugetSettings), | |
logger, | |
cancellationToken); | |
// Extract the package into the target directory. | |
await PackageExtractor.ExtractPackageAsync( | |
downloadResult.PackageSource, | |
downloadResult.PackageStream, | |
packagePathResolver, | |
packageExtractionContext, | |
cancellationToken); | |
} | |
} | |
private IEnumerable<SourcePackageDependencyInfo> GetPackagesToInstall(SourceRepositoryProvider sourceRepositoryProvider, | |
ILogger logger, IEnumerable<ExtensionConfiguration> extensions, | |
HashSet<SourcePackageDependencyInfo> allPackages) | |
{ | |
// Create a package resolver context (this is used to help figure out which actual package versions to install). | |
var resolverContext = new PackageResolverContext( | |
DependencyBehavior.Lowest, | |
extensions.Select(x => x.Package), | |
Enumerable.Empty<string>(), | |
Enumerable.Empty<PackageReference>(), | |
Enumerable.Empty<PackageIdentity>(), | |
allPackages, | |
sourceRepositoryProvider.GetRepositories().Select(s => s.PackageSource), | |
logger); | |
var resolver = new PackageResolver(); | |
// Work out the actual set of packages to install. | |
var packagesToInstall = resolver.Resolve(resolverContext, CancellationToken.None) | |
.Select(p => allPackages.Single(x => PackageIdentityComparer.Default.Equals(x, p))); | |
return packagesToInstall; | |
} | |
private async Task<PackageIdentity> GetPackageIdentity( | |
ExtensionConfiguration extConfig, SourceCacheContext cache, ILogger nugetLogger, | |
IEnumerable<SourceRepository> repositories, CancellationToken cancelToken) | |
{ | |
// Go through each repository. | |
// If a repository contains only pre-release packages (e.g. AutoStep CI), and | |
// the configuration doesn't permit pre-release versions, | |
// the search will look at other ones (e.g. NuGet). | |
foreach (var sourceRepository in repositories) | |
{ | |
// Get a 'resource' from the repository. | |
var findPackageResource = await sourceRepository.GetResourceAsync<FindPackageByIdResource>(); | |
// Get the list of all available versions of the package in the repository. | |
var allVersions = await findPackageResource.GetAllVersionsAsync(extConfig.Package, cache, nugetLogger, cancelToken); | |
NuGetVersion selected; | |
// Have we specified a version range? | |
if (extConfig.Version != null) | |
{ | |
if (!VersionRange.TryParse(extConfig.Version, out var range)) | |
{ | |
throw new InvalidOperationException("Invalid version range provided."); | |
} | |
// Find the best package version match for the range. | |
// Consider pre-release versions, but only if the extension is configured to use them. | |
var bestVersion = range.FindBestMatch(allVersions.Where(v => extConfig.PreRelease || !v.IsPrerelease)); | |
selected = bestVersion; | |
} | |
else | |
{ | |
// No version; choose the latest, allow pre-release if configured. | |
selected = allVersions.LastOrDefault(v => v.IsPrerelease == extConfig.PreRelease); | |
} | |
if (selected is object) | |
{ | |
return new PackageIdentity(extConfig.Package, selected); | |
} | |
} | |
return null; | |
} | |
/// <summary> | |
/// Searches the package dependency graph for the chain of all packages to install. | |
/// </summary> | |
private async Task GetPackageDependencies(PackageIdentity package, SourceCacheContext cacheContext, NuGetFramework framework, | |
ILogger logger, IEnumerable<SourceRepository> repositories, DependencyContext hostDependencies, | |
ISet<SourcePackageDependencyInfo> availablePackages, CancellationToken cancelToken) | |
{ | |
// Don't recurse over a package we've already seen. | |
if (availablePackages.Contains(package)) | |
{ | |
return; | |
} | |
foreach (var sourceRepository in repositories) | |
{ | |
// Get the dependency info for the package. | |
var dependencyInfoResource = await sourceRepository.GetResourceAsync<DependencyInfoResource>(); | |
var dependencyInfo = await dependencyInfoResource.ResolvePackage( | |
package, | |
framework, | |
cacheContext, | |
logger, | |
cancelToken); | |
// No info for the package in this repository. | |
if (dependencyInfo == null) | |
{ | |
continue; | |
} | |
// Filter the dependency info. | |
// Don't bring in any dependencies that are provided by the host. | |
var actualSourceDep = new SourcePackageDependencyInfo( | |
dependencyInfo.Id, | |
dependencyInfo.Version, | |
dependencyInfo.Dependencies.Where(dep => !DependencySuppliedByHost(hostDependencies, dep)), | |
dependencyInfo.Listed, | |
dependencyInfo.Source); | |
availablePackages.Add(actualSourceDep); | |
// Recurse through each package. | |
foreach (var dependency in actualSourceDep.Dependencies) | |
{ | |
await GetPackageDependencies( | |
new PackageIdentity(dependency.Id, dependency.VersionRange.MinVersion), | |
cacheContext, | |
framework, | |
logger, | |
repositories, | |
hostDependencies, | |
availablePackages, | |
cancelToken); | |
} | |
break; | |
} | |
} | |
private bool DependencySuppliedByHost(DependencyContext hostDependencies, PackageDependency dep) | |
{ | |
if(RuntimeProvidedPackages.IsPackageProvidedByRuntime(dep.Id)) | |
{ | |
return true; | |
} | |
// See if a runtime library with the same ID as the package is available in the host's runtime libraries. | |
var runtimeLib = hostDependencies.RuntimeLibraries.FirstOrDefault(r => r.Name == dep.Id); | |
if (runtimeLib is object) | |
{ | |
// What version of the library is the host using? | |
var parsedLibVersion = NuGetVersion.Parse(runtimeLib.Version); | |
if (parsedLibVersion.IsPrerelease) | |
{ | |
// Always use pre-release versions from the host, otherwise it becomes | |
// a nightmare to develop across multiple active versions. | |
return true; | |
} | |
else | |
{ | |
// Does the host version satisfy the version range of the requested package? | |
// If so, we can provide it; otherwise, we cannot. | |
return dep.VersionRange.Satisfies(parsedLibVersion); | |
} | |
} | |
return false; | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment