// 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.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using NuGet.CommandLine.XPlat.Utility; using NuGet.Commands; using NuGet.Common; using NuGet.Configuration; using NuGet.Credentials; using NuGet.Frameworks; using NuGet.LibraryModel; using NuGet.Packaging; using NuGet.Packaging.Core; using NuGet.ProjectModel; using NuGet.Protocol.Core.Types; using NuGet.Shared; using NuGet.Versioning; namespace NuGet.CommandLine.XPlat { internal class AddPackageReferenceCommandRunner : IPackageReferenceCommandRunner { public async Task<int> ExecuteCommand(PackageReferenceArgs packageReferenceArgs, MSBuildAPIUtility msBuild) { packageReferenceArgs.Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgAddingReference, packageReferenceArgs.PackageId, packageReferenceArgs.ProjectPath)); if (packageReferenceArgs.NoRestore) { packageReferenceArgs.Logger.LogWarning(string.Format(CultureInfo.CurrentCulture, Strings.Warn_AddPkgWithoutRestore)); VersionRange versionRange = default; if (packageReferenceArgs.NoVersion) { versionRange = packageReferenceArgs.Prerelease ? VersionRange.Parse("*-*") : VersionRange.Parse("*"); } else { versionRange = VersionRange.Parse(packageReferenceArgs.PackageVersion); } var libraryDependency = new LibraryDependency() { LibraryRange = new LibraryRange( name: packageReferenceArgs.PackageId, versionRange: versionRange, typeConstraint: LibraryDependencyTarget.Package) }; msBuild.AddPackageReference(packageReferenceArgs.ProjectPath, libraryDependency, packageReferenceArgs.NoVersion); return 0; } // 1. Get project dg file packageReferenceArgs.Logger.LogDebug("Reading project Dependency Graph"); var dgSpec = ReadProjectDependencyGraph(packageReferenceArgs); if (dgSpec == null) { // Logging non localized error on debug stream. packageReferenceArgs.Logger.LogDebug(Strings.Error_NoDgSpec); throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_NoDgSpec)); } packageReferenceArgs.Logger.LogDebug("Project Dependency Graph Read"); var projectFullPath = Path.GetFullPath(packageReferenceArgs.ProjectPath); if (msBuild.VirtualProjectBuilder?.IsValidEntryPointPath(projectFullPath) == true) { projectFullPath = msBuild.VirtualProjectBuilder.GetVirtualProjectPath(projectFullPath); } var matchingPackageSpecs = dgSpec .Projects .Where(p => p.RestoreMetadata.ProjectStyle == ProjectStyle.PackageReference && PathUtility.GetStringComparerBasedOnOS().Equals(Path.GetFullPath(p.RestoreMetadata.ProjectPath), projectFullPath)) .ToArray(); // This ensures that the DG specs generated in previous steps contain exactly 1 project with the same path as the project requesting add package. // Throw otherwise since we cannot proceed further. if (matchingPackageSpecs.Length != 1) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_UnsupportedProject, packageReferenceArgs.PackageId, packageReferenceArgs.ProjectPath)); } // Keep the user specified frameworks as aliases to match against TargetAlias var userSpecifiedFrameworks = new List<string>(); if (packageReferenceArgs.Frameworks?.Any() == true) { userSpecifiedFrameworks.AddRange(packageReferenceArgs.Frameworks); } var originalPackageSpec = matchingPackageSpecs.FirstOrDefault(); // Check if the project files are correct for CPM if (originalPackageSpec.RestoreMetadata.CentralPackageVersionsEnabled && !msBuild.AreCentralVersionRequirementsSatisfied(packageReferenceArgs, originalPackageSpec)) { return 1; } // 2. Determine the version // Setup the Credential Service before making any potential http calls. DefaultCredentialServiceUtility.SetupDefaultCredentialService(packageReferenceArgs.Logger, !packageReferenceArgs.Interactive); if (packageReferenceArgs.Sources?.Any() == true) { // Convert relative path to absolute path if there is any List<string> sources = new List<string>(); foreach (string source in packageReferenceArgs.Sources) { sources.Add(UriUtility.GetAbsolutePath(Environment.CurrentDirectory, source)); } originalPackageSpec.RestoreMetadata.Sources = sources.Where(ns => !string.IsNullOrEmpty(ns)) .Select(ns => new PackageSource(ns)) .ToList(); } PackageDependency packageDependency = default; if (packageReferenceArgs.NoVersion) { if (originalPackageSpec.RestoreMetadata.CentralPackageVersionsEnabled) { var centralVersion = originalPackageSpec .TargetFrameworks .Where(tf => tf.CentralPackageVersions.ContainsKey(packageReferenceArgs.PackageId)) .Select(tf => tf.CentralPackageVersions[packageReferenceArgs.PackageId]) .FirstOrDefault(); if (centralVersion != null) { packageDependency = new PackageDependency(packageReferenceArgs.PackageId, centralVersion.VersionRange); } } if (packageDependency == null) { var latestVersion = await GetLatestVersionAsync(originalPackageSpec, packageReferenceArgs.PackageId, packageReferenceArgs.Logger, packageReferenceArgs.Prerelease); if (latestVersion == null) { if (!packageReferenceArgs.Prerelease) { latestVersion = await GetLatestVersionAsync(originalPackageSpec, packageReferenceArgs.PackageId, packageReferenceArgs.Logger, !packageReferenceArgs.Prerelease); if (latestVersion != null) { throw new CommandException(string.Format(CultureInfo.CurrentCulture, Strings.PrereleaseVersionsAvailable, latestVersion)); } } throw new CommandException(Messages.Error_NoVersionsAvailable(packageReferenceArgs.PackageId)); } packageDependency = new PackageDependency(packageReferenceArgs.PackageId, new VersionRange(minVersion: latestVersion, includeMinVersion: true)); } } else { packageDependency = new PackageDependency(packageReferenceArgs.PackageId, VersionRange.Parse(packageReferenceArgs.PackageVersion)); } // Create a copy to avoid modifying the original spec which may be shared. var updatedPackageSpec = originalPackageSpec.Clone(); if (packageReferenceArgs.Frameworks?.Any() == true) { // If user specified frameworks then just use them to add the dependency PackageSpecOperations.AddOrUpdateDependency(updatedPackageSpec, packageDependency, userSpecifiedFrameworks); } else { PackageSpecOperations.AddOrUpdateDependency(updatedPackageSpec, packageDependency); } var updatedDgSpec = dgSpec.WithReplacedSpec(updatedPackageSpec).WithoutRestores(); updatedDgSpec.AddRestore(updatedPackageSpec.RestoreMetadata.ProjectUniqueName); // 3. Run Restore Preview packageReferenceArgs.Logger.LogDebug("Running Restore preview"); var restorePreviewResult = await PreviewAddPackageReferenceAsync(packageReferenceArgs, updatedDgSpec); packageReferenceArgs.Logger.LogDebug("Restore Review completed"); // 4. Process Restore Result var compatibleFrameworks = new HashSet<string>( restorePreviewResult .Result .CompatibilityCheckResults .Where(t => t.Success) .Select(t => t.Graph.TargetAlias), StringComparer.OrdinalIgnoreCase); if (packageReferenceArgs.Frameworks?.Any() == true) { // If the user has specified frameworks then we intersect that with the compatible frameworks. var userSpecifiedFrameworkSet = new HashSet<string>( userSpecifiedFrameworks, StringComparer.OrdinalIgnoreCase); compatibleFrameworks.IntersectWith(userSpecifiedFrameworkSet); } if (compatibleFrameworks.Count == 0) { // Package is compatible with none of the project TFMs // Do not add a package reference, throw appropriate error packageReferenceArgs.Logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_AddPkgIncompatibleWithAllFrameworks, packageReferenceArgs.PackageId, packageReferenceArgs.Frameworks?.Any() == true ? Strings.AddPkg_UserSpecified : Strings.AddPkg_All, packageReferenceArgs.ProjectPath)); return 1; } // 5. Write to Project if (!TryFindResolvedVersion(userSpecifiedFrameworks, packageDependency.Id, restorePreviewResult.Result, packageReferenceArgs.Logger, out NuGetVersion resolvedVersion)) { return 1; } // Ignore the graphs with RID if (compatibleFrameworks.Count == restorePreviewResult.Result.CompatibilityCheckResults.Count(r => string.IsNullOrEmpty(r.Graph.RuntimeIdentifier))) { // Package is compatible with all the project TFMs // Add an unconditional package reference to the project packageReferenceArgs.Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgCompatibleWithAllFrameworks, packageReferenceArgs.PackageId, packageReferenceArgs.ProjectPath)); // generate a library dependency with all the metadata like Include, Exclude and SuppressParent var libraryDependency = GenerateLibraryDependency(updatedPackageSpec, packageReferenceArgs.PackageDirectory, packageDependency, resolvedVersion); msBuild.AddPackageReference(packageReferenceArgs.ProjectPath, libraryDependency, packageReferenceArgs.NoVersion); } else { // Package is compatible with some of the project TFMs // Add conditional package references to the project for the compatible TFMs packageReferenceArgs.Logger.LogInformation(string.Format(CultureInfo.CurrentCulture, Strings.Info_AddPkgCompatibleWithSubsetFrameworks, packageReferenceArgs.PackageId, packageReferenceArgs.ProjectPath)); // generate a library dependency with all the metadata like Include, Exlude and SuppressParent var libraryDependency = GenerateLibraryDependency(updatedPackageSpec, packageReferenceArgs.PackageDirectory, packageDependency, resolvedVersion); // compatibleFrameworks already contains aliases from TargetAlias msBuild.AddPackageReferencePerTFM(packageReferenceArgs.ProjectPath, libraryDependency, compatibleFrameworks, packageReferenceArgs.NoVersion); } // 6. Commit restore result await RestoreRunner.CommitAsync(restorePreviewResult, CancellationToken.None); return 0; } internal static bool TryFindResolvedVersion(List<string> userSpecifiedFrameworks, string packageId, RestoreResult restoreResult, ILogger logger, out NuGetVersion resolvedVersion) { // get the package resolved version from restore preview result (LibraryType libraryType, resolvedVersion) = GetPackageVersionFromRestoreResult(restoreResult, packageId, userSpecifiedFrameworks); if (libraryType == LibraryType.Unresolved) { logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_AddPkgUnresolved, packageId)); return false; } if (libraryType == LibraryType.Project || libraryType == LibraryType.ExternalProject) { // If the package is a project or external project, we cannot add it as a package reference. logger.LogError(string.Format(CultureInfo.CurrentCulture, Strings.Error_AddPkgProjectReference, packageId)); return false; } return true; } internal static string GetAliasForFramework(PackageSpec spec, NuGetFramework framework) { return spec.TargetFrameworks.FirstOrDefault(e => e.FrameworkName.Equals(framework))?.TargetAlias; } public static async Task<NuGetVersion> GetLatestVersionAsync(PackageSpec originalPackageSpec, string packageId, ILogger logger, bool prerelease) { IList<PackageSource> sources = AddPackageCommandUtility.EvaluateSources(originalPackageSpec.RestoreMetadata.Sources, originalPackageSpec.RestoreMetadata.ConfigFilePaths); return await AddPackageCommandUtility.GetLatestVersionFromSourcesAsync(sources, logger, packageId, prerelease, CancellationToken.None); } /// <summary> /// Returns the library dependency for the package reference to be added if appropriate. May return null if the package is not compatible with the project or if it is a project reference. /// </summary> internal static LibraryDependency GenerateLibraryDependency( PackageSpec project, string customPackagesPath, PackageDependency packageDependency, NuGetVersion resolvedVersion) { // correct package version to write in project file var version = packageDependency.VersionRange; // update default packages path if user specified custom package directory var packagesPath = project.RestoreMetadata.PackagesPath; // get if the project is onboarded to CPM var isCentralPackageManagementEnabled = project.RestoreMetadata.CentralPackageVersionsEnabled; if (!string.IsNullOrEmpty(customPackagesPath)) { packagesPath = customPackagesPath; } // create a path resolver to get nuspec file of the package var pathResolver = new FallbackPackagePathResolver( packagesPath, project.RestoreMetadata.FallbackFolders); var info = pathResolver.GetPackageInfo(packageDependency.Id, resolvedVersion); var packageDirectory = info?.PathResolver.GetInstallPath(packageDependency.Id, resolvedVersion); var nuspecFile = info?.PathResolver.GetManifestFileName(packageDependency.Id, resolvedVersion); var nuspecFilePath = Path.GetFullPath(Path.Combine(packageDirectory, nuspecFile)); // read development dependency from nuspec file NuspecReader nuspecReader = new(nuspecFilePath); var developmentDependency = nuspecReader.GetDevelopmentDependency(); string packageId = nuspecReader.GetId(); if (developmentDependency) { var orderedFrameworksWithOriginalIndex = project.TargetFrameworks .Select((frameworkInfo, originalIndex) => (frameworkInfo, originalIndex)) .OrderBy(tuple => tuple.frameworkInfo.FrameworkName.ToString(), StringComparer.Ordinal); foreach (var (frameworkInfo, originalIndex) in orderedFrameworksWithOriginalIndex) { var index = frameworkInfo.Dependencies.FirstIndex(dep => dep.Name.Equals(packageDependency.Id, StringComparison.OrdinalIgnoreCase)); var dependency = frameworkInfo.Dependencies[index]; var includeType = dependency.IncludeType; var suppressParent = dependency.SuppressParent; // if suppressParent and IncludeType aren't set by user, then only update those as per dev dependency if (suppressParent == LibraryIncludeFlagUtils.DefaultSuppressParent && includeType == LibraryIncludeFlags.All) { suppressParent = LibraryIncludeFlags.All; includeType = LibraryIncludeFlags.All & ~LibraryIncludeFlags.Compile; } dependency = new LibraryDependency(dependency) { IncludeType = includeType, LibraryRange = new LibraryRange(dependency.LibraryRange) { VersionRange = version, Name = packageId }, SuppressParent = suppressParent, VersionCentrallyManaged = isCentralPackageManagementEnabled, }; var newDependencies = frameworkInfo.Dependencies.SetItem(index, dependency); project.TargetFrameworks[originalIndex] = new TargetFrameworkInformation(frameworkInfo) { Dependencies = newDependencies }; return dependency; } } return new LibraryDependency() { LibraryRange = new LibraryRange( name: packageId, versionRange: version, typeConstraint: LibraryDependencyTarget.Package), VersionCentrallyManaged = isCentralPackageManagementEnabled }; } private static async Task<RestoreResultPair> PreviewAddPackageReferenceAsync(PackageReferenceArgs packageReferenceArgs, DependencyGraphSpec dgSpec) { // Set user agent and connection settings. XPlatUtility.ConfigureProtocol(); var providerCache = new RestoreCommandProvidersCache(); using (var cacheContext = new SourceCacheContext()) { cacheContext.NoCache = false; cacheContext.IgnoreFailedSources = false; // Pre-loaded request provider containing the graph file var providers = new List<IPreLoadedRestoreRequestProvider> { new DependencyGraphSpecRequestProvider(providerCache, dgSpec) }; var restoreContext = new RestoreArgs() { CacheContext = cacheContext, Log = packageReferenceArgs.Logger, MachineWideSettings = new XPlatMachineWideSetting(), GlobalPackagesFolder = packageReferenceArgs.PackageDirectory, PreLoadedRequestProviders = providers // Sources : No need to pass it, because SourceRepositories contains the already built SourceRepository objects }; // Generate Restore Requests. There will always be 1 request here since we are restoring for 1 project. var restoreRequests = await RestoreRunner.GetRequests(restoreContext); // Run restore without commit. This will always return 1 Result pair since we are restoring for 1 request. var restoreResult = await RestoreRunner.RunWithoutCommit(restoreRequests, restoreContext); return restoreResult.Single(); } } private static DependencyGraphSpec ReadProjectDependencyGraph(PackageReferenceArgs packageReferenceArgs) { DependencyGraphSpec spec = null; if (File.Exists(packageReferenceArgs.DgFilePath)) { spec = DependencyGraphSpec.Load(packageReferenceArgs.DgFilePath); } return spec; } private static (LibraryType, NuGetVersion) GetPackageVersionFromRestoreResult(RestoreResult restoreResult, string packageId, List<string> userSpecifiedFrameworks) { // Get the restore graphs from the restore result var restoreGraphs = restoreResult .RestoreGraphs; if (userSpecifiedFrameworks.Count > 1) { // If the user specified frameworks then we get the flattened graphs only from the compatible frameworks. var userSpecifiedFrameworkSet = new HashSet<string>( userSpecifiedFrameworks, StringComparer.OrdinalIgnoreCase); restoreGraphs = restoreGraphs .Where(r => userSpecifiedFrameworkSet.Contains(r.TargetAlias)); } foreach (var restoreGraph in restoreGraphs) { var matchingPackageEntries = restoreGraph .Flattened .Select(p => p) .Where(p => p.Key.Name.Equals(packageId, StringComparison.OrdinalIgnoreCase)); if (matchingPackageEntries.Any()) { var firstMatchingEntry = matchingPackageEntries .First(); // If we have found that the project is selected, then we return null. if (firstMatchingEntry.Key.Type == LibraryType.Project || firstMatchingEntry.Key.Type == LibraryType.ExternalProject) { return (firstMatchingEntry.Key.Type, null); } return (firstMatchingEntry.Key.Type, firstMatchingEntry .Key .Version); } } return (LibraryType.Unresolved, null); } } } |