1 /** 2 * Copyright: Copyright Jason White, 2016 3 * License: MIT 4 * Authors: Jason White 5 * 6 * Description: 7 * Generates input for GraphViz. 8 */ 9 module button.cli.graph; 10 11 import button.cli.options : GraphOptions, GlobalOptions; 12 13 import io.text, 14 io.file; 15 16 import io.stream : isSink; 17 18 import button.resource, 19 button.task, 20 button.edgedata, 21 button.graph, 22 button.state, 23 button.build; 24 25 int graphCommand(GraphOptions opts, GlobalOptions globalOpts) 26 { 27 import std.array : array; 28 import std.algorithm.iteration : filter; 29 import std.parallelism : TaskPool, totalCPUs; 30 31 if (opts.threads == 0) 32 opts.threads = totalCPUs; 33 34 auto pool = new TaskPool(opts.threads - 1); 35 scope (exit) pool.finish(true); 36 37 try 38 { 39 string path = buildDescriptionPath(opts.path); 40 41 auto state = new BuildState(path.stateName); 42 43 state.begin(); 44 scope (exit) state.rollback(); 45 46 if (!opts.cached) 47 path.syncState(state, pool, true); 48 49 BuildStateGraph graph = state.buildGraph(opts.edges); 50 51 if (opts.changes) 52 { 53 // Construct the minimal subgraph based on pending vertices 54 auto resourceRoots = state.enumerate!(Index!Resource) 55 .filter!(v => state.degreeIn(v) == 0 && state[v].update()) 56 .array; 57 58 auto taskRoots = state.pending!Task 59 .filter!(v => state.degreeIn(v) == 0) 60 .array; 61 62 graph = graph.subgraph(resourceRoots, taskRoots); 63 } 64 65 graph.graphviz(state, stdout, opts.full); 66 } 67 catch (BuildException e) 68 { 69 stderr.println(":: Error: ", e.msg); 70 return 1; 71 } 72 73 return 0; 74 } 75 76 /** 77 * Escape a label string to be consumed by GraphViz. 78 */ 79 private string escapeLabel(string label) pure 80 { 81 import std.array : appender; 82 import std.exception : assumeUnique; 83 84 auto result = appender!(char[]); 85 86 foreach (c; label) 87 { 88 if (c == '\\' || c == '"') 89 result.put('\\'); 90 result.put(c); 91 } 92 93 return assumeUnique(result.data); 94 } 95 96 unittest 97 { 98 assert(escapeLabel(`gcc -c "foo.c"`) == `gcc -c \"foo.c\"`); 99 } 100 101 /** 102 * Generates input suitable for GraphViz. 103 */ 104 void graphviz(Stream)( 105 BuildStateGraph graph, 106 BuildState state, 107 Stream stream, 108 bool full 109 ) 110 if (isSink!Stream) 111 { 112 import io.text; 113 import std.range : enumerate; 114 115 alias A = Index!Resource; 116 alias B = Index!Task; 117 118 stream.println("digraph G {"); 119 scope (success) stream.println("}"); 120 121 // Vertices 122 stream.println(" subgraph {\n" ~ 123 " node [shape=ellipse, fillcolor=lightskyblue2, style=filled];" 124 ); 125 foreach (id; graph.vertices!A) 126 { 127 immutable v = state[id]; 128 immutable name = full ? v.toString : v.toShortString; 129 stream.printfln(` "r:%s" [label="%s", tooltip="%s"];`, id, 130 name.escapeLabel, v.toString.escapeLabel); 131 } 132 stream.println(" }"); 133 134 stream.println(" subgraph {\n" ~ 135 " node [shape=box, fillcolor=gray91, style=filled];" 136 ); 137 foreach (id; graph.vertices!B) 138 { 139 immutable v = state[id]; 140 immutable name = full ? v.toPrettyString : v.toPrettyShortString; 141 stream.printfln(` "t:%s" [label="%s", tooltip="%s"];`, id, 142 name.escapeLabel, v.toPrettyString.escapeLabel); 143 } 144 stream.println(" }"); 145 146 // Cluster cycles, if any 147 foreach (i, scc; enumerate(graph.cycles)) 148 { 149 stream.printfln(" subgraph cluster_%d {", i++); 150 151 foreach (v; scc.vertices!A) 152 stream.printfln(` "r:%s";`, v); 153 154 foreach (v; scc.vertices!B) 155 stream.printfln(` "t:%s";`, v); 156 157 stream.println(" }"); 158 } 159 160 // Edge style, indexed by EdgeType. 161 static immutable styles = [ 162 "invis", // Should never get indexed 163 "solid", // Explicit 164 "dashed", // Implicit 165 "bold", // Both explicit and implicit 166 ]; 167 168 // Edges 169 foreach (edge; graph.edges!(A, B)) 170 { 171 stream.printfln(` "r:%s" -> "t:%s" [style=%s];`, 172 edge.from, edge.to, styles[edge.data]); 173 } 174 175 foreach (edge; graph.edges!(B, A)) 176 { 177 stream.printfln(` "t:%s" -> "r:%s" [style=%s];`, 178 edge.from, edge.to, styles[edge.data]); 179 } 180 } 181 182 /// Ditto 183 void graphviz(Stream)(Graph!(Resource, Task) graph, Stream stream) 184 if (isSink!Stream) 185 { 186 import io.text; 187 import std.range : enumerate; 188 189 alias A = Resource; 190 alias B = Task; 191 192 stream.println("digraph G {"); 193 scope (success) stream.println("}"); 194 195 // Vertices 196 stream.println(" subgraph {\n" ~ 197 " node [shape=ellipse, fillcolor=lightskyblue2, style=filled];" 198 ); 199 foreach (v; graph.vertices!Resource) 200 { 201 stream.printfln(` "r:%s"`, v.escapeLabel); 202 } 203 stream.println(" }"); 204 205 stream.println(" subgraph {\n" ~ 206 " node [shape=box, fillcolor=gray91, style=filled];" 207 ); 208 foreach (v; graph.vertices!Task) 209 { 210 stream.printfln(` "t:%s"`, v.escapeLabel); 211 } 212 stream.println(" }"); 213 214 // Cluster cycles, if any 215 foreach (i, scc; enumerate(graph.cycles)) 216 { 217 stream.printfln(" subgraph cluster_%d {", i++); 218 219 foreach (v; scc.vertices!Resource) 220 stream.printfln(` "r:%s";`, v.escapeLabel); 221 222 foreach (v; scc.vertices!Task) 223 stream.printfln(` "t:%s";`, v.escapeLabel); 224 225 stream.println(" }"); 226 } 227 228 // Edges 229 // TODO: Style as dashed edge if implicit edge 230 foreach (edge; graph.edges!(Resource, Task)) 231 stream.printfln(` "r:%s" -> "t:%s";`, 232 edge.from.escapeLabel, edge.to.escapeLabel); 233 234 foreach (edge; graph.edges!(Task, Resource)) 235 stream.printfln(` "t:%s" -> "r:%s";`, 236 edge.from.escapeLabel, edge.to.escapeLabel); 237 }