AnalyzeBuildGraph.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  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.Collections;
  5. using System.Collections.Generic;
  6. using System.Linq;
  7. using System.IO;
  8. using System.Text;
  9. using System.Threading;
  10. using Microsoft.Build.Framework;
  11. using Microsoft.Build.Utilities;
  12. using NuGet.Frameworks;
  13. using NuGet.Versioning;
  14. using RepoTools.BuildGraph;
  15. using RepoTasks.ProjectModel;
  16. using RepoTasks.Utilities;
  17. namespace RepoTasks
  18. {
  19. public class AnalyzeBuildGraph : Task, ICancelableTask
  20. {
  21. private readonly CancellationTokenSource _cts = new CancellationTokenSource();
  22. /// <summary>
  23. /// Repositories that we are building new versions of.
  24. /// </summary>
  25. [Required]
  26. public ITaskItem[] Solutions { get; set; }
  27. [Required]
  28. public ITaskItem[] Artifacts { get; set; }
  29. [Required]
  30. public ITaskItem[] Dependencies { get; set; }
  31. [Required]
  32. public string Properties { get; set; }
  33. public string StartGraphAt { get; set; }
  34. /// <summary>
  35. /// The order in which to build repositories
  36. /// </summary>
  37. [Output]
  38. public ITaskItem[] RepositoryBuildOrder { get; set; }
  39. public void Cancel()
  40. {
  41. _cts.Cancel();
  42. }
  43. public override bool Execute()
  44. {
  45. var packageArtifacts = Artifacts.Select(ArtifactInfo.Parse)
  46. .OfType<ArtifactInfo.Package>()
  47. .Where(p => !p.IsSymbolsArtifact);
  48. var factory = new SolutionInfoFactory(Log, BuildEngine5);
  49. var props = MSBuildListSplitter.GetNamedProperties(Properties);
  50. Log.LogMessage(MessageImportance.High, $"Beginning cross-repo analysis on {Solutions.Length} solutions. Hang tight...");
  51. var solutions = factory.Create(Solutions, props, _cts.Token);
  52. Log.LogMessage($"Found {solutions.Count} and {solutions.Sum(p => p.Projects.Count)} projects");
  53. if (_cts.IsCancellationRequested)
  54. {
  55. return false;
  56. }
  57. EnsureConsistentGraph(packageArtifacts, solutions);
  58. RepositoryBuildOrder = GetRepositoryBuildOrder(packageArtifacts, solutions.Where(s => s.ShouldBuild));
  59. return !Log.HasLoggedErrors;
  60. }
  61. private struct VersionMismatch
  62. {
  63. public SolutionInfo Solution;
  64. public ProjectInfo Project;
  65. public string PackageId;
  66. public string ActualVersion;
  67. public NuGetVersion ExpectedVersion;
  68. }
  69. private void EnsureConsistentGraph(IEnumerable<ArtifactInfo.Package> packages, IEnumerable<SolutionInfo> solutions)
  70. {
  71. // ensure versions cascade
  72. var buildPackageMap = packages.ToDictionary(p => p.PackageInfo.Id, p => p, StringComparer.OrdinalIgnoreCase);
  73. var dependencyMap = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase);
  74. foreach (var dep in Dependencies)
  75. {
  76. if (!dependencyMap.TryGetValue(dep.ItemSpec, out var versions))
  77. {
  78. dependencyMap[dep.ItemSpec] = versions = new List<string>();
  79. }
  80. else if (dep.GetMetadata("NoWarn") == null || dep.GetMetadata("NoWarn").IndexOf("KRB" + KoreBuildErrors.MultipleExternalDependencyVersions) < 0)
  81. {
  82. Log.LogKoreBuildWarning(
  83. KoreBuildErrors.MultipleExternalDependencyVersions,
  84. message: $"Multiple versions of external dependency '{dep.ItemSpec}' are defined. In most cases, there should only be one version of external dependencies.");
  85. }
  86. versions.Add(dep.GetMetadata("Version"));
  87. }
  88. var inconsistentVersions = new List<VersionMismatch>();
  89. var reposThatShouldPatch = new HashSet<string>();
  90. // TODO cleanup the 4-deep nested loops
  91. foreach (var solution in solutions)
  92. foreach (var project in solution.Projects)
  93. foreach (var tfm in project.Frameworks)
  94. foreach (var dependency in tfm.Dependencies)
  95. {
  96. if (!buildPackageMap.TryGetValue(dependency.Key, out var package))
  97. {
  98. // This dependency is not one of the packages that will be compiled by this run of Universe.
  99. if (!dependencyMap.TryGetValue(dependency.Key, out var externalVersions)
  100. || !externalVersions.Contains(dependency.Value.Version))
  101. {
  102. Log.LogKoreBuildError(
  103. project.FullPath,
  104. KoreBuildErrors.UndefinedExternalDependency,
  105. message: $"Undefined external dependency on {dependency.Key}/{dependency.Value.Version}");
  106. }
  107. continue;
  108. }
  109. var refVersion = VersionRange.Parse(dependency.Value.Version);
  110. if (refVersion.IsFloating && refVersion.Float.Satisfies(package.PackageInfo.Version))
  111. {
  112. continue;
  113. }
  114. else if (package.PackageInfo.Version.Equals(refVersion.MinVersion))
  115. {
  116. continue;
  117. }
  118. if (!solution.ShouldBuild && solution.Shipped)
  119. {
  120. reposThatShouldPatch.Add(Path.GetFileName(Path.GetDirectoryName(solution.FullPath)));
  121. }
  122. inconsistentVersions.Add(new VersionMismatch
  123. {
  124. Solution = solution,
  125. Project = project,
  126. PackageId = dependency.Key,
  127. ActualVersion = dependency.Value.Version,
  128. ExpectedVersion = package.PackageInfo.Version,
  129. });
  130. }
  131. if (inconsistentVersions.Count != 0)
  132. {
  133. var sb = new StringBuilder();
  134. sb.AppendLine();
  135. sb.AppendLine($"Repos are inconsistent. The following projects have PackageReferences that should be updated");
  136. foreach (var solution in inconsistentVersions.GroupBy(p => p.Solution.FullPath))
  137. {
  138. sb.Append(" - ").AppendLine(Path.GetFileName(solution.Key));
  139. foreach (var project in solution.GroupBy(p => p.Project.FullPath))
  140. {
  141. sb.Append(" - ").AppendLine(Path.GetFileName(project.Key));
  142. foreach (var mismatchedReference in project)
  143. {
  144. sb.AppendLine($" + {mismatchedReference.PackageId}/{{{mismatchedReference.ActualVersion} => {mismatchedReference.ExpectedVersion}}}");
  145. }
  146. }
  147. }
  148. sb.AppendLine();
  149. Log.LogMessage(MessageImportance.High, sb.ToString());
  150. Log.LogWarning("Package versions are inconsistent. See build log for details.");
  151. // reduced to warning for now.
  152. // TODO: address the complexity of LKG dependencies
  153. // Log.LogError("Package versions are inconsistent. See build log for details.");
  154. }
  155. foreach (var repo in reposThatShouldPatch)
  156. {
  157. Log.LogError($"{repo} should not be a 'ShippedRepository'. Version changes in other repositories mean it should be patched to perserve cascading version upgrades.");
  158. }
  159. }
  160. private ITaskItem[] GetRepositoryBuildOrder(IEnumerable<ArtifactInfo.Package> artifacts, IEnumerable<SolutionInfo> solutions)
  161. {
  162. var repositories = solutions.Select(s =>
  163. {
  164. var repoName = Path.GetFileName(Path.GetDirectoryName(s.FullPath));
  165. var repo = new Repository(repoName)
  166. {
  167. RootDir = Path.GetDirectoryName(s.FullPath)
  168. };
  169. var packages = artifacts
  170. .Where(a => a.RepoName.Equals(repoName, StringComparison.OrdinalIgnoreCase))
  171. .ToDictionary(p => p.PackageInfo.Id, p => p, StringComparer.OrdinalIgnoreCase);
  172. foreach (var proj in s.Projects)
  173. {
  174. IList<Project> projectGroup;
  175. if (packages.ContainsKey(proj.PackageId))
  176. {
  177. // this project is a package producer and consumer
  178. packages.Remove(proj.PackageId);
  179. projectGroup = repo.Projects;
  180. }
  181. else
  182. {
  183. // this project is a package consumer
  184. projectGroup = repo.SupportProjects;
  185. }
  186. projectGroup.Add(new Project(proj.PackageId)
  187. {
  188. Repository = repo,
  189. PackageReferences = new HashSet<string>(proj
  190. .Frameworks
  191. .SelectMany(f => f.Dependencies.Keys)
  192. .Concat(proj.Tools.Select(t => t.Id)), StringComparer.OrdinalIgnoreCase),
  193. });
  194. }
  195. foreach (var packageId in packages.Keys)
  196. {
  197. // these packages are produced from something besides a csproj. e.g. .Sources packages
  198. repo.Projects.Add(new Project(packageId) { Repository = repo });
  199. }
  200. return repo;
  201. }).ToList();
  202. var graph = GraphBuilder.Generate(repositories, StartGraphAt, Log);
  203. var repositoriesWithOrder = new List<(ITaskItem repository, int order)>();
  204. foreach (var repository in repositories)
  205. {
  206. var graphNodeRepository = graph.FirstOrDefault(g => g.Repository.Name == repository.Name);
  207. if (graphNodeRepository == null)
  208. {
  209. // StartGraphAt was specified so the graph is incomplete.
  210. continue;
  211. }
  212. var order = TopologicalSort.GetOrder(graphNodeRepository);
  213. var repositoryTaskItem = new TaskItem(repository.Name);
  214. repositoryTaskItem.SetMetadata("Order", order.ToString());
  215. repositoryTaskItem.SetMetadata("RootPath", repository.RootDir);
  216. repositoriesWithOrder.Add((repositoryTaskItem, order));
  217. }
  218. Log.LogMessage(MessageImportance.High, "Repository build order:");
  219. foreach (var buildGroup in repositoriesWithOrder.GroupBy(r => r.order).OrderBy(g => g.Key))
  220. {
  221. var buildGroupRepos = buildGroup.Select(b => b.repository.ItemSpec);
  222. Log.LogMessage(MessageImportance.High, $"{buildGroup.Key.ToString().PadLeft(2, ' ')}: {string.Join(", ", buildGroupRepos)}");
  223. }
  224. return repositoriesWithOrder
  225. .OrderBy(r => r.order)
  226. .Select(r => r.repository)
  227. .ToArray();
  228. }
  229. }
  230. }