All Downloads are FREE. Search and download functionalities are using the official Maven repository.

clojure.tools.deps.graph.clj Maven / Gradle / Ivy

The newest version!
;   Copyright (c) Rich Hickey. All rights reserved.
;   The use and distribution terms for this software are covered by the
;   Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php)
;   which can be found in the file epl-v10.html at the root of this distribution.
;   By using this software in any fashion, you are agreeing to be bound by
;   the terms of this license.
;   You must not remove this notice, or any other, from this software.

(ns clojure.tools.deps.graph
  (:require
    [clojure.edn :as edn]
    [clojure.java.io :as jio]
    [clojure.string :as str]
    [clojure.tools.cli :as cli]
    [clojure.tools.deps :as deps]
    [clojure.tools.deps.extensions :as ext]
    [clojure.tools.deps.script.parse :as parse]
    [clojure.tools.deps.util.session :as session]
    [clojure.tools.deps.util.io :as io :refer [printerrln]]
    [dorothy.core :as dot]
    [dorothy.jvm :as dotjvm]
    [clojure.string :as str])
  (:import
    [java.io IOException]
    [clojure.lang IExceptionInfo]))

(set! *warn-on-reflection* true)

(defn parse-syms
  "Parses a concatenated string of libs into a collection of symbols
  Ex: (parse-libs \"org.clojure/clojure,org.clojure/test.check\")
  Returns: [org.clojure/clojure org.clojure/test.check]"
  [s]
  (->> (str/split (or s "") #",")
    (remove str/blank?)
    (map symbol)))

;; Examples:
;;   no opts - read deps.edn, expand, and show deps image in viewer
;;   -o deps.png - read deps.edn, expand, and output deps image to deps.png
;;   -d mydeps.edn -o mydeps.png - read mydeps.edn, expand, and output deps image to mydeps.png
;;   -t -o trace.png - read deps.edn, trace expansion, output trace-100.png, ...
;;   -d mydeps.edn -t -o trace.png - read mydeps.edn, trace, output trace-100.png, ...
;;   -f trace.edn -o trace.png - read trace file, output trace-100.png, ...
;;   --size - include sizes in dep graph nodes

(def ^:private opts
  [;; input
   ["-d" "--deps DEPSFILE" "deps.edn file to read, default ./deps.edn" :default "deps.edn"]
   ;; trace mode
   ["-t" "--trace" "Trace mode, output one image per trace step"]
   ["-f" "--trace-file TRACEFILE" "Read trace directly from file, output one image per trace step"]
   ;; options
   ["-o" "--output FILE" "Save output file (or files if trace), don't show"]
   ["-a" "--aliases ALIASES" "Concatenated alias names to enable" :parse-fn parse/parse-kws]
   [nil "--trace-omit LIBS" "Comma delimited list of libs to omit in trace imgs"
    :default '[org.clojure/clojure]
    :parse-fn parse-syms]
   [nil "--size" "Include sizes in dep graph nodes"]])

(defn parse-opts
  "Parse the command line opts to make-classpath"
  [args]
  (cli/parse-opts args opts))

(defn node-id
  [lib]
  (if (= (ns lib) (name lib))
    (keyword (ns lib))
    (keyword lib)))

(defn make-node
  [id rows style-attrs]
  [id (merge {:shape :record
              :label (str/join "|" rows)
              :style :filled
              :color :black
              :fillcolor :lightgrey}
        style-attrs)])

(defn get-size-path
  [path]
  (let [f (jio/file path)]
    (if (.exists f)
      (if (.isFile f)
        (.length f)
        0) ;; TODO: sum dir size?
      0)))

(defn get-size
  [lib coord config]
  (let [{:deps/keys [manifest]} (ext/manifest-type lib coord config)
        paths (ext/coord-paths lib coord manifest config)]
    (->> paths (map get-size-path) (reduce +))))

(defn make-dep-node
  [lib coord config opts style-attrs]
  (let [id (node-id lib)
        summary (ext/coord-summary lib coord)
        space (str/index-of summary " ")
        rows [(subs summary 0 space)
              (subs summary (inc space))]
        rows (if (:size opts)
               (let [size (get-size lib coord config)]
                 (if (pos? size)
                   (conj rows (format "%10.1f kb" (/ size 1024.0)))
                   rows))
               rows)]
    (make-node id rows style-attrs)))

(defn make-edges
  [lib {:keys [dependents] :as coord}]
  (if (seq dependents)
    (map (fn [dlib] [(node-id dlib) (node-id lib)]) dependents)
    [[:root (node-id lib)]]))

(defn make-graph
  [lib-map config output opts]
  (let [statements (into [(make-node :root ["deps.edn"] {:shape :box :fillcolor :cadetblue1}) ]
                     (mapcat
                       (fn [[lib coord]]
                         (into [(make-dep-node lib coord config opts nil)]
                           (make-edges lib coord)))
                       lib-map))]
    ;(clojure.pprint/pprint statements)
    (let [d (dot/dot (dot/digraph (concat [{:rankdir :LR, :splines :polyline}] statements)))]
      (if output
        (dotjvm/save! d output {:format :png})
        (dotjvm/show! d)))))

(defn output-trace
  [trace output config trace-omit]
  (let [omitted-libs (set trace-omit)
        trace' (remove (fn [{:keys [lib include]}]
                         (and (not include) (contains? omitted-libs lib)))
                 trace)]
    (println "Writing" (inc (count trace')) "trace images, omitted" (inc (- (count trace) (count trace'))) "frames")
    (loop [[step & steps] trace'
           stmts [[:root {:label "deps.edn"
                          :shape :box
                          :fillcolor :cadetblue1
                          :style :filled}]]
           i 100]
      (if step
        (let [{:keys [lib coord use-coord path include reason vmap]} step
              nx (symbol (namespace lib) (str (name lib) "-CONSIDER"))
              dependee-id (if-let [parent (last path)] (node-id parent) :root)
              nx-stmts [(make-dep-node nx use-coord config nil {:fillcolor (if include :green2 :brown1)})
                        [dependee-id (node-id nx) {:label reason}]]]
          (print ".") (flush)
          (-> (dot/digraph (concat [{:rankdir :LR, :splines :polyline}] (into stmts nx-stmts)))
            dot/dot
            (dotjvm/save! (str output i ".png") {:format :png}))
          (recur steps
            (case reason
              ;; add new node and link from parent to it
              (:new-top-dep :new-dep)
              (into stmts [(make-dep-node lib use-coord config nil nil) [dependee-id (node-id lib)]])

              ;; add new node and remove previous node, link from parent to it
              :newer-version
              ;; todo: remove edges to dependents of old version
              ;; todo: remove then orphaned deps?
              (into (remove (fn [[id b]] (and (= id (node-id lib)) (not (keyword? b)))) stmts)
                [(make-dep-node lib use-coord config nil nil)
                 [dependee-id (node-id lib)]])

              ;; just link to existing node
              (:same-version :old-version :use-top)
              (into stmts [[dependee-id (node-id lib)]])

              ;; no change
              ;; (:excluded :parent-omitted)
              stmts)
            (inc i)))
        (do
          (println)
          (-> (dot/digraph (concat [{:rankdir :LR, :splines :polyline}] stmts))
            dot/dot
            (dotjvm/save! (str output i ".png") {:format :png})))))))

