aspnetcore/tools/PinVersions/PinVersionUtility.cs

208 lines
9.0 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using NuGet.Common;
using NuGet.Frameworks;
using NuGet.LibraryModel;
using NuGet.ProjectModel;
using NuGet.Protocol;
using NuGet.Protocol.Core.Types;
using NuGet.Versioning;
using UniverseTools;
namespace PinVersions
{
class PinVersionUtility
{
private readonly string _repositoryRoot;
private readonly FindPackageByIdResource[] _findPackageResources;
private readonly ConcurrentDictionary<string, Task<NuGetVersion>> _exactMatches = new ConcurrentDictionary<string, Task<NuGetVersion>>(StringComparer.OrdinalIgnoreCase);
private readonly DependencyGraphSpecProvider _provider;
private readonly SourceCacheContext _sourceCacheContext;
public PinVersionUtility(string repositoryRoot, List<string> pinSources, DependencyGraphSpecProvider provider)
{
_repositoryRoot = repositoryRoot;
_findPackageResources = new FindPackageByIdResource[pinSources.Count];
for (var i = 0; i < pinSources.Count; i++)
{
var repository = FactoryExtensionsV3.GetCoreV3(Repository.Factory, pinSources[i].Trim());
_findPackageResources[i] = repository.GetResource<FindPackageByIdResource>();
}
_provider = provider;
_sourceCacheContext = new SourceCacheContext();
}
public void Execute()
{
var solutionPinMetadata = GetPinVersionMetadata();
foreach (var item in solutionPinMetadata)
{
var projectPinMetadata = item.Value;
var specProject = projectPinMetadata.PackageSpec;
if (!(projectPinMetadata.Packages.Any() || projectPinMetadata.CLIToolReferences.Any()))
{
Console.WriteLine($"No package or tool references to pin for {specProject.FilePath}.");
continue;
}
var projectFileInfo = new FileInfo(specProject.FilePath);
var pinnedReferencesFile = Path.Combine(
specProject.RestoreMetadata.OutputPath,
projectFileInfo.Name + ".pinnedversions.targets");
Directory.CreateDirectory(Path.GetDirectoryName(pinnedReferencesFile));
Console.WriteLine($"Pinning package versions for {specProject.FilePath}.");
var pinnedReferences = new XElement("ItemGroup");
foreach (var packageReference in projectPinMetadata.Packages)
{
(var tfm, var libraryRange, var exactVersion) = packageReference;
Console.WriteLine($"Pinning reference {libraryRange.Name}({libraryRange.VersionRange} to {exactVersion}.");
var metadata = new List<XAttribute>
{
new XAttribute("Update", libraryRange.Name),
new XAttribute("Version", exactVersion.ToNormalizedString()),
};
if (tfm != NuGetFramework.AnyFramework)
{
metadata.Add(new XAttribute("Condition", $"'$(TargetFramework)'=='{tfm.GetShortFolderName()}'"));
}
pinnedReferences.Add(new XElement("PackageReference", metadata));
}
foreach (var toolReference in projectPinMetadata.CLIToolReferences)
{
(var libraryRange, var exactVersion) = toolReference;
Console.WriteLine($"Pinning CLI Tool {libraryRange.Name}({libraryRange.VersionRange} to {exactVersion}.");
var metadata = new List<XAttribute>
{
new XAttribute("Update", libraryRange.Name),
new XAttribute("Version", exactVersion.ToNormalizedString()),
};
pinnedReferences.Add(new XElement("DotNetCliToolReference", metadata));
}
var pinnedVersionRoot = new XElement("Project", pinnedReferences);
File.WriteAllText(pinnedReferencesFile, pinnedVersionRoot.ToString());
}
}
private IDictionary<string, PinVersionMetadata> GetPinVersionMetadata()
{
var repositoryDirectoryInfo = new DirectoryInfo(_repositoryRoot);
var projects = new Dictionary<string, PinVersionMetadata>(StringComparer.OrdinalIgnoreCase);
foreach (var slnFile in repositoryDirectoryInfo.EnumerateFiles("*.sln"))
{
var graphSpec = _provider.GetDependencyGraphSpec(repositoryDirectoryInfo.Name, slnFile.FullName);
foreach (var specProject in graphSpec.Projects)
{
if (!projects.TryGetValue(specProject.FilePath, out var pinMetadata))
{
pinMetadata = new PinVersionMetadata(specProject);
projects[specProject.FilePath] = pinMetadata;
}
var allDependencies = specProject.Dependencies.Select(dependency => new { Dependency = dependency, FrameworkName = NuGetFramework.AnyFramework })
.Concat(specProject.TargetFrameworks.SelectMany(tfm => tfm.Dependencies.Select(dependency => new { Dependency = dependency, tfm.FrameworkName })))
.Where(d => d.Dependency.LibraryRange.TypeConstraintAllows(LibraryDependencyTarget.Package));
foreach (var dependency in allDependencies)
{
var reference = dependency.Dependency;
var versionRange = reference.LibraryRange.VersionRange;
if (!versionRange.IsFloating)
{
continue;
}
var exactVersion = GetExactVersion(reference.Name, versionRange);
if (exactVersion == null)
{
continue;
}
var projectStyle = specProject.RestoreMetadata.ProjectStyle;
if (projectStyle == ProjectStyle.PackageReference)
{
pinMetadata.Packages.Add((dependency.FrameworkName, reference.LibraryRange, exactVersion));
}
else if (projectStyle == ProjectStyle.DotnetCliTool)
{
pinMetadata.CLIToolReferences.Add((reference.LibraryRange, exactVersion));
}
else
{
throw new NotSupportedException($"Unknown project style '{projectStyle}'.");
}
}
}
}
return projects;
}
private NuGetVersion GetExactVersion(string name, VersionRange range)
{
if (range.MinVersion == null)
{
throw new Exception($"Unsupported version range {range}.");
}
if (!_exactMatches.TryGetValue(name, out var versionTask))
{
versionTask = _exactMatches.GetOrAdd(name, GetExactVersionAsync(name, range.MinVersion));
}
return versionTask.Result;
}
private async Task<NuGetVersion> GetExactVersionAsync(string name, NuGetVersion floatingVersion)
{
foreach (var findPackageResource in _findPackageResources)
{
var packageVersions = await findPackageResource.GetAllVersionsAsync(name, _sourceCacheContext, NullLogger.Instance, default(CancellationToken));
var matchingVersions = packageVersions.Where(v => v.Version == floatingVersion.Version).ToList();
switch (matchingVersions.Count)
{
case 0:
continue;
case 1:
return matchingVersions[0];
default:
throw new Exception($"More than one version for {name} found that matches the specified version constraint: {string.Join(" ", matchingVersions)}.");
}
}
return null;
}
private struct PinVersionMetadata
{
public PinVersionMetadata(PackageSpec packageSpec)
{
PackageSpec = packageSpec;
Packages = new List<(NuGetFramework, LibraryRange, NuGetVersion)>();
CLIToolReferences = new List<(LibraryRange, NuGetVersion)>();
}
public PackageSpec PackageSpec { get; }
public List<(NuGetFramework, LibraryRange, NuGetVersion)> Packages { get; }
public List<(LibraryRange, NuGetVersion)> CLIToolReferences { get; }
}
}
}