Program.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. using System;
  4. using System.Collections.Generic;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Net.Http;
  8. using System.Text;
  9. using System.Threading;
  10. using System.Threading.Tasks;
  11. using System.Xml;
  12. using System.Xml.Linq;
  13. using Microsoft.Extensions.CommandLineUtils;
  14. using NuGet.Common;
  15. using NuGet.Configuration;
  16. using NuGet.Packaging;
  17. using NuGet.Protocol;
  18. using NuGet.Protocol.Core.Types;
  19. using NuGet.Versioning;
  20. namespace PackageBaselineGenerator;
  21. /// <summary>
  22. /// This generates Baseline.props with information about the last RTM release.
  23. /// </summary>
  24. class Program : CommandLineApplication
  25. {
  26. static void Main(string[] args)
  27. {
  28. new Program().Execute(args);
  29. }
  30. private readonly CommandOption _sources;
  31. private readonly CommandOption _output;
  32. private readonly CommandOption _update;
  33. private static readonly string[] _defaultSources = new string[] { "https://api.nuget.org/v3/index.json" };
  34. public Program()
  35. {
  36. _sources = Option(
  37. "-s|--package-sources <Sources>",
  38. "The NuGet source(s) of packages to fetch",
  39. CommandOptionType.MultipleValue);
  40. _output = Option("-o|--output <OUT>", "The generated file output path", CommandOptionType.SingleValue);
  41. _update = Option("-u|--update", "Regenerate the input (Baseline.xml) file.", CommandOptionType.NoValue);
  42. Invoke = () => Run().GetAwaiter().GetResult();
  43. }
  44. private async Task<int> Run()
  45. {
  46. if (_output.HasValue() && _update.HasValue())
  47. {
  48. await Error.WriteLineAsync("'--output' and '--update' options must not be used together.");
  49. return 1;
  50. }
  51. var inputPath = Path.Combine(Directory.GetCurrentDirectory(), "Baseline.xml");
  52. var input = XDocument.Load(inputPath);
  53. var sources = _sources.HasValue() ? _sources.Values.Select(s => s.TrimEnd('/')) : _defaultSources;
  54. var packageSources = sources.Select(s => new PackageSource(s));
  55. var providers = Repository.Provider.GetCoreV3(); // Get v2 and v3 API support
  56. var sourceRepositories = packageSources.Select(ps => new SourceRepository(ps, providers));
  57. if (_update.HasValue())
  58. {
  59. var updateResult = await RunUpdateAsync(inputPath, input, sourceRepositories);
  60. if (updateResult != 0)
  61. {
  62. return updateResult;
  63. }
  64. }
  65. List<(string packageBase, bool feedV3)> packageBases = new List<(string, bool)>();
  66. foreach (var sourceRepository in sourceRepositories)
  67. {
  68. var feedType = await sourceRepository.GetFeedType(CancellationToken.None);
  69. var feedV3 = feedType == FeedType.HttpV3;
  70. var packageBase = sourceRepository.PackageSource + "/package";
  71. if (feedV3)
  72. {
  73. var resources = await sourceRepository.GetResourceAsync<ServiceIndexResourceV3>();
  74. packageBase = resources.GetServiceEntryUri(ServiceTypes.PackageBaseAddress).ToString().TrimEnd('/');
  75. }
  76. packageBases.Add((packageBase, feedV3));
  77. }
  78. var output = _output.HasValue()
  79. ? _output.Value()
  80. : Path.Combine(Directory.GetCurrentDirectory(), "Baseline.Designer.props");
  81. var packageCache = Environment.GetEnvironmentVariable("NUGET_PACKAGES") ??
  82. Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "packages");
  83. var tempDir = Path.Combine(Directory.GetCurrentDirectory(), "obj", "tmp");
  84. Directory.CreateDirectory(tempDir);
  85. var baselineVersion = input.Root.Attribute("Version").Value;
  86. // Baseline and .NET Core versions always align in non-preview releases.
  87. var parsedVersion = Version.Parse(baselineVersion);
  88. var defaultTarget = ((parsedVersion.Major < 5) ? "netcoreapp" : "net") +
  89. $"{parsedVersion.Major}.{parsedVersion.Minor}";
  90. var doc = new XDocument(
  91. new XComment(" Auto generated. Do not edit manually, use eng/tools/BaselineGenerator/ to recreate. "),
  92. new XElement("Project",
  93. new XElement("PropertyGroup",
  94. new XElement("MSBuildAllProjects", "$(MSBuildAllProjects);$(MSBuildThisFileFullPath)"),
  95. new XElement("AspNetCoreBaselineVersion", baselineVersion))));
  96. var client = new HttpClient();
  97. foreach (var pkg in input.Root.Descendants("Package"))
  98. {
  99. var id = pkg.Attribute("Id").Value;
  100. var version = pkg.Attribute("Version").Value;
  101. var packageFileName = $"{id}.{version}.nupkg";
  102. var nupkgPath = Path.Combine(packageCache, id.ToLowerInvariant(), version, packageFileName);
  103. if (!File.Exists(nupkgPath))
  104. {
  105. nupkgPath = Path.Combine(tempDir, packageFileName);
  106. }
  107. if (!File.Exists(nupkgPath))
  108. {
  109. foreach ((string packageBase, bool feedV3) in packageBases)
  110. {
  111. var url = feedV3 ?
  112. $"{packageBase}/{id.ToLowerInvariant()}/{version}/{id.ToLowerInvariant()}.{version}.nupkg" :
  113. $"{packageBase}/{id}/{version}";
  114. Console.WriteLine($"Downloading {url}");
  115. try
  116. {
  117. using (var response = await client.GetStreamAsync(url))
  118. {
  119. using (var file = File.Create(nupkgPath))
  120. {
  121. await response.CopyToAsync(file);
  122. }
  123. }
  124. }
  125. catch (HttpRequestException e) when (e.StatusCode == System.Net.HttpStatusCode.NotFound)
  126. {
  127. // If it's not found, continue onto the next one.
  128. continue;
  129. }
  130. }
  131. if (!File.Exists(nupkgPath))
  132. {
  133. throw new Exception($"Could not download package {id} @ {version} using any input feed");
  134. }
  135. }
  136. using (var reader = new PackageArchiveReader(nupkgPath))
  137. {
  138. doc.Root.Add(new XComment($" Package: {id}"));
  139. var propertyGroup = new XElement(
  140. "PropertyGroup",
  141. new XAttribute("Condition", $" '$(PackageId)' == '{id}' "),
  142. new XElement("BaselinePackageVersion", version));
  143. doc.Root.Add(propertyGroup);
  144. foreach (var group in reader.NuspecReader.GetDependencyGroups())
  145. {
  146. // Don't bother generating empty ItemGroup elements.
  147. if (!group.Packages.Any())
  148. {
  149. continue;
  150. }
  151. // Handle changes to $(DefaultNetCoreTargetFramework) even if some projects are held back.
  152. var targetCondition = $"'$(TargetFramework)' == '{group.TargetFramework.GetShortFolderName()}'";
  153. if (string.Equals(
  154. group.TargetFramework.GetShortFolderName(),
  155. defaultTarget,
  156. StringComparison.OrdinalIgnoreCase))
  157. {
  158. targetCondition =
  159. $"('$(TargetFramework)' == '$(DefaultNetCoreTargetFramework)' OR '$(TargetFramework)' == '{defaultTarget}')";
  160. }
  161. var itemGroup = new XElement(
  162. "ItemGroup",
  163. new XAttribute("Condition", $" '$(PackageId)' == '{id}' AND {targetCondition} "));
  164. doc.Root.Add(itemGroup);
  165. foreach (var dependency in group.Packages)
  166. {
  167. itemGroup.Add(
  168. new XElement("BaselinePackageReference",
  169. new XAttribute("Include", dependency.Id),
  170. new XAttribute("Version", dependency.VersionRange.ToString())));
  171. }
  172. }
  173. }
  174. }
  175. var settings = new XmlWriterSettings
  176. {
  177. OmitXmlDeclaration = true,
  178. Encoding = Encoding.UTF8,
  179. Indent = true,
  180. };
  181. using (var writer = XmlWriter.Create(output, settings))
  182. {
  183. doc.Save(writer);
  184. }
  185. Console.WriteLine($"Generated file in {output}");
  186. return 0;
  187. }
  188. private async Task<int> RunUpdateAsync(
  189. string documentPath,
  190. XDocument document,
  191. IEnumerable<SourceRepository> sourceRepositories)
  192. {
  193. var packageMetadataResources = await Task.WhenAll(sourceRepositories.Select(async sr =>
  194. await sr.GetResourceAsync<PackageMetadataResource>()));
  195. var logger = new Logger(Error, Out);
  196. var hasChanged = false;
  197. using (var cacheContext = new SourceCacheContext { NoCache = true })
  198. {
  199. var versionAttribute = document.Root.Attribute("Version");
  200. hasChanged = await TryUpdateVersionAsync(
  201. versionAttribute,
  202. "Microsoft.AspNetCore.App.Runtime.win-x64",
  203. packageMetadataResources,
  204. logger,
  205. cacheContext);
  206. foreach (var package in document.Root.Descendants("Package"))
  207. {
  208. var id = package.Attribute("Id").Value;
  209. versionAttribute = package.Attribute("Version");
  210. var attributeChanged = await TryUpdateVersionAsync(
  211. versionAttribute,
  212. id,
  213. packageMetadataResources,
  214. logger,
  215. cacheContext);
  216. hasChanged |= attributeChanged;
  217. }
  218. }
  219. if (hasChanged)
  220. {
  221. await Out.WriteLineAsync($"Updating {documentPath}.");
  222. var settings = new XmlWriterSettings
  223. {
  224. Async = true,
  225. CheckCharacters = true,
  226. CloseOutput = false,
  227. Encoding = Encoding.UTF8,
  228. Indent = true,
  229. IndentChars = " ",
  230. NewLineOnAttributes = false,
  231. OmitXmlDeclaration = true,
  232. WriteEndDocumentOnClose = true,
  233. };
  234. using (var stream = File.OpenWrite(documentPath))
  235. {
  236. using (var writer = XmlWriter.Create(stream, settings))
  237. {
  238. await document.SaveAsync(writer, CancellationToken.None);
  239. }
  240. }
  241. }
  242. else
  243. {
  244. await Out.WriteLineAsync("No new versions found");
  245. }
  246. return 0;
  247. }
  248. private static async Task<bool> TryUpdateVersionAsync(
  249. XAttribute versionAttribute,
  250. string packageId,
  251. IEnumerable<PackageMetadataResource> packageMetadataResources,
  252. ILogger logger,
  253. SourceCacheContext cacheContext)
  254. {
  255. var currentVersion = NuGetVersion.Parse(versionAttribute.Value);
  256. var versionRange = new VersionRange(
  257. currentVersion,
  258. new FloatRange(NuGetVersionFloatBehavior.Patch, currentVersion));
  259. var searchMetadatas = await Task.WhenAll(
  260. packageMetadataResources.Select(async pmr => await pmr.GetMetadataAsync(
  261. packageId,
  262. includePrerelease: false,
  263. includeUnlisted: true, // Microsoft.AspNetCore.DataOrotection.Redis package is not listed.
  264. sourceCacheContext: cacheContext,
  265. log: logger,
  266. token: CancellationToken.None)));
  267. // Find the latest version among each search metadata
  268. NuGetVersion latestVersion = null;
  269. foreach (var searchMetadata in searchMetadatas)
  270. {
  271. var potentialLatestVersion = versionRange.FindBestMatch(
  272. searchMetadata.Select(metadata => metadata.Identity.Version));
  273. if (latestVersion == null ||
  274. (potentialLatestVersion != null && potentialLatestVersion.CompareTo(latestVersion) > 0))
  275. {
  276. latestVersion = potentialLatestVersion;
  277. }
  278. }
  279. if (latestVersion == null)
  280. {
  281. logger.LogWarning($"Unable to find latest version of '{packageId}'.");
  282. return false;
  283. }
  284. var hasChanged = false;
  285. if (latestVersion != currentVersion)
  286. {
  287. hasChanged = true;
  288. versionAttribute.Value = latestVersion.ToNormalizedString();
  289. }
  290. return hasChanged;
  291. }
  292. private class Logger : ILogger
  293. {
  294. private readonly TextWriter _error;
  295. private readonly TextWriter _out;
  296. public Logger(TextWriter error, TextWriter @out)
  297. {
  298. _error = error;
  299. _out = @out;
  300. }
  301. public void Log(LogLevel level, string data)
  302. {
  303. switch (level)
  304. {
  305. case LogLevel.Debug:
  306. LogDebug(data);
  307. break;
  308. case LogLevel.Error:
  309. LogError(data);
  310. break;
  311. case LogLevel.Information:
  312. LogInformation(data);
  313. break;
  314. case LogLevel.Minimal:
  315. LogMinimal(data);
  316. break;
  317. case LogLevel.Verbose:
  318. LogVerbose(data);
  319. break;
  320. case LogLevel.Warning:
  321. LogWarning(data);
  322. break;
  323. }
  324. }
  325. public void Log(ILogMessage message) => Log(message.Level, message.Message);
  326. public Task LogAsync(LogLevel level, string data)
  327. {
  328. Log(level, data);
  329. return Task.CompletedTask;
  330. }
  331. public Task LogAsync(ILogMessage message) => LogAsync(message.Level, message.Message);
  332. public void LogDebug(string data) => _out.WriteLine($"Debug: {data}");
  333. public void LogError(string data) => _error.WriteLine($"Error: {data}");
  334. public void LogInformation(string data) => _out.WriteLine($"Information: {data}");
  335. public void LogInformationSummary(string data) => _out.WriteLine($"Summary: {data}");
  336. public void LogMinimal(string data) => _out.WriteLine($"Minimal: {data}");
  337. public void LogVerbose(string data) => _out.WriteLine($"Verbose: {data}");
  338. public void LogWarning(string data) => _out.WriteLine($"Warning: {data}");
  339. }
  340. }