Multi Project Validator
A tool which searches recursively for project.json files, runs a set of analyses and reports on the result. For CLI we have only one rule currently, that Dependencies between projects must be equivalent to avoid stomping.
This commit is contained in:
parent
b25c2743ca
commit
6ff7404a48
10 changed files with 404 additions and 1 deletions
|
@ -1,7 +1,7 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 14
|
||||
VisualStudioVersion = 14.0.24720.0
|
||||
VisualStudioVersion = 14.0.23107.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.DotNet.Cli", "src\Microsoft.DotNet.Cli\Microsoft.DotNet.Cli.xproj", "{60CF7E6C-D6C8-439D-B7B7-D8A27E29BE2C}"
|
||||
EndProject
|
||||
|
@ -57,6 +57,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "LoadContextTest", "test\Loa
|
|||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.DotNet.Tools.New", "src\Microsoft.DotNet.Tools.New\Microsoft.DotNet.Tools.New.xproj", "{BC765FBF-AD7A-4A99-9902-5540C5A74181}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{0722D325-24C8-4E83-B5AF-0A083E7F0749}"
|
||||
EndProject
|
||||
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MultiProjectValidator", "tools\MultiProjectValidator\MultiProjectValidator.xproj", "{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -421,6 +425,22 @@ Global
|
|||
{BC765FBF-AD7A-4A99-9902-5540C5A74181}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
|
||||
{BC765FBF-AD7A-4A99-9902-5540C5A74181}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
|
||||
{BC765FBF-AD7A-4A99-9902-5540C5A74181}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.MinSizeRel|x64.Build.0 = Debug|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.Release|x64.Build.0 = Release|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85}.RelWithDebInfo|x64.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
@ -448,5 +468,6 @@ Global
|
|||
{947DD232-8D9B-4B78-9C6A-94F807D2DD58} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7}
|
||||
{7A75ACC4-3C2F-44E1-B492-0EC08704E9FF} = {17735A9D-BFD9-4585-A7CB-3208CA6EA8A7}
|
||||
{BC765FBF-AD7A-4A99-9902-5540C5A74181} = {ED2FE3E2-F7E7-4389-8231-B65123F2076F}
|
||||
{08A68C6A-86F6-4ED2-89A7-B166D33E9F85} = {0722D325-24C8-4E83-B5AF-0A083E7F0749}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
@ -11,6 +11,7 @@ namespace Microsoft.DotNet.Cli.Utils
|
|||
{
|
||||
internal static class Constants
|
||||
{
|
||||
public static readonly string ProjectFileName = "project.json";
|
||||
public static readonly string ExeSuffix = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
|
||||
|
||||
// Priority order of runnable suffixes to look for and run
|
||||
|
|
36
tools/MultiProjectValidator/AnalysisResult.cs
Normal file
36
tools/MultiProjectValidator/AnalysisResult.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MultiProjectValidator
|
||||
{
|
||||
public class AnalysisResult
|
||||
{
|
||||
|
||||
private List<string> _messages;
|
||||
private bool _passed;
|
||||
|
||||
public AnalysisResult(List<string> messages, bool passed)
|
||||
{
|
||||
this._messages = messages;
|
||||
this._passed = passed;
|
||||
}
|
||||
|
||||
public List<string> Messages
|
||||
{
|
||||
get
|
||||
{
|
||||
return _messages;
|
||||
}
|
||||
}
|
||||
|
||||
public bool Passed
|
||||
{
|
||||
get
|
||||
{
|
||||
return _passed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.ProjectModel;
|
||||
|
||||
namespace MultiProjectValidator.AnalysisRules
|
||||
{
|
||||
public class DependencyMismatchRule : IAnalysisRule
|
||||
{
|
||||
public AnalysisResult Evaluate(List<ProjectContext> projectContexts)
|
||||
{
|
||||
var targetGroupedContexts = GroupContextsByTarget(projectContexts);
|
||||
|
||||
var failureMessages = EvaluateTargetContextGroups(targetGroupedContexts);
|
||||
var pass = failureMessages.Count == 0;
|
||||
|
||||
var result = new AnalysisResult(failureMessages, pass);
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<string> EvaluateTargetContextGroups(Dictionary<string, List<ProjectContext>> targetGroupedContexts)
|
||||
{
|
||||
var failureMessages = new List<string>();
|
||||
|
||||
foreach (var target in targetGroupedContexts.Keys)
|
||||
{
|
||||
var targetContexts = targetGroupedContexts[target];
|
||||
|
||||
failureMessages.AddRange(EvaluateTargetContextGroup(targetContexts));
|
||||
}
|
||||
|
||||
return failureMessages;
|
||||
}
|
||||
|
||||
private List<string> EvaluateTargetContextGroup(List<ProjectContext> targetContexts)
|
||||
{
|
||||
var failedMessages = new List<string>();
|
||||
var assemblyVersionMap = new Dictionary<string, string>();
|
||||
|
||||
foreach(var context in targetContexts)
|
||||
{
|
||||
var libraries = context.LibraryManager.GetLibraries();
|
||||
|
||||
foreach(var library in libraries)
|
||||
{
|
||||
var name = library.Identity.Name;
|
||||
var version = library.Identity.Version.ToString();
|
||||
|
||||
if (assemblyVersionMap.ContainsKey(name))
|
||||
{
|
||||
var existingVersion = assemblyVersionMap[name];
|
||||
if (!string.Equals(existingVersion, version, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string message =
|
||||
$"Dependency mismatch in {context.ProjectFile.ProjectFilePath} for dependency {name}. Versions {version}, {existingVersion}";
|
||||
|
||||
failedMessages.Add(message);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
assemblyVersionMap[name] = version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return failedMessages;
|
||||
}
|
||||
|
||||
private Dictionary<string, List<ProjectContext>> GroupContextsByTarget(List<ProjectContext> projectContexts)
|
||||
{
|
||||
var targetContextMap = new Dictionary<string, List<ProjectContext>>();
|
||||
foreach (var context in projectContexts)
|
||||
{
|
||||
var target = context.TargetFramework + context.RuntimeIdentifier;
|
||||
|
||||
if (targetContextMap.ContainsKey(target))
|
||||
{
|
||||
targetContextMap[target].Add(context);
|
||||
}
|
||||
else
|
||||
{
|
||||
targetContextMap[target] = new List<ProjectContext>()
|
||||
{
|
||||
context
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return targetContextMap;
|
||||
}
|
||||
}
|
||||
}
|
13
tools/MultiProjectValidator/IAnalysisRule.cs
Normal file
13
tools/MultiProjectValidator/IAnalysisRule.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.ProjectModel;
|
||||
|
||||
namespace MultiProjectValidator
|
||||
{
|
||||
public interface IAnalysisRule
|
||||
{
|
||||
AnalysisResult Evaluate(List<ProjectContext> projectContexts);
|
||||
}
|
||||
}
|
18
tools/MultiProjectValidator/MultiProjectValidator.xproj
Normal file
18
tools/MultiProjectValidator/MultiProjectValidator.xproj
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="14.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">14.0</VisualStudioVersion>
|
||||
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.Props" Condition="'$(VSToolsPath)' != ''" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>08a68c6a-86f6-4ed2-89a7-b166d33e9f85</ProjectGuid>
|
||||
<RootNamespace>ProjectSanity</RootNamespace>
|
||||
<BaseIntermediateOutputPath Condition="'$(BaseIntermediateOutputPath)'=='' ">..\..\artifacts\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<OutputPath Condition="'$(OutputPath)'=='' ">..\..\artifacts\tools\$(MSBuildProjectName)\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VSToolsPath)\DNX\Microsoft.DNX.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||
</Project>
|
114
tools/MultiProjectValidator/Program.cs
Normal file
114
tools/MultiProjectValidator/Program.cs
Normal file
|
@ -0,0 +1,114 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.DotNet.ProjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace MultiProjectValidator
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
var rootPath = ParseAndValidateArgs(args);
|
||||
|
||||
List<ProjectContext> projects = null;
|
||||
try
|
||||
{
|
||||
projects = ProjectLoader.Load(rootPath);
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine("Failed to load projects from path: " + rootPath);
|
||||
Exit(1);
|
||||
}
|
||||
|
||||
var analyzer = ProjectAnalyzer.Create(projects);
|
||||
var analysisResults = analyzer.DoAnalysis();
|
||||
var failed = analysisResults.Where((a) => !a.Passed).Any();
|
||||
|
||||
PrintAnalysisResults(analysisResults);
|
||||
|
||||
return failed ? 1 : 0;
|
||||
}
|
||||
|
||||
private static void PrintAnalysisResults(List<AnalysisResult> analysisResults)
|
||||
{
|
||||
Console.WriteLine("Project Validation Results");
|
||||
|
||||
var failedCount = analysisResults.Where((a) => !a.Passed).Count();
|
||||
var passedCount = analysisResults.Where((a) => a.Passed).Count();
|
||||
|
||||
Console.WriteLine($"{passedCount} Successful Analysis Rules");
|
||||
Console.WriteLine($"{failedCount} Failed Analysis Rules");
|
||||
|
||||
if(failedCount != 0)
|
||||
{
|
||||
Console.WriteLine("Failure Messages");
|
||||
|
||||
foreach(var result in analysisResults)
|
||||
{
|
||||
if (!result.Passed)
|
||||
{
|
||||
foreach(var message in result.Messages)
|
||||
{
|
||||
Console.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AnyAnalysisFailed(List<AnalysisResult> analysisResults)
|
||||
{
|
||||
foreach (var result in analysisResults)
|
||||
{
|
||||
if (!result.Passed)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ParseAndValidateArgs(string[] args)
|
||||
{
|
||||
if (args.Length != 1)
|
||||
{
|
||||
PrintHelp();
|
||||
Exit(1);
|
||||
}
|
||||
|
||||
string rootPath = args[0];
|
||||
|
||||
if (!Directory.Exists(rootPath))
|
||||
{
|
||||
Console.WriteLine("Root Directory does not exist: " + rootPath);
|
||||
Exit(1);
|
||||
}
|
||||
|
||||
return rootPath;
|
||||
}
|
||||
|
||||
private static void Exit(int code)
|
||||
{
|
||||
Environment.Exit(code);
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
var help = @"
|
||||
Multi-Project Validator
|
||||
|
||||
Description:
|
||||
This tool recursively searches for project.json's from the given root path.
|
||||
It then applies a set of analysis rules, determines whether they pass/fail
|
||||
and then sets exit code to reflect.
|
||||
|
||||
Usage:
|
||||
pjvalidate [root path of recursive search]";
|
||||
|
||||
Console.WriteLine(help);
|
||||
}
|
||||
}
|
||||
}
|
46
tools/MultiProjectValidator/ProjectAnalyzer.cs
Normal file
46
tools/MultiProjectValidator/ProjectAnalyzer.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.ProjectModel;
|
||||
using MultiProjectValidator.AnalysisRules;
|
||||
|
||||
namespace MultiProjectValidator
|
||||
{
|
||||
public class ProjectAnalyzer
|
||||
{
|
||||
|
||||
public static ProjectAnalyzer Create(List<ProjectContext> projectContexts)
|
||||
{
|
||||
// Any Additional rules would be added here
|
||||
var rules = new List<IAnalysisRule>
|
||||
{
|
||||
new DependencyMismatchRule()
|
||||
};
|
||||
|
||||
return new ProjectAnalyzer(rules, projectContexts);
|
||||
}
|
||||
|
||||
private List<ProjectContext> projectContexts;
|
||||
private List<IAnalysisRule> rules;
|
||||
|
||||
private ProjectAnalyzer(List<IAnalysisRule> rules, List<ProjectContext> projectContexts)
|
||||
{
|
||||
this.rules = rules;
|
||||
this.projectContexts = projectContexts;
|
||||
}
|
||||
|
||||
public List<AnalysisResult> DoAnalysis()
|
||||
{
|
||||
var results = new List<AnalysisResult>();
|
||||
|
||||
foreach(var rule in rules)
|
||||
{
|
||||
results.Add(rule.Evaluate(projectContexts));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
41
tools/MultiProjectValidator/ProjectLoader.cs
Normal file
41
tools/MultiProjectValidator/ProjectLoader.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.DotNet.ProjectModel;
|
||||
|
||||
namespace MultiProjectValidator
|
||||
{
|
||||
public class ProjectLoader
|
||||
{
|
||||
private static readonly string PROJECT_FILENAME = "project.json";
|
||||
|
||||
public static List<ProjectContext> Load(string rootPath, bool recursive=true)
|
||||
{
|
||||
var projectFiles = DiscoverProjectFiles(rootPath);
|
||||
var projectContextList = LoadProjectContexts(projectFiles);
|
||||
|
||||
return projectContextList;
|
||||
}
|
||||
|
||||
private static string[] DiscoverProjectFiles(string rootPath)
|
||||
{
|
||||
return Directory.GetFiles(rootPath, PROJECT_FILENAME, SearchOption.AllDirectories);
|
||||
}
|
||||
|
||||
private static List<ProjectContext> LoadProjectContexts(string[] projectFiles)
|
||||
{
|
||||
var projectContexts = new List<ProjectContext>();
|
||||
|
||||
foreach (var file in projectFiles)
|
||||
{
|
||||
var fileTargetContexts = ProjectContext.CreateContextForEachTarget(file);
|
||||
|
||||
projectContexts.AddRange(fileTargetContexts);
|
||||
}
|
||||
|
||||
return projectContexts;
|
||||
}
|
||||
}
|
||||
}
|
19
tools/MultiProjectValidator/project.json
Normal file
19
tools/MultiProjectValidator/project.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"version": "1.0.0-*",
|
||||
"name": "pjvalidate",
|
||||
"compilationOptions": {
|
||||
"emitEntryPoint": true
|
||||
},
|
||||
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.Runtime": "1.0.1-beta-*",
|
||||
"NETStandard.Library": "1.0.0-rc2-23608",
|
||||
"Microsoft.DotNet.ProjectModel": "1.0.0-*",
|
||||
"Microsoft.DotNet.Cli.Utils": "1.0.0-*",
|
||||
"System.Linq": "4.0.1-rc2-23608"
|
||||
},
|
||||
|
||||
"frameworks": {
|
||||
"dnxcore50": { }
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue