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 }