Script that automates the generation of pretty dependency graphs from the output of nix-store -q --graph <package>
.
The above graphs show the dependency tree for Nix (top-left), for both SQLAlchemy and knex (top-right image), and for Git (bottom). The Nix dependency tree was generated with the following command
nix-visualize /nix/store/<hash>-nix-1.11.4 -c config.cfg -s nix -o nix.png
the database image was generated with
nix-visualize /nix/store/<hash>-python3.4-SQLAlchemy-1.0.15 /nix/store/<hash>-knex-0.8.6-nodejs-4.6.0 -c config.cfg -s dbs -o dbs.png
and the git image was generated with
nix-visualize /nix/store/<hash>-git-2.10.1 -c config.cfg -s git -o git.png
The configuration parameters to generate all of these images are provided in config.cfg
This project has a flake that outputs packages.${system}.nix-visualize
which is also the default package.
You can run it like this: nix run github:craigmbooth/nix-visualize -- <args>
The file default.nix in the root of this directory contains the definition for nix-visualize
.
So, for example, you could download the zip file of this repo and then unpack it, cd into it and run nix-build ./default.nix
, after which ./result/bin/nix-visualize
is available.
Install the prerequisites for the package. On Linux based distributions:
graphviz
graphviz-devel
(on CentOS, orgraphviz-dev
on Debian based distros)gcc
python-devel
(on CentOS, orpython-dev
on Debian based distros)tkinter
andtk-devel
You can then either download this repo and issue the command
python setup.py install
Or, the package is available on PyPI under the name nix-visualize
, so it can be pip installed
pip install nix-visualize
After installation, the minimal way to run the CLI is
nix-visualize <path-to-nix-store-object>
which will generate a graph of the dependency tree for the nix store object using sensible defaults for both appearance and graph layout. In order to override settings, use a configuration file in .ini format.
usage: visualize_tree.py [-h] [--configfile CONFIGFILE]
[--configsection CONFIGSECTION] [--output OUTPUT]
[--verbose] [--no-verbose]
packages [packages ...]
The command line options have the following meanings:
packages
: Add any number of positional arguments, specifying full paths to nix store objects. This packages will be graphed.--configfile
, or-c
: A configuration file in .ini format--configsection
, or-s
: If the configuration file contains more than one section, you must specify this option--output
, or-o
: The name of the output file (defaults to frame.png). Output filename extension determines the output format. Common supported formats include: png, jpg, pdf, and svg. For a full list of supported formats, see matplotlib.pyplot.savefig. In addition to matplotlib.pyplot.savefig supported output formats, the tool supports output in csv format to allow post-processing the output data. Specify output file with .csv extension to output the result in textual csv format.--verbose
: If this flag is present then print extra information to stdout.
If there is only a single section in the configuration file, it is only necessary to specify the --configfile
option. If the config file contains more than one section it is also necessary to specify --configsection
.
There
aspect_ratio [default 2.0]
: Ratio of x size of image to y sizedpi [default 300]
: pixels per inchimg_y_height_inches [default 24]
: size of the output image y dimension in inchesfont_scale [default 1.0]
: fonts are printed at size 12*font_scalecolor_scatter [default 1.0]
: The amount of randomness in the colors. If this is zero, all nodes on the same level are the same color.edge_color [default #888888]
: Hex code for color of lines linking nodesfont_color [default #888888]
: Hex code for color of font labeling nodesedge_alpha [default 0.3]
: Opacity of edges. 1.0 is fully opaque, 0.0 is transparentedge_width_scale [default 1.0]
: Factor by which to scale the width of the edgesshow_labels [default 1]
: If this is 0 then hide labelsy_sublevels [default 5]
: Number of discrete y-levels to use, see section on vertical positioningy_sublevel_spacing [default 0.2]
: Spacing between sublevels in units of the inter-level spacing. Typically you should avoid having y_sublevels*y_sublevel_spacing be greater than 1color_map [default rainbow]
: The name of a matplotlib colormap to usenum_iterations [default 100]
: Number of iterations to use in the horizontal position solvermax_displacement [default 2.5]
: The maximum distance a node can be moved in a single timesteprepulsive_force_normalization [default 2.0]
: Multiplicative factor for forces from nodes on the same level pushing each other apart. If your graph looks too bunched up, increase this numberattractive_force_normalization [default 1.0]
: Multiplicative factor for forces from nodes on the above level attracting their children. If your graph is too spread out, try increasing this numbermin_node_size [default 100.0]
: Minimum size of a node (node size denotes how many packages depend on it)add_size_per_out_link [default 200]
: For each package that depends on a given node, add this much sizemax_node_size_over_min_node_size [default 5.0]
: The maximum node size, in units of the minimum node sizetmax [default 30.0]
: Amount of time to integrate for. If your graph has not had time to settle down, increase this.
Packages are sorted vertically such that all packages are above everything that they depend upon, and horizontally so that they are close to their direct requirements, while not overlapping more than is necessary.
Since dependency trees are acyclic, it is possible to sort the tree so that every package appears below everything it depends on. The first step of the graph layout is to perform this sort, which I refer to in the code as adding "levels" to packages. The bottom of the tree, level n, consists of any packages that can be built without any external dependencies. The level above that, level n-1 contains any packages that can be built using only packages on level n. The level above that, n-2, contains any packages that can be built using only packages on levels n-1 and n. In this way, all packages on the tree sit above any of their dependencies, and the package we're diagramming out sits at the top of the tree.
In order to keep labels legible, after putting the packages on levels, some of them are given a small vertical offset. This is done by sorting each level by x-position, and then cycling
through sublevels and offsetting each node by an amount equal to y_sublevel_spacing
Initially the horizontal positions for packages are chosen randomly, but the structure of the underlying graph is made clearer if we try to optimize for two things:
- A package should be vertically aligned with the things it depends upon (i.e. the nodes on the level above it that it is linked to), in order to minimize edge crossing as far as possible
- A package should try not to be too close to another package on the same level, so as not to have nodes overlap.
This software was written at 37,000 feet. Thank you to American Airlines for putting me on a janky old plane for a 9 hour flight with no television.