// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using System.Xml.Linq; using Microsoft.Extensions.CommandLineUtils; using NuGet.Common; using NuGet.Configuration; using NuGet.Packaging; using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Versioning; namespace PackageBaselineGenerator; /// /// This generates Baseline.props with information about the last RTM release. /// class Program : CommandLineApplication { static void Main(string[] args) { new Program().Execute(args); } private readonly CommandOption _sources; private readonly CommandOption _output; private readonly CommandOption _update; private static readonly string[] _defaultSources = new string[] { "https://api.nuget.org/v3/index.json" }; public Program() { _sources = Option( "-s|--package-sources ", "The NuGet source(s) of packages to fetch", CommandOptionType.MultipleValue); _output = Option("-o|--output ", "The generated file output path", CommandOptionType.SingleValue); _update = Option("-u|--update", "Regenerate the input (Baseline.xml) file.", CommandOptionType.NoValue); Invoke = () => Run().GetAwaiter().GetResult(); } private async Task Run() { if (_output.HasValue() && _update.HasValue()) { await Error.WriteLineAsync("'--output' and '--update' options must not be used together."); return 1; } var inputPath = Path.Combine(Directory.GetCurrentDirectory(), "Baseline.xml"); var input = XDocument.Load(inputPath); var sources = _sources.HasValue() ? _sources.Values.Select(s => s.TrimEnd('/')) : _defaultSources; var packageSources = sources.Select(s => new PackageSource(s)); var providers = Repository.Provider.GetCoreV3(); // Get v2 and v3 API support var sourceRepositories = packageSources.Select(ps => new SourceRepository(ps, providers)); if (_update.HasValue()) { var updateResult = await RunUpdateAsync(inputPath, input, sourceRepositories); if (updateResult != 0) { return updateResult; } } List<(string packageBase, bool feedV3)> packageBases = new List<(string, bool)>(); foreach (var sourceRepository in sourceRepositories) { var feedType = await sourceRepository.GetFeedType(CancellationToken.None); var feedV3 = feedType == FeedType.HttpV3; var packageBase = sourceRepository.PackageSource + "/package"; if (feedV3) { var resources = await sourceRepository.GetResourceAsync(); packageBase = resources.GetServiceEntryUri(ServiceTypes.PackageBaseAddress).ToString().TrimEnd('/'); } packageBases.Add((packageBase, feedV3)); } var output = _output.HasValue() ? _output.Value() : Path.Combine(Directory.GetCurrentDirectory(), "Baseline.Designer.props"); var packageCache = Environment.GetEnvironmentVariable("NUGET_PACKAGES") ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages"); var tempDir = Path.Combine(Directory.GetCurrentDirectory(), "obj", "tmp"); Directory.CreateDirectory(tempDir); var baselineVersion = input.Root.Attribute("Version").Value; // Baseline and .NET Core versions always align in non-preview releases. var parsedVersion = Version.Parse(baselineVersion); var defaultTarget = ((parsedVersion.Major < 5) ? "netcoreapp" : "net") + $"{parsedVersion.Major}.{parsedVersion.Minor}"; var doc = new XDocument( new XComment(" Auto generated. Do not edit manually, use eng/tools/BaselineGenerator/ to recreate. "), new XElement("Project", new XElement("PropertyGroup", new XElement("MSBuildAllProjects", "$(MSBuildAllProjects);$(MSBuildThisFileFullPath)"), new XElement("AspNetCoreBaselineVersion", baselineVersion)))); var client = new HttpClient(); foreach (var pkg in input.Root.Descendants("Package")) { var id = pkg.Attribute("Id").Value; var version = pkg.Attribute("Version").Value; var packageFileName = $"{id}.{version}.nupkg"; var nupkgPath = Path.Combine(packageCache, id.ToLowerInvariant(), version, packageFileName); if (!File.Exists(nupkgPath)) { nupkgPath = Path.Combine(tempDir, packageFileName); } if (!File.Exists(nupkgPath)) { foreach ((string packageBase, bool feedV3) in packageBases) { var url = feedV3 ? $"{packageBase}/{id.ToLowerInvariant()}/{version}/{id.ToLowerInvariant()}.{version}.nupkg" : $"{packageBase}/{id}/{version}"; Console.WriteLine($"Downloading {url}"); try { using (var response = await client.GetStreamAsync(url)) { using (var file = File.Create(nupkgPath)) { await response.CopyToAsync(file); } } } catch (HttpRequestException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound) { // If it's not found, continue onto the next one. continue; } } if (!File.Exists(nupkgPath)) { throw new Exception($"Could not download package {id} @ {version} using any input feed"); } } using (var reader = new PackageArchiveReader(nupkgPath)) { doc.Root.Add(new XComment($" Package: {id}")); var propertyGroup = new XElement( "PropertyGroup", new XAttribute("Condition", $" '$(PackageId)' == '{id}' "), new XElement("BaselinePackageVersion", version)); doc.Root.Add(propertyGroup); foreach (var group in reader.NuspecReader.GetDependencyGroups()) { // Don't bother generating empty ItemGroup elements. if (!group.Packages.Any()) { continue; } // Handle changes to $(DefaultNetCoreTargetFramework) even if some projects are held back. var targetCondition = $"'$(TargetFramework)' == '{group.TargetFramework.GetShortFolderName()}'"; if (string.Equals( group.TargetFramework.GetShortFolderName(), defaultTarget, StringComparison.OrdinalIgnoreCase)) { targetCondition = $"('$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' OR '$(TargetFramework)' == '{defaultTarget}')"; } var itemGroup = new XElement( "ItemGroup", new XAttribute("Condition", $" '$(PackageId)' == '{id}' AND {targetCondition} ")); doc.Root.Add(itemGroup); foreach (var dependency in group.Packages) { itemGroup.Add( new XElement("BaselinePackageReference", new XAttribute("Include", dependency.Id), new XAttribute("Version", dependency.VersionRange.ToString()))); } } } } var settings = new XmlWriterSettings { OmitXmlDeclaration = true, Encoding = Encoding.UTF8, Indent = true, }; using (var writer = XmlWriter.Create(output, settings)) { doc.Save(writer); } Console.WriteLine($"Generated file in {output}"); return 0; } private async Task RunUpdateAsync( string documentPath, XDocument document, IEnumerable sourceRepositories) { var packageMetadataResources = await Task.WhenAll(sourceRepositories.Select(async sr => await sr.GetResourceAsync())); var logger = new Logger(Error, Out); var hasChanged = false; using (var cacheContext = new SourceCacheContext { NoCache = true }) { var versionAttribute = document.Root.Attribute("Version"); hasChanged = await TryUpdateVersionAsync( versionAttribute, "Microsoft.AspNetCore.App.Runtime.win-x64", packageMetadataResources, logger, cacheContext); foreach (var package in document.Root.Descendants("Package")) { var id = package.Attribute("Id").Value; versionAttribute = package.Attribute("Version"); var attributeChanged = await TryUpdateVersionAsync( versionAttribute, id, packageMetadataResources, logger, cacheContext); hasChanged |= attributeChanged; } } if (hasChanged) { await Out.WriteLineAsync($"Updating {documentPath}."); var settings = new XmlWriterSettings { Async = true, CheckCharacters = true, CloseOutput = false, Encoding = Encoding.UTF8, Indent = true, IndentChars = " ", NewLineOnAttributes = false, OmitXmlDeclaration = true, WriteEndDocumentOnClose = true, }; using (var stream = File.OpenWrite(documentPath)) { using (var writer = XmlWriter.Create(stream, settings)) { await document.SaveAsync(writer, CancellationToken.None); } } } else { await Out.WriteLineAsync("No new versions found"); } return 0; } private static async Task TryUpdateVersionAsync( XAttribute versionAttribute, string packageId, IEnumerable packageMetadataResources, ILogger logger, SourceCacheContext cacheContext) { var currentVersion = NuGetVersion.Parse(versionAttribute.Value); var versionRange = new VersionRange( currentVersion, new FloatRange(NuGetVersionFloatBehavior.Patch, currentVersion)); var searchMetadatas = await Task.WhenAll( packageMetadataResources.Select(async pmr => await pmr.GetMetadataAsync( packageId, includePrerelease: false, includeUnlisted: true, // Microsoft.AspNetCore.DataOrotection.Redis package is not listed. sourceCacheContext: cacheContext, log: logger, token: CancellationToken.None))); // Find the latest version among each search metadata NuGetVersion latestVersion = null; foreach (var searchMetadata in searchMetadatas) { var potentialLatestVersion = versionRange.FindBestMatch( searchMetadata.Select(metadata => metadata.Identity.Version)); if (latestVersion == null || (potentialLatestVersion != null && potentialLatestVersion.CompareTo(latestVersion) > 0)) { latestVersion = potentialLatestVersion; } } if (latestVersion == null) { logger.LogWarning($"Unable to find latest version of '{packageId}'."); return false; } var hasChanged = false; if (latestVersion != currentVersion) { hasChanged = true; versionAttribute.Value = latestVersion.ToNormalizedString(); } return hasChanged; } private class Logger : ILogger { private readonly TextWriter _error; private readonly TextWriter _out; public Logger(TextWriter error, TextWriter @out) { _error = error; _out = @out; } public void Log(LogLevel level, string data) { switch (level) { case LogLevel.Debug: LogDebug(data); break; case LogLevel.Error: LogError(data); break; case LogLevel.Information: LogInformation(data); break; case LogLevel.Minimal: LogMinimal(data); break; case LogLevel.Verbose: LogVerbose(data); break; case LogLevel.Warning: LogWarning(data); break; } } public void Log(ILogMessage message) => Log(message.Level, message.Message); public Task LogAsync(LogLevel level, string data) { Log(level, data); return Task.CompletedTask; } public Task LogAsync(ILogMessage message) => LogAsync(message.Level, message.Message); public void LogDebug(string data) => _out.WriteLine($"Debug: {data}"); public void LogError(string data) => _error.WriteLine($"Error: {data}"); public void LogInformation(string data) => _out.WriteLine($"Information: {data}"); public void LogInformationSummary(string data) => _out.WriteLine($"Summary: {data}"); public void LogMinimal(string data) => _out.WriteLine($"Minimal: {data}"); public void LogVerbose(string data) => _out.WriteLine($"Verbose: {data}"); public void LogWarning(string data) => _out.WriteLine($"Warning: {data}"); } }