From 9f7e295af87b21957af228e7b56ce42186434d41 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 5 Apr 2018 13:24:49 -0700 Subject: [PATCH] Add task to generate the repo to repo graph (#1036) --- build/GenerateCode.targets | 17 + build/repo.targets | 1 + build/tasks/CodeGen/DirectedGraphXml.cs | 42 ++ build/tasks/CodeGen/GenerateSubmoduleGraph.cs | 226 ++++++++++ build/tasks/CodeGen/RepositoryProject.cs | 46 ++ build/tasks/ProjectModel/ProjectInfo.cs | 5 +- .../tasks/ProjectModel/ProjectInfoFactory.cs | 2 - build/tasks/ProjectModel/SolutionInfo.cs | 8 + build/tasks/RepoTasks.csproj | 2 +- build/tasks/RepoTasks.tasks | 1 + build/tasks/tasks.sln | 34 ++ modules/Scaffolding | 2 +- modules/SubmoduleGraph.dgml | 421 ++++++++++++++++++ 13 files changed, 800 insertions(+), 7 deletions(-) create mode 100644 build/GenerateCode.targets create mode 100644 build/tasks/CodeGen/DirectedGraphXml.cs create mode 100644 build/tasks/CodeGen/GenerateSubmoduleGraph.cs create mode 100644 build/tasks/CodeGen/RepositoryProject.cs create mode 100644 build/tasks/tasks.sln create mode 100644 modules/SubmoduleGraph.dgml diff --git a/build/GenerateCode.targets b/build/GenerateCode.targets new file mode 100644 index 0000000000..069e358ae6 --- /dev/null +++ b/build/GenerateCode.targets @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/build/repo.targets b/build/repo.targets index cb419361ca..2f1ecdeaa5 100644 --- a/build/repo.targets +++ b/build/repo.targets @@ -6,6 +6,7 @@ + diff --git a/build/tasks/CodeGen/DirectedGraphXml.cs b/build/tasks/CodeGen/DirectedGraphXml.cs new file mode 100644 index 0000000000..e90d66ca6b --- /dev/null +++ b/build/tasks/CodeGen/DirectedGraphXml.cs @@ -0,0 +1,42 @@ + +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Xml.Linq; + +namespace RepoTasks.CodeGen +{ + class DirectedGraphXml + { + private readonly XNamespace _ns = "http://schemas.microsoft.com/vs/2009/dgml"; + private readonly XDocument _doc; + private readonly XElement _nodes; + private readonly XElement _links; + + public DirectedGraphXml() + { + _doc = new XDocument(new XElement(_ns + "DirectedGraph")); + _nodes = new XElement(_ns + "Nodes"); + _links = new XElement(_ns + "Links"); + _doc.Root.Add(_nodes); + _doc.Root.Add(_links); + } + + public void AddNode(string id) + { + _nodes.Add(new XElement(_ns + "Node", new XAttribute("Id", id), new XAttribute("Label", id))); + } + + public void AddLink(string source, string target) + { + _links.Add(new XElement(_ns + "Link", + new XAttribute("Source", source), + new XAttribute("Target", target))); + } + + public void Save(string path) + { + _doc.Save(path); + } + } +} diff --git a/build/tasks/CodeGen/GenerateSubmoduleGraph.cs b/build/tasks/CodeGen/GenerateSubmoduleGraph.cs new file mode 100644 index 0000000000..7940b5cba2 --- /dev/null +++ b/build/tasks/CodeGen/GenerateSubmoduleGraph.cs @@ -0,0 +1,226 @@ + +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System.Text; +using System.Threading; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using NuGet.Frameworks; +using NuGet.Versioning; +using RepoTools.BuildGraph; +using RepoTasks.ProjectModel; +using RepoTasks.Utilities; +using RepoTasks.CodeGen; +using NuGet.Packaging.Core; + +namespace RepoTasks +{ + public class GenerateSubmoduleGraph : Task, ICancelableTask + { + private readonly CancellationTokenSource _cts = new CancellationTokenSource(); + + /// + /// Repositories that we are building new versions of. + /// + [Required] + public ITaskItem[] Solutions { get; set; } + + [Required] + public ITaskItem[] Artifacts { get; set; } + + [Required] + public ITaskItem[] Repositories { get; set; } + + [Required] + public string RepositoryRoot { get; set; } + + [Required] + public string Properties { get; set; } + + public void Cancel() + { + _cts.Cancel(); + } + + public override bool Execute() + { + var packageArtifacts = Artifacts.Select(ArtifactInfo.Parse) + .OfType() + .Where(p => !p.IsSymbolsArtifact) + .ToDictionary(p => p.PackageInfo.Id, p => p, StringComparer.OrdinalIgnoreCase); + + var factory = new SolutionInfoFactory(Log, BuildEngine5); + var props = MSBuildListSplitter.GetNamedProperties(Properties); + + Log.LogMessage(MessageImportance.High, $"Beginning cross-repo analysis on {Solutions.Length} solutions. Hang tight..."); + + if (!props.TryGetValue("Configuration", out var defaultConfig)) + { + defaultConfig = "Debug"; + } + + var solutions = factory.Create(Solutions, props, defaultConfig, _cts.Token).OrderBy(f => f.Directory).ToList(); + Log.LogMessage($"Found {solutions.Count} and {solutions.Sum(p => p.Projects.Count)} projects"); + + if (_cts.IsCancellationRequested) + { + return false; + } + + return GenerateGraph(packageArtifacts, solutions); + } + + private bool GenerateGraph(IDictionary packageArtifacts, IReadOnlyList solutions) + { + var repoGraph = new AdjacencyMatrix(solutions.Count); + var packageToProjectMap = new Dictionary(); + + for (var i = 0; i < solutions.Count; i++) + { + var sln = repoGraph[i] = solutions[i]; + + foreach (var proj in sln.Projects) + { + if (!proj.IsPackable || proj.FullPath.Contains("samples")) + { + continue; + } + + var id = new PackageIdentity(proj.PackageId, new NuGetVersion(proj.PackageVersion)); + + if (packageToProjectMap.TryGetValue(id, out var otherProj)) + { + Log.LogError($"Both {proj.FullPath} and {otherProj.FullPath} produce {id}"); + continue; + } + + packageToProjectMap.Add(id, proj); + } + + var sharedSrc = Path.Combine(sln.Directory, "shared"); + if (Directory.Exists(sharedSrc)) + { + foreach (var dir in Directory.GetDirectories(sharedSrc, "*.Sources")) + { + var id = Path.GetFileName(dir); + var artifactInfo = packageArtifacts[id]; + var sharedSrcProj = new ProjectInfo(dir, + Array.Empty(), + Array.Empty(), + true, + artifactInfo.PackageInfo.Id, + artifactInfo.PackageInfo.Version.ToNormalizedString()); + sharedSrcProj.SolutionInfo = sln; + var identity = new PackageIdentity(artifactInfo.PackageInfo.Id, artifactInfo.PackageInfo.Version); + packageToProjectMap.Add(identity, sharedSrcProj); + } + } + } + + if (Log.HasLoggedErrors) + { + return false; + } + + for (var i = 0; i < solutions.Count; i++) + { + var sln = repoGraph[i]; + + var deps = from proj in sln.Projects + from tfm in proj.Frameworks + from dep in tfm.Dependencies.Values + select dep; + + foreach (var dep in deps) + { + if (packageToProjectMap.TryGetValue(new PackageIdentity(dep.Id, new NuGetVersion(dep.Version)), out var target)) + { + var j = repoGraph.FindIndex(target.SolutionInfo); + repoGraph.SetLink(i, j); + } + } + + var toolDeps = from proj in sln.Projects + from tool in proj.Tools + select tool; + + foreach (var toolDep in toolDeps) + { + if (packageToProjectMap.TryGetValue(new PackageIdentity(toolDep.Id, new NuGetVersion(toolDep.Version)), out var target)) + { + var j = repoGraph.FindIndex(target.SolutionInfo); + repoGraph.SetLink(i, j); + } + } + } + + CreateDgml(repoGraph); + return !Log.HasLoggedErrors; + } + + + private void CreateDgml(AdjacencyMatrix repoGraph) + { + var dgml = new DirectedGraphXml(); + + for (var i = 0; i < repoGraph.Count; i++) + { + var node = repoGraph[i]; + var nodeName = Path.GetFileName(node.Directory); + dgml.AddNode(nodeName); + + for (var j = 0; j < repoGraph.Count; j++) + { + if (j == i) continue; + if (repoGraph.HasLink(i, j)) + { + var target = repoGraph[j]; + var targetName = Path.GetFileName(target.Directory); + dgml.AddLink(nodeName, targetName); + } + } + } + + dgml.Save(Path.Combine(RepositoryRoot, "modules", "SubmoduleGraph.dgml")); + } + + private class AdjacencyMatrix + { + private readonly bool[,] _matrix; + private readonly SolutionInfo[] _items; + + public AdjacencyMatrix(int size) + { + _matrix = new bool[size, size]; + _items = new SolutionInfo[size]; + Count = size; + } + + public SolutionInfo this[int idx] + { + get => _items[idx]; + set => _items[idx] = value; + } + + public int FindIndex(SolutionInfo item) + { + return Array.FindIndex(_items, t => t.Equals(item)); + } + + public int Count { get; } + + public bool HasLink(int source, int target) => _matrix[source, target]; + + public void SetLink(int source, int target) + { + _matrix[source, target] = true; + } + } + } +} diff --git a/build/tasks/CodeGen/RepositoryProject.cs b/build/tasks/CodeGen/RepositoryProject.cs new file mode 100644 index 0000000000..1cb3b76391 --- /dev/null +++ b/build/tasks/CodeGen/RepositoryProject.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Text; +using Microsoft.Build.Construction; +using Microsoft.Build.Evaluation; + +namespace RepoTasks.CodeGen +{ + class RepositoryProject + { + private readonly ProjectRootElement _doc; + + public RepositoryProject(string repositoryRoot) + { + _doc = ProjectRootElement.Create(NewProjectFileOptions.None); + var import = _doc.CreateImportElement(@"$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"); + var propGroup = _doc.AddPropertyGroup(); + if (repositoryRoot[repositoryRoot.Length - 1] != '\\') + { + repositoryRoot += '\\'; + } + propGroup.AddProperty("RepositoryRoot", repositoryRoot); + _doc.AddItemGroup(); + _doc.PrependChild(import); + _doc.AddImport(@"$(MSBuildToolsPath)\Microsoft.Common.targets"); + } + + public void AddProjectReference(string path) + { + _doc.AddItem("ProjectReference", path); + } + + public void AddProperty(string name, string value) + { + _doc.AddProperty(name, value); + } + + public void Save(string filePath) + { + _doc.Save(filePath, Encoding.UTF8); + } + } +} diff --git a/build/tasks/ProjectModel/ProjectInfo.cs b/build/tasks/ProjectModel/ProjectInfo.cs index 1dd4339185..4f4b7a3a9c 100644 --- a/build/tasks/ProjectModel/ProjectInfo.cs +++ b/build/tasks/ProjectModel/ProjectInfo.cs @@ -10,7 +10,6 @@ namespace RepoTasks.ProjectModel internal class ProjectInfo { public ProjectInfo(string fullPath, - string projectExtensionsPath, IReadOnlyList frameworks, IReadOnlyList tools, bool isPackable, @@ -28,7 +27,6 @@ namespace RepoTasks.ProjectModel FullPath = fullPath; FileName = Path.GetFileName(fullPath); Directory = Path.GetDirectoryName(FullPath); - ProjectExtensionsPath = projectExtensionsPath ?? Path.Combine(Directory, "obj"); IsPackable = isPackable; PackageId = packageId; PackageVersion = packageVersion; @@ -36,12 +34,13 @@ namespace RepoTasks.ProjectModel public string FullPath { get; } public string FileName { get; } - public string ProjectExtensionsPath { get; } public string Directory { get; } public string PackageId { get; } public string PackageVersion { get; } public bool IsPackable { get; } + public SolutionInfo SolutionInfo { get; set; } + public IReadOnlyList Frameworks { get; } public IReadOnlyList Tools { get; } } diff --git a/build/tasks/ProjectModel/ProjectInfoFactory.cs b/build/tasks/ProjectModel/ProjectInfoFactory.cs index 2acf90be1c..42c0bd3fee 100644 --- a/build/tasks/ProjectModel/ProjectInfoFactory.cs +++ b/build/tasks/ProjectModel/ProjectInfoFactory.cs @@ -27,7 +27,6 @@ namespace RepoTasks.ProjectModel { var project = GetProject(path, projectCollection); var instance = project.CreateProjectInstance(ProjectInstanceSettings.ImmutableWithFastItemLookup); - var projExtPath = instance.GetPropertyValue("MSBuildProjectExtensionsPath"); var targetFrameworks = instance.GetPropertyValue("TargetFrameworks"); var targetFramework = instance.GetPropertyValue("TargetFramework"); @@ -63,7 +62,6 @@ namespace RepoTasks.ProjectModel var packageVersion = instance.GetPropertyValue("PackageVersion"); return new ProjectInfo(path, - projExtPath, frameworks, tools, isPackable, diff --git a/build/tasks/ProjectModel/SolutionInfo.cs b/build/tasks/ProjectModel/SolutionInfo.cs index 8b9710081e..21af5e5291 100644 --- a/build/tasks/ProjectModel/SolutionInfo.cs +++ b/build/tasks/ProjectModel/SolutionInfo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.IO; namespace RepoTasks.ProjectModel { @@ -21,13 +22,20 @@ namespace RepoTasks.ProjectModel } FullPath = fullPath; + Directory = Path.GetDirectoryName(fullPath); ConfigName = configName; Projects = projects ?? throw new ArgumentNullException(nameof(projects)); ShouldBuild = shouldBuild; Shipped = shipped; + + foreach (var proj in Projects) + { + proj.SolutionInfo = this; + } } public string FullPath { get; } + public string Directory { get; } public string ConfigName { get; } public IReadOnlyList Projects { get; } public bool ShouldBuild { get; } diff --git a/build/tasks/RepoTasks.csproj b/build/tasks/RepoTasks.csproj index fbfd560a7c..7c95128fdd 100644 --- a/build/tasks/RepoTasks.csproj +++ b/build/tasks/RepoTasks.csproj @@ -4,7 +4,7 @@ - netstandard2.0 + netcoreapp2.0 diff --git a/build/tasks/RepoTasks.tasks b/build/tasks/RepoTasks.tasks index 7e014a92a3..6f2032d491 100644 --- a/build/tasks/RepoTasks.tasks +++ b/build/tasks/RepoTasks.tasks @@ -9,6 +9,7 @@ + diff --git a/build/tasks/tasks.sln b/build/tasks/tasks.sln new file mode 100644 index 0000000000..917ff4ff83 --- /dev/null +++ b/build/tasks/tasks.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepoTasks", "RepoTasks.csproj", "{A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Debug|x64.ActiveCfg = Debug|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Debug|x64.Build.0 = Debug|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Debug|x86.ActiveCfg = Debug|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Debug|x86.Build.0 = Debug|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Release|Any CPU.Build.0 = Release|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Release|x64.ActiveCfg = Release|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Release|x64.Build.0 = Release|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Release|x86.ActiveCfg = Release|Any CPU + {A114791F-35B7-4E5B-8E5B-9A91E0B6E4AE}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/modules/Scaffolding b/modules/Scaffolding index 6baf0d2715..6558238ab8 160000 --- a/modules/Scaffolding +++ b/modules/Scaffolding @@ -1 +1 @@ -Subproject commit 6baf0d2715a825c49e7f91189c7ae9ffcb2b4a18 +Subproject commit 6558238ab8c2add9b75b8022d1445ed20c71c7a1 diff --git a/modules/SubmoduleGraph.dgml b/modules/SubmoduleGraph.dgml new file mode 100644 index 0000000000..cdcd0a92cd --- /dev/null +++ b/modules/SubmoduleGraph.dgml @@ -0,0 +1,421 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file