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.build;
10 
11 import std.range : ElementType;
12 import std.parallelism : TaskPool;
13 
14 import button.graph;
15 import button.task;
16 import button.resource;
17 import button.edge, button.edgedata;
18 import button.state;
19 import button.textcolor;
20 import button.rule;
21 import button.log;
22 import button.context;
23 
24 alias BuildStateGraph = Graph!(
25         Index!Resource,
26         Index!Task,
27         EdgeType,
28         EdgeType,
29         );
30 
31 /**
32  * An exception relating to the build.
33  */
34 class BuildException : Exception
35 {
36     this(string msg)
37     {
38         super(msg);
39     }
40 }
41 
42 /**
43  * Constructs the name of the build state file based on the build description
44  * file name.
45  */
46 @property string stateName(string fileName) pure nothrow
47 {
48     import std.path : dirName, baseName, buildNormalizedPath;
49 
50     immutable dir  = dirName(fileName);
51     immutable base = baseName(fileName);
52 
53     string prefix;
54 
55     // Prepend a '.' if there isn't one already.
56     if (base.length > 0 && base[0] != '.')
57         prefix = ".";
58 
59     return buildNormalizedPath(dir, prefix ~ base ~ ".state");
60 }
61 
62 unittest
63 {
64     assert("button.json".stateName == ".button.json.state");
65     assert(".button.json".stateName == ".button.json.state");
66     assert(".button.test.json".stateName == ".button.test.json.state");
67     assert("./button.json".stateName == ".button.json.state");
68     assert("test/button.json".stateName == "test/.button.json.state");
69     assert("/test/.button.json".stateName == "/test/.button.json.state");
70 }
71 
72 /**
73  * Generates a graph from a set of rules.
74  */
75 Graph!(Resource, Task) graph(R)(auto ref R rules)
76     if (is(ElementType!R : const(Rule)))
77 {
78     auto g = new typeof(return)();
79 
80     foreach (r; rules)
81     {
82         g.put(r.task);
83 
84         foreach (v; r.inputs)
85         {
86             g.put(v);
87             g.put(v, r.task);
88         }
89 
90         foreach (v; r.outputs)
91         {
92             g.put(v);
93             g.put(r.task, v);
94         }
95     }
96 
97     return g;
98 }
99 
100 unittest
101 {
102     import button.command;
103 
104     immutable Rule[] rules = [
105         {
106             inputs: [Resource("foo.c"), Resource("baz.h")],
107             task: Task([Command(["gcc", "-c", "foo.c", "-o", "foo.o"])]),
108             outputs: [Resource("foo.o")]
109         },
110         {
111             inputs: [Resource("bar.c"), Resource("baz.h")],
112             task: Task([Command(["gcc", "-c", "bar.c", "-o", "bar.o"])]),
113             outputs: [Resource("bar.o")]
114         },
115         {
116             inputs: [Resource("foo.o"), Resource("bar.o")],
117             task: Task([Command(["gcc", "foo.o", "bar.o", "-o", "foobar"])]),
118             outputs: [Resource("foobar")]
119         }
120     ];
121 
122     auto g = graph(rules);
123     assert(g.length!Task == 3);
124     assert(g.length!Resource == 6);
125 }
126 
127 /**
128  * Generates the explicit subgraph from the build state.
129  */
130 Graph!(Resource, Task) explicitGraph(BuildState state)
131 {
132     auto g = new typeof(return)();
133 
134     // Add all tasks.
135     foreach (v; state.enumerate!Task)
136         g.put(v);
137 
138     // Add all explicit edges.
139     // FIXME: This isn't very efficient.
140     foreach (e; state.edges!(Task, Resource, EdgeType)(EdgeType.explicit))
141     {
142         auto r = state[e.to];
143         g.put(r);
144         g.put(state[e.from], r);
145     }
146 
147     foreach (e; state.edges!(Resource, Task, EdgeType)(EdgeType.explicit))
148     {
149         auto r = state[e.from];
150         g.put(r);
151         g.put(r, state[e.to]);
152     }
153 
154     return g;
155 }
156 
157 /**
158  * Parses the build description.
159  *
160  * Returns: A range of rules.
161  *
162  * Throws: BuildException if the build description could not be opened.
163  */
164 Rules rules(string path)
165 {
166     import io.file;
167     import io.range : byBlock;
168     import button.rule;
169 
170     import std.exception : ErrnoException;
171 
172     try
173     {
174         auto r = File(path).byBlock!char;
175         return parseRules(&r);
176     }
177     catch (ErrnoException e)
178     {
179         throw new BuildException("Failed to open build description: " ~ e.msg);
180     }
181 }
182 
183 /**
184  * Synchronizes the build state with the given set of rules.
185  *
186  * After the synchronization, the graph created from the set of rules will be a
187  * subgraph of the graph created from the build state.
188  */
189 void syncState(R)(R rules, BuildState state, TaskPool pool, bool dryRun = false)
190     if (is(ElementType!R : const(Rule)))
191 {
192     import util.change;
193     import std.array : array;
194 
195     auto g1 = explicitGraph(state);
196     auto g2 = graph(rules);
197 
198     // Analyze the build description graph. If any errors are detected, no
199     // changes are made to the database.
200     g2.checkCycles();
201     g2.checkRaces();
202 
203     auto resourceDiff     = g1.diffVertices!Resource(g2).array;
204     auto taskDiff         = g1.diffVertices!Task(g2).array;
205     auto resourceEdgeDiff = g1.diffEdges!(Resource, Task)(g2);
206     auto taskEdgeDiff     = g1.diffEdges!(Task, Resource)(g2);
207 
208     foreach (c; resourceDiff)
209     {
210         // Add the resource to the database. Note that since we only diffed
211         // against the explicit subgraph of the database, we may be trying to
212         // insert resources that are already in the database.
213         if (c.type == ChangeType.added)
214             state.add(c.value);
215     }
216 
217     foreach (c; taskDiff)
218     {
219         // Any new tasks must be executed.
220         if (c.type == ChangeType.added)
221             state.addPending(state.put(c.value));
222     }
223 
224     // Add new edges and remove old edges.
225     foreach (c; resourceEdgeDiff)
226     {
227         final switch (c.type)
228         {
229         case ChangeType.added:
230             state.put(c.value.from.identifier, c.value.to.key, EdgeType.explicit);
231             break;
232         case ChangeType.removed:
233             state.remove(c.value.from.identifier, c.value.to.key, EdgeType.explicit);
234             break;
235         case ChangeType.none:
236             break;
237         }
238     }
239 
240     foreach (c; taskEdgeDiff)
241     {
242         final switch (c.type)
243         {
244         case ChangeType.added:
245             auto taskid = state.find(c.value.from.key);
246             auto resid = state.find(c.value.to.identifier);
247             state.addPending(taskid);
248             state.put(taskid, resid, EdgeType.explicit);
249             break;
250         case ChangeType.removed:
251             auto taskid = state.find(c.value.from.key);
252             auto resid = state.find(c.value.to.identifier);
253 
254             auto r = state[resid];
255 
256             // When an edge from a task to a resource is removed, the resource
257             // should be deleted.
258             r.remove(dryRun);
259 
260             // Update the resource status in the database.
261             //state[resid] = r;
262 
263             state.remove(taskid, resid, EdgeType.explicit);
264             break;
265         case ChangeType.none:
266             break;
267         }
268     }
269 
270     // Remove old vertices
271     foreach (c; taskDiff)
272     {
273         if (c.type == ChangeType.removed)
274         {
275             immutable id = state.find(c.value.key);
276 
277             // Delete all outputs from this task. Note that any edges associated
278             // with this task are automatically removed when the task is removed
279             // from the database (because of "ON CASCADE DELETE").
280             auto outgoing = state.outgoing!(EdgeIndex!(Task, Resource))(id);
281             foreach (e; pool.parallel(outgoing))
282                 state[e.vertex].remove(dryRun);
283 
284             state.remove(id);
285         }
286     }
287 
288     foreach (c; resourceDiff)
289     {
290         // Only remove iff this resource has no outgoing implicit edges.
291         // Note that, at this point, there can be no explicit edges left.
292         if (c.type == ChangeType.removed)
293         {
294             immutable id = state.find(c.value.identifier);
295             if (state.outgoing(id).empty)
296                 state.remove(id);
297         }
298     }
299 
300     // TODO: Find resources with no associated edges and remove them. These
301     // resources won't cause any issues, but can slow things down if too many
302     // accumulate.
303 }
304 
305 /// Ditto
306 void syncState(string path, BuildState state, TaskPool pool, bool dryRun = false)
307 {
308     syncState(rules(path), state, pool, dryRun);
309 }
310 
311 /**
312  * Constructs a graph from the build state. Only connected resources are added
313  * to the graph.
314  */
315 BuildStateGraph buildGraph(BuildState state, EdgeType type = EdgeType.both)
316 {
317     auto g = new typeof(return)();
318 
319     // Add all tasks
320     foreach (v; state.enumerate!(Index!Task))
321         g.put(v);
322 
323     // Add all edges
324     foreach (v; state.edges!(Resource, Task, EdgeType))
325     {
326         if (v.data & type)
327         {
328             g.put(v.from);
329             g.put(v.from, v.to, v.data);
330         }
331     }
332 
333     foreach (v; state.edges!(Task, Resource, EdgeType))
334     {
335         if (v.data & type)
336         {
337             g.put(v.to);
338             g.put(v.from, v.to, v.data);
339         }
340     }
341 
342     return g;
343 }
344 
345 /**
346  * Helper function for constructing a subgraph from the build state.
347  */
348 private void buildGraph(Vertex, G, Visited)(BuildState state, G graph, Vertex v,
349         ref Visited visited)
350 {
351     if (v in visited)
352         return;
353 
354     visited.put(v, true);
355 
356     graph.put(v);
357 
358     foreach (neighbor; state.outgoing!EdgeType(v))
359     {
360         buildGraph(state, graph, neighbor.vertex, visited);
361         graph.put(v, neighbor.vertex, neighbor.data);
362     }
363 }
364 
365 /**
366  * Constructs a subgraph from the build state starting at the given roots.
367  */
368 BuildStateGraph buildGraph(Resources, Tasks)
369         (BuildState state, Resources resources, Tasks tasks)
370     if (is(ElementType!Resources : Index!Resource) &&
371         is(ElementType!Tasks : Index!Task))
372 {
373     alias G = typeof(return);
374     auto g = new G();
375 
376     G.Visited!bool visited;
377 
378     foreach (v; resources)
379         buildGraph(state, g, v, visited);
380 
381     foreach (v; tasks)
382         buildGraph(state, g, v, visited);
383 
384     return g;
385 }
386 
387 /**
388  * Checks for cycles.
389  *
390  * Throws: BuildException exception if one or more cycles are found.
391  */
392 void checkCycles(Graph!(Resource, Task) graph)
393 {
394     import std.format : format;
395     import std.algorithm.iteration : map;
396     import std.algorithm.comparison : min;
397 
398     // TODO: Construct the string instead of printing them.
399     import io;
400 
401     immutable cycles = graph.cycles;
402 
403     if (!cycles.length) return;
404 
405     foreach (i, scc; cycles)
406     {
407         printfln("Cycle %d:", i+1);
408 
409         auto resources = scc.vertices!(Resource);
410         auto tasks = scc.vertices!(Task);
411 
412         println("    ", resources[0]);
413         println(" -> ", tasks[0].toPrettyString);
414 
415         foreach_reverse(j; 1 .. min(resources.length, tasks.length))
416         {
417             println(" -> ", resources[j]);
418             println(" -> ", tasks[j].toPrettyString);
419         }
420 
421         // Make the cycle obvious
422         println(" -> ", resources[0]);
423     }
424 
425     immutable plural = cycles.length > 1 ? "s" : "";
426 
427     throw new BuildException(
428         "Found %d cycle%s. Use also `button graph` to visualize the cycle%s."
429         .format(cycles.length, plural, plural)
430         );
431 }
432 
433 /**
434  * Checks for race conditions.
435  *
436  * Throws: BuildException exception if one or more race conditions are found.
437  */
438 void checkRaces(Graph!(Resource, Task) graph)
439 {
440     import std.format : format;
441     import std.algorithm : filter, map, joiner;
442     import std.array : array;
443     import std.typecons : tuple;
444 
445     auto races = graph.vertices!(Resource)
446                       .filter!(v => graph.degreeIn(v) > 1)
447                       .map!(v => tuple(v, graph.degreeIn(v)))
448                       .array;
449 
450     if (races.length == 0)
451         return;
452 
453     if (races.length == 1)
454     {
455         immutable r = races[0];
456         throw new BuildException(
457             "Found a race condition! The resource `%s` is an output of %d tasks."
458             .format(r[0], r[1])
459             );
460     }
461 
462     throw new BuildException(
463         "Found %d race conditions:\n"
464         "%s\n"
465         "Use `button graph` to see which tasks they are."
466         .format(
467             races.length,
468             races.map!(r => " * `%s` is an output of %d tasks".format(r[0], r[1]))
469                  .joiner("\n")
470             )
471         );
472 }
473 
474 /**
475  * Finds changed resources and marks them as pending in the build state.
476  */
477 void queueChanges(BuildState state, TaskPool pool, TextColor color)
478 {
479     import std.array : array;
480     import std.algorithm.iteration : filter;
481     import std.range : takeOne;
482     import io.text : println;
483 
484     // FIXME: The parallel foreach fails if this is not an array.
485     auto resources = state.enumerate!(Index!Resource)
486         .array;
487 
488     foreach (v; pool.parallel(resources))
489     {
490         auto r = state[v];
491 
492         if (state.degreeIn(v) == 0)
493         {
494             if (r.update())
495             {
496                 // An input changed
497                 state[v] = r;
498                 state.addPending(v);
499             }
500         }
501         else if (r.statusKnown)
502         {
503             if (r.update())
504             {
505                 // An output changed. In this case, it must be regenerated. So, we
506                 // add its task to the queue.
507                 // TODO: Use the logger to do this instead.
508                 synchronized println(
509                         " - ", color.warning, "Warning", color.reset,
510                         ": Output file `", color.purple, r, color.reset,
511                         "` was changed externally and will be regenerated.");
512 
513                 // A resource should only ever have one incoming edge. If that
514                 // is not the case, then we've got bigger problems.
515                 auto incoming = state
516                     .incoming!(NeighborIndex!(Index!Resource))(v)
517                     .takeOne;
518                 assert(incoming.length == 1,
519                         "Output resource has does not have 1 incoming edge!");
520                 state.addPending(incoming[0].vertex);
521             }
522         }
523     }
524 }
525 
526 /**
527  * Syncs the build state with implicit dependencies.
528  */
529 void syncStateImplicit(BuildState state, Index!Task v,
530         Resource[] inputs, Resource[] outputs)
531 {
532     import std.algorithm.iteration : splitter, uniq, filter, map, joiner;
533     import std.array : array;
534     import std.algorithm.sorting : sort;
535     import std.format : format;
536     import util.change;
537 
538     auto inputDiff = changes(
539             state.incoming!Resource(v, EdgeType.implicit).array.sort(),
540             inputs.sort().uniq
541             );
542 
543     auto outputDiff = changes(
544             state.outgoing!Resource(v, EdgeType.implicit).array.sort(),
545             outputs.sort().uniq
546             );
547 
548     foreach (c; inputDiff)
549     {
550         if (c.type == ChangeType.added)
551         {
552             auto r = c.value;
553 
554             // A new implicit input. If the resource is *not* an output
555             // resource, then we are fine. Otherwise, it is an error because we
556             // are potentially changing the build order with this new edge.
557             auto id = state.find(r.path);
558             if (id == Index!Resource.Invalid)
559             {
560                 // Resource doesn't exist. It is impossible to change the build
561                 // order by adding it.
562                 r.update();
563                 state.put(state.put(r), v, EdgeType.implicit);
564             }
565             else if (state.degreeIn(id) == 0 ||
566                      state.edgeExists(id, v, EdgeType.explicit))
567             {
568                 // Resource exists, but no task is outputting to it or an
569                 // explicit edge already exists. In these situations it is
570                 // impossible to change the build order by adding an edge to it.
571                 state.put(id, v, EdgeType.implicit);
572             }
573             else
574             {
575                 throw new TaskError(
576                     ("Implicit task input '%s' would change the build order." ~
577                     " It must be explicitly added to the build description.")
578                     .format(r)
579                     );
580             }
581         }
582         else if (c.type == ChangeType.removed)
583         {
584             // Build state has edges that weren't discovered implicitly. These
585             // can either be explicit edges that weren't found, removed implicit
586             // edges, or both.
587             auto id = state.find(c.value.path);
588             assert(id != Index!Resource.Invalid);
589             state.remove(id, v, EdgeType.implicit);
590         }
591     }
592 
593     foreach (c; outputDiff)
594     {
595         if (c.type == ChangeType.added)
596         {
597             auto r = c.value;
598 
599             // A new implicit output. The resource must either not exist or be a
600             // dangling resource awaiting garbage collection. Otherwise, it is
601             // an error.
602             auto id = state.find(r.path);
603             if (id == Index!Resource.Invalid)
604             {
605                 // Resource doesn't exist. It is impossible to change the
606                 // builder order by adding it.
607                 r.update();
608                 state.put(v, state.put(r), EdgeType.implicit);
609             }
610             else if ((state.degreeIn(id) == 0 && state.degreeOut(id) == 0) ||
611                      state.edgeExists(v, id, EdgeType.explicit))
612             {
613                 // Resource exists, but it is neither an input nor an output
614                 // (i.e., an orphan that hasn't been garbage collected), or an
615                 // explicit edge already exists. In these situations it is
616                 // impossible to change the build order by adding an implicit
617                 // edge.
618                 state.put(v, id, EdgeType.implicit);
619             }
620             else
621             {
622                 throw new TaskError(
623                     ("Implicit task output '%s' would change the build order." ~
624                     " It must be explicitly added to the build description.")
625                     .format(r)
626                     );
627             }
628         }
629         else if (c.type == ChangeType.removed)
630         {
631             // Build state has edges that weren't discovered implicitly. These
632             // can be either explicit edges that weren't found or removed
633             // implicit edges. We only care about the latter case here.
634             auto id = state.find(c.value.path);
635             assert(id != Index!Resource.Invalid);
636 
637             state[id].remove(false);
638             state.remove(v, id, EdgeType.implicit);
639             state.remove(id);
640         }
641     }
642 }
643 
644 /**
645  * Called when a resource vertex is visited.
646  *
647  * Returns true if we should continue traversing the graph.
648  */
649 bool visitResource(BuildContext* ctx, Index!Resource v, size_t degreeIn,
650         size_t degreeChanged)
651 {
652     scope (success)
653         ctx.state.removePending(v);
654 
655     // Do not consider ourselves changed if none of our dependencies changed.
656     if (degreeChanged == 0)
657         return false;
658 
659     // Leaf resources are already checked for changes when discovering roots
660     // from which to construct the subgraph. Thus, there is no need to do it
661     // here as well.
662     if (degreeIn == 0)
663         return true;
664 
665     // This is an output resource. Its parent task may have changed it. Thus,
666     // check for any change.
667     auto r = ctx.state[v];
668     if (r.update())
669     {
670         ctx.state[v] = r;
671         return true;
672     }
673 
674     return false;
675 }
676 
677 /**
678  * Called when a task vertex is visited.
679  *
680  * Returns true if we should continue traversing the graph.
681  */
682 bool visitTask(BuildContext* ctx, Index!Task v, size_t degreeIn,
683         size_t degreeChanged)
684 {
685     import std.datetime : StopWatch, AutoStart;
686     import core.time : TickDuration;
687     import std.format : format;
688     import std.exception : collectException;
689 
690     immutable pending = ctx.state.isPending(v);
691 
692     if (degreeChanged == 0 && !pending)
693         return false;
694 
695     // We add this as pending if it isn't already just in case the build is
696     // interrupted or if it fails.
697     if (!pending) ctx.state.addPending(v);
698 
699     auto task = ctx.state[v];
700 
701     auto taskLogger = ctx.logger.taskStarted(v, task, ctx.dryRun);
702 
703     // Assume the command would succeed in a dryrun
704     if (ctx.dryRun)
705     {
706         taskLogger.succeeded(TickDuration.zero);
707         return true;
708     }
709 
710     auto sw = StopWatch(AutoStart.yes);
711 
712     Task.Result result;
713     Exception e = task.execute(*ctx, taskLogger).collectException(result);
714 
715     sw.stop();
716 
717     immutable duration = sw.peek();
718 
719     if (e)
720     {
721         taskLogger.failed(duration, e);
722         throw e;
723     }
724 
725     synchronized (ctx.state)
726     {
727         e = syncStateImplicit(ctx.state, v, result.inputs, result.outputs)
728                 .collectException;
729     }
730 
731     if (e)
732     {
733         taskLogger.failed(duration, e);
734         throw e;
735     }
736 
737     taskLogger.succeeded(duration);
738 
739     // Only remove this from the set of pending tasks if it succeeds completely.
740     // If it fails, it should get executed again on the next run such that other
741     // tasks that depend on this (if any) can be executed.
742     ctx.state.removePending(v);
743 
744     return true;
745 }
746 
747 /**
748  * Traverses the graph, executing the tasks.
749  *
750  * This is the heart of the build system. Everything else is just support code.
751  */
752 void build(BuildStateGraph graph, ref BuildContext context)
753 {
754     graph.traverse!(visitResource, visitTask)(&context, context.pool);
755 }
756 
757 /**
758  * Finds the path to the build description.
759  *
760  * Throws: BuildException if no build description could be found.
761  */
762 string findBuildPath(string root)
763 {
764     import std.file : exists, isFile;
765     import std.path : buildPath, dirName;
766 
767     auto path = buildPath(root, "button.json");
768     if (exists(path) && isFile(path))
769         return path;
770 
771     // Search in the parent directory, if any
772     auto parent = dirName(root);
773 
774     // Can't go any further up.
775     if (parent == root)
776         throw new BuildException("Could not find a build description.");
777 
778     return findBuildPath(parent);
779 }
780 
781 /**
782  * Returns the path to the build description.
783  */
784 string buildDescriptionPath(string path)
785 {
786     import std.file : getcwd;
787 
788     if (!path.length)
789         return findBuildPath(getcwd());
790 
791     return path;
792 }