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 }