├── .gitignore ├── LICENSE.txt ├── README.md ├── config.cfg ├── default.nix ├── flake.lock ├── flake.nix ├── images ├── dbs.png ├── git.png ├── horizontal.png ├── levels.png ├── levels.xcf ├── nix.png └── sublevels.png ├── nix ├── derivation.nix └── packages.nix ├── nix_visualize ├── __init__.py ├── graph_objects.py ├── util.py └── visualize_tree.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | result/ 11 | 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # IPython Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | venv/ 85 | ENV/ 86 | 87 | # Spyder project settings 88 | .spyderproject 89 | 90 | # Rope project settings 91 | .ropeproject 92 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nix Dependency Visualizer 2 | 3 | Script that automates the generation of pretty dependency graphs from the output of ``nix-store -q --graph ``. 4 | 5 | ## Example Images 6 | 7 | 8 | 9 | 10 | The above graphs show the dependency tree for Nix (top-left), for both [SQLAlchemy](http://www.sqlalchemy.org/) and [knex](http://knexjs.org/) (top-right image), and for Git (bottom). The Nix dependency tree was generated with the following command 11 | 12 | nix-visualize /nix/store/-nix-1.11.4 -c config.cfg -s nix -o nix.png 13 | 14 | the database image was generated with 15 | 16 | nix-visualize /nix/store/-python3.4-SQLAlchemy-1.0.15 /nix/store/-knex-0.8.6-nodejs-4.6.0 -c config.cfg -s dbs -o dbs.png 17 | 18 | and the git image was generated with 19 | 20 | nix-visualize /nix/store/-git-2.10.1 -c config.cfg -s git -o git.png 21 | 22 | The configuration parameters to generate all of these images are provided in [config.cfg](config.cfg) 23 | 24 | ## Installation 25 | 26 | ### Nix Installation (preferred) 27 | 28 | #### Flakes 29 | 30 | This project has a flake that outputs `packages.${system}.nix-visualize` which is also the default package. 31 | 32 | You can run it like this: `nix run github:craigmbooth/nix-visualize -- ` 33 | 34 | #### Traditional 35 | 36 | The file [default.nix](default.nix) in the root of this directory contains the definition for `nix-visualize`. 37 | 38 | So, for example, you could [download the zip file of this repo](https://github.com/craigmbooth/nix-visualize/archive/master.zip) and then unpack it, cd into it and run `nix-build ./default.nix`, after which `./result/bin/nix-visualize` is available. 39 | 40 | ### Non-Nix Installation 41 | 42 | Install the prerequisites for the package. On Linux based distributions: 43 | 44 | * `graphviz` 45 | * `graphviz-devel` (on CentOS, or `graphviz-dev` on Debian based distros) 46 | * `gcc` 47 | * `python-devel` (on CentOS, or `python-dev` on Debian based distros) 48 | * `tkinter` and `tk-devel` 49 | 50 | You can then either download this repo and issue the command 51 | 52 | python setup.py install 53 | 54 | Or, the package is available [on PyPI](https://pypi.python.org/pypi/nix_visualize/) under the name `nix-visualize`, so it can be pip installed 55 | 56 | pip install nix-visualize 57 | 58 | ## Command Line Options 59 | 60 | After installation, the minimal way to run the CLI is 61 | 62 | nix-visualize 63 | 64 | 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. 65 | 66 | usage: visualize_tree.py [-h] [--configfile CONFIGFILE] 67 | [--configsection CONFIGSECTION] [--output OUTPUT] 68 | [--verbose] [--no-verbose] 69 | packages [packages ...] 70 | 71 | The command line options have the following meanings: 72 | 73 | * `packages`: Add any number of positional arguments, specifying full paths to nix store objects. This packages will be graphed. 74 | * `--configfile`, or `-c`: A configuration file in .ini format 75 | * `--configsection`, or `-s`: If the configuration file contains more than one section, you must specify this option 76 | * `--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](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html). In addition to [matplotlib.pyplot.savefig](https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.savefig.html) 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. 77 | * `--verbose`: If this flag is present then print extra information to stdout. 78 | 79 | ## Configuration Files 80 | 81 | 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``. 82 | 83 | There 84 | 85 | ### List of parameters 86 | 87 | * `aspect_ratio [default 2.0]`: Ratio of x size of image to y size 88 | * `dpi [default 300]`: pixels per inch 89 | * `img_y_height_inches [default 24]`: size of the output image y dimension in inches 90 | * `font_scale [default 1.0]`: fonts are printed at size 12*font_scale 91 | * `color_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. 92 | * `edge_color [default #888888]`: Hex code for color of lines linking nodes 93 | * `font_color [default #888888]`: Hex code for color of font labeling nodes 94 | * `edge_alpha [default 0.3]`: Opacity of edges. 1.0 is fully opaque, 0.0 is transparent 95 | * `edge_width_scale [default 1.0]`: Factor by which to scale the width of the edges 96 | * `show_labels [default 1]`: If this is 0 then hide labels 97 | * `y_sublevels [default 5]`: Number of discrete y-levels to use, see section on vertical positioning 98 | * `y_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 1 99 | * `color_map [default rainbow]`: The name of a [matplotlib colormap](http://matplotlib.org/examples/color/colormaps_reference.html) to use 100 | * `num_iterations [default 100]`: Number of iterations to use in the horizontal position solver 101 | * `max_displacement [default 2.5]`: The maximum distance a node can be moved in a single timestep 102 | * `repulsive_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 number 103 | * `attractive_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 number 104 | * `min_node_size [default 100.0]`: Minimum size of a node (node size denotes how many packages depend on it) 105 | * `add_size_per_out_link [default 200]`: For each package that depends on a given node, add this much size 106 | * `max_node_size_over_min_node_size [default 5.0]`: The maximum node size, in units of the minimum node size 107 | * `tmax [default 30.0]`: Amount of time to integrate for. If your graph has not had time to settle down, increase this. 108 | 109 | ## Graph Layout Algorithm 110 | 111 | 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. 112 | 113 | ### Vertical Positioning 114 | 115 | 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. 116 | 117 | 118 | 119 | #### Adding vertical offsets 120 | 121 | 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`` 122 | 123 | 124 | 125 | ### Horizontal Positioning 126 | 127 | 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: 128 | 129 | 1. 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 130 | 2. A package should try not to be too close to another package on the same level, so as not to have nodes overlap. 131 | 132 | 133 | 134 | ## Credits 135 | 136 | 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. 137 | -------------------------------------------------------------------------------- /config.cfg: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------ 2 | # Example configurations for nix-visualize 3 | #------------------------------------------------------------------------------ 4 | # n.b. All parameters are defined in the README 5 | 6 | [nix] 7 | # The settings used to generate the Nix dependency tree in the README 8 | aspect_ratio: 1 9 | font_scale: 0.6 10 | font_color: #000000 11 | img_y_height_inches: 6 12 | dpi: 300 13 | color_map: autumn 14 | min_node_size: 75 15 | max_node_size_over_min_node_size: 5.0 16 | add_size_per_out_link: 50 17 | 18 | [dbs] 19 | # The settings used to generate the SQLAlchemy and knex dependency tree 20 | # in the README 21 | aspect_ratio: 2 22 | font_scale: 0.32 23 | font_color: #000000 24 | img_y_height_inches: 6 25 | dpi: 300 26 | color_map: Accent 27 | color_scatter: 0.0 28 | top_level_spacing: 80 29 | min_node_size: 30 30 | max_node_size_over_min_node_size: 3.0 31 | add_size_per_out_link: 20 32 | max_displacement: 25.5 33 | num_iterations: 2500 34 | tmax: 800.0 35 | y_sublevels: 6 36 | y_sublevel_spacing: 0.12 37 | repulsive_force_normalization: 8.0 38 | 39 | [git] 40 | # The settings used to generate the Git dependency tree in the README 41 | color_map: summer_r 42 | aspect_ratio: 3 43 | min_node_size: 1000 44 | max_node_size_over_min_node_size: 3.0 45 | add_size_per_out_link: 50 46 | tmax: 600 47 | edge_width_scale: 3.0 48 | font_scale: 2.0 49 | y_sublevel_spacing: 0 50 | n_iterations: 20000 51 | attractive_force_normalization: 3.0 52 | repulsive_force_normaliztion: 3.0 -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | ( 2 | import 3 | ( 4 | let 5 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 6 | in 7 | fetchTarball { 8 | url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 9 | sha256 = lock.nodes.flake-compat.locked.narHash; 10 | } 11 | ) 12 | {src = ./.;} 13 | ) 14 | .defaultNix 15 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "locked": { 5 | "lastModified": 1696426674, 6 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 7 | "owner": "edolstra", 8 | "repo": "flake-compat", 9 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "edolstra", 14 | "repo": "flake-compat", 15 | "type": "github" 16 | } 17 | }, 18 | "flake-parts": { 19 | "inputs": { 20 | "nixpkgs-lib": "nixpkgs-lib" 21 | }, 22 | "locked": { 23 | "lastModified": 1701473968, 24 | "narHash": "sha256-YcVE5emp1qQ8ieHUnxt1wCZCC3ZfAS+SRRWZ2TMda7E=", 25 | "owner": "hercules-ci", 26 | "repo": "flake-parts", 27 | "rev": "34fed993f1674c8d06d58b37ce1e0fe5eebcb9f5", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "hercules-ci", 32 | "repo": "flake-parts", 33 | "type": "github" 34 | } 35 | }, 36 | "nixpkgs": { 37 | "locked": { 38 | "lastModified": 1703438236, 39 | "narHash": "sha256-aqVBq1u09yFhL7bj1/xyUeJjzr92fXVvQSSEx6AdB1M=", 40 | "owner": "NixOS", 41 | "repo": "nixpkgs", 42 | "rev": "5f64a12a728902226210bf01d25ec6cbb9d9265b", 43 | "type": "github" 44 | }, 45 | "original": { 46 | "owner": "NixOS", 47 | "ref": "nixos-unstable", 48 | "repo": "nixpkgs", 49 | "type": "github" 50 | } 51 | }, 52 | "nixpkgs-lib": { 53 | "locked": { 54 | "dir": "lib", 55 | "lastModified": 1701253981, 56 | "narHash": "sha256-ztaDIyZ7HrTAfEEUt9AtTDNoCYxUdSd6NrRHaYOIxtk=", 57 | "owner": "NixOS", 58 | "repo": "nixpkgs", 59 | "rev": "e92039b55bcd58469325ded85d4f58dd5a4eaf58", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "dir": "lib", 64 | "owner": "NixOS", 65 | "ref": "nixos-unstable", 66 | "repo": "nixpkgs", 67 | "type": "github" 68 | } 69 | }, 70 | "root": { 71 | "inputs": { 72 | "flake-compat": "flake-compat", 73 | "flake-parts": "flake-parts", 74 | "nixpkgs": "nixpkgs" 75 | } 76 | } 77 | }, 78 | "root": "root", 79 | "version": 7 80 | } 81 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Description for the project"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | flake-parts.url = "github:hercules-ci/flake-parts"; 7 | flake-compat.url = "github:edolstra/flake-compat"; 8 | }; 9 | 10 | outputs = inputs @ {flake-parts, ...}: 11 | flake-parts.lib.mkFlake {inherit inputs;} { 12 | imports = [ 13 | nix/packages.nix 14 | ]; 15 | systems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin"]; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /images/dbs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmbooth/nix-visualize/5b9beae330ac940df56433d347494505e2038904/images/dbs.png -------------------------------------------------------------------------------- /images/git.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmbooth/nix-visualize/5b9beae330ac940df56433d347494505e2038904/images/git.png -------------------------------------------------------------------------------- /images/horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmbooth/nix-visualize/5b9beae330ac940df56433d347494505e2038904/images/horizontal.png -------------------------------------------------------------------------------- /images/levels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmbooth/nix-visualize/5b9beae330ac940df56433d347494505e2038904/images/levels.png -------------------------------------------------------------------------------- /images/levels.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmbooth/nix-visualize/5b9beae330ac940df56433d347494505e2038904/images/levels.xcf -------------------------------------------------------------------------------- /images/nix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmbooth/nix-visualize/5b9beae330ac940df56433d347494505e2038904/images/nix.png -------------------------------------------------------------------------------- /images/sublevels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmbooth/nix-visualize/5b9beae330ac940df56433d347494505e2038904/images/sublevels.png -------------------------------------------------------------------------------- /nix/derivation.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | buildPythonPackage, 4 | matplotlib, 5 | networkx, 6 | pygraphviz, 7 | pandas, 8 | self, 9 | }: 10 | buildPythonPackage { 11 | pname = "nix-visualize"; 12 | version = "1.0.5"; 13 | src = lib.cleanSource self; 14 | 15 | propagatedBuildInputs = [ 16 | matplotlib 17 | networkx 18 | pygraphviz 19 | pandas 20 | ]; 21 | } 22 | -------------------------------------------------------------------------------- /nix/packages.nix: -------------------------------------------------------------------------------- 1 | {self, ...}: { 2 | perSystem = { 3 | pkgs, 4 | self', 5 | ... 6 | }: { 7 | packages.default = self'.packages.nix-visualize; 8 | packages.nix-visualize = pkgs.python3Packages.callPackage ./derivation.nix {inherit self;}; 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /nix_visualize/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigmbooth/nix-visualize/5b9beae330ac940df56433d347494505e2038904/nix_visualize/__init__.py -------------------------------------------------------------------------------- /nix_visualize/graph_objects.py: -------------------------------------------------------------------------------- 1 | """Definitions for edges and nodes for Nix dependency visualizer""" 2 | 3 | import os 4 | from . import util 5 | 6 | class Edge(object): 7 | """Class represents the relationship between two packages.""" 8 | 9 | def __init__(self, node_from, node_to): 10 | self.nfrom_raw=node_from 11 | self.nto_raw=node_to 12 | self.nfrom = os.path.basename(self.nfrom_raw) 13 | self.nto = os.path.basename(self.nto_raw) 14 | 15 | def __repr__(self): 16 | return "{} -> {}".format(self.nfrom, self.nto) 17 | 18 | 19 | class Node(object): 20 | """Class represents an individual package""" 21 | 22 | def __init__(self, name): 23 | self.raw_name = name 24 | self.children = [] 25 | self.parents = [] 26 | self.in_degree = 0 27 | self.out_degree = 0 28 | self.level = -1 29 | 30 | self.x = 0 31 | self.y = 0 32 | 33 | def add_parent(self, nfrom, nto): 34 | self.parents.append(nto) 35 | self.out_degree = len(self.parents) 36 | 37 | def add_child(self, nfrom, nto): 38 | self.children.append(nfrom) 39 | self.in_degree = len(self.children) 40 | 41 | def add_level(self): 42 | """Add the Node's level. Level is this package's position in the 43 | hierarchy. 0 is the top-level package. That package's dependencies 44 | are level 1, their dependencies are level 2. 45 | """ 46 | 47 | if self.level >= 0: 48 | return self.level 49 | elif len(self.parents) > 0: 50 | parent_levels = [p.add_level() for p in self.parents] 51 | self.level = max(parent_levels) + 1 52 | return self.level 53 | else: 54 | self.level = 0 55 | return 0 56 | 57 | def __repr__(self): 58 | return util.remove_nix_hash(self.raw_name) 59 | 60 | def __hash__(self): 61 | """A package is uniquely identified by its name""" 62 | return hash((self.raw_name,)) 63 | 64 | def __eq__(self, other): 65 | if isinstance(other, self.__class__): 66 | return self.raw_name == other.raw_name 67 | else: 68 | return False 69 | 70 | def to_dict(self): 71 | """Return Node as dictionary""" 72 | return { 73 | 'raw_name': self.raw_name, 74 | 'level': self.level, 75 | } 76 | -------------------------------------------------------------------------------- /nix_visualize/util.py: -------------------------------------------------------------------------------- 1 | """Miscellaneous utilities for the Nix tree visualizer""" 2 | 3 | class TreeCLIError(Exception): 4 | """Exception to be raised by the CLI""" 5 | pass 6 | 7 | 8 | def remove_nix_hash(string): 9 | """Given a nix store name of the form -, remove 10 | the hash 11 | """ 12 | return "-".join(string.split("-")[1:]) 13 | 14 | def clamp(n, maxabs): 15 | """Clamp a number to be between -maxabs and maxabs""" 16 | return max(-maxabs, min(n, maxabs)) 17 | -------------------------------------------------------------------------------- /nix_visualize/visualize_tree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | """Script that visualizes dependencies of Nix packages""" 4 | import argparse 5 | import configparser 6 | import itertools 7 | import os 8 | import random 9 | import shlex 10 | import subprocess 11 | import sys 12 | import tempfile 13 | import logging 14 | import csv 15 | 16 | import pandas as pd 17 | import networkx as nx 18 | import pygraphviz as pgv 19 | import matplotlib 20 | matplotlib.use('Agg') 21 | import matplotlib.pyplot as plt 22 | import warnings 23 | warnings.filterwarnings("ignore") 24 | 25 | 26 | from . import util 27 | from .graph_objects import Node, Edge 28 | 29 | logger = logging.getLogger(__name__) 30 | 31 | #: Default values for things we expect in the config file 32 | CONFIG_OPTIONS = { 33 | "aspect_ratio": (2, float), 34 | "dpi": (300, int), 35 | "font_scale": (1.0, float), 36 | "color_scatter": (1.0, float), 37 | "edge_color": ("#888888", str), 38 | "font_color": ("#888888", str), 39 | "color_map": ("rainbow", str), 40 | "img_y_height_inches": (24, float), 41 | "y_sublevels": (5, int), 42 | "y_sublevel_spacing": (0.2, float), 43 | "num_iterations": (100, int), 44 | "edge_alpha": (0.3, float), 45 | "edge_width_scale": (1.0, float), 46 | "max_displacement": (2.5, float), 47 | "top_level_spacing": (100, float), 48 | "repulsive_force_normalization": (2.0, float), 49 | "attractive_force_normalization": (1.0, float), 50 | "add_size_per_out_link": (200, int), 51 | "max_node_size_over_min_node_size": (5.0, float), 52 | "min_node_size": (100.0, float), 53 | "tmax": (30.0, float), 54 | "show_labels": (1, int) 55 | } 56 | 57 | def _is_csv_out(filename): 58 | _fname, extension = os.path.splitext(filename) 59 | fileformat = extension[1:] 60 | return bool(fileformat == "csv") 61 | 62 | class Graph(object): 63 | """Class representing a dependency tree""" 64 | 65 | def __init__(self, packages, config, output_file, do_write=True): 66 | """Initialize a graph from the result of a nix-store command""" 67 | 68 | csv_out = _is_csv_out(output_file) 69 | if csv_out: 70 | logger.info("CSV output: skip parsing visualization config file") 71 | else: 72 | self.config = self._parse_config(config) 73 | 74 | self.nodes = [] 75 | self.edges = [] 76 | 77 | self.root_package_names = [os.path.basename(x) for x in packages] 78 | 79 | for package in packages: 80 | # Run nix-store -q --graph . This generates a graphviz 81 | # file with package dependencies 82 | cmd = ("nix-store -q --graph {}".format(package)) 83 | res = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, 84 | stderr=subprocess.PIPE) 85 | 86 | stdout, stderr = res.communicate() 87 | 88 | if res.returncode != 0: 89 | raise util.TreeCLIError("nix-store call failed, message " 90 | "{}".format(stderr)) 91 | 92 | package_nodes, package_edges = self._get_edges_and_nodes(stdout) 93 | 94 | self.nodes.extend(package_nodes) 95 | self.edges.extend(package_edges) 96 | 97 | self.nodes = list(set(self.nodes)) 98 | 99 | self._add_edges_to_nodes() 100 | 101 | # The package itself is level 0, its direct dependencies are 102 | # level 1, their direct dependencies are level 2, etc. 103 | for n in self.nodes: 104 | n.add_level() 105 | 106 | self.depth = max([x.level for x in self.nodes]) + 1 107 | 108 | logger.info("Graph has {} nodes, {} edges and a depth of {}".format( 109 | len(self.nodes), len(self.edges), self.depth)) 110 | 111 | # Transform the Nodes and Edges into a networkx graph 112 | self.G = nx.DiGraph() 113 | for node in self.nodes: 114 | self.G.add_node(node) 115 | for parent in node.parents: 116 | self.G.add_edge(node, parent) 117 | 118 | if csv_out: 119 | self._output_csv(output_file) 120 | else: 121 | self._add_pos_to_nodes() 122 | if do_write is True: 123 | self.write_frame_image(filename=output_file) 124 | 125 | def _output_csv(self, filename): 126 | df = pd.DataFrame.from_records([node.to_dict() for node in self.nodes]) 127 | df.sort_values(by=["level"], ascending=False, inplace=True) 128 | df.to_csv( 129 | path_or_buf=filename, 130 | quoting=csv.QUOTE_ALL, 131 | sep=",", 132 | index=False, 133 | encoding="utf-8" 134 | ) 135 | logger.info("Wrote: %s", filename) 136 | 137 | def _parse_config(self, config, verbose=True): 138 | """Load visualization parameters from config file or take defaults 139 | if they are not in there 140 | """ 141 | 142 | configfile = config[0] 143 | configsection = config[1] 144 | 145 | return_configs = {} 146 | 147 | if configfile is not None: 148 | configs = configparser.ConfigParser() 149 | configs.read(configfile) 150 | if len(configs.sections()) > 1: 151 | if configsection is None: 152 | raise util.TreeCLIError("Config file {} contains more than " 153 | "one section, so -s must be set".format( 154 | configfile)) 155 | elif configsection not in configs.sections(): 156 | raise util.TreeCLIError("Config file {} does not contain a " 157 | "section named {}".format( 158 | configfile, configsection)) 159 | else: 160 | # There is only one section in the file, just read it 161 | configsection = configs.sections()[0] 162 | else: 163 | logger.info("--configfile not set, using all defaults") 164 | return {k: v[0] for k, v in CONFIG_OPTIONS.items()} 165 | 166 | logger.info("Reading section [{}] of file {}".format(configsection, 167 | configfile)) 168 | # Loop through config options. If there is a corresponding key in the 169 | # config file, overwrite, else take the value from the defaults 170 | for param, (p_default, p_dtype) in CONFIG_OPTIONS.items(): 171 | try: 172 | return_configs[param] = p_dtype( 173 | configs.get(configsection, param)) 174 | logger.debug("Setting {} to {}".format(param, 175 | return_configs[param])) 176 | except (configparser.NoOptionError, ValueError): 177 | return_configs[param] = p_dtype(p_default) 178 | logger.info( "Adding default of {} for {}".format( 179 | p_dtype(p_default), param)) 180 | 181 | return return_configs 182 | 183 | def write_frame_image(self, filename="nix-tree.png"): 184 | """Dump the graph to an image file""" 185 | 186 | try: 187 | cmap = getattr(matplotlib.cm, self.config["color_map"]) 188 | except AttributeError: 189 | raise util.TreeCLIError("Colormap {} does not exist".format( 190 | self.config["color_map"])) 191 | 192 | pos = {n: (n.x, n.y) for n in self.nodes} 193 | col_scale = 255.0/(self.depth+1.0) 194 | col = [(x.level+random.random()*self.config["color_scatter"])*col_scale 195 | for x in self.G.nodes()] 196 | col = [min([x,255]) for x in col] 197 | 198 | img_y_height=self.config["img_y_height_inches"] 199 | 200 | size_min = self.config["min_node_size"] 201 | size_max = self.config["max_node_size_over_min_node_size"] * size_min 202 | 203 | plt.figure(1, figsize=(img_y_height*self.config["aspect_ratio"], 204 | img_y_height)) 205 | node_size = [min(size_min + (x.out_degree-1)* 206 | self.config["add_size_per_out_link"], 207 | size_max) if x.level > 0 else size_max for 208 | x in self.G.nodes()] 209 | 210 | # Draw edges 211 | nx.draw(self.G, pos, node_size=node_size, arrows=False, 212 | with_labels=self.config["show_labels"], 213 | edge_color=self.config["edge_color"], 214 | font_size=12*self.config["font_scale"], 215 | node_color=col, vmin=0, vmax=256, 216 | width=self.config["edge_width_scale"], 217 | alpha=self.config["edge_alpha"], nodelist=[]) 218 | 219 | # Draw nodes 220 | nx.draw(self.G, pos, node_size=node_size, arrows=False, 221 | with_labels=self.config["show_labels"], 222 | font_size=12*self.config["font_scale"], 223 | node_color=col, vmin=0, vmax=255, edgelist=[], 224 | font_weight="light", cmap=cmap, 225 | font_color=self.config["font_color"]) 226 | 227 | logger.info("Writing image file: {}".format(filename)) 228 | plt.savefig(filename, dpi=self.config["dpi"]) 229 | plt.close() 230 | 231 | def _add_pos_to_nodes(self): 232 | """Populates every node with an x an y position using the following 233 | iterative algorithm: 234 | 235 | * start at t=0 236 | * Apply an x force to each node that is proportional to the offset 237 | between its x position and the average position of its parents 238 | * Apply an x force to each node that pushes it away from its siblings 239 | with a force proportional to 1/d, where d is the distance between 240 | the node and its neighbor 241 | * advance time forward by dt=tmax/num_iterations, displace particles 242 | by F*dt 243 | * repeat until the number of iterations has been exhausted 244 | """ 245 | 246 | logger.info("Adding positions to nodes") 247 | 248 | #: The distance between levels in arbitrary units. Used to set a 249 | #: scale on the diagram 250 | level_height = 10 251 | 252 | #: Maximum displacement of a point on a single iteration 253 | max_displacement = level_height * self.config["max_displacement"] 254 | 255 | #: The timestep to take on each iteration 256 | dt = self.config["tmax"]/self.config["num_iterations"] 257 | 258 | number_top_level = len([x for x in self.nodes if x.level == 0]) 259 | 260 | count_top_level = 0 261 | # Initialize x with a random position unless you're the top level 262 | # package, then space nodes evenly 263 | for n in self.nodes: 264 | if n.level == 0: 265 | n.x = float(count_top_level)*self.config["top_level_spacing"] 266 | count_top_level += 1 267 | n.y = self.depth * level_height 268 | else: 269 | n.x = ((number_top_level + 1) * 270 | self.config["top_level_spacing"] * random.random()) 271 | 272 | for iternum in range(self.config["num_iterations"]): 273 | if iternum in range(0,self.config["num_iterations"], 274 | int(self.config["num_iterations"]/10)): 275 | logger.debug("Completed iteration {} of {}".format(iternum, 276 | self.config["num_iterations"])) 277 | total_abs_displacement = 0.0 278 | 279 | for level in range(1, self.depth): 280 | 281 | # Get the y-offset by cycling with other nodes in the 282 | # same level 283 | xpos = [(x.raw_name, x.x) for x in self.level(level)] 284 | xpos = sorted(xpos, key=lambda x:x[1]) 285 | xpos = zip(xpos, 286 | itertools.cycle(range(self.config["y_sublevels"]))) 287 | pos_sorter = {x[0][0]: x[1] for x in xpos} 288 | 289 | for n in self.level(level): 290 | n.y = ((self.depth - n.level) * level_height + 291 | pos_sorter[n.raw_name] * 292 | self.config["y_sublevel_spacing"]*level_height) 293 | 294 | 295 | for lev_node in self.level(level): 296 | # We pull nodes toward their parents 297 | dis = [parent.x - lev_node.x for 298 | parent in lev_node.parents] 299 | 300 | # And push nodes away from their siblings with force 1/r 301 | sibs = self.level(level) 302 | sdis = [1.0/(sib.x - lev_node.x) for 303 | sib in sibs if abs(sib.x-lev_node.x) > 1e-3] 304 | 305 | total_sdis = ( 306 | sum(sdis) * 307 | self.config["repulsive_force_normalization"]) 308 | total_displacement = ( 309 | self.config["attractive_force_normalization"] * 310 | float(sum(dis)) / len(dis)) 311 | 312 | # Limit each of the displacements to the max displacement 313 | dx_parent = util.clamp(total_displacement, max_displacement) 314 | lev_node.dx_parent = dx_parent 315 | 316 | dx_sibling = util.clamp(total_sdis, max_displacement) 317 | lev_node.dx_sibling = -dx_sibling 318 | 319 | for lev_node in self.level(level): 320 | lev_node.x += lev_node.dx_parent * dt 321 | lev_node.x += lev_node.dx_sibling * dt 322 | total_abs_displacement += (abs(lev_node.dx_parent * dt) + 323 | abs(lev_node.dx_sibling * dt)) 324 | 325 | def level(self, level): 326 | """Return a list of all nodes on a given level 327 | """ 328 | return [x for x in self.nodes if x.level == level] 329 | 330 | def levels(self, min_level=0): 331 | """An iterator over levels, yields all the nodes in each level""" 332 | for i in range(min_level,self.depth): 333 | yield self.level(i) 334 | 335 | def _get_edges_and_nodes(self, raw_lines): 336 | """Transform a raw GraphViz file into Node and Edge objects. Note 337 | that at this point the nodes and edges are not linked into a graph 338 | they are simply two lists of items.""" 339 | 340 | tempf = tempfile.NamedTemporaryFile(delete=False) 341 | tempf.write(raw_lines) 342 | tempf.close() 343 | G = pgv.AGraph(tempf.name) 344 | 345 | all_edges = [] 346 | all_nodes = [] 347 | 348 | for node in G.nodes(): 349 | if (node.name not in [n.raw_name for n in all_nodes]): 350 | all_nodes.append(Node(node.name)) 351 | 352 | for edge in G.edges(): 353 | all_edges.append(Edge(edge[0], edge[1])) 354 | 355 | return all_nodes, all_edges 356 | 357 | def _add_edges_to_nodes(self): 358 | """Given the lists of Edges and Nodes, add parents and children to 359 | nodes by following each edge 360 | """ 361 | 362 | for edge in self.edges: 363 | nfrom = [n for n in self.nodes if n.raw_name == edge.nfrom] 364 | nto = [n for n in self.nodes if n.raw_name == edge.nto] 365 | nfrom = nfrom[0] 366 | nto = nto[0] 367 | 368 | if nfrom.raw_name == nto.raw_name: 369 | # Disallow self-references 370 | continue 371 | 372 | if nto not in nfrom.parents: 373 | nfrom.add_parent(nfrom, nto) 374 | if nfrom not in nto.children: 375 | nto.add_child(nfrom, nto) 376 | 377 | def __repr__(self): 378 | """Basic print of Graph, show the package name and the number of 379 | dependencies on each level 380 | """ 381 | head = self.level(0) 382 | ret_str = "Graph of package: {}".format(head[0].raw_name) 383 | for ilevel, level in enumerate(self.levels(min_level=1)): 384 | ret_str += "\n\tOn level {} there are {} packages".format( 385 | ilevel+1, len(level)) 386 | return ret_str 387 | 388 | 389 | def init_logger(debug=False): 390 | """Sets up logging for this cli""" 391 | log_level = logging.DEBUG if debug else logging.INFO 392 | logging.basicConfig(format="%(levelname)s %(message)s\033[1;0m", 393 | stream=sys.stderr, level=log_level) 394 | logging.addLevelName(logging.CRITICAL, 395 | "\033[1;37m[\033[1;31mCRIT\033[1;37m]\033[0;31m") 396 | logging.addLevelName(logging.ERROR, 397 | "\033[1;37m[\033[1;33mERR \033[1;37m]\033[0;33m") 398 | logging.addLevelName(logging.WARNING, 399 | "\033[1;37m[\033[1;33mWARN\033[1;37m]\033[0;33m") 400 | logging.addLevelName(logging.INFO, 401 | "\033[1;37m[\033[1;32mINFO\033[1;37m]\033[0;37m") 402 | logging.addLevelName(logging.DEBUG, 403 | "\033[1;37m[\033[1;34mDBUG\033[1;37m]\033[0;34m") 404 | 405 | 406 | def main(): 407 | """Parse command line arguments, instantiate graph and dump image""" 408 | parser = argparse.ArgumentParser() 409 | parser.add_argument("packages", 410 | help="Full path to a package in the Nix store. " 411 | "This package will be diagrammed", nargs='+') 412 | parser.add_argument("--configfile", "-c", help="ini file with layout and " 413 | "style configuration", required=False) 414 | parser.add_argument("--configsection", "-s", help="section from ini file " 415 | "to read") 416 | parser.add_argument("--output", "-o", help="output filename, " 417 | "default is 'frame.png'. " 418 | "Output filename extension determines the output " 419 | "format. Common supported formats include: " 420 | "png, jpg, pdf, and svg. For a full list of " 421 | "supported formats, see " 422 | "matplotlib.pyplot.savefig() documentation. " 423 | "In addition to savefig() supported output " 424 | "formats, the tool supports output in csv to " 425 | "allow post-processing the output data. Specify " 426 | "output file with .csv extension to output the " 427 | "result in textual csv format." 428 | , default="frame.png", required=False) 429 | parser.add_argument('--verbose', dest='verbose', action='store_true') 430 | parser.add_argument('--no-verbose', dest='verbose', action='store_false') 431 | parser.set_defaults(verbose=False) 432 | args = parser.parse_args() 433 | 434 | init_logger(debug=args.verbose) 435 | 436 | try: 437 | graph = Graph(args.packages, (args.configfile, args.configsection), 438 | args.output) 439 | except util.TreeCLIError as e: 440 | sys.stderr.write("ERROR: {}\n".format(e.message)) 441 | sys.exit(1) 442 | 443 | 444 | if __name__ == "__main__": 445 | main() 446 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import setuptools 3 | import sys 4 | 5 | PACKAGE_NAME = "nix_visualize" 6 | VERSION = "1.0.5" 7 | setuptools.setup( 8 | name=PACKAGE_NAME, 9 | version=VERSION, 10 | packages=[PACKAGE_NAME], 11 | description="CLI to automate generation of pretty Nix dependency trees", 12 | author="Craig Booth", 13 | author_email="craigmbooth@gmail.com", 14 | url="https://github.com/craigmbooth/nix-dependency-visualizer", 15 | download_url = "https://github.com/craigmbooth/nix-dependency-visualizer/tarball/"+VERSION, 16 | keywords=["nix", "matplotlib"], 17 | classifiers=[], 18 | install_requires=[ 19 | "matplotlib>=1.5", 20 | "networkx>=1.11", 21 | "pygraphviz>=1.3", 22 | "pandas>=1.4" 23 | ], 24 | data_files = [], 25 | entry_points={"console_scripts": [ 26 | "nix-visualize=nix_visualize.visualize_tree:main" 27 | ]} 28 | ) 29 | --------------------------------------------------------------------------------