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 }