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