(defn- run
  [{:keys [deps trace trace-file output aliases trace-omit size] :as opts}]
  (try
    (if trace-file
      (do
        (when-not output (throw (ex-info "Must specify output file name in trace mode" {})))
        (let [tf (jio/file trace-file)]
          (if (.exists tf)
            (output-trace (-> tf slurp edn/read-string :log) nil output trace-omit)
            (throw (ex-info (str "Trace file does not exist: " trace-file) {})))))
      (let [{:keys [root-edn user-edn project-edn]} (deps/find-edn-maps (or deps "deps.edn"))
            master-edn (deps/merge-edns [root-edn user-edn project-edn])
            combined-aliases (deps/combine-aliases master-edn aliases)
            basis (session/with-session
                    (deps/calc-basis master-edn {:resolve-args (merge combined-aliases {:trace true})
                                                 :classpath-args combined-aliases}))
            lib-map (:libs basis)]
        (if trace
          (output-trace (-> lib-map meta :trace :log) output basis trace-omit)
          (make-graph lib-map basis output {:size size}))))
    (catch IOException e
      (if (str/starts-with? (.getMessage e) "Cannot run program")
        (throw (ex-info "tools.deps.graph requires Graphviz (https://graphviz.gitlab.io/download) to be installed to generate graphs." {} e))))))

(defn graph
  "Create deps graphs. By default reads deps.edn in current directory, creates deps graph,
  and shows using a viewer. Use ctrl-c to exit.

  Options:
    :deps Path to deps file (default = \"./deps.edn\")
    :trace Flag to use trace mode (default = false)
    :trace-file Path to trace.edn file to read
    :output Output file path
    :trace-omit collection of lib symbols to omit in trace mode (default = nil)
    :size Flag to include sizes in images (default = false)"
  [opts]
  (try
    (run opts)
    (catch Throwable t
      (printerrln (.getMessage t))
      (when-not (instance? IExceptionInfo t)
        (.printStackTrace t))
      (System/exit 1))
    (finally
      (shutdown-agents))))

(defn -main
  "Create deps graphs. By default, reads deps.edn in current directory, creates deps graph,
  and shows using a viewer. Use ctrl-c to exit.

  Options:
    -d DEPSFILE - deps.edn file to read, default=deps.edn
    -t - Trace mode, will output one image per expansion step
    -f TRACEFILE - Trace file mode - read trace file, don't use deps.edn file
    -o FILE - Output file, in trace mode required and will create N images
    -a - Concatenated alias names when reading deps file
    --trace-omit - Comma-delimited list of libs to skip in trace images
    --size - Include jar size in dep graph nodes"
  [& args]
  (let [{:keys [options errors]} (parse-opts args)]
    (when (seq errors)
      (run! println errors)
      (System/exit 1))
    (graph options)))




© 2015 - 2025 Weber Informatics LLC | Privacy Policy