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> _exactMatches = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); private readonly DependencyGraphSpecProvider _provider; private readonly SourceCacheContext _sourceCacheContext; public PinVersionUtility(string repositoryRoot, List 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(); } _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 { 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 { 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 GetPinVersionMetadata() { var repositoryDirectoryInfo = new DirectoryInfo(_repositoryRoot); var projects = new Dictionary(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 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; } } } }