src/NuGet.Clients/NuGet.VisualStudio.Implementation/PreinstalledPackageInstaller.cs (371 lines of code) (raw):

// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. #nullable disable using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Threading; using Microsoft.VisualStudio.Shell; using Microsoft.Win32; using NuGet.Common; using NuGet.Frameworks; using NuGet.PackageManagement; using NuGet.PackageManagement.VisualStudio; using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.Packaging.PackageExtraction; using NuGet.Packaging.Signing; using NuGet.ProjectManagement; using NuGet.ProjectManagement.Projects; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.VisualStudio.Implementation.Extensibility; using NuGet.VisualStudio.Implementation.Resources; using Task = System.Threading.Tasks.Task; namespace NuGet.VisualStudio { /// <summary> /// Provides functionality for installing packages already on disk from an installer (MSI or VSIX). /// </summary> internal class PreinstalledPackageInstaller { private const string RegistryKeyRoot = @"SOFTWARE\NuGet\Repository"; #pragma warning disable CS0618 // Type or member is obsolete private readonly IVsPackageInstallerServices _packageServices; #pragma warning restore CS0618 // Type or member is obsolete private readonly IVsSolutionManager _solutionManager; private readonly ISourceRepositoryProvider _sourceProvider; private readonly VsPackageInstaller _installer; private readonly IVsProjectAdapterProvider _vsProjectAdapterProvider; private readonly Configuration.ISettings _settings; public Action<string> InfoHandler { get; set; } public PreinstalledPackageInstaller( #pragma warning disable CS0618 // Type or member is obsolete IVsPackageInstallerServices packageServices, #pragma warning restore CS0618 // Type or member is obsolete IVsSolutionManager solutionManager, Configuration.ISettings settings, ISourceRepositoryProvider sourceProvider, VsPackageInstaller installer, IVsProjectAdapterProvider vsProjectAdapterProvider) { _packageServices = packageServices; _solutionManager = solutionManager; _sourceProvider = sourceProvider; _vsProjectAdapterProvider = vsProjectAdapterProvider; _installer = installer; _settings = settings; } /// <summary> /// Gets the folder location where packages have been laid down for the specified extension. /// </summary> /// <param name="extensionId">The installed extension.</param> /// <param name="vsExtensionManager">The VS Extension manager instance.</param> /// <param name="throwingErrorHandler"> /// An error handler that accepts the error message string and then throws /// the appropriate exception. /// </param> /// <returns>The absolute path to the extension's packages folder.</returns> internal string GetExtensionRepositoryPath(string extensionId, object vsExtensionManager, Action<string> throwingErrorHandler) { var extensionManagerShim = new ExtensionManagerShim(vsExtensionManager, throwingErrorHandler); string installPath; if (!extensionManagerShim.TryGetExtensionInstallPath(extensionId, out installPath)) { throwingErrorHandler(string.Format(CultureInfo.CurrentCulture, VsResources.PreinstalledPackages_InvalidExtensionId, extensionId)); Debug.Fail("The throwingErrorHandler did not throw"); } return Path.Combine(installPath, "Packages"); } /// <summary> /// Gets the folder location where packages have been laid down in a registry-specified location. /// </summary> /// <param name="keyName">The registry key name that specifies the packages location.</param> /// <param name="registryKeys">The optional list of parent registry keys to look in (used for unit tests).</param> /// <param name="throwingErrorHandler"> /// An error handler that accepts the error message string and then throws /// the appropriate exception. /// </param> /// <returns>The absolute path to the packages folder specified in the registry.</returns> internal string GetRegistryRepositoryPath(string keyName, IEnumerable<IRegistryKey> registryKeys, Action<string> throwingErrorHandler) { IRegistryKey repositoryKey = null; string repositoryValue = null; // When pulling the repository from the registry, use CurrentUser first, falling back onto LocalMachine // Documented here: https://docs.microsoft.com/nuget/visual-studio-extensibility/visual-studio-templates#registry-specified-folder-path registryKeys = registryKeys ?? new[] { new RegistryKeyWrapper(RegistryHive.CurrentUser), new RegistryKeyWrapper(RegistryHive.LocalMachine, RegistryView.Registry32) }; // Find the first registry key that supplies the necessary subkey/value foreach (var registryKey in registryKeys) { repositoryKey = registryKey.OpenSubKey(RegistryKeyRoot); if (repositoryKey != null) { repositoryValue = repositoryKey.GetValue(keyName) as string; if (!string.IsNullOrEmpty(repositoryValue)) { break; } repositoryKey.Close(); } } if (repositoryKey == null) { throwingErrorHandler(string.Format(CultureInfo.CurrentCulture, VsResources.PreinstalledPackages_RegistryKeyError, RegistryKeyRoot)); Debug.Fail("throwingErrorHandler did not throw"); } if (string.IsNullOrEmpty(repositoryValue)) { throwingErrorHandler(string.Format(CultureInfo.CurrentCulture, VsResources.PreinstalledPackages_InvalidRegistryValue, keyName, RegistryKeyRoot)); Debug.Fail("throwingErrorHandler did not throw"); } // Ensure a trailing slash so that the path always gets read as a directory repositoryValue = PathUtility.EnsureTrailingSlash(repositoryValue); return Path.GetDirectoryName(repositoryValue); } /// <summary> /// Installs one or more packages into the specified project. /// </summary> /// <param name="packageInstaller">The package installer service that performs the actual package installation.</param> /// <param name="project">The target project for installation.</param> /// <param name="configuration"> /// The packages to install, where to install them from, and additional options for /// their installation. /// </param> /// <param name="repositorySettings">The repository settings for the packages being installed.</param> /// <param name="preferPackageReferenceFormat">Install packages to the project as PackageReference if the project type supports it</param> /// <param name="warningHandler"> /// An action that accepts a warning message and presents it to the user, allowing /// execution to continue. /// </param> /// <param name="errorHandler"> /// An action that accepts an error message and presents it to the user, allowing /// execution to continue. /// </param> internal async Task PerformPackageInstallAsync( EnvDTE.Project project, PreinstalledPackageConfiguration configuration, bool preferPackageReferenceFormat, Action<string> warningHandler, Action<string> errorHandler) { await NuGetUIThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); var repositoryPath = configuration.RepositoryPath; var repositorySource = new Configuration.PackageSource(repositoryPath); var failedPackageErrors = new List<string>(); // find the project var defaultProjectContext = new VSAPIProjectContext { PackageExtractionContext = new PackageExtractionContext( PackageSaveMode.Defaultv2, PackageExtractionBehavior.XmlDocFileSaveMode, ClientPolicyContext.GetClientPolicy(_settings, NullLogger.Instance), NullLogger.Instance) }; var nuGetProject = await _solutionManager.GetOrCreateProjectAsync(project, defaultProjectContext); if (preferPackageReferenceFormat && await NuGetProjectUpgradeUtility.IsNuGetProjectUpgradeableAsync(nuGetProject, project, needsAPackagesConfig: false)) { nuGetProject = await _solutionManager.UpgradeProjectToPackageReferenceAsync(nuGetProject); } // For BuildIntegratedNuGetProject, nuget will ignore preunzipped configuration. var buildIntegratedProject = nuGetProject as BuildIntegratedNuGetProject; var repository = (buildIntegratedProject == null && configuration.IsPreunzipped) ? _sourceProvider.CreateRepository(repositorySource, FeedType.FileSystemUnzipped) : _sourceProvider.CreateRepository(repositorySource); var repoProvider = new PreinstalledRepositoryProvider(errorHandler, _sourceProvider); repoProvider.AddFromSource(repository); var packageManager = _installer.CreatePackageManager(repoProvider); var gatherCache = new GatherCache(); var sources = repoProvider.GetRepositories().ToList(); // store expanded node state var expandedNodes = await VsHierarchyUtility.GetAllExpandedNodesAsync(); try { foreach (var package in configuration.Packages) { var packageIdentity = new PackageIdentity(package.Id, package.Version); // Does the project already have this package installed? #pragma warning disable CS0618 // Type or member is obsolete if (_packageServices.IsPackageInstalled(project, package.Id)) { // If so, is it the right version? if (!_packageServices.IsPackageInstalledEx(project, package.Id, package.Version.ToNormalizedString())) #pragma warning restore CS0618 // Type or member is obsolete { // No? Raise a warning (likely written to the Output window) and ignore this package. warningHandler(string.Format(CultureInfo.CurrentCulture, VsResources.PreinstalledPackages_VersionConflict, package.Id, package.Version)); } // Yes? Just silently ignore this package! } else { try { if (InfoHandler != null) { InfoHandler(string.Format(CultureInfo.CurrentCulture, VsResources.PreinstalledPackages_PackageInstallStatus, package.Id, package.Version)); } // Skip assembly references and disable binding redirections should be done together var disableBindingRedirects = package.SkipAssemblyReferences; var projectContext = new VSAPIProjectContext(package.SkipAssemblyReferences, disableBindingRedirects); var loggerAdapter = new LoggerAdapter(projectContext); projectContext.PackageExtractionContext = new PackageExtractionContext( PackageSaveMode.Defaultv2, PackageExtractionBehavior.XmlDocFileSaveMode, ClientPolicyContext.GetClientPolicy(_settings, loggerAdapter), loggerAdapter); // This runs from the UI thread await _installer.InstallInternalCoreAsync( packageManager, gatherCache, nuGetProject, packageIdentity, sources, projectContext, includePrerelease: false, ignoreDependencies: package.IgnoreDependencies, token: CancellationToken.None); } catch (InvalidOperationException exception) { failedPackageErrors.Add(package.Id + "." + package.Version + " : " + exception.Message); } catch (AggregateException aggregateEx) { var ex = aggregateEx.Flatten().InnerExceptions.FirstOrDefault(); if (ex is InvalidOperationException) { failedPackageErrors.Add(package.Id + "." + package.Version + " : " + ex.Message); } else { throw; } } } } if (failedPackageErrors.Any()) { var errorString = new StringBuilder(); errorString.AppendFormat(CultureInfo.CurrentCulture, VsResources.PreinstalledPackages_FailedToInstallPackage, repositoryPath); errorString.AppendLine(); errorString.AppendLine(); errorString.Append(string.Join(Environment.NewLine, failedPackageErrors)); errorHandler(errorString.ToString()); } // RepositorySettings = null in unit tests if (project.IsWebSite()) { CreateRefreshFilesInBin( project, repositoryPath, configuration.Packages.Where(p => p.SkipAssemblyReferences)); CopyNativeBinariesToBin(project, repositoryPath, configuration.Packages); } } finally { // collapse nodes await VsHierarchyUtility.CollapseAllNodesAsync(expandedNodes); } } /// <summary> /// For Website projects, adds necessary "refresh" files in the bin folder for added references. /// </summary> /// <param name="project">The target Website project.</param> /// <param name="repositoryPath">The local repository path.</param> /// <param name="packageInfos">The packages that were installed.</param> private void CreateRefreshFilesInBin(EnvDTE.Project project, string repositoryPath, IEnumerable<PreinstalledPackageInfo> packageInfos) { IEnumerable<PackageIdentity> packageNames = packageInfos.Select(pi => new PackageIdentity(pi.Id, pi.Version)); AddRefreshFilesForReferences(project, repositoryPath, packageNames); } /// <summary> /// Adds refresh files to the specified project for all assemblies references belonging to the packages /// specified by packageNames. /// </summary> /// <param name="project">The project.</param> /// <param name="repositoryPath">The file system pointing to 'packages' folder under the solution.</param> /// <param name="packageNames">The package names.</param> private void AddRefreshFilesForReferences(EnvDTE.Project project, string repositoryPath, IEnumerable<PackageIdentity> packageNames) { if (project == null) { throw new ArgumentNullException(nameof(project)); } if (repositoryPath == null) { throw new ArgumentNullException(nameof(repositoryPath)); } if (!packageNames.Any()) { return; } VSAPIProjectContext context = new VSAPIProjectContext(skipAssemblyReferences: true, bindingRedirectsDisabled: true); var logger = new LoggerAdapter(context); context.PackageExtractionContext = new PackageExtractionContext( PackageSaveMode.Defaultv2, PackageExtractionBehavior.XmlDocFileSaveMode, ClientPolicyContext.GetClientPolicy(_settings, logger), logger); WebSiteProjectSystem projectSystem = new WebSiteProjectSystem(_vsProjectAdapterProvider.CreateAdapterForFullyLoadedProject(project), context); foreach (var packageName in packageNames) { string packagePath = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", packageName.Id, packageName.Version); DirectoryInfo packageFolder = new DirectoryInfo(Path.Combine(repositoryPath, packagePath)); using var reader = new PackageFolderReader(packageFolder); var frameworkGroups = reader.GetReferenceItems(); var groups = reader.GetReferenceItems(); var fwComparer = NuGetFrameworkFullComparer.Instance; FrameworkReducer reducer = new FrameworkReducer(); NuGetFramework targetGroupFramework = reducer.GetNearest(projectSystem.TargetFramework, groups.Select(e => e.TargetFramework)); if (targetGroupFramework != null) { var refGroup = groups.FirstOrDefault(e => fwComparer.Equals(targetGroupFramework, e.TargetFramework)); foreach (string refItem in refGroup.Items) { string sourcePath = Path.Combine(packageFolder.FullName, refItem.Replace('/', Path.DirectorySeparatorChar)); // create one refresh file for each assembly reference, as per required by Website projects // projectSystem.CreateRefreshFile(assemblyPath); RefreshFileUtility.CreateRefreshFile(projectSystem, sourcePath); } } } } /// <summary> /// By convention, we copy all files under the NativeBinaries folder under package root to the bin folder of /// the Website. /// </summary> /// <param name="project">The target Website project.</param> /// <param name="repositoryPath">The local repository path.</param> /// <param name="packageInfos">The packages that were installed.</param> private void CopyNativeBinariesToBin(EnvDTE.Project project, string repositoryPath, IEnumerable<PreinstalledPackageInfo> packageInfos) { var context = new VSAPIProjectContext { PackageExtractionContext = new PackageExtractionContext( PackageSaveMode.Defaultv2, PackageExtractionBehavior.XmlDocFileSaveMode, ClientPolicyContext.GetClientPolicy(_settings, NullLogger.Instance), NullLogger.Instance) }; var projectSystem = new VsMSBuildProjectSystem(_vsProjectAdapterProvider.CreateAdapterForFullyLoadedProject(project), context); foreach (var packageInfo in packageInfos) { var packagePath = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", packageInfo.Id, packageInfo.Version); CopyNativeBinaries(projectSystem, Path.Combine(repositoryPath, packagePath)); } } private static void CopyNativeBinaries(VsMSBuildProjectSystem projectSystem, string packagePath) { const string nativeBinariesFolder = "NativeBinaries"; const string binFolder = "bin"; DirectoryInfo nativeBinariesPath = new DirectoryInfo(Path.Combine(packagePath, nativeBinariesFolder)); if (nativeBinariesPath.Exists) { FileInfo[] nativeFiles = nativeBinariesPath.GetFiles("*.*", SearchOption.AllDirectories); foreach (FileInfo file in nativeFiles) { string targetPath = Path.Combine(binFolder, file.FullName.Substring(nativeBinariesPath.FullName.Length + 1)); // skip over NativeBinaries/ word using (Stream stream = file.OpenRead()) { projectSystem.AddFile(targetPath, stream); } } } } } }