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 }