├── .circleci └── config.yml ├── .github └── CODEOWNERS ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── ORIGINATOR ├── README.md ├── images ├── active-status-deps.png ├── conflicts.png ├── rook-deps.png ├── rook-focus.png └── rook-pruned.png ├── project.clj ├── sample-multiproject ├── archie │ └── project.clj ├── bravo │ └── project.clj ├── delta │ └── project.clj ├── gamma │ └── project.clj └── project.clj └── src ├── com └── walmartlabs │ └── vizdeps │ └── common.clj └── leiningen ├── vizconflicts.clj └── vizdeps.clj /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Clojure CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-clojure/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/clojure:lein-2.7.1 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/postgres:9.4 16 | 17 | working_directory: ~/repo 18 | 19 | environment: 20 | LEIN_ROOT: "true" 21 | # Customize the JVM maximum heap limit 22 | JVM_OPTS: -Xmx3200m 23 | 24 | steps: 25 | - checkout 26 | 27 | # Download and cache dependencies 28 | - restore_cache: 29 | keys: 30 | - v1-dependencies-{{ checksum "project.clj" }} 31 | # fallback to using the latest cache if no exact match is found 32 | - v1-dependencies- 33 | 34 | - run: lein deps 35 | 36 | - save_cache: 37 | paths: 38 | - ~/.m2 39 | key: v1-dependencies-{{ checksum "project.clj" }} 40 | 41 | # run tests! 42 | - run: lein test 43 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @hlship 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | .idea 13 | *.iml 14 | 15 | 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0 -- 13 Mar 2023 2 | 3 | Updated dependencies, and allowed `--focus` to be a regular expression, rather than just a substring match. 4 | 5 | [Closed Issues](https://github.com/clj-commons/vizdeps/milestone/1?closed=1) 6 | 7 | ## 0.1.7 -- 8 Mar 2018 8 | 9 | Add missing `tools.cli` dependency 10 | 11 | ## 0.1.6 -- 14 Mar 2017 12 | 13 | Update README and remove debugging output accidentally included in 0.1.5. 14 | 15 | ## 0.1.5 -- 14 Mar 2017 16 | 17 | Added --focus option to vizdeps task. 18 | 19 | ## 0.1.4 -- 8 Mar 2017 20 | 21 | [Closed Issues](https://github.com/walmartlabs/vizdeps/milestone/1?closed=1) 22 | 23 | Add support for :managed-dependencies. 24 | 25 | New task: vizconflicts, which iterates across the modules of a multi-module 26 | project, and creates a graph showing dependencies and versions for all 27 | artifact version conflicts. 28 | 29 | The vizdeps task now accepts the -p / --prune option, which is used 30 | when investigating version conflicts; --prune identifies all artifacts 31 | for which a version conflict exists, and removes from the diagram 32 | any artifacts that do not have version conflicts, or transitively depend 33 | on artifacts with version conflicts. 34 | 35 | The -H / --highlight option now supports multiple values. 36 | 37 | Edges (the lines that represent dependencies) are now weighted; 38 | highlighted (blue) edges are higher weight than normal, 39 | and version conflict (red) edges are even higher weight. Higher weight 40 | lines are generally straighter, with other nodes and edges moved out of 41 | the way. This improves clarity of complex dependency charts. 42 | 43 | In addition, highlight and version conflict edges are drawn slightly thicker, 44 | as are highlighted nodes. 45 | 46 | ## 0.1.3 -- 24 Feb 2017 47 | 48 | Group, module, and version each on their own line. 49 | Added -H / --highlight option to highlight an artifact and dependencies to it, in blue. 50 | 51 | ## 0.1.2 -- 17 Feb 2017 52 | 53 | Added the -s / --save-dot option. 54 | 55 | vizdeps now attempts to show all linkages for each dependency: 56 | if two dependencies A and B have a shared dependency C, then there 57 | will be arrows from A to C and B to C. Previously, there would be 58 | a single arrow, from either A to C or from B to C. 59 | 60 | Changed dependency versions are highlighted in red. 61 | For example, if A depends on `[C "0.1.1"]` and B depends on 62 | `[C "0.2.7"]` then the dependency from A to C will be marked in 63 | red and labeled "0.1.1" (this assumes the B dependency took 64 | precedence). 65 | 66 | `lein deps :tree` reports much the same information with its 67 | "possibly confusing dependencies found" message. 68 | 69 | ## 0.1.1 -- 29 Jul 2016 70 | 71 | Create the output folder if it does not already exist. 72 | 73 | ## 0.1.0 -- 29 Jul 2016 74 | 75 | Initial release. 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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, and 10 | distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 13 | owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all other entities 16 | that control, are controlled by, or are under common control with that entity. 17 | For the purposes of this definition, "control" means (i) the power, direct or 18 | indirect, to cause the direction or management of such entity, whether by 19 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 20 | outstanding shares, or (iii) beneficial ownership of such entity. 21 | 22 | "You" (or "Your") shall mean an individual or Legal Entity exercising 23 | permissions granted by this License. 24 | 25 | "Source" form shall mean the preferred form for making modifications, including 26 | but not limited to software source code, documentation source, and configuration 27 | files. 28 | 29 | "Object" form shall mean any form resulting from mechanical transformation or 30 | translation of a Source form, including but not limited to compiled object code, 31 | generated documentation, and conversions to other media types. 32 | 33 | "Work" shall mean the work of authorship, whether in Source or Object form, made 34 | available under the License, as indicated by a copyright notice that is included 35 | in or attached to the work (an example is provided in the Appendix below). 36 | 37 | "Derivative Works" shall mean any work, whether in Source or Object form, that 38 | is based on (or derived from) the Work and for which the editorial revisions, 39 | annotations, elaborations, or other modifications represent, as a whole, an 40 | original work of authorship. For the purposes of this License, Derivative Works 41 | shall not include works that remain separable from, or merely link (or bind by 42 | name) to the interfaces of, the Work and Derivative Works thereof. 43 | 44 | "Contribution" shall mean any work of authorship, including the original version 45 | of the Work and any modifications or additions to that Work or Derivative Works 46 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 47 | by the copyright owner or by an individual or Legal Entity authorized to submit 48 | on behalf of the copyright owner. For the purposes of this definition, 49 | "submitted" means any form of electronic, verbal, or written communication sent 50 | to the Licensor or its representatives, including but not limited to 51 | communication on electronic mailing lists, source code control systems, and 52 | issue tracking systems that are managed by, or on behalf of, the Licensor for 53 | the purpose of discussing and improving the Work, but excluding communication 54 | that is conspicuously marked or otherwise designated in writing by the copyright 55 | owner as "Not a Contribution." 56 | 57 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 58 | of whom a Contribution has been received by Licensor and subsequently 59 | incorporated within the Work. 60 | 61 | 2. Grant of Copyright License. 62 | 63 | Subject to the terms and conditions of this License, each Contributor hereby 64 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 65 | irrevocable copyright license to reproduce, prepare Derivative Works of, 66 | publicly display, publicly perform, sublicense, and distribute the Work and such 67 | Derivative Works in Source or Object form. 68 | 69 | 3. Grant of Patent License. 70 | 71 | Subject to the terms and conditions of this License, each Contributor hereby 72 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 73 | irrevocable (except as stated in this section) patent license to make, have 74 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 75 | such license applies only to those patent claims licensable by such Contributor 76 | that are necessarily infringed by their Contribution(s) alone or by combination 77 | of their Contribution(s) with the Work to which such Contribution(s) was 78 | submitted. If You institute patent litigation against any entity (including a 79 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 80 | Contribution incorporated within the Work constitutes direct or contributory 81 | patent infringement, then any patent licenses granted to You under this License 82 | for that Work shall terminate as of the date such litigation is filed. 83 | 84 | 4. Redistribution. 85 | 86 | You may reproduce and distribute copies of the Work or Derivative Works thereof 87 | in any medium, with or without modifications, and in Source or Object form, 88 | provided that You meet the following conditions: 89 | 90 | You must give any other recipients of the Work or Derivative Works a copy of 91 | this License; and 92 | You must cause any modified files to carry prominent notices stating that You 93 | changed the files; and 94 | You must retain, in the Source form of any Derivative Works that You distribute, 95 | all copyright, patent, trademark, and attribution notices from the Source form 96 | of the Work, excluding those notices that do not pertain to any part of the 97 | Derivative Works; and 98 | If the Work includes a "NOTICE" text file as part of its distribution, then any 99 | Derivative Works that You distribute must include a readable copy of the 100 | attribution notices contained within such NOTICE file, excluding those notices 101 | that do not pertain to any part of the Derivative Works, in at least one of the 102 | following places: within a NOTICE text file distributed as part of the 103 | Derivative Works; within the Source form or documentation, if provided along 104 | with the Derivative Works; or, within a display generated by the Derivative 105 | Works, if and wherever such third-party notices normally appear. The contents of 106 | the NOTICE file are for informational purposes only and do not modify the 107 | License. You may add Your own attribution notices within Derivative Works that 108 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 109 | provided that such additional attribution notices cannot be construed as 110 | modifying the License. 111 | You may add Your own copyright statement to Your modifications and may provide 112 | additional or different license terms and conditions for use, reproduction, or 113 | distribution of Your modifications, or for any such Derivative Works as a whole, 114 | provided Your use, reproduction, and distribution of the Work otherwise complies 115 | with the conditions stated in this License. 116 | 117 | 5. Submission of Contributions. 118 | 119 | Unless You explicitly state otherwise, any Contribution intentionally submitted 120 | for inclusion in the Work by You to the Licensor shall be under the terms and 121 | conditions of this License, without any additional terms or conditions. 122 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 123 | any separate license agreement you may have executed with Licensor regarding 124 | such Contributions. 125 | 126 | 6. Trademarks. 127 | 128 | This License does not grant permission to use the trade names, trademarks, 129 | service marks, or product names of the Licensor, except as required for 130 | reasonable and customary use in describing the origin of the Work and 131 | reproducing the content of the NOTICE file. 132 | 133 | 7. Disclaimer of Warranty. 134 | 135 | Unless required by applicable law or agreed to in writing, Licensor provides the 136 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 137 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 138 | including, without limitation, any warranties or conditions of TITLE, 139 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 140 | solely responsible for determining the appropriateness of using or 141 | redistributing the Work and assume any risks associated with Your exercise of 142 | permissions under this License. 143 | 144 | 8. Limitation of Liability. 145 | 146 | In no event and under no legal theory, whether in tort (including negligence), 147 | contract, or otherwise, unless required by applicable law (such as deliberate 148 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 149 | liable to You for damages, including any direct, indirect, special, incidental, 150 | or consequential damages of any character arising as a result of this License or 151 | out of the use or inability to use the Work (including but not limited to 152 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 153 | any and all other commercial damages or losses), even if such Contributor has 154 | been advised of the possibility of such damages. 155 | 156 | 9. Accepting Warranty or Additional Liability. 157 | 158 | While redistributing the Work or Derivative Works thereof, You may choose to 159 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 160 | other liability obligations and/or rights consistent with this License. However, 161 | in accepting such obligations, You may act only on Your own behalf and on Your 162 | sole responsibility, not on behalf of any other Contributor, and only if You 163 | agree to indemnify, defend, and hold each Contributor harmless for any liability 164 | incurred by, or claims asserted against, such Contributor by reason of your 165 | accepting any such warranty or additional liability. 166 | 167 | END OF TERMS AND CONDITIONS 168 | 169 | APPENDIX: How to apply the Apache License to your work 170 | 171 | To apply the Apache License to your work, attach the following boilerplate 172 | notice, with the fields enclosed by brackets "[]" replaced with your own 173 | identifying information. (Don't include the brackets!) The text should be 174 | enclosed in the appropriate comment syntax for the file format. We also 175 | recommend that a file or class name and description of purpose be included on 176 | the same "printed page" as the copyright notice for easier identification within 177 | third-party archives. 178 | 179 | Copyright [yyyy] [name of copyright owner] 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /ORIGINATOR: -------------------------------------------------------------------------------- 1 | @hlship 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # org.clj-commons/lein-vizdeps 2 | 3 | [![Clojars Project](https://img.shields.io/clojars/v/org.clj-commons/lein-vizdeps.svg)](https://clojars.org/org.clj-commons/lein-vizdeps) 4 | 5 | An alternative to `lein deps :tree` that uses [Graphviz](http://graphviz.org) to present 6 | a dependency diagram of all the artifacts (Maven-speak for "libraries") in your project. 7 | 8 | Here's an example of a relatively small project: 9 | 10 | ![active-status](images/active-status-deps.png) 11 | 12 | A single artifact may be 13 | a transitive dependency of multiple other artifacts. 14 | `vizdeps` can show this (`lein deps :tree` doesn't), and will highlight in red any dependencies 15 | with a version mismatch. 16 | This can make it *much* easier to identify version conflicts and provide the best 17 | exclusions and overrides. 18 | 19 | These dependency graphs can get large; using the `--vertical` option may make large 20 | trees more readable. 21 | 22 | ![rook](images/rook-deps.png) 23 | 24 | To keep the graph from getting any more cluttered, the `org.clojure/clojure` artifact 25 | is treated specially (just the dependency from the project root is shown). 26 | 27 | The `--prune` option is used when managing version conflicts; it removes uninteresting artifacts. 28 | Those that remain either have a version conflict (such as `commons-codec`, below) or 29 | transitively depend on an artifact with such a conflict: 30 | 31 | ![rook-purged](images/rook-pruned.png) 32 | 33 | Often, you are struck trying to track down why a specific artifact is included. 34 | In large projects, the Graphviz chart will often be complex enough that Graphviz will 35 | have difficulty finding a workable layout. 36 | Use the `-f` / `--focus` option to limit which artifacts are shown. 37 | For example, `lein vizdeps --vertical --focus jackson-core`: 38 | 39 | ![rook-focused](images/rook-focus.png) 40 | 41 | This limits the rendered artifacts to just those that match the specified focus artifact, 42 | or depend on such artifacts. 43 | 44 | ## Installation 45 | 46 | Put `[org.clj-commons/lein-vizdeps "1.0"]` into the `:plugins` vector of your `:user` 47 | profile. 48 | 49 | The plugin makes use of the `dot` command, part of Graphviz, 50 | which must be installed. 51 | On OS X, Graphviz can be installed using [Homebrew](https://brew.sh/): 52 | 53 | brew install graphviz 54 | 55 | On other platforms, Graphviz can be [downloaded](http://www.graphviz.org/Download.php). 56 | 57 | 58 | ## vizdeps task 59 | 60 | ``` 61 | Usage: lein vizdeps [options] 62 | 63 | Options: 64 | -d, --dev Include :dev dependencies in the graph. 65 | -f, --focus ARTIFACT Excludes artifacts whose names do not match a supplied value. Repeatable. 66 | -H, --highlight ARTIFACT Highlight the artifact, and any dependencies to it, in blue. Repeatable. 67 | -n, --no-view If given, the image will not be opened after creation. 68 | -o, --output-file FILE target/dependencies.pdf Output file path. Extension chooses format: pdf or png. 69 | -p, --prune Exclude artifacts and dependencies that do not involve version conflicts. 70 | -s, --save-dot Save the generated GraphViz DOT file as well as the output file. 71 | -v, --vertical Use a vertical, not horizontal, layout. 72 | -h, --help This usage summary. 73 | ``` 74 | 75 | The `--highlight` option can be repeated; any artifact that contains any of the provided strings will be highlighted. 76 | 77 | The `--focus` option allows you to mark some dependencies for inclusion; every artifact that does not match, or does not 78 | transitively depend on a marked artifact, is excluded. 79 | This is very useful when trying to work out how a specific artifact is transitively included. 80 | The value specified may be a regular expression. 81 | 82 | ## vizconflicts task 83 | 84 | ``` 85 | Usage: lein vizconflicts [options] 86 | 87 | Options: 88 | -o, --output-file FILE target/conflicts.pdf Output file path. Extension chooses format: pdf or png. 89 | -X, --exclude NAME Exclude any project whose name matches the value. Repeatable. 90 | -a, --artifact NAME If given, then only artifacts whose name matches are included. Repeatable. 91 | -s, --save-dot Save the generated GraphViz DOT file as well as the output file. 92 | -n, --no-view If given, the image will not be opened after creation. 93 | -h, --help This usage summary. 94 | ``` 95 | 96 | `vizconflicts` is used in concert with [lein-sub](https://github.com/kumarshantanu/lein-sub) to analyze 97 | dependencies between and across a multi-module project. 98 | `visconflicts` identifies all artifacts in use across all sub-modules, and identifies where different 99 | versions of the same artifact are used. 100 | The generated document includes a diagram for each artifact that has such version conflicts. 101 | 102 | For very large projects, the resulting diagram can be very large (even overwhelming Graphviz's 103 | ability to create a legible layout). 104 | Projects can be excluded, using the `--exclude` option. 105 | Alternately, you can focus on a subset of conflicting artifacts using the `--artifact` option. 106 | 107 | When different versions of the same artifact are in use, the output document will include a diagram of how that 108 | artifact is used across the different modules: 109 | 110 | ![conflicts](images/conflicts.png) 111 | 112 | The lines in each chart identify dependencies; solid lines are explicit dependencies, 113 | dotted lines are transitive dependencies. 114 | 115 | When one version of the artifact is the majority (based on total number of dependencies), 116 | it is highlighted in blue (and other versions are drawn in red). 117 | 118 | ## License 119 | 120 | Copyright © 2016-2023 Walmartlabs 121 | 122 | Distributed under the Apache Software License 2.0. 123 | -------------------------------------------------------------------------------- /images/active-status-deps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/vizdeps/f964a4441b26a509484cda1115d13fae5d561264/images/active-status-deps.png -------------------------------------------------------------------------------- /images/conflicts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/vizdeps/f964a4441b26a509484cda1115d13fae5d561264/images/conflicts.png -------------------------------------------------------------------------------- /images/rook-deps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/vizdeps/f964a4441b26a509484cda1115d13fae5d561264/images/rook-deps.png -------------------------------------------------------------------------------- /images/rook-focus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/vizdeps/f964a4441b26a509484cda1115d13fae5d561264/images/rook-focus.png -------------------------------------------------------------------------------- /images/rook-pruned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clj-commons/vizdeps/f964a4441b26a509484cda1115d13fae5d561264/images/rook-pruned.png -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject org.clj-commons/lein-vizdeps "1.0" 2 | :description "Visualize Leiningen project dependencies using Graphviz." 3 | :url "https://github.com/walmartlabs/vizdeps" 4 | :license {:name "Apache Sofware License 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0.html"} 6 | :dependencies [[org.clojure/clojure "1.11.1"] 7 | [org.clojure/tools.cli "1.0.214"] 8 | ;; Note: 0.0.7 breaks api we depend on: 9 | [dorothy "0.0.6"] 10 | [medley "1.4.0"] 11 | [com.stuartsierra/dependency "1.0.0"]] 12 | :eval-in-leiningen true) 13 | -------------------------------------------------------------------------------- /sample-multiproject/archie/project.clj: -------------------------------------------------------------------------------- 1 | (defproject sample/archie "0.1.0-SNAPSHOT" 2 | :dependencies [[org.clojure/tools.logging "0.3.1"] 3 | [commons-codec "1.10"]]) -------------------------------------------------------------------------------- /sample-multiproject/bravo/project.clj: -------------------------------------------------------------------------------- 1 | (defproject sample/bravo "0.1.0-SNAPSHOT" 2 | :plugins [[lein-parent "0.3.1"]] 3 | :parent-project {:path "../project.clj" 4 | :inherit [:managed-dependencies]} 5 | :dependencies [[com.stuartsierra/component] 6 | [ring/ring-core "1.5.0"] 7 | [commons-codec "1.6"]]) -------------------------------------------------------------------------------- /sample-multiproject/delta/project.clj: -------------------------------------------------------------------------------- 1 | (defproject sample/delta "0.1.0-SNAPSHOT" 2 | :plugins [[lein-parent "0.3.1"]] 3 | :parent-project {:path "../project.clj" 4 | :inherit [:managed-dependencies]} 5 | 6 | :dependencies [[sample/archie "0.1.0-SNAPSHOT"] 7 | [sample/bravo "0.1.0-SNAPSHOT"] 8 | [ring/ring-core "1.5.1"]]) -------------------------------------------------------------------------------- /sample-multiproject/gamma/project.clj: -------------------------------------------------------------------------------- 1 | (defproject sample/gamma "0.1.0-SNAPSHOT" 2 | :dependencies [[com.stuartsierra/component "0.3.1"] 3 | [ring/ring-core "1.5.1"]]) -------------------------------------------------------------------------------- /sample-multiproject/project.clj: -------------------------------------------------------------------------------- 1 | (defproject sample/big-project "0.1.0-SNAPSHOT" 2 | :plugins [[lein-sub "0.3.0"]] 3 | :managed-dependencies [[com.stuartsierra/component "0.3.2"]] 4 | :sub ["archie" "bravo" "gamma" "delta"]) -------------------------------------------------------------------------------- /src/com/walmartlabs/vizdeps/common.clj: -------------------------------------------------------------------------------- 1 | (ns com.walmartlabs.vizdeps.common 2 | (:require 3 | [clojure.string :as str] 4 | [clojure.java.io :as io] 5 | [dorothy.core :as d] 6 | [clojure.java.browse :refer [browse-url]] 7 | [clojure.tools.cli :refer [parse-opts]] 8 | [leiningen.core.classpath :as classpath] 9 | [leiningen.core.main :as main]) 10 | (:import (java.io File))) 11 | 12 | (defn gen-node-id 13 | "Create a unique string, based on the provided node-name (keyword, symbol, or string)." 14 | [node-name] 15 | (str (gensym (str (name node-name) "-")))) 16 | 17 | (defn ^:private allowed-extension 18 | [path] 19 | (let [x (str/last-index-of path ".") 20 | ext (subs path (inc x))] 21 | (#{"png" "pdf"} ext))) 22 | 23 | (defn graph-attrs 24 | [options] 25 | {:rankdir (if (:vertical options) :TD :LR)}) 26 | 27 | (def cli-help ["-h" "--help" "This usage summary."]) 28 | 29 | (def cli-save-dot ["-s" "--save-dot" "Save the generated GraphViz DOT file as well as the output file."]) 30 | 31 | (def cli-no-view 32 | ["-n" "--no-view" "If given, the image will not be opened after creation." 33 | :default false]) 34 | 35 | (defn cli-output-file 36 | [default-path] 37 | ["-o" "--output-file FILE" "Output file path. Extension chooses format: pdf or png." 38 | :id :output-path 39 | :default default-path 40 | :validate [allowed-extension "Supported output formats are 'pdf' and 'png'."]]) 41 | 42 | (def cli-vertical 43 | ["-v" "--vertical" "Use a vertical, not horizontal, layout."]) 44 | 45 | (defn conj-option 46 | "Used as :assoc-fn for an option to conj'es the values together." 47 | [m k v] 48 | (update m k conj v)) 49 | 50 | (defn ^:private usage 51 | [command summary errors] 52 | (->> [(str "Usage: lein " command " [options]") 53 | "" 54 | "Options:" 55 | summary] 56 | (str/join \newline) 57 | println) 58 | 59 | (when errors 60 | (println "\nErrors:") 61 | (doseq [e errors] (println " " e))) 62 | 63 | nil) 64 | 65 | (defn parse-cli-options 66 | "Parses the CLI options; handles --help and errors (returning nil) or just 67 | returns the parsed options." 68 | [command cli-options args] 69 | (let [{:keys [options errors summary]} (parse-opts args cli-options)] 70 | (if (or (:help options) errors) 71 | (usage command summary errors) 72 | options))) 73 | 74 | (defn write-files-and-view 75 | "Given a Graphviz document string (dot) and CLI options, write the file(s) and, optionally, 76 | open the output file." 77 | [dot options] 78 | (let [{:keys [output-path no-view]} options 79 | ^File output-file (io/file output-path) 80 | output-format (-> output-path allowed-extension keyword) 81 | output-dir (.getParentFile output-file)] 82 | 83 | (when output-dir 84 | (.mkdirs output-dir)) 85 | 86 | (when (:save-dot options) 87 | (let [x (str/last-index-of output-path ".") 88 | dot-path (str (subs output-path 0 x) ".dot") 89 | ^File dot-file (io/file dot-path)] 90 | (spit dot-file dot))) 91 | 92 | (main/debug "Create output diagram") 93 | 94 | (d/save! dot output-file {:format output-format}) 95 | 96 | (when-not no-view 97 | (browse-url output-path))) 98 | 99 | nil) 100 | 101 | (defn ^:private build-dependency-map 102 | "Consumes a hierarchy and produces a map from artifact to version, used to identify 103 | which dependency linkages have had their version changed." 104 | ([hierarchy] 105 | (build-dependency-map {} hierarchy)) 106 | ([version-map hierarchy] 107 | (reduce-kv (fn [m dep sub-hierarchy] 108 | (-> m 109 | (assoc (first dep) dep) 110 | (build-dependency-map sub-hierarchy))) 111 | version-map 112 | hierarchy))) 113 | 114 | (defn flatten-dependencies 115 | [project] 116 | "Resolves dependencies for the project and returns a map from artifact 117 | symbol to artifact coord vector." 118 | (-> (classpath/managed-dependency-hierarchy :dependencies :managed-dependencies 119 | project) 120 | build-dependency-map)) 121 | 122 | (defn matches-any 123 | "Creates a predicate that is true when the provided elem (string, symbol, keyword) 124 | matches any of the provided string labels, each of which can be a regular expression 125 | (string, re-pattern)" 126 | [labels] 127 | (let [patterns (map re-pattern labels)] 128 | (fn [elem] 129 | (let [elem-label (str elem)] 130 | (some #(re-find % elem-label) patterns))))) 131 | -------------------------------------------------------------------------------- /src/leiningen/vizconflicts.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.vizconflicts 2 | "Graphviz visualization of conflicts in a multi-module project." 3 | (:require 4 | [com.walmartlabs.vizdeps.common :as common 5 | :refer [gen-node-id]] 6 | [medley.core :refer [map-vals remove-vals filter-vals filter-keys]] 7 | [clojure.pprint :refer [pprint]] 8 | [leiningen.core.project :as project] 9 | [dorothy.core :as d] 10 | [leiningen.core.main :as main] 11 | [clojure.string :as str])) 12 | 13 | (defn ^:private projects-map 14 | "Generates a map from sub module name (a symbol) to initialized project. 15 | 16 | The --exclude option will prevent a module from being read at all, as if it were 17 | not present. This can be useful to simplify the conflict diagram by omitting 18 | modules that are used just for testing, for example." 19 | [root-project options] 20 | (let [{:keys [exclude]} options] 21 | (reduce (fn [m module-name] 22 | (main/info "Reading project" module-name) 23 | (let [project (-> (str module-name "/project.clj") 24 | project/read 25 | project/init-project) 26 | {project-name :name 27 | project-group :group} project 28 | artifact-symbol (symbol project-group project-name)] 29 | (assoc m artifact-symbol project))) 30 | {} 31 | (cond->> (:sub root-project) 32 | (seq exclude) (remove (common/matches-any exclude)))))) 33 | 34 | (defn ^:private artifact-versions-map 35 | [options project->artifact->version-map] 36 | (let [{:keys [artifact]} options 37 | result (reduce-kv (fn [m project-name artifact-versions] 38 | (reduce-kv (fn [m artifact-name artifact-version] 39 | (update-in m [artifact-name artifact-version] conj project-name)) 40 | m 41 | artifact-versions)) 42 | {} 43 | project->artifact->version-map)] 44 | (cond->> result 45 | (seq artifact) (filter-keys (common/matches-any artifact))))) 46 | 47 | (defn ^:private no-conflicts? 48 | [version->project-map] 49 | (-> version->project-map count (< 2))) 50 | 51 | (defn ^:private to-label 52 | ([artifact-symbol] 53 | (to-label artifact-symbol nil)) 54 | ([artifact-symbol version] 55 | (let [sym-ns (namespace artifact-symbol)] 56 | (apply str sym-ns 57 | (when sym-ns 58 | "/\n") 59 | (name artifact-symbol) 60 | (when version "\n") 61 | version)))) 62 | 63 | (defn ^:private direct-dependency? 64 | [projects project-name artifact-symbol] 65 | (some (fn [dep] 66 | (= artifact-symbol (first dep))) 67 | (-> projects (get project-name) :dependencies))) 68 | 69 | (defn ^:private add-project-node 70 | "Returns a tuple of graph and node id." 71 | [graph project-name] 72 | (if-let [node-id (get-in graph [:project-node-ids project-name])] 73 | [graph node-id] 74 | (let [new-node-id (gen-node-id project-name) 75 | project-node {:label (to-label project-name) 76 | :shape :doubleoctagon} 77 | graph' (-> graph 78 | (assoc-in [:project-node-ids project-name] new-node-id) 79 | (assoc-in [:nodes new-node-id] project-node))] 80 | [graph' new-node-id]))) 81 | 82 | (defn ^:private add-project-to-artifact-edges 83 | [graph artifact-symbol version->project-map projects majority-version] 84 | (reduce-kv (fn [g-1 version project-names] 85 | (let [artifact-node-id (gen-node-id artifact-symbol) 86 | majority? (and majority-version 87 | (= version majority-version)) 88 | minority? (and majority-version 89 | (not= version majority-version)) 90 | artifact-node (cond-> {:label (to-label artifact-symbol version)} 91 | minority? (assoc :color :red 92 | :fontcolor :red) 93 | majority? (assoc :color :blue 94 | :fontcolor :blue))] 95 | (reduce (fn [g-2 project-name] 96 | (let [[graph' project-node-id] (add-project-node g-2 project-name) 97 | direct? (direct-dependency? projects project-name artifact-symbol) 98 | options (cond-> {} 99 | (not direct?) (assoc :style :dotted) 100 | minority? (assoc :color :red) 101 | majority? (assoc :color :blue)) 102 | edge [project-node-id artifact-node-id options]] 103 | (update graph' :edges conj edge))) 104 | (assoc-in g-1 [:nodes artifact-node-id] artifact-node) 105 | project-names))) 106 | graph 107 | version->project-map)) 108 | 109 | (defn ^:private add-project-to-project-edges 110 | [graph version->project-map projects] 111 | (let [{:keys [project-node-ids]} graph 112 | involved-projects (into #{} 113 | (apply concat (vals version->project-map))) 114 | project-dependencies (reduce (fn [m project-name] 115 | (assoc m project-name 116 | (into #{} 117 | (->> (get-in projects [project-name :dependencies]) 118 | (map first) 119 | (keep involved-projects))))) 120 | {} 121 | involved-projects) 122 | edges (for [[from-project-name dependencies] project-dependencies 123 | :let [from-node-id (get project-node-ids from-project-name)] 124 | to-project-name dependencies] 125 | [from-node-id (get project-node-ids to-project-name)])] 126 | (update graph :edges concat edges))) 127 | 128 | (defn ^:private identify-majority-version 129 | [version->project-map] 130 | (let [counts (->> version->project-map 131 | vals 132 | (map count)) 133 | total-count (reduce + counts) 134 | max-count (apply max counts)] 135 | (when (< 0.5 (/ max-count total-count)) 136 | (->> version->project-map 137 | (filter-vals #(-> % count (= max-count))) 138 | keys 139 | first)))) 140 | 141 | (defn ^:private node-graph 142 | [options projects artifact->versions-map] 143 | (when (empty? artifact->versions-map) 144 | (main/info "No artifact version conflicts detected.")) 145 | (let [base-graph {:nodes {} 146 | :project-node-ids {}}] 147 | (reduce-kv (fn [statements artifact-symbol version->project-map] 148 | (main/info (format "Artifact %s, %d versions: %s" 149 | (str artifact-symbol) 150 | (count version->project-map) 151 | (->> version->project-map 152 | keys 153 | sort 154 | (map pr-str) 155 | (str/join ", ")) [])) 156 | (let [majority-version (identify-majority-version version->project-map) 157 | graph (-> base-graph 158 | (add-project-to-artifact-edges artifact-symbol version->project-map projects majority-version) 159 | (add-project-to-project-edges version->project-map projects))] 160 | (conj statements 161 | (d/subgraph (gen-node-id (str "cluster_" (name artifact-symbol))) 162 | [(merge (common/graph-attrs options) 163 | {:label (str artifact-symbol \newline 164 | (->> version->project-map 165 | keys 166 | sort 167 | (str/join " -- ")))}) 168 | (-> graph :nodes seq) 169 | (:edges graph)])))) 170 | [{:rankdir :LR}] 171 | artifact->versions-map))) 172 | 173 | 174 | (def ^:private cli-options 175 | [(common/cli-output-file "target/conflicts.pdf") 176 | ["-X" "--exclude NAME" "Exclude any project whose name matches the value. Repeatable." 177 | :assoc-fn common/conj-option] 178 | ["-a" "--artifact NAME" "If given, then only artifacts whose name matches are included. Repeatable." 179 | :assoc-fn common/conj-option] 180 | common/cli-save-dot 181 | common/cli-no-view 182 | common/cli-help]) 183 | 184 | (defn vizconflicts 185 | "Identify and visualize dependency conflicts across a multi-module project." 186 | {:pass-through-help true} 187 | [project & args] 188 | (when-let [options (common/parse-cli-options "vizconflicts" cli-options args)] 189 | (let [projects (projects-map project options) 190 | dot (->> projects 191 | (map-vals common/flatten-dependencies) 192 | ;; Reduce the inner maps to symbol -> version number string 193 | (map-vals #(map-vals second %)) 194 | (artifact-versions-map options) 195 | (remove-vals no-conflicts?) 196 | (into (sorted-map)) 197 | (node-graph options projects) 198 | d/digraph 199 | d/dot)] 200 | (common/write-files-and-view dot options) 201 | (main/info "Wrote conflicts chart to:" (:output-path options))))) 202 | -------------------------------------------------------------------------------- /src/leiningen/vizdeps.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.vizdeps 2 | "Graphviz visualization of project dependencies." 3 | (:require 4 | [com.walmartlabs.vizdeps.common :as common 5 | :refer [gen-node-id]] 6 | [com.stuartsierra.dependency :as dep] 7 | [leiningen.core.main :as main] 8 | [dorothy.core :as d] 9 | [leiningen.core.classpath :as classpath] 10 | [leiningen.core.project :as project] 11 | [clojure.string :as str])) 12 | 13 | (defn ^:private artifact->label 14 | [artifact] 15 | {:pre [artifact]} 16 | (let [{:keys [artifact-name version]} artifact 17 | ^String group (some-> artifact-name namespace name) 18 | ^String module (name artifact-name)] 19 | (str group 20 | (when group "/\n") 21 | module 22 | \newline 23 | version))) 24 | 25 | (defn ^:private normalize-artifact 26 | [dependency] 27 | (let [artifact (first dependency)] 28 | (if-not (= (namespace artifact) (name artifact)) 29 | dependency 30 | (assoc dependency 0 (symbol (name artifact)))))) 31 | 32 | (defn ^:private immediate-dependencies 33 | [project dependency] 34 | (if (some? dependency) 35 | (-> (#'classpath/get-dependencies 36 | :dependencies nil 37 | (assoc project :dependencies [dependency])) 38 | (get dependency) 39 | ;; Tracking dependencies on Clojure itself overwhelms the graph 40 | (as-> $ 41 | (remove #(= 'org.clojure/clojure (first %)) 42 | $)) 43 | vec) 44 | [])) 45 | 46 | (declare ^:private add-dependency-tree) 47 | 48 | (defn ^:private add-dependency-node 49 | [dependency-graph artifact-name artifact-version dependencies] 50 | (reduce (fn [g dep] 51 | (let [[dep-name dep-version] dep 52 | g-1 (add-dependency-tree g dep-name dep-version)] 53 | (if-let [dep-artifact (get-in g-1 [:artifacts dep-name])] 54 | (let [dep-map {:artifact-name dep-name 55 | :version dep-version 56 | :conflict? (not= dep-version 57 | (:version dep-artifact))}] 58 | (update-in g-1 [:artifacts artifact-name :deps] conj dep-map)) 59 | ;; If the artifact is excluded, the dependency graph will 60 | ;; not contain the artifact. 61 | g-1))) 62 | ;; Start with a new node for the artifact 63 | (assoc-in dependency-graph 64 | [:artifacts artifact-name] 65 | {:artifact-name artifact-name 66 | :version artifact-version 67 | :node-id (gen-node-id artifact-name) 68 | :deps []}) 69 | dependencies)) 70 | 71 | (defn ^:private add-dependency-tree 72 | [dependency-graph artifact-name artifact-version] 73 | (let [[_ resolved-version :as resolved-dependency] (get-in dependency-graph [:dependencies artifact-name]) 74 | ;; When using managed dependencies, the version (from :dependencies) may be nil, 75 | ;; so subtitute the version from the resolved dependency in that case. 76 | version (or artifact-version resolved-version) 77 | artifact (get-in dependency-graph [:artifacts artifact-name])] 78 | (main/debug (format "Processing %s %s" 79 | (str artifact-name) version)) 80 | 81 | (cond 82 | 83 | (nil? resolved-dependency) 84 | (do 85 | (main/debug "Skipping excluded artifact") 86 | dependency-graph) 87 | 88 | ;; Has the artifact already been added? 89 | (some? artifact) 90 | dependency-graph 91 | 92 | :else 93 | (add-dependency-node dependency-graph 94 | artifact-name 95 | resolved-version 96 | ;; Find the dependencies of the resolved (not requested) artifact 97 | ;; and version. Recursively add those artifacts to the graph 98 | ;; and set up dependencies. 99 | (immediate-dependencies (:project dependency-graph) 100 | resolved-dependency))))) 101 | 102 | (defn ^:private dependency-order 103 | "Returns the artifact names in dependency order." 104 | [artifacts] 105 | (let [tuples (for [artifact (vals artifacts) 106 | dep (:deps artifact)] 107 | [(:artifact-name artifact) 108 | (:artifact-name dep)]) 109 | graph (reduce (fn [g [artifact-name dependency-name]] 110 | (dep/depend g artifact-name dependency-name)) 111 | (dep/graph) 112 | tuples)] 113 | (dep/topo-sort graph))) 114 | 115 | (defn ^:private prune-artifacts 116 | "Navigates the nodes to identify dependencies that include conflicts. 117 | Marks nodes that are referenced with conflicts, then marks any nodes that 118 | have a dependency to that node as well. The root node is always kept; 119 | other unmarked nodes are culled." 120 | [artifacts] 121 | (main/debug "Pruning artifacts") 122 | (let [order (dependency-order artifacts) 123 | mark-graph (fn [artifacts artifact-name] 124 | (assoc-in artifacts [artifact-name :conflict?] true)) 125 | get-transitives (fn [artifacts artifact] 126 | (->> artifact 127 | :deps 128 | (filter #(->> % 129 | :artifact-name 130 | artifacts 131 | ;; May be nil here, when an earlier processed artifact 132 | ;; was culled. Otherwise, check if the :conflict? flag 133 | ;; was set on the artifact. 134 | :conflict?)))) 135 | marked-graph (reduce (fn [artifacts-1 artifact-name] 136 | (->> (artifacts-1 artifact-name) 137 | :deps 138 | (filter :conflict?) 139 | (map :artifact-name) 140 | (reduce mark-graph artifacts-1))) 141 | artifacts 142 | order)] 143 | (reduce (fn [artifacts-1 artifact-name] 144 | (let [artifact (artifacts-1 artifact-name) 145 | ;; Get transitive dependencies to conflict artifacts (dropping 146 | ;; dependencies to non-conflict artifacts, if any). 147 | transitives (get-transitives artifacts-1 artifact) 148 | keep? (or (:conflict? artifact) 149 | (:root? artifact) 150 | (seq transitives))] 151 | (if keep? 152 | (assoc artifacts-1 artifact-name 153 | (assoc artifact 154 | :conflict? true 155 | :deps transitives)) 156 | ;; Otherwise we don't need this artifact at all 157 | (dissoc artifacts-1 artifact-name)))) 158 | marked-graph 159 | order))) 160 | 161 | 162 | (defn ^:private highlight-artifacts 163 | [artifacts highlight-terms] 164 | (let [highlight-set (->> artifacts 165 | keys 166 | (filter (common/matches-any highlight-terms)) 167 | set) 168 | artifacts-highlighted (reduce (fn [m artifact-name] 169 | (assoc-in m [artifact-name :highlight?] true)) 170 | artifacts 171 | highlight-set) 172 | ;; Now, find dependencies that target highlighted artifacts 173 | ;; and mark them as highlighted as well. 174 | add-highlight (fn [dep] 175 | (if (-> dep :artifact-name highlight-set) 176 | (assoc dep :highlight? true) 177 | dep))] 178 | (reduce-kv (fn [artifacts-3 artifact-name artifact] 179 | (assoc artifacts-3 artifact-name 180 | (update artifact :deps 181 | #(map add-highlight %)))) 182 | {} 183 | artifacts-highlighted))) 184 | 185 | (defn ^:private apply-focus 186 | "Identify a number of artifacts that match a focus term. Only keep such artifacts, and those 187 | that transitively depend on them." 188 | [artifacts focus-terms] 189 | (let [focus-set (->> artifacts 190 | keys 191 | (filter (common/matches-any focus-terms)) 192 | set) 193 | keep-focus (fn [artifacts dep] 194 | (-> artifacts 195 | (get (:artifact-name dep)) 196 | :focused?)) 197 | reducer (fn [m artifact-name] 198 | (let [artifact (get artifacts artifact-name) 199 | focus-deps (->> artifact 200 | :deps 201 | (filter #(keep-focus m %)))] 202 | (if (or (focus-set artifact-name) 203 | (seq focus-deps)) 204 | (assoc m artifact-name 205 | (assoc artifact :focused? true 206 | :deps focus-deps)) 207 | m)))] 208 | (reduce reducer 209 | {} 210 | (dependency-order artifacts)))) 211 | 212 | (defn ^:private artifacts-map 213 | "Builds a map from artifact name (symbol) to an artifact record, with keys 214 | :artifact-name, :version, :node-id, :highlight?, :focus?, :conflict?, :root? and 215 | :deps. 216 | 217 | Each :dep has keys :artifact-name, :version, :conflict?, and :highlight?." 218 | [project options] 219 | (let [profiles (if-not (:dev options) 220 | [:user] 221 | [:user :dev]) 222 | {:keys [:prune highlight focus]} options 223 | project' (project/set-profiles project profiles) 224 | root-artifact-name (symbol (-> project :group str) (-> project :name str)) 225 | root-dependency [root-artifact-name (:version project)] 226 | dependency-map (common/flatten-dependencies project') 227 | root-dependencies (->> project' 228 | :dependencies 229 | (map normalize-artifact))] 230 | (-> (add-dependency-node {:artifacts {} 231 | :project project 232 | :dependencies dependency-map} 233 | root-artifact-name 234 | (:version project) 235 | root-dependencies) 236 | ;; Just need the artifacts from here out 237 | :artifacts 238 | ;; Ensure the root artifact is drawn properly and never pruned 239 | (assoc-in [root-artifact-name :root?] true) 240 | (cond-> 241 | prune 242 | prune-artifacts 243 | 244 | (seq focus) 245 | (apply-focus focus) 246 | 247 | (seq highlight) 248 | (highlight-artifacts highlight))))) 249 | 250 | (defn ^:private node-graph 251 | [artifacts options] 252 | (concat 253 | [(d/graph-attrs (common/graph-attrs options))] 254 | ;; All nodes: 255 | (for [artifact (vals artifacts)] 256 | [(:node-id artifact) 257 | (cond-> {:label (artifact->label artifact)} 258 | (:root? artifact) 259 | (assoc :shape :doubleoctagon) 260 | 261 | (:highlight? artifact) 262 | (assoc :color :blue 263 | :penwidth 2 264 | :fontcolor :blue))]) 265 | 266 | ;; Now, all edges: 267 | (for [artifact (vals artifacts) 268 | :let [node-id (:node-id artifact)] 269 | dep (:deps artifact)] 270 | [node-id 271 | (get-in artifacts [(:artifact-name dep) :node-id]) 272 | (cond-> {} 273 | (:highlight? dep) 274 | (assoc :color :blue 275 | :penwidth 2 276 | :weight 100) 277 | 278 | (:conflict? dep) 279 | (assoc :color :red 280 | :penwidth 2 281 | :weight 500 282 | :label (:version dep)))]))) 283 | 284 | (defn ^:private build-dot 285 | [project options] 286 | (-> (artifacts-map project options) 287 | (node-graph options) 288 | d/digraph 289 | d/dot)) 290 | 291 | (def ^:private cli-options 292 | [["-d" "--dev" "Include :dev dependencies in the graph."] 293 | ["-f" "--focus ARTIFACT" "Excludes artifacts whose names do not match a supplied value. Repeatable." 294 | :assoc-fn common/conj-option] 295 | ["-H" "--highlight ARTIFACT" "Highlight the artifact, and any dependencies to it, in blue. Repeatable." 296 | :assoc-fn common/conj-option] 297 | common/cli-no-view 298 | (common/cli-output-file "target/dependencies.pdf") 299 | ["-p" "--prune" "Exclude artifacts and dependencies that do not involve version conflicts."] 300 | common/cli-save-dot 301 | common/cli-vertical 302 | common/cli-help]) 303 | 304 | (defn vizdeps 305 | "Visualizes dependencies using Graphviz. 306 | 307 | Normally, this will generate an image and raise a frame to view it. 308 | Command line options allow the image to be written to a file instead." 309 | {:pass-through-help true} 310 | [project & args] 311 | (when-let [options (common/parse-cli-options "vizdeps" cli-options args)] 312 | (let [dot (build-dot project options)] 313 | (common/write-files-and-view dot options) 314 | (main/info "Wrote dependency chart to:" (:output-path options))))) 315 | --------------------------------------------------------------------------------