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