AnalyzeBuildGraph.cs 11 KB

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