graph.scm 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. ;;; GNU Guix --- Functional package management for GNU
  2. ;;; Copyright © 2015-2016, 2020-2022 Ludovic Courtès <ludo@gnu.org>
  3. ;;; Copyright © 2016 Ricardo Wurmus <rekado@elephly.net>
  4. ;;;
  5. ;;; This file is part of GNU Guix.
  6. ;;;
  7. ;;; GNU Guix is free software; you can redistribute it and/or modify it
  8. ;;; under the terms of the GNU General Public License as published by
  9. ;;; the Free Software Foundation; either version 3 of the License, or (at
  10. ;;; your option) any later version.
  11. ;;;
  12. ;;; GNU Guix is distributed in the hope that it will be useful, but
  13. ;;; WITHOUT ANY WARRANTY; without even the implied warranty of
  14. ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. ;;; GNU General Public License for more details.
  16. ;;;
  17. ;;; You should have received a copy of the GNU General Public License
  18. ;;; along with GNU Guix. If not, see <http://www.gnu.org/licenses/>.
  19. (define-module (guix graph)
  20. #:use-module (guix store)
  21. #:use-module (guix monads)
  22. #:use-module (guix records)
  23. #:use-module (guix sets)
  24. #:autoload (guix diagnostics) (formatted-message)
  25. #:autoload (guix i18n) (G_)
  26. #:use-module (srfi srfi-1)
  27. #:use-module (srfi srfi-9)
  28. #:use-module (srfi srfi-26)
  29. #:use-module (srfi srfi-34)
  30. #:use-module (ice-9 match)
  31. #:use-module (ice-9 string-fun)
  32. #:use-module (ice-9 vlist)
  33. #:export (node-type
  34. node-type?
  35. node-type-identifier
  36. node-type-label
  37. node-type-edges
  38. node-type-convert
  39. node-type-name
  40. node-type-description
  41. node-edges
  42. node-back-edges
  43. traverse/depth-first
  44. node-transitive-edges
  45. node-reachable-count
  46. shortest-path
  47. %graph-backends
  48. %d3js-backend
  49. %graphviz-backend
  50. %graphml-backend
  51. lookup-backend
  52. graph-backend?
  53. graph-backend
  54. graph-backend-name
  55. graph-backend-description
  56. export-graph))
  57. ;;; Commentary:
  58. ;;;
  59. ;;; This module provides an abstract way to represent graphs and to manipulate
  60. ;;; them. It comes with several such representations for packages,
  61. ;;; derivations, and store items. It also provides a generic interface for
  62. ;;; exporting graphs in an external format, including a Graphviz
  63. ;;; implementation thereof.
  64. ;;;
  65. ;;; Code:
  66. ;;;
  67. ;;; Node types.
  68. ;;;
  69. (define-record-type* <node-type> node-type make-node-type
  70. node-type?
  71. (identifier node-type-identifier) ;node -> M identifier
  72. (label node-type-label) ;node -> string
  73. (edges node-type-edges) ;node -> M list of nodes
  74. (convert node-type-convert ;any -> M list of nodes
  75. (default (lift1 list %store-monad)))
  76. (name node-type-name) ;string
  77. (description node-type-description)) ;string
  78. (define (%node-edges type nodes cons-edge)
  79. (with-monad %store-monad
  80. (match type
  81. (($ <node-type> identifier label node-edges)
  82. (define (add-edge node edges)
  83. (>>= (node-edges node)
  84. (lambda (nodes)
  85. (return (fold (cut cons-edge node <> <>)
  86. edges nodes)))))
  87. (mlet %store-monad ((edges (foldm %store-monad
  88. add-edge vlist-null nodes)))
  89. (return (lambda (node)
  90. (reverse (vhash-foldq* cons '() node edges)))))))))
  91. (define (node-edges type nodes)
  92. "Return, as a monadic value, a one-argument procedure that, given a node of TYPE,
  93. returns its edges. NODES is taken to be the sinks of the global graph."
  94. (%node-edges type nodes
  95. (lambda (source target edges)
  96. (vhash-consq source target edges))))
  97. (define (node-back-edges type nodes)
  98. "Return, as a monadic value, a one-argument procedure that, given a node of TYPE,
  99. returns its back edges. NODES is taken to be the sinks of the global graph."
  100. (%node-edges type nodes
  101. (lambda (source target edges)
  102. (vhash-consq target source edges))))
  103. (define (traverse/depth-first proc seed nodes node-edges)
  104. "Do a depth-first traversal of NODES along NODE-EDGES, calling PROC with
  105. each node and the current result, and visiting each reachable node exactly
  106. once. NODES must be a list of nodes, and NODE-EDGES must be a one-argument
  107. procedure as returned by 'node-edges' or 'node-back-edges'."
  108. (let loop ((nodes (append-map node-edges nodes))
  109. (result seed)
  110. (visited (setq)))
  111. (match nodes
  112. (()
  113. result)
  114. ((head . tail)
  115. (if (set-contains? visited head)
  116. (loop tail result visited)
  117. (let ((edges (node-edges head)))
  118. (loop (append edges tail)
  119. (proc head result)
  120. (set-insert head visited))))))))
  121. (define (node-transitive-edges nodes node-edges)
  122. "Return the list of nodes directly or indirectly connected to NODES
  123. according to the NODE-EDGES procedure. NODE-EDGES must be a one-argument
  124. procedure that, given a node, returns its list of direct dependents; it is
  125. typically returned by 'node-edges' or 'node-back-edges'."
  126. (traverse/depth-first cons '() nodes node-edges))
  127. (define (node-reachable-count nodes node-edges)
  128. "Return the number of nodes reachable from NODES along NODE-EDGES."
  129. (traverse/depth-first (lambda (_ count)
  130. (+ 1 count))
  131. 0
  132. nodes node-edges))
  133. (define (shortest-path node1 node2 type)
  134. "Return as a monadic value the shortest path, represented as a list, from
  135. NODE1 to NODE2 of the given TYPE. Return #f when there is no path."
  136. (define node-edges
  137. (node-type-edges type))
  138. (define (find-shortest lst)
  139. ;; Return the shortest path among LST, where each path is represented as a
  140. ;; vlist.
  141. (let loop ((lst lst)
  142. (best +inf.0)
  143. (shortest #f))
  144. (match lst
  145. (()
  146. shortest)
  147. ((head . tail)
  148. (let ((len (vlist-length head)))
  149. (if (< len best)
  150. (loop tail len head)
  151. (loop tail best shortest)))))))
  152. (define (find-path node path paths)
  153. ;; Return the a vhash that maps nodes to paths, with each path from the
  154. ;; given node to NODE2.
  155. (define (augment-paths child paths)
  156. ;; When using %REFERENCE-NODE-TYPE, nodes can contain self references,
  157. ;; hence this test.
  158. (if (eq? child node)
  159. (store-return paths)
  160. (find-path child vlist-null paths)))
  161. (cond ((eq? node node2)
  162. (store-return (vhash-consq node (vlist-cons node path)
  163. paths)))
  164. ((vhash-assq node paths)
  165. (store-return paths))
  166. (else
  167. ;; XXX: We could stop recursing if one if CHILDREN is NODE2, but in
  168. ;; practice it's good enough.
  169. (mlet* %store-monad ((children (node-edges node))
  170. (paths (foldm %store-monad
  171. augment-paths
  172. paths
  173. children)))
  174. (define sub-paths
  175. (filter-map (lambda (child)
  176. (match (vhash-assq child paths)
  177. (#f #f)
  178. ((_ . path) path)))
  179. children))
  180. (match sub-paths
  181. (()
  182. (return (vhash-consq node #f paths)))
  183. (lst
  184. (return (vhash-consq node
  185. (vlist-cons node (find-shortest sub-paths))
  186. paths))))))))
  187. (mlet %store-monad ((paths (find-path node1
  188. (vlist-cons node1 vlist-null)
  189. vlist-null)))
  190. (return (match (vhash-assq node1 paths)
  191. ((_ . #f) #f)
  192. ((_ . path) (vlist->list path))))))
  193. ;;;
  194. ;;; Graphviz export.
  195. ;;;
  196. (define-record-type <graph-backend>
  197. (graph-backend name description prologue epilogue node edge)
  198. graph-backend?
  199. (name graph-backend-name)
  200. (description graph-backend-description)
  201. (prologue graph-backend-prologue)
  202. (epilogue graph-backend-epilogue)
  203. (node graph-backend-node)
  204. (edge graph-backend-edge))
  205. (define %colors
  206. ;; See colortbl.h in Graphviz.
  207. #("red" "magenta" "blue" "cyan3" "darkseagreen"
  208. "peachpuff4" "darkviolet" "dimgrey" "darkgoldenrod"))
  209. (define (pop-color hint)
  210. "Return a Graphviz color based on HINT, an arbitrary object."
  211. (let ((index (hash hint (vector-length %colors))))
  212. (vector-ref %colors index)))
  213. (define (emit-prologue name port)
  214. (format port "digraph \"Guix ~a\" {\n"
  215. name))
  216. (define (emit-epilogue port)
  217. (display "\n}\n" port))
  218. (define (emit-node id label port)
  219. (format port " \"~a\" [label = \"~a\", shape = box, fontname = sans];~%"
  220. id label))
  221. (define (emit-edge id1 id2 port)
  222. (format port " \"~a\" -> \"~a\" [color = ~a];~%"
  223. id1 id2 (pop-color id1)))
  224. (define %graphviz-backend
  225. (graph-backend "graphviz"
  226. "Generate graph in DOT format for use with Graphviz."
  227. emit-prologue emit-epilogue
  228. emit-node emit-edge))
  229. ;;;
  230. ;;; d3js export.
  231. ;;;
  232. (define (emit-d3js-prologue name port)
  233. (format port "\
  234. <!DOCTYPE html>
  235. <html>
  236. <head>
  237. <meta charset=\"utf-8\">
  238. <style>
  239. text {
  240. font: 10px sans-serif;
  241. pointer-events: none;
  242. }
  243. </style>
  244. <script type=\"text/javascript\" src=\"~a\"></script>
  245. </head>
  246. <body>
  247. <script type=\"text/javascript\">
  248. var nodes = {},
  249. nodeArray = [],
  250. links = [];
  251. " (search-path %load-path "guix/d3.v3.js")))
  252. (define (emit-d3js-epilogue port)
  253. (format port "</script><script type=\"text/javascript\" src=\"~a\"></script></body></html>"
  254. (search-path %load-path "guix/graph.js")))
  255. (define (emit-d3js-node id label port)
  256. (format port "\
  257. nodes[\"~a\"] = {\"id\": \"~a\", \"label\": \"~a\", \"index\": nodeArray.length};
  258. nodeArray.push(nodes[\"~a\"]);~%"
  259. id id label id))
  260. (define (emit-d3js-edge id1 id2 port)
  261. (format port "links.push({\"source\": \"~a\", \"target\": \"~a\"});~%"
  262. id1 id2))
  263. (define %d3js-backend
  264. (graph-backend "d3js"
  265. "Generate chord diagrams with d3js."
  266. emit-d3js-prologue emit-d3js-epilogue
  267. emit-d3js-node emit-d3js-edge))
  268. ;;;
  269. ;;; Cypher export.
  270. ;;;
  271. (define (emit-cypher-prologue name port)
  272. (format port ""))
  273. (define (emit-cypher-epilogue port)
  274. (format port ""))
  275. (define (emit-cypher-node id label port)
  276. (format port "MERGE (p:Package { id: ~s }) SET p.name = ~s;~%"
  277. id label ))
  278. (define (emit-cypher-edge id1 id2 port)
  279. (format port "MERGE (a:Package { id: ~s });~%" id1)
  280. (format port "MERGE (b:Package { id: ~s });~%" id2)
  281. (format port "MATCH (a:Package { id: ~s }), (b:Package { id: ~s }) CREATE UNIQUE (a)-[:NEEDS]->(b);~%"
  282. id1 id2))
  283. (define %cypher-backend
  284. (graph-backend "cypher"
  285. "Generate Cypher queries."
  286. emit-cypher-prologue emit-cypher-epilogue
  287. emit-cypher-node emit-cypher-edge))
  288. ;;;
  289. ;;; GraphML export.
  290. ;;;
  291. (define (emit-graphml-prologue name port)
  292. (format port "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
  293. <graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\"
  294. xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"
  295. xsi:schemaLocation=\"http://graphml.graphdrawing.org/xmlns
  296. http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd\">
  297. <graph id=\"G\" edgedefault=\"directed\">~%"))
  298. (define (emit-graphml-epilogue port)
  299. (format port " </graph>
  300. </graphml>"))
  301. (define (emit-graphml-node id label port)
  302. (format port " <node id=\"~a\"/>~%"
  303. (string-replace-substring (object->string id) "\"" "\\\"")))
  304. (define (emit-graphml-edge id1 id2 port)
  305. (format port " <edge source=\"~a\" target=\"~a\"/>~%"
  306. (string-replace-substring (object->string id1) "\"" "\\\"")
  307. (string-replace-substring (object->string id2) "\"" "\\\"")))
  308. (define %graphml-backend
  309. (graph-backend "graphml"
  310. "Generate GraphML."
  311. emit-graphml-prologue emit-graphml-epilogue
  312. emit-graphml-node emit-graphml-edge))
  313. ;;;
  314. ;;; Shared.
  315. ;;;
  316. (define %graph-backends
  317. (list %graphviz-backend
  318. %d3js-backend
  319. %cypher-backend
  320. %graphml-backend))
  321. (define (lookup-backend name)
  322. "Return the graph backend called NAME. Raise an error if it is not found."
  323. (or (find (lambda (backend)
  324. (string=? (graph-backend-name backend) name))
  325. %graph-backends)
  326. (raise (formatted-message (G_ "~a: unknown graph backend") name))))
  327. (define* (export-graph sinks port
  328. #:key
  329. reverse-edges? node-type (max-depth +inf.0)
  330. (backend %graphviz-backend))
  331. "Write to PORT the representation of the DAG with the given SINKS, using the
  332. given BACKEND. Use NODE-TYPE to traverse the DAG. When REVERSE-EDGES? is
  333. true, draw reverse arrows. Do not represent nodes whose distance to one of
  334. the SINKS is greater than MAX-DEPTH."
  335. (match backend
  336. (($ <graph-backend> _ _ emit-prologue emit-epilogue emit-node emit-edge)
  337. (emit-prologue (node-type-name node-type) port)
  338. (match node-type
  339. (($ <node-type> node-identifier node-label node-edges)
  340. (let loop ((nodes sinks)
  341. (depths (make-list (length sinks) 0))
  342. (visited (set)))
  343. (match nodes
  344. (()
  345. (with-monad %store-monad
  346. (emit-epilogue port)
  347. (store-return #t)))
  348. ((head . tail)
  349. (match depths
  350. ((depth . depths)
  351. (mlet %store-monad ((id (node-identifier head)))
  352. (if (set-contains? visited id)
  353. (loop tail depths visited)
  354. (mlet* %store-monad ((dependencies
  355. (if (= depth max-depth)
  356. (return '())
  357. (node-edges head)))
  358. (ids
  359. (mapm %store-monad
  360. node-identifier
  361. dependencies)))
  362. (emit-node id (node-label head) port)
  363. (for-each (lambda (dependency dependency-id)
  364. (if reverse-edges?
  365. (emit-edge dependency-id id port)
  366. (emit-edge id dependency-id port)))
  367. dependencies ids)
  368. (loop (append dependencies tail)
  369. (append (make-list (length dependencies)
  370. (+ 1 depth))
  371. depths)
  372. (set-insert id visited)))))))))))))))
  373. ;;; graph.scm ends here