Skip to content

Instantly share code, notes, and snippets.

@alistairjevans
Created April 17, 2020 11:31
Show Gist options
  • Save alistairjevans/4de1dccfb7288e0460b7b04f9a700a04 to your computer and use it in GitHub Desktop.
Save alistairjevans/4de1dccfb7288e0460b7b04f9a700a04 to your computer and use it in GitHub Desktop.
Complete Example Package Loader
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