Program.cs 16 KB

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