Skip to content

Instantly share code, notes, and snippets.

@sergey-tihon
Created November 20, 2014 06:50
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save sergey-tihon/46824acffb8c288fc5fe to your computer and use it in GitHub Desktop.
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/
#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