|
// 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 enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NuGet.CommandLine.XPlat.Utility;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Credentials;
using NuGet.LibraryModel;
using NuGet.Packaging.Core;
using NuGet.ProjectModel;
using NuGet.Protocol.Model;
using NuGet.Versioning;
namespace NuGet.CommandLine.XPlat.Commands.Package.Update;
internal static class PackageUpdateCommandRunner
{
// This overload sets static state, so should not be used in tests.
internal static Task<int> Run(PackageUpdateArgs args, IVirtualProjectBuilder? virtualProjectBuilder, CancellationToken cancellationToken)
{
ILoggerWithColor logger = new CommandOutputLogger(args.LogLevel)
{
HidePrefixForInfoAndMinimal = true
};
XPlatUtility.ConfigureProtocol();
DefaultCredentialServiceUtility.SetupDefaultCredentialService(logger, nonInteractive: !args.Interactive);
// MSBuildAPIUtility's output is different to what we want for package update.
// While it would probably be a good idea to align the output of all commands using MSBuildAPIUtility,
// in order to meet deadlines, we'll suppress its output, and leave improvements for later.
MSBuildAPIUtility msBuild = new(NullLogger.Instance, virtualProjectBuilder);
var restoreHelper = new PackageUpdateIO(args.Project, msBuild, EnvironmentVariableWrapper.Instance);
return Run(args, logger, restoreHelper, cancellationToken);
}
internal static async Task<int> Run(PackageUpdateArgs args, ILoggerWithColor logger, IPackageUpdateIO packageUpdateIO, CancellationToken cancellationToken)
{
// 1. Get DGSpec for project/solution
// 2. Find suitable version of package(s) to update
// 3. Preview restore to validate changes
// 4. Update MSBuild files
// 5. Commit restore if everything successful
// 1. Get DGSpec for project/solution
logger.LogVerbose(Strings.PackageUpdate_LoadingDGSpec);
var dgSpec = packageUpdateIO.GetDependencyGraphSpec(args.Project);
if (dgSpec is null || dgSpec.Restore is null || dgSpec.Restore.Count == 0)
{
logger.LogMinimal(
string.Format(CultureInfo.CurrentCulture, Strings.Error_PathIsMissingOrInvalid, args.Project),
ConsoleColor.Red);
return ExitCodes.Error;
}
if (args.Vulnerable)
{
logger.LogInformation(Format.PackageUpdate_UpdatingVulnerablePackages(args.Project));
}
else
{
logger.LogInformation(Format.PackageUpdate_UpdatingOutdatedPackages(args.Project));
}
// 2. Find suitable version of package(s) to update
// Source provider will be needed to find the package version and to restore, so create it here.
logger.LogVerbose(Strings.PackageUpdate_FindingUpdateVersions);
var (exitCode, projectPackageUpdates, totalPackagesScanned) = await SelectPackagesToUpdateAsync(args, dgSpec, logger, packageUpdateIO, cancellationToken);
if (exitCode.HasValue)
{
return exitCode.Value;
}
// 3. Preview restore to validate changes
logger.LogDebug(Strings.PackageUpdate_PreviewRestore);
var updatedDgSpec = GetUpdatedDependencyGraphSpec(dgSpec, projectPackageUpdates);
var restorePreviewResult = await packageUpdateIO.PreviewUpdatePackageReferenceAsync(updatedDgSpec, logger, cancellationToken);
if (!restorePreviewResult.Success)
{
logger.LogMinimal(Strings.PackageUpdate_PreviewRestoreFailed, ConsoleColor.Red);
return ExitCodes.Error;
}
// 4. Update MSBuild files
HashSet<string> uniquePackagesUpdated = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var (projectPath, packagesToUpdate) in projectPackageUpdates)
{
var projectName = Path.GetFileNameWithoutExtension(projectPath);
logger.LogInformation($" {projectName}:");
var updatedPackageSpec = updatedDgSpec.GetProjectSpec(projectPath);
foreach (var packageResult in packagesToUpdate)
{
logger.LogInformation($" {Format.PackageUpdate_UpdatedMessage(packageResult.Package.Id, packageResult.Package.CurrentVersion.ToShortString(), packageResult.Package.NewVersion.ToShortString())}");
packageUpdateIO.UpdatePackageReference(updatedPackageSpec, restorePreviewResult, packageResult.TargetFrameworkAliases, packageResult.Package, logger);
uniquePackagesUpdated.Add(packageResult.Package.Id);
}
}
// 5. Commit restore if everything successful
await packageUpdateIO.CommitAsync(restorePreviewResult, CancellationToken.None);
int uniquePackageCount = uniquePackagesUpdated.Count;
logger.LogInformation("");
logger.LogMinimal(Format.PackageUpdate_FinalSummary(uniquePackageCount, totalPackagesScanned), ConsoleColor.Green);
return ExitCodes.Success;
}
private static async Task<(List<PackageUpdateResult> vulnerablePackages, HashSet<string> packagesScanned)> SelectVulnerablePackagesToUpdateAsync(
IReadOnlyList<PackageWithVersionRange>? packages,
DependencyGraphSpec dgSpec,
string projectPath,
ILoggerWithColor logger,
IPackageUpdateIO packageUpdateIO,
CancellationToken cancellationToken)
{
LockFile assetsFile = await packageUpdateIO.GetProjectAssetsFileAsync(dgSpec, projectPath, logger, cancellationToken);
PackageSpec projectSpec = assetsFile.PackageSpec;
bool auditModeAll = IsNuGetAuditModeSetToAll(projectSpec);
if (!auditModeAll)
{
logger.LogWarning(Strings.PackageUpdate_AuditModeIsDirect);
}
// Log messages don't have package version in a usable way, so we have to first find the list of package ids,
// then check each TFM's package list against that list.
HashSet<string> packageIdsWithVulnerabilities = assetsFile
.LogMessages
.Where(log => log.Code >= NuGetLogCode.NU1901 && log.Code <= NuGetLogCode.NU1904 && !string.IsNullOrEmpty(log.LibraryId))
.Select(log => log.LibraryId!)
.Where(id => packages is null || packages.Count == 0 || packages.Any(p => string.Equals(p.Id, id, StringComparison.OrdinalIgnoreCase)))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
HashSet<string> scannedPackages =
auditModeAll
? assetsFile.Libraries
.Where(l => l.Type == "package" && (packages is null || packages.Count == 0 || packages.Any(p => string.Equals(p.Id, l.Name, StringComparison.OrdinalIgnoreCase))))
.Select(l => l.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase)
: assetsFile.PackageSpec.TargetFrameworks
.SelectMany(tfm => tfm.Dependencies)
.Where(d => d.LibraryRange.TypeConstraint == LibraryDependencyTarget.Package && (packages is null || packages.Count == 0 || packages.Any(p => string.Equals(p.Id, d.Name, StringComparison.OrdinalIgnoreCase))))
.Select(d => d.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var packagesToUpdateResult = new List<PackageUpdateResult>();
if (packageIdsWithVulnerabilities.Count > 0)
{
var remappedLogger = new RemappedLevelLogger(
logger,
new RemappedLevelLogger.Mapping
{
Information = LogLevel.Verbose,
});
IReadOnlyList<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> knownVulnerabilities =
await packageUpdateIO.GetKnownVulnerabilitiesAsync(remappedLogger, cancellationToken);
List<(PackageIdentity package, List<string> targetFrameworkAliases)> packagesToUpdate = assetsFile
.Targets
.SelectMany(tf => tf.Libraries.Select(library => (tf.TargetFramework, library)))
.Where(tuple => tuple.library.Type == "package" && packageIdsWithVulnerabilities.Contains(tuple.library.Name!) && PackageHasVulnerability(tuple.library.Name!, tuple.library.Version!, knownVulnerabilities))
.GroupBy(
pair => new PackageIdentity(pair.library.Name!, pair.library.Version),
pair => assetsFile.PackageSpec.TargetFrameworks.Single(tfm => tfm.FrameworkName == pair.TargetFramework).TargetAlias,
(key, g) => (key, g.Distinct().ToList()))
.ToList();
PackageSourceMapping sourceMapping = packageUpdateIO.GetPackageSourceMapping();
foreach (var (packageIdentity, tfmAliases) in packagesToUpdate)
{
IReadOnlyList<string>? mappedSources = sourceMapping.IsEnabled ? sourceMapping.GetConfiguredPackageSources(packageIdentity.Id) : null;
if (mappedSources is not null && mappedSources.Count == 0)
{
logger.LogError(Messages.Error_PackageSourceMappingNotFound(packageIdentity.Id));
continue;
}
var nonVulnerableVersion = await packageUpdateIO.GetNonVulnerableAsync(packageIdentity.Id, mappedSources, packageIdentity.Version, NullLogger.Instance, knownVulnerabilities, cancellationToken);
if (nonVulnerableVersion is null)
{
logger.LogMinimal(Format.PackageUpdate_AllVersionsHaveAdvisories(packageIdentity.Id), ConsoleColor.Yellow);
}
else
{
packagesToUpdateResult.Add(new PackageUpdateResult
{
Package = new PackageToUpdate
{
Id = packageIdentity.Id,
CurrentVersion = new VersionRange(packageIdentity.Version),
NewVersion = VersionRange.Parse(nonVulnerableVersion.OriginalVersion!)
},
TargetFrameworkAliases = tfmAliases
});
}
}
}
return (packagesToUpdateResult, scannedPackages);
bool PackageHasVulnerability(string packageId, NuGetVersion version, IReadOnlyList<IReadOnlyDictionary<string, IReadOnlyList<PackageVulnerabilityInfo>>> knownVulnerabilities)
{
if (version is null)
{
throw new ArgumentException("Package version must be specified when checking for vulnerabilities.");
}
foreach (var vulnDict in knownVulnerabilities)
{
if (vulnDict.TryGetValue(packageId, out var vulnInfoList))
{
foreach (var vulnInfo in vulnInfoList)
{
if (vulnInfo.Versions.Satisfies(version))
{
return true;
}
}
}
}
return false;
}
}
private static async Task<(int? exitCode, Dictionary<string, List<PackageUpdateResult>> projectPackageUpdates, int totalPackagesScanned)>
SelectPackagesToUpdateAsync(
PackageUpdateArgs args,
DependencyGraphSpec dgSpec,
ILoggerWithColor logger,
IPackageUpdateIO packageUpdateIO,
CancellationToken cancellationToken)
{
bool noPackagesSpecified = args.Packages is null || args.Packages.Count == 0;
var projectPackageUpdates = new Dictionary<string, List<PackageUpdateResult>>(StringComparer.OrdinalIgnoreCase);
int? exitCode;
int totalPackagesScanned;
if (args.Vulnerable)
{
foreach (var projectPath in dgSpec.Restore)
{
PackageSpec projectSpec = dgSpec.GetProjectSpec(projectPath);
if (!NuGetAuditEnabled(projectSpec))
{
logger.LogError(Strings.PackageUpdate_AuditDisabled);
return (ExitCodes.InvalidArgs, projectPackageUpdates, 0);
}
}
(exitCode, totalPackagesScanned) = await ProcessProjectsInParallelAsync(
dgSpec,
projectPackageUpdates,
async (projectPath, ct) =>
{
(List<PackageUpdateResult> packagesToUpdate, HashSet<string> scannedPackages) =
await SelectVulnerablePackagesToUpdateAsync(args.Packages, dgSpec, projectPath, logger, packageUpdateIO, ct);
return (packagesToUpdate, scannedPackages, null);
},
cancellationToken);
if (exitCode.HasValue)
{
return (exitCode.Value, projectPackageUpdates, totalPackagesScanned);
}
}
else if (noPackagesSpecified)
{
(exitCode, totalPackagesScanned) = await ProcessProjectsInParallelAsync(
dgSpec,
projectPackageUpdates,
async (projectPath, ct) =>
{
PackageSpec projectSpec = dgSpec.GetProjectSpec(projectPath);
(List<PackageUpdateResult>? packagesToUpdate, HashSet<string> scannedPackages) =
await SelectAllPackagesWithUpdatesAsync(projectSpec, logger, packageUpdateIO, ct);
// SelectAllPackagesWithUpdatesAsync logs the error when returning null
int? errorCode = packagesToUpdate is null ? ExitCodes.Error : null;
return (packagesToUpdate, scannedPackages, errorCode);
},
cancellationToken);
if (exitCode.HasValue)
{
return (exitCode.Value, projectPackageUpdates, totalPackagesScanned);
}
}
else
{
(exitCode, totalPackagesScanned) = await ProcessProjectsInParallelAsync(
dgSpec,
projectPackageUpdates,
async (projectPath, ct) =>
{
PackageSpec projectSpec = dgSpec.GetProjectSpec(projectPath);
(List<PackageUpdateResult>? packagesToUpdate, HashSet<string> scannedPackages) =
await SelectSpecificPackagesToUpdateAsync(args.Packages!, projectSpec, logger, packageUpdateIO, ct);
int? errorCode = packagesToUpdate is null ? ExitCodes.Error : null;
return (packagesToUpdate, scannedPackages, errorCode);
},
cancellationToken);
if (exitCode.HasValue)
{
return (exitCode.Value, projectPackageUpdates, totalPackagesScanned);
}
}
// Check if any packages were found to update
if (projectPackageUpdates.Count == 0)
{
if (args.Vulnerable)
{
logger.LogMinimal(Strings.PackageUpdate_NoVulnerablePackages, ConsoleColor.Green);
return (ExitCodes.Success, projectPackageUpdates, totalPackagesScanned);
}
else
{
logger.LogMinimal(Strings.PackageUpdate_AlreadyUpToDate, ConsoleColor.Green);
return (ExitCodes.NoPackagesNeedUpdating, projectPackageUpdates, totalPackagesScanned);
}
}
return (null, projectPackageUpdates, totalPackagesScanned);
}
private static async Task<(int? exitCode, int totalPackagesScanned)> ProcessProjectsInParallelAsync(
DependencyGraphSpec dgSpec,
Dictionary<string, List<PackageUpdateResult>> projectPackageUpdates,
Func<string, CancellationToken, Task<(List<PackageUpdateResult>? packagesToUpdate, HashSet<string> scannedPackages, int? errorExitCode)>> processProject,
CancellationToken cancellationToken)
{
var scannedPackages = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var lockObject = new object();
int? exitCode = null;
var parallelOptions = new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount,
CancellationToken = cancellationToken
};
await Parallel.ForEachAsync(dgSpec.Restore, parallelOptions, async (projectPath, ct) =>
{
(List<PackageUpdateResult>? packagesToUpdate, HashSet<string> projectScannedPackages, int? errorExitCode) =
await processProject(projectPath, ct);
lock (lockObject)
{
if (errorExitCode.HasValue)
{
exitCode = errorExitCode.Value;
return;
}
scannedPackages.UnionWith(projectScannedPackages);
if (packagesToUpdate is not null && packagesToUpdate.Count > 0)
{
projectPackageUpdates[projectPath] = packagesToUpdate;
}
}
});
return (exitCode, scannedPackages.Count);
}
internal static async Task<(List<PackageUpdateResult>?, HashSet<string> scannedPackages)> SelectSpecificPackagesToUpdateAsync(
IReadOnlyList<PackageWithVersionRange> packages,
PackageSpec project,
ILoggerWithColor logger,
IPackageUpdateIO packageUpdateIO,
CancellationToken cancellationToken)
{
if (packages is null || packages.Count == 0)
{
throw new ArgumentException(Strings.ArgumentNullOrEmpty, nameof(packages));
}
var sourceMapping = packageUpdateIO.GetPackageSourceMapping();
var packagesToUpdate = new List<PackageUpdateResult>();
var scannedPackages = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
bool hasErrors = false;
foreach (var package in packages)
{
IReadOnlyList<string>? mappedSources = sourceMapping.IsEnabled ? sourceMapping.GetConfiguredPackageSources(package.Id) : null;
if (mappedSources is not null && mappedSources.Count == 0)
{
logger.LogError(Messages.Error_PackageSourceMappingNotFound(package.Id));
hasErrors = true;
continue;
}
var (existingVersion, packageTfms) = GetReferencedVersion(package.Id, project, logger);
if (existingVersion is null)
{
if (packageTfms.Count > 0)
{
hasErrors = true;
}
continue;
}
scannedPackages.Add(package.Id);
VersionRange upgradeVersion;
if (package.VersionRange is not null)
{
upgradeVersion = package.VersionRange;
if (upgradeVersion == existingVersion)
{
logger.LogMinimal(Messages.Warning_AlreadyUsingSameVersion(package.Id, upgradeVersion.OriginalString!), ConsoleColor.Yellow);
continue;
}
}
else
{
bool usePrerelease = existingVersion.HasLowerBound && existingVersion.MinVersion.IsPrerelease;
var latestVersion = await packageUpdateIO.GetLatestVersionAsync(package.Id, usePrerelease, mappedSources, NullLogger.Instance, cancellationToken);
if (latestVersion is null)
{
logger.LogMinimal(Messages.Error_NoVersionsAvailable(package.Id), ConsoleColor.Red);
hasErrors = true;
continue;
}
upgradeVersion = VersionRange.Parse(latestVersion.OriginalVersion!);
if (upgradeVersion == existingVersion)
{
logger.LogMinimal(Messages.Warning_AlreadyHighestVersion(package.Id, latestVersion.OriginalVersion!, project.FilePath), ConsoleColor.Yellow);
continue;
}
}
var packageToUpdate = new PackageToUpdate
{
Id = package.Id,
CurrentVersion = existingVersion,
NewVersion = upgradeVersion
};
packagesToUpdate.Add(new PackageUpdateResult
{
Package = packageToUpdate,
TargetFrameworkAliases = packageTfms
});
}
return (hasErrors ? null : packagesToUpdate, scannedPackages);
}
/// <summary>Gets the package's referenced version range and TFMs which it's referenced in.</summary>
/// <param name="packageId">The package identifier to find.</param>
/// <param name="project">The project's restore inputs.</param>
/// <param name="logger">The logger to output errors.</param>
/// <returns>
/// <para>When the package is found and no problems occur, it returns the requested <see cref="VersionRange"/>
/// and list of target framework aliases that the package is used in.</para>
/// <para>If the <see cref="VersionRange"/> is null and the target framework list contains at least one value,
/// then there /// was an error, which the method will have logged. Note that the target framework list
/// may not be complete.</para>
/// <para>When the version range is null and the target framework list is empty, it means that the project
/// does not reference the package (and no error was logged).</para>
/// </returns>
private static (VersionRange? version, List<string> targetFrameworks)
GetReferencedVersion(string packageId, PackageSpec project, ILoggerWithColor logger)
{
VersionRange? existingVersion = null;
List<string> frameworks = new();
foreach (var tfm in project.TargetFrameworks)
{
foreach (var dependency in tfm.Dependencies)
{
if (string.Equals(packageId, dependency.Name, StringComparison.OrdinalIgnoreCase))
{
frameworks.Add(tfm.TargetAlias);
if (dependency.AutoReferenced)
{
logger.LogMinimal(
Messages.Error_CannotUpgradeAutoReferencedPackage(project.FilePath, packageId),
ConsoleColor.Red);
return (null, []);
}
VersionRange tfmVersionRange;
if (project.RestoreMetadata.CentralPackageFloatingVersionsEnabled)
{
if (!tfm.CentralPackageVersions.TryGetValue(
packageId,
out CentralPackageVersion? centralVersion))
{
logger.LogMinimal(
Messages.Error_CouldNotFindPackageVersionForCpmPackage(packageId),
ConsoleColor.Red);
return (null, []);
}
tfmVersionRange = centralVersion.VersionRange;
}
else
{
tfmVersionRange = dependency.LibraryRange.VersionRange ?? VersionRange.All;
}
if (existingVersion == null)
{
existingVersion = tfmVersionRange;
}
else
{
if (tfmVersionRange != existingVersion)
{
logger.LogMinimal(
Messages.Unsupported_UpdatePackageWithDifferentPerTfmVersions(packageId, project.FilePath),
ConsoleColor.Red);
return (null, []);
}
}
}
}
}
return (existingVersion, frameworks);
}
internal static async Task<(List<PackageUpdateResult>? packagesToUpdate, HashSet<string> scannedPackages)> SelectAllPackagesWithUpdatesAsync(
PackageSpec project,
ILoggerWithColor logger,
IPackageUpdateIO packageUpdateIO,
CancellationToken cancellationToken)
{
var allProjectPackages = GetAllPackagesReferencedByProject(project, logger);
if (allProjectPackages is null)
{
return (null, new HashSet<string>(StringComparer.OrdinalIgnoreCase));
}
var sourceMapping = packageUpdateIO.GetPackageSourceMapping();
var packagesToUpdate = new List<PackageUpdateResult>();
var scannedPackages = new HashSet<string>(allProjectPackages.Select(p => p.identity.Id), StringComparer.OrdinalIgnoreCase);
bool successful = true;
foreach (var package in allProjectPackages)
{
IReadOnlyList<string>? mappedSources = sourceMapping.IsEnabled ? sourceMapping.GetConfiguredPackageSources(package.identity.Id) : null;
if (mappedSources is not null && mappedSources.Count == 0)
{
logger.LogError(Messages.Error_PackageSourceMappingNotFound(package.identity.Id));
successful = false;
continue;
}
// package.identity.VersionRange is the project's referenced version.
Debug.Assert(package.identity.VersionRange != null);
bool usePrerelease = package.identity.VersionRange.HasLowerBound && package.identity.VersionRange.MinVersion.IsPrerelease;
var latestVersion = await packageUpdateIO.GetLatestVersionAsync(package.identity.Id, usePrerelease, mappedSources, NullLogger.Instance, cancellationToken);
if (latestVersion is null)
{
logger.LogMinimal(Messages.Error_NoVersionsAvailable(package.identity.Id), ConsoleColor.Red);
successful = false;
continue;
}
var upgradeVersion = VersionRange.Parse(latestVersion.OriginalVersion!);
if (upgradeVersion.ToString() == package.identity.VersionRange.ToString())
{
// Already using the highest version.
continue;
}
var result = new PackageUpdateResult
{
Package = new PackageToUpdate
{
Id = package.identity.Id,
CurrentVersion = package.identity.VersionRange,
NewVersion = upgradeVersion
},
TargetFrameworkAliases = package.tfms
};
packagesToUpdate.Add(result);
}
return successful ? (packagesToUpdate, scannedPackages) : (null, scannedPackages);
}
private static List<(PackageWithVersionRange identity, List<string> tfms)>? GetAllPackagesReferencedByProject(PackageSpec project, ILoggerWithColor logger)
{
var allPackages = new Dictionary<string, (VersionRange version, List<string> tfms, bool hasError)>(StringComparer.OrdinalIgnoreCase);
bool hasErrors = false;
foreach (var tfm in project.TargetFrameworks)
{
foreach (var dependency in tfm.Dependencies)
{
if (!string.IsNullOrEmpty(dependency.Name) && !dependency.AutoReferenced)
{
if (allPackages.TryGetValue(dependency.Name, out var existing))
{
if (existing.hasError)
{
continue;
}
if (existing.Item1 != dependency.LibraryRange.VersionRange)
{
logger.LogMinimal(
Messages.Unsupported_UpdatePackageWithDifferentPerTfmVersions(dependency.Name, project.FilePath),
ConsoleColor.Red);
hasErrors = true;
allPackages[dependency.Name] = (existing.version, existing.tfms, hasError: true);
}
existing.tfms.Add(tfm.TargetAlias);
}
else
{
VersionRange version = dependency.LibraryRange.VersionRange ?? VersionRange.All;
List<string> tfms = [tfm.TargetAlias];
allPackages[dependency.Name] = (version, tfms, hasError: false);
}
}
}
}
if (hasErrors)
{
return null;
}
List<(PackageWithVersionRange package, List<string> tfms)> result = new(allPackages.Count);
foreach (var kvp in allPackages)
{
var package = new PackageWithVersionRange { Id = kvp.Key, VersionRange = kvp.Value.version };
result.Add((package, kvp.Value.tfms));
}
return result;
}
private static DependencyGraphSpec GetUpdatedDependencyGraphSpec(DependencyGraphSpec currentDgSpec, Dictionary<string, List<PackageUpdateResult>> projectPackageUpdates)
{
var updatedDgSpec = new DependencyGraphSpec();
// Add all projects from the current dgSpec, replacing those with updates
foreach (var project in currentDgSpec.Projects)
{
if (projectPackageUpdates.TryGetValue(project.FilePath, out var packagesToUpdate))
{
// Clone and update this project
var updatedPackageSpec = project.Clone();
foreach (var packageResult in packagesToUpdate)
{
PackageDependency packageDependency = new PackageDependency(packageResult.Package.Id, packageResult.Package.NewVersion);
PackageSpecOperations.AddOrUpdateDependency(updatedPackageSpec, packageDependency);
}
updatedDgSpec.AddProject(updatedPackageSpec);
}
else
{
// Keep the original project unchanged
updatedDgSpec.AddProject(project);
}
}
// Add restore entries for all projects from the current dgSpec
foreach (var restorePath in currentDgSpec.Restore)
{
updatedDgSpec.AddRestore(restorePath);
}
return updatedDgSpec;
}
private static bool NuGetAuditEnabled(PackageSpec projectSpec) =>
bool.TryParse(projectSpec?.RestoreMetadata?.RestoreAuditProperties?.EnableAudit, out bool result)
? result
: true;
private static bool IsNuGetAuditModeSetToAll(PackageSpec projectSpec) =>
string.Equals(projectSpec?.RestoreMetadata?.RestoreAuditProperties?.AuditMode, "all", StringComparison.OrdinalIgnoreCase);
internal record PackageToUpdate
{
public required string Id { get; init; }
public required VersionRange CurrentVersion { get; init; }
public required VersionRange NewVersion { get; init; }
}
internal record PackageUpdateResult
{
public required PackageToUpdate Package { get; init; }
public required List<string> TargetFrameworkAliases { get; init; }
}
private static class Format
{
internal static string PackageUpdate_UpdatingOutdatedPackages(string projectPath)
{
return string.Format(CultureInfo.CurrentCulture, Strings.PackageUpdate_UpdatingOutdatedPackages, projectPath);
}
internal static string PackageUpdate_UpdatingVulnerablePackages(string projectPath)
{
return string.Format(CultureInfo.CurrentCulture, Strings.PackageUpdate_UpdatingVulnerablePackages, projectPath);
}
internal static string PackageUpdate_UpdatedMessage(string packageId, string currentVersion, string newVersion)
{
return string.Format(CultureInfo.CurrentCulture, Strings.PackageUpdate_UpdatedMessage, packageId, currentVersion, newVersion);
}
internal static string PackageUpdate_FinalSummary(int updatedCount, int scannedCount)
{
return string.Format(CultureInfo.CurrentCulture, Strings.PackageUpdate_FinalSummary, updatedCount, scannedCount);
}
internal static string PackageUpdate_AllVersionsHaveAdvisories(string packageId)
{
return string.Format(CultureInfo.CurrentCulture, Strings.PackageUpdate_AllVersionsHaveAdvisories, packageId);
}
}
// These exit codes are documented, so consider changing them or adding new ones a breaking change.
internal static class ExitCodes
{
public const int Success = 0;
// System.CommandLine returns 1 on parse ererors, so even if this const isn't used, the value 1 is still returned.
public const int InvalidArgs = 1;
public const int NoPackagesNeedUpdating = 2;
public const int Error = 3;
}
}
|