Created
November 20, 2014 06:50
-
-
Save sergey-tihon/46824acffb8c288fc5fe to your computer and use it in GitHub Desktop.
NuGet dependency visualizer with F# and Graphviz, read more here https://sergeytihon.wordpress.com/2014/11/20/nuget-dependency-visualizer-with-f-and-graphviz/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#r @"packages\Streams.0.2.5\lib\Streams.Core.dll" | |
open System | |
open System.IO | |
open System.Collections.Generic | |
open Nessos.Streams | |
// make Visual Studio use the script directory | |
Directory.SetCurrentDirectory(__SOURCE_DIRECTORY__) | |
type PackageType = | |
| InScopeOfAnalysis // lightgreen | |
| WeDependOnPackage // grey | |
| PackageDependOnUs // lightblue | |
type DependencySet = | |
{ dependent: string; | |
packageType: PackageType | |
dependencies: string Set} | |
// ================================ | |
// Generate the graph using GraphViz | |
// ================================ | |
module GraphViz = | |
// change this as needed for your local environment | |
let graphVizPath = @"d:\Program Files (x86)\graphviz-2.38\bin\" | |
let getName (n) = | |
sprintf "\"%s\"" n // be sure to quote the type name! | |
let toCsv sep strList = | |
match strList with | |
| [] -> "" | |
| _ -> List.reduce (fun s1 s2 -> s1 + sep + s2) strList | |
let writeDepSet writer depSet = | |
let fromNode = getName depSet.dependent | |
let toNodes = | |
depSet.dependencies | |
|> Seq.map getName | |
|> Seq.sort // make it more human readable | |
|> Seq.toList | |
|> toCsv "; " | |
fprintfn writer " %s -> { rank=none; %s }" fromNode toNodes | |
// Create a DOT file for graphviz to read. | |
let createDotFile dotFilename depSets = | |
use writer = new System.IO.StreamWriter(path=dotFilename) | |
fprintfn writer "digraph G {" | |
fprintfn writer " page=\"40,60\"; " | |
fprintfn writer " ratio=auto;" | |
fprintfn writer " rankdir=LR;" | |
fprintfn writer " fontsize=10;" | |
// Write edges | |
depSets | |
|> Seq.sort // make it more human readable | |
|> Seq.iter (writeDepSet writer) | |
// Write color information | |
depSets | |
|> Seq.iter (fun depSet-> | |
let fromNode = getName depSet.dependent | |
let color = | |
match depSet.packageType with | |
| InScopeOfAnalysis -> "green" //color=".7 .3 1.0"] | |
| PackageDependOnUs -> "lightblue" | |
| WeDependOnPackage -> "grey" | |
fprintfn writer " %s [color=%s,style=filled];" fromNode color | |
) | |
fprintfn writer " }" | |
// shell out to run a command line program | |
let startProcessAndCaptureOutput cmd cmdParams = | |
let debug = false | |
if debug then | |
printfn "Process: %s %s" cmd cmdParams | |
let si = new System.Diagnostics.ProcessStartInfo(cmd, cmdParams) | |
si.UseShellExecute <- false | |
si.RedirectStandardOutput <- true | |
use p = new System.Diagnostics.Process() | |
p.StartInfo <- si | |
if p.Start() then | |
if debug then | |
use stdOut = p.StandardOutput | |
stdOut.ReadToEnd() |> printfn "%s" | |
printfn "Process complete" | |
else | |
printfn "Process failed" | |
/// Generate an image file from a DOT file | |
/// algo = dot, neato | |
/// format = gif, png, jpg, svg | |
let generateImageFile dotFilename algo format imgFilename = | |
let cmd = sprintf @"""%s%s.exe""" graphVizPath algo | |
let inFile = System.IO.Path.Combine(__SOURCE_DIRECTORY__,dotFilename) | |
let outFile = System.IO.Path.Combine(__SOURCE_DIRECTORY__,imgFilename) | |
let cmdParams = sprintf "-T%s -o\"%s\" \"%s\"" format outFile inFile | |
startProcessAndCaptureOutput cmd cmdParams | |
// ================================ | |
// NuGet packages analysis | |
// ================================ | |
#I @"packages\Nuget.Core.2.8.3\lib\net40-Client" | |
#r "NuGet.Core.dll" | |
#r "System.Xml.Linq.dll" | |
let repository = | |
NuGet.PackageRepositoryFactory.Default.CreateRepository | |
"https://nuget.org/api/v2" | |
// Download info about all NuGet packages | |
let allNuGetPackages = | |
repository.GetPackages() | |
|> Stream.ofSeq | |
|> Stream.filter (fun p -> | |
// I need this to see the progress of download | |
printfn "%s" p.Id | |
true | |
) | |
// Select only latest versions to analyze | |
// If you need more accurate analysis you should not avoid versioning | |
let latestVersionOfNuGetPackages = | |
allNuGetPackages | |
|> Stream.groupBy (fun p -> p.Id) | |
|> Stream.map (fun (key, packages) -> | |
packages | |
|> Stream.ofSeq | |
|> Stream.filter (fun x-> x.Published.HasValue) | |
|> Stream.maxBy (fun x->x.Published.Value)) | |
// Build index based on package.Id | |
let packages = | |
latestVersionOfNuGetPackages | |
|> Stream.map (fun x->x.Id.ToLowerInvariant(), x) | |
|> Stream.toSeq | |
|> Map.ofSeq | |
// Print graph into file | |
let printGraph name (selectedPackageIds:Dictionary<_,_>) = | |
let depSet = | |
latestVersionOfNuGetPackages | |
|> Stream.filter (fun p->selectedPackageIds.ContainsKey(p.Id.ToLowerInvariant())) | |
|> Stream.map (fun p -> | |
{dependent = p.Id; | |
packageType = selectedPackageIds.[p.Id.ToLowerInvariant()]; | |
dependencies = | |
seq { | |
for set in p.DependencySets do | |
for dep in set.Dependencies do | |
if selectedPackageIds.ContainsKey (dep.Id.ToLowerInvariant()) | |
&& packages.ContainsKey(dep.Id.ToLowerInvariant()) | |
then yield packages.[dep.Id.ToLowerInvariant()].Id | |
}|> Set.ofSeq}) | |
// create DOT file | |
let dotFilename = name+ ".dot" | |
GraphViz.createDotFile dotFilename (Stream.toSeq depSet) | |
// create SVG file | |
let svgFilename = dotFilename + ".svg" | |
GraphViz.generateImageFile dotFilename "dot" "svg" svgFilename | |
//GraphViz.generateImageFile dotFilename "dot" "png" (dotFilename + ".png") | |
// Create dependency graph based on initial `selector` | |
let createGraph name selector = | |
// Ids of packages that will be displayed on graph | |
let selectedPackageIds = Dictionary<string, PackageType>() | |
// Mark package with all dependant packages | |
let rec markPackage (id:string) mark = | |
let key = id.ToLowerInvariant() | |
if not <| selectedPackageIds.ContainsKey key then | |
selectedPackageIds.Add(key, mark) |> ignore | |
if packages.ContainsKey key then | |
let package = packages.[key] | |
for set in package.DependencySets do | |
for dep in set.Dependencies do | |
markPackage dep.Id WeDependOnPackage | |
else | |
printfn "Reference to unlisted package '%s'" key | |
else | |
if (mark = InScopeOfAnalysis && selectedPackageIds.[key] <> mark) | |
then selectedPackageIds.[key] <- mark | |
// Find and mark of F# packages | |
latestVersionOfNuGetPackages | |
|> Stream.filter selector | |
|> Stream.iter (fun p-> | |
printfn "Base package: %s" p.Id | |
markPackage p.Id InScopeOfAnalysis) | |
// Check if package has marked dependant package | |
let isDependOnMarkedPackage (package:NuGet.IPackage) = | |
seq { | |
for set in package.DependencySets do | |
for dep in set.Dependencies do | |
yield dep.Id.ToLowerInvariant() | |
} | |
|> Seq.exists (fun id-> | |
match selectedPackageIds.TryGetValue id with | |
| true, InScopeOfAnalysis | |
| true, PackageDependOnUs | |
-> true | |
| _ -> false | |
) | |
// Find all packages that depend on marked/F# packages | |
let state = ref true | |
while !state do | |
state := false | |
for p in Stream.toSeq latestVersionOfNuGetPackages do | |
let key = p.Id.ToLowerInvariant() | |
if not (selectedPackageIds.ContainsKey(key)) && | |
isDependOnMarkedPackage p | |
then state := true | |
printfn "\tDependent package: %s" key | |
selectedPackageIds.Add(key, PackageDependOnUs) | |
printGraph name selectedPackageIds | |
// ================================ | |
// Samples | |
// ================================ | |
let isFSharpPackage (p:NuGet.IPackage) = | |
let s = String.Join(":", [p.Title; p.Tags; p.Id; p.Description]).ToLowerInvariant() | |
(s.Contains "fsharp" || s.Contains "f#") | |
&& not(s.Contains "pdfsharp") | |
&& not(s.Contains "rdfsharp") | |
&& not(s.Contains "funscript") // too much dependencies | |
createGraph "FSharp.Ecosystem" isFSharpPackage | |
createGraph "FSharp.Compiler.Service" (fun p-> p.Id = "FSharp.Compiler.Service") | |
createGraph "FsPickler" (fun p-> p.Id = "FsPickler") | |
createGraph "FSharp.Data" (fun p-> p.Id.Contains("FSharp.Data")) | |
createGraph "FSharp.Core" (fun p-> p.Id.StartsWith("FSharp.Core")) | |
createGraph "FSharpx" (fun p-> p.Id.Contains("FSharpx")) | |
createGraph "Roslyn" (fun p-> p.Id.Contains("Roslyn")) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment