My new place of work has a lot of nuget packages, and I wanted to understand the dependencies between them. To do this I wrote a simple shell script to find all the packages.config
files on my machine, and output all the relationships in a way which I could view them.
The format for viewing I use for this is Graphviz’s dot language, and the resulting output can be pasted into WebGraphviz to view.
RESULT_FILE="graph.dot" # the output file
NAME_MATCH='Microsoft\.' # leave this as a blank string if you want no filtering
echo '' > $RESULT_FILE # clear out the file
echo 'digraph Dependencies {' >> $RESULT_FILE
echo ' rankdir=LR;' >> $RESULT_FILE # we want a left to right graph, as it's a little easier to read
# find all packages.config, recursively beaneath the path passed into the script
find $1 -iname packages.config | while read line; do
# find any csproj file next to the packages.config
project_path="$(dirname $line)/*.csproj"
# check it exists (e.g. to not error on a /.nuget/packages.config path)
if [ -f $project_path ]; then
# find the name of the assembly
# (our projects are not named with the company prefix, but the assemblies/packages are)
asm_name=$(grep -oP '<RootNamespace>\K(.*)(?=<)' $project_path)
# Ignore any tests projects (optional)
if [[ ${line} != *"Tests"* ]]; then
# find all lines in the packages.config where the package name has a prefix
grep -Po "package id=\"\K($NAME_MATCH.*?)(?=\")" $line | while read package; do
# write it to the result
echo " \"$asm_name\" -> \"$package\"" >> $RESULT_FILE
done
fi
fi
done
echo '}' >> $RESULT_FILE
To use this, you just need to call it with the path you want to visualise:
$ ./graph.sh /d/dev/projects/ledger
Note on the grep
usage I am using a non-capturing look behind (everything before \K
) and a non-capturing look ahead (the (?=\")
part), as if you just use a ’normal’ expression, the parts which match which I don’t care about also get outputed by grep. In C# I would have written the expression like this:
var packageName = Regex.Match(line, "package id=\"(.*?)\"").Groups[1].Value;
As an example, if I run this over my directory with all of the Ledger code in it, and filter out test dependencies (e.g. remove Shouldy, NSubstitute, Xunit), you get the following dot file:
digraph Dependencies {
rankdir=LR;
"Ledger.Acceptance" -> "Newtonsoft.Json"
"Ledger.Tests" -> "Newtonsoft.Json"
"Ledger.Tests" -> "RabbitMQ.Client"
"Ledger.Stores.Postgres" -> "Dapper"
"Ledger.Stores.Postgres" -> "Ledger"
"Ledger.Stores.Postgres" -> "Newtonsoft.Json"
"Ledger.Stores.Postgres" -> "Npgsql"
"Ledger.Stores.Postgres.Tests" -> "Dapper"
"Ledger.Stores.Postgres.Tests" -> "Ledger"
"Ledger.Stores.Postgres.Tests" -> "Ledger.Acceptance"
"Ledger.Stores.Postgres.Tests" -> "Newtonsoft.Json"
"Ledger.Stores.Postgres.Tests" -> "Npgsql"
"Ledger.Stores.Fs" -> "Ledger"
"Ledger.Stores.Fs" -> "Newtonsoft.Json"
"Ledger.Stores.Fs.Tests" -> "Ledger"
"Ledger.Stores.Fs.Tests" -> "Ledger.Acceptance"
"Ledger.Stores.Fs.Tests" -> "Newtonsoft.Json"
"Ledger.Stores.Fs.Tests" -> "structuremap"
}
Which renders into the following graph:
In the process of writing this though, I did have to go back into the projects and find out why the Ledger.Tests
was referencing RabbitMQ.Client
(example of appending events to a queue) and why Ledger.Stores.Fs.Tests
referened Structuremap
(it looks like I forgot to remove the reference after rewriting how Acceptance tests were setup).
The gist with all the code in can be found here: graph.sh.
Hope this is useful to others too!