From be3bc5c365bf4c7adc5d1d09863c2dd23162cc4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matija=20Ko=C5=BEelj?= Date: Wed, 4 Jan 2023 14:31:29 +0100 Subject: [PATCH] Add depedency loader for NuGet package loading --- src/Connected.Host/DependencyLoader.cs | 343 +++++++++++++++++++++++++ 1 file changed, 343 insertions(+) create mode 100644 src/Connected.Host/DependencyLoader.cs diff --git a/src/Connected.Host/DependencyLoader.cs b/src/Connected.Host/DependencyLoader.cs new file mode 100644 index 0000000..dbf160d --- /dev/null +++ b/src/Connected.Host/DependencyLoader.cs @@ -0,0 +1,343 @@ +using System.Reflection; +using System.Runtime.Versioning; +using Connected.Host.Configuration; +using Microsoft.Extensions.DependencyModel; +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; + + +namespace Connected.Host; + +public class DependencyLoader +{ + /// + /// Loads packages from the specified repositories into memory. + /// + /// A list of package Id and version descriptors to load. + /// A list of NuGet repositories to check. + /// + /// Thrown when a package identity could not be resolved. + private async Task Load(IEnumerable packages, IEnumerable repositoryLocations, CancellationToken? cancellationToken = null) + { + cancellationToken ??= CancellationToken.None; + + //TODO: Wire up actual logger + var logger = new NuGet.Common.NullLogger(); + + /* + * Define a source provider with specified sources. + */ + var packageSources = GetPackageSources(repositoryLocations); + /* + * Define a package source provider with empty settings with the specified package sources. + */ + var packageSourceProvider = new PackageSourceProvider(Settings.LoadMachineWideSettings(Settings.DefaultSettingsFileName), packageSources); + /* + * Define a repository provider with our package source and the latest API capabilities. + */ + var repositoryProvider = new SourceRepositoryProvider(packageSourceProvider, Repository.Provider.GetCoreV3()); + /* + * Setup contexts for dependency resolution and package downloading. + * First, the disposable cache + */ + using var sourceCacheContext = new SourceCacheContext(); + /* + * Next, the compatible framework versions. + */ + var version = Assembly.GetExecutingAssembly() + .GetCustomAttributes(typeof(TargetFrameworkAttribute), false) + .SingleOrDefault() as TargetFrameworkAttribute; + + var targetFramework = NuGetFramework.ParseFrameworkName(version.FrameworkName, DefaultFrameworkNameProvider.Instance); + /* + * Complete list of all touched packages, to be filled out by installer and downloader. + */ + var allPackages = new HashSet(); + /* + * Default dependency context. + */ + var dependencyContext = DependencyContext.Default; + /* + * Extract list of repositories. + */ + var repositories = repositoryProvider.GetRepositories(); + /* + * Get all package identities and packages. + */ + foreach (var dependency in packages) + { + var packageIdentity = await GetPackageIdentity(dependency, sourceCacheContext, logger, repositories, cancellationToken.Value); + + if (packageIdentity is null) + throw new InvalidOperationException($"Cannot find package {dependency.Name}."); + + await GetPackageDependencies(packageIdentity, sourceCacheContext, targetFramework, logger, repositories, dependencyContext, allPackages, cancellationToken.Value); + } + + var packagesToInstall = GetPackagesToInstall(repositoryProvider, logger, packages, allPackages); + /* + * Install packages in standard location. + */ + var packageDirectory = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, ".packages"); + + var nugetSettings = Settings.LoadDefaultSettings(packageDirectory); + + await InstallPackages(sourceCacheContext, logger, packagesToInstall, packageDirectory, nugetSettings, cancellationToken.Value); + } + /// + /// Installs the list of packages and loads them into memory. + /// + /// The shared context for resolved packages. + /// A standard NuGet logger. + /// A list of packages to install. + /// The root directory into which the packages are downloaded and extracted. + /// A set of NuGet settings, containing local repos and download behavior settings. + /// + private async Task InstallPackages(SourceCacheContext sourceCacheContext, NuGet.Common.ILogger logger, + IEnumerable 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(cancellationToken); + + /* + * Download package (from online, local or shared cache). + */ + var downloadResult = await downloadResource.GetDownloadResourceResultAsync( + package, + new PackageDownloadContext(sourceCacheContext), + SettingsUtility.GetGlobalPackagesFolder(nugetSettings), + logger, + cancellationToken); + + /* + * Extract package. + */ + var extractedFiles = await PackageExtractor.ExtractPackageAsync( + downloadResult.PackageSource, + downloadResult.PackageStream, + packagePathResolver, + packageExtractionContext, + cancellationToken); + + /* + * Install applicable dlls. + */ + foreach (var assembly in extractedFiles.Where(e => e.EndsWith(".dll"))) + { + try + { + Assembly.LoadFrom(assembly); + } + catch (Exception e) + { + logger.LogWarning($"Could not load assembly. {e}"); + } + } + } + } + + /// + /// Generates a list of package sources from a list of repository urls. + /// + /// A list of URIs of specified sources. + /// A list of package sources for the specified NuGet repository URIs. + private IEnumerable GetPackageSources(IEnumerable sources) + { + foreach (var source in sources) + yield return new PackageSource(source); + } + + /// + /// Returns a list of missing packages to satisfy all dependencies. + /// + /// A source repository provider for all resources to load packages from. + /// A standard NuGet logger. + /// A list of packages for which to resolve all dependencies. + /// A list of available packages to resolve from. + /// + private IEnumerable GetPackagesToInstall(SourceRepositoryProvider sourceRepositoryProvider, NuGet.Common.ILogger logger, IEnumerable packages, HashSet availablePackages) + { + /* + * Create resolver to resolve missing packages + */ + var resolverContext = new PackageResolverContext( + DependencyBehavior.Lowest, + packages.Select(x => x.Name), + Enumerable.Empty(), + Enumerable.Empty(), + Enumerable.Empty(), + availablePackages, + sourceRepositoryProvider.GetRepositories().Select(s => s.PackageSource), + logger); + + var packagesToInstall = new PackageResolver() + .Resolve(resolverContext, CancellationToken.None) + .Select(p => availablePackages.Single(x => PackageIdentityComparer.Default.Equals(x, p))); + + return packagesToInstall; + } + /// + /// Resolves a package identity from the supplied Id and version. + /// + /// The version and id of the required package. + /// The shared cache in which resolved packages are stored. + /// A standard NuGet logger. + /// A list of repositories to check when resolving the package version. + /// + /// Returns the resolved package identity or null if it could not be resolved. + /// Thrown when the version range could not be parsed. + private async Task GetPackageIdentity(MicroServiceDescriptor microserviceDescriptor, SourceCacheContext cache, NuGet.Common.ILogger nugetLogger, IEnumerable repositories, CancellationToken cancelToken) + { + /* + * Go through repositories in order. + */ + foreach (var sourceRepository in repositories) + { + /* + * Get resource, available versions and attempt to resolve the appropriate version. If none is available, take the last one. + */ + var findPackageResource = await sourceRepository.GetResourceAsync(); + + var availableVersions = await findPackageResource.GetAllVersionsAsync(microserviceDescriptor.Name, cache, nugetLogger, cancelToken); + + NuGetVersion selected; + + if (microserviceDescriptor.Version is not null) + { + if (!VersionRange.TryParse(microserviceDescriptor.Version, out var range)) + throw new InvalidOperationException("Invalid version range provided."); + + var bestVersion = range.FindBestMatch(availableVersions); + + selected = bestVersion; + } + else + selected = availableVersions.LastOrDefault(); + + if (selected is not null) + return new PackageIdentity(microserviceDescriptor.Name, selected); + } + + return null; + } + /// + /// Searches the package dependency graph for the chain of all packages to install. + /// + private async Task GetPackageDependencies(PackageIdentity package, + SourceCacheContext cacheContext, + NuGetFramework framework, + NuGet.Common.ILogger logger, + IEnumerable repositories, + DependencyContext hostDependencies, + ISet availablePackages, + CancellationToken cancelToken) + { + /* + * Already present, skip. + */ + if (availablePackages.Contains(package)) + return; + + foreach (var sourceRepository in repositories) + { + /* + * Get dependency info. + */ + var dependencyInfoResource = await sourceRepository.GetResourceAsync(); + + var dependencyInfo = await dependencyInfoResource.ResolvePackage( + package, + framework, + cacheContext, + logger, + cancelToken); + + if (dependencyInfo is null) + continue; + /* + * Only return packages not provided by the host. + */ + var sourceDependencies = new SourcePackageDependencyInfo( + dependencyInfo.Id, + dependencyInfo.Version, + dependencyInfo.Dependencies.Where(dependency => !DependencySuppliedByHost(hostDependencies, dependency)), + dependencyInfo.Listed, + dependencyInfo.Source); + + availablePackages.Add(sourceDependencies); + /* + * Get the dependency's packages. + */ + foreach (var dependency in sourceDependencies.Dependencies) + { + await GetPackageDependencies( + new PackageIdentity(dependency.Id, dependency.VersionRange.MinVersion), + cacheContext, + framework, + logger, + repositories, + hostDependencies, + availablePackages, + cancelToken); + } + + break; + } + } + /// + /// Checks if a package dependency is already provided by the host. + /// + /// The context packages. + /// The dependency to check for. + /// True if the host provides the dependency, false otherwise. + private bool DependencySuppliedByHost(DependencyContext hostDependencies, PackageDependency dependency) + { + /* + * Check runtimes for package with same id. + */ + var runtimeLib = hostDependencies.RuntimeLibraries.FirstOrDefault(r => r.Name == dependency.Id); + + if (runtimeLib is null) + return false; + /* + * Check for version compatibility. + */ + var parsedLibVersion = NuGetVersion.Parse(runtimeLib.Version); + + if (parsedLibVersion.IsPrerelease) + /* + * Latest and greatest. Always accept due to system compatiblity reasons. + */ + return true; + + /* + * Check for version compatibility. + */ + return dependency.VersionRange.Satisfies(parsedLibVersion); + + } + /// + /// Loads packages from the specified repositories into memory. + /// + /// A list of package Id and version descriptors to load. + /// A list of NuGet repositories to check. + internal static async Task LoadPackages(IEnumerable packages, IEnumerable repositories) + { + await new DependencyLoader().Load(packages, repositories); + } +} \ No newline at end of file