build-graph 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/context/getting-started.md +65 -0
  4. data/context/index.yaml +18 -0
  5. data/context/visualization.md +79 -0
  6. data/lib/build/graph/edge.rb +9 -20
  7. data/lib/build/graph/node.rb +15 -21
  8. data/lib/build/graph/task.rb +37 -20
  9. data/lib/build/graph/version.rb +7 -20
  10. data/lib/build/graph/visualization.rb +55 -0
  11. data/lib/build/graph/walker.rb +43 -43
  12. data/lib/build/graph.rb +9 -27
  13. data/license.md +21 -0
  14. data/readme.md +59 -0
  15. data/releases.md +9 -0
  16. data.tar.gz.sig +0 -0
  17. metadata +43 -142
  18. metadata.gz.sig +0 -0
  19. data/spec/build/graph/build_test.rb +0 -85
  20. data/spec/build/graph/edge_spec.rb +0 -39
  21. data/spec/build/graph/graph_spec.rb +0 -172
  22. data/spec/build/graph/inherit_spec.rb +0 -51
  23. data/spec/build/graph/listing.txt +0 -8
  24. data/spec/build/graph/node_spec.rb +0 -80
  25. data/spec/build/graph/process_graph.rb +0 -98
  26. data/spec/build/graph/program/Benchmark.cpp +0 -72
  27. data/spec/build/graph/program/Benchmark.cpp.d +0 -1
  28. data/spec/build/graph/program/Benchmark.cpp.o +0 -0
  29. data/spec/build/graph/program/Benchmark.h +0 -65
  30. data/spec/build/graph/program/DictionarySort.h +0 -270
  31. data/spec/build/graph/program/ParallelMergeSort.h +0 -278
  32. data/spec/build/graph/program/dictionary-sort +0 -0
  33. data/spec/build/graph/program/main.cpp +0 -131
  34. data/spec/build/graph/program/main.cpp.d +0 -1
  35. data/spec/build/graph/program/main.cpp.o +0 -0
  36. data/spec/build/graph/task_spec.rb +0 -69
  37. data/spec/build/graph/walker_spec.rb +0 -125
  38. data/spec/spec_helper.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aec6fa0f0986fcf74ca7e3c3debb16f96d7001acbb6f2db9ffb6266973520d39
4
- data.tar.gz: 13568cfcc7dc26b1d2a1a4dbbcbe26b3b127756caaff3eb92873ce139d094c53
3
+ metadata.gz: 732e8eb3e448d4b4d2e29b99cfa776dd3fee02bcac14bf93c87112e907bca776
4
+ data.tar.gz: 030b9b4aa3f3aadddd4f29569c8ff43151ec10d5a921bc7dd58300cc9808b78b
5
5
  SHA512:
6
- metadata.gz: 368c3f2a8b146ff538ef5381f1d394965a4d7fde720fcd0757b36c578614da8b4ad81a968611a32ca05372278554a08fd4484ea1dd7ab5f59bbcce9564189bdd
7
- data.tar.gz: 92233e7a0535b6b89a0e40716c527357c345bf441387e500d86690d690f0bea21e7b1c86c58e335933efe0535f6599f6e0d0125823136484c60e87c0d908710a
6
+ metadata.gz: 6cd9d0b4486e3617da1496598baf89a16508fac212ad0c92023948098eab6e1d53ad7fc0c75e9de3f5b0bba5b8f5acebab9cd833d2fb72a6d9220df6babb6183
7
+ data.tar.gz: b71e886594c0b7aba83db98e4c7da0d5ed97d4f3341006edaa723246a531631457d224ec2a46b5caa25326f938c19861058bb6c28f6b06e1eee207f51a591d7d
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,65 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use `build-graph` to build a dependency graph for file-based build systems.
4
+
5
+ ## Installation
6
+
7
+ Add the gem to your project:
8
+
9
+ ~~~ bash
10
+ $ bundle add build-graph
11
+ ~~~
12
+
13
+ Or install it directly:
14
+
15
+ ~~~ bash
16
+ $ gem install build-graph
17
+ ~~~
18
+
19
+ ## Usage
20
+
21
+ A build graph is an abstract set of `[input, process, output]` nodes. A node executes its process within the context of a `Task` which represents a specific set of inputs and outputs and is managed within a `Walker` that walks over graph nodes, regenerating tasks where required. If inputs or outputs change (i.e. become dirty), the old task is nullified.
22
+
23
+ A `Walker` is used to traverse the build graph once. As it walks over the graph it builds a set of `Edge` relationships between nodes and only traverses relationships which are complete (`Walker#wait_on_paths`). Parent nodes also wait until all their children are complete (`Walker#wait_on_nodes`). It also keeps track of failures (`Walker#failed?`) and fails all dependencies of a node.
24
+
25
+ A `Task` is instantiated once per node when traversing the graph. The task represents a specific process being applied to the graph, e.g. build, clean, etc. It is responsible for actually performing any real actions and providing the methods to do so. A `Task` contains all details about the specific graph state at that point in time, e.g. `Task#children` and updating the node state in `Task#exit`. Statistics on the build graph are also captured through `Task` and `Walker`, e.g. number of nodes visited, etc.
26
+
27
+ ### Inputs and Outputs
28
+
29
+ Inputs to a node should be all on-disk state and any additional parameters which cause its behaviour to produce different results.
30
+
31
+ Outputs from a node should be all files that are generated directly by the processes within the node and sometimes its children.
32
+
33
+ ### Dirty Propagation
34
+
35
+ A `Node` has a set of `#inputs` and `#outputs` but these are abstract. For example, `#outputs` could be `:inherit` which means that the node symbolically has all the outputs of all its direct children. A `Task`, at the time of execution, captures its inputs and outputs and these may be monitored for changes in real time.
36
+
37
+ File changes are currently detected using `File::mtime` as this is generally a good trade off between efficiency and accuracy.
38
+
39
+ When a task is marked as dirty, it also marks all its outputs as being dirty, which in turn could mark other tasks as dirty. This is the mechanism by which dirtiness propagates through the graph. The walker should only have to traverse the graph once to build it completely. If multiple updates are required (i.e. building one part of the graph implicitly dirties another part of the graph), the specification of the graph is incomplete and this may lead to problems within the build graph.
40
+
41
+ ### Example Graph
42
+
43
+ ~~~ ruby
44
+ target("Library/UnitTest", [] => :inherit) do
45
+ library([UnitTest.cpp] => "UnitTest.a") do
46
+ compile([UnitTest.cpp] => "UnitTest.o")
47
+ link([UnitTest.o] => "libUnitTest.a")
48
+ end
49
+
50
+ copy headers: ["UnitTest.hpp"]
51
+
52
+ # Outputs become libUnitTest.a and UnitTest.hpp
53
+ end
54
+
55
+ target("Executable/UnitTest", [] => :inherit) do
56
+ depends("Library/UnitTest")
57
+
58
+ executable("main.cpp" => "UnitTest") do
59
+ compile("main.cpp" => "main.o")
60
+ link(["main.o", "libUnitTest.a"] => "UnitTest")
61
+ end
62
+
63
+ # Outputs become UnitTest
64
+ end
65
+ ~~~
@@ -0,0 +1,18 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: Build::Graph is a framework for build systems, with specific functionality
5
+ for dealing with file based processes.
6
+ metadata:
7
+ documentation_uri: https://ioquatix.github.io/build-graph
8
+ funding_uri: https://github.com/sponsors/ioquatix
9
+ source_code_uri: https://github.com/ioquatix/build-graph.git
10
+ files:
11
+ - path: getting-started.md
12
+ title: Getting Started
13
+ description: This guide explains how to use `build-graph` to build a dependency
14
+ graph for file-based build systems.
15
+ - path: visualization.md
16
+ title: Visualization
17
+ description: This guide explains how to use <code class="language-ruby">Build::Graph::Visualization</code>
18
+ to generate Mermaid flowchart diagrams from a build graph.
@@ -0,0 +1,79 @@
1
+ # Visualization
2
+
3
+ This guide explains how to use {ruby Build::Graph::Visualization} to generate Mermaid flowchart diagrams from a build graph.
4
+
5
+ ## Overview
6
+
7
+ When debugging or documenting a build graph, it is useful to see the relationships between inputs and outputs visually. `Build::Graph::Visualization` produces a [Mermaid](https://mermaid.js.org) `flowchart LR` diagram from a completed {ruby Build::Graph::Walker}, showing each file as a node and each build step as a directed edge.
8
+
9
+ ## Usage
10
+
11
+ After running a walker, pass it to {ruby Build::Graph::Visualization#generate}:
12
+
13
+ ~~~ ruby
14
+ require "build/graph/visualization"
15
+
16
+ walker = Build::Graph::Walker.new do |walker, node|
17
+ task = Build::Graph::Task.new(walker, node)
18
+ task.visit do
19
+ # perform the actual build step here
20
+ end
21
+ end
22
+
23
+ walker.update(root_node)
24
+
25
+ visualization = Build::Graph::Visualization.new
26
+ diagram = visualization.generate(walker)
27
+
28
+ puts diagram
29
+ ~~~
30
+
31
+ The output is a Mermaid diagram string that can be embedded in documentation, printed to the terminal, or written to a file:
32
+
33
+ ~~~
34
+ flowchart LR
35
+ _src_main_c[main.c]
36
+ _obj_main_o[main.o]
37
+ _src_main_c --> _obj_main_o
38
+ _obj_main_o[main.o]
39
+ _bin_program[program]
40
+ _obj_main_o --> _bin_program
41
+ ~~~
42
+
43
+ ## Traversal Without Building
44
+
45
+ To generate a diagram without actually executing any build steps (e.g. for documentation or dry-run inspection), use {ruby Build::Graph::Task#traverse} instead of `visit` in the walker block:
46
+
47
+ ~~~ ruby
48
+ walker = Build::Graph::Walker.new do |walker, node|
49
+ task = Build::Graph::Task.new(walker, node)
50
+ task.traverse
51
+ end
52
+
53
+ walker.update(root_node)
54
+
55
+ diagram = Build::Graph::Visualization.new.generate(walker)
56
+ ~~~
57
+
58
+ Unlike `visit`, `traverse` skips input validation and does not require any files to exist on disk. It registers all node outputs with the walker so that dependent nodes can still be resolved correctly.
59
+
60
+ However, `traverse` only follows **declared** edges — the inputs and outputs as written in the build definition. Some build systems discover additional dependencies at build time (for example, a C compiler producing a `.d` file that lists every header it included). Those discovered edges will not appear in the diagram.
61
+
62
+ ## Complete Graph Visualization
63
+
64
+ To get a complete and accurate picture of the graph, including any dependencies discovered during execution, use {ruby Build::Graph::Task#visit} with an empty block instead of `traverse`:
65
+
66
+ ~~~ ruby
67
+ walker = Build::Graph::Walker.new do |walker, node|
68
+ task = Build::Graph::Task.new(walker, node)
69
+ task.visit do
70
+ # perform the actual build step here, e.g. compile, link, etc.
71
+ end
72
+ end
73
+
74
+ walker.update(root_node)
75
+
76
+ diagram = Build::Graph::Visualization.new.generate(walker)
77
+ ~~~
78
+
79
+ This executes the build steps and captures the full set of inputs and outputs — including any that are only known after running the task — giving a diagram that accurately reflects what was built and why.
@@ -1,29 +1,16 @@
1
- # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
1
+ # frozen_string_literal: true
20
2
 
21
- require 'fiber'
3
+ # Released under the MIT License.
4
+ # Copyright, 2014-2026, by Samuel Williams.
5
+
6
+ require "fiber"
22
7
 
23
8
  module Build
24
9
  module Graph
25
10
  # Represents a set of inputs to a graph node.
26
11
  class Edge
12
+ # Create a new edge, optionally pre-populated with a number of pending traversals.
13
+ # @parameter count [Integer] the initial number of pending traversals.
27
14
  def initialize(count = 0)
28
15
  @fiber = Fiber.current
29
16
 
@@ -48,10 +35,12 @@ module Build
48
35
  succeeded?
49
36
  end
50
37
 
38
+ # @returns [Boolean] whether any traversing task failed.
51
39
  def failed?
52
40
  @failed.size != 0
53
41
  end
54
42
 
43
+ # @returns [Boolean] whether all traversing tasks succeeded.
55
44
  def succeeded?
56
45
  @failed.size == 0
57
46
  end
@@ -1,25 +1,10 @@
1
- # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
1
+ # frozen_string_literal: true
20
2
 
21
- require 'build/files/state'
22
- require 'build/files'
3
+ # Released under the MIT License.
4
+ # Copyright, 2014-2026, by Samuel Williams.
5
+
6
+ require "build/files/state"
7
+ require "build/files"
23
8
 
24
9
  module Build
25
10
  module Graph
@@ -44,6 +29,7 @@ module Build
44
29
  @inputs.map{|path| path.modified_time}.max
45
30
  end
46
31
 
32
+ # @returns [Boolean] whether any input or output file is missing from the filesystem.
47
33
  def missing?
48
34
  @outputs.any?{|path| !path.exist?} || @inputs.any?{|path| !path.exist?}
49
35
  end
@@ -74,6 +60,7 @@ module Build
74
60
  return false
75
61
  end
76
62
 
63
+ # @returns [Boolean] whether this node is equal to another by comparing inputs and outputs.
77
64
  def == other
78
65
  self.equal?(other) or
79
66
  self.class == other.class and
@@ -81,18 +68,25 @@ module Build
81
68
  @outputs == other.outputs
82
69
  end
83
70
 
71
+ # @returns [Boolean] whether this node is equal to another, for use in Hash and Set.
84
72
  def eql?(other)
85
73
  self.equal?(other) or self == other
86
74
  end
87
75
 
76
+ # @returns [Integer] a hash value derived from inputs and outputs.
88
77
  def hash
89
78
  @inputs.hash ^ @outputs.hash
90
79
  end
91
80
 
81
+ # @returns [String] a human-readable representation of the node.
92
82
  def inspect
93
83
  "#<#{self.class} #{@inputs.inspect} => #{@outputs.inspect}>"
94
84
  end
95
85
 
86
+ # Create a top-level node that inherits its outputs from its children.
87
+ # @parameter inputs [Build::Files::List] the input files for this node.
88
+ # @parameter outputs [Symbol] the output strategy, defaults to `:inherit`.
89
+ # @returns [Node] the constructed top-level node.
96
90
  def self.top(inputs = Files::Paths::NONE, outputs = :inherit, **options, &block)
97
91
  self.new(inputs, outputs, block, **options)
98
92
  end
@@ -1,41 +1,35 @@
1
- # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2015-2026, by Samuel Williams.
20
5
 
21
6
  module Build
22
7
  module Graph
8
+ # An error that represents a transient build failure which can be retried.
23
9
  class TransientError < StandardError
24
10
  end
25
11
 
12
+ # Mixed in to errors raised when child tasks have failed.
26
13
  module ChildrenFailed
14
+ # @returns [String] a human-readable error message.
27
15
  def self.to_s
28
16
  "Children tasks failed!"
29
17
  end
30
18
  end
31
19
 
20
+ # Mixed in to errors raised when tasks generating inputs have failed.
32
21
  module InputsFailed
22
+ # @returns [String] a human-readable error message.
33
23
  def self.to_s
34
24
  "Tasks generating inputs failed!"
35
25
  end
36
26
  end
37
27
 
28
+ # Represents a single unit of work within a build graph walk.
38
29
  class Task
30
+ # Create a new task associated with the given walker and node.
31
+ # @parameter walker [Walker] the walker driving the graph traversal.
32
+ # @parameter node [Node] the node this task is responsible for updating.
39
33
  def initialize(walker, node)
40
34
  @walker = walker
41
35
 
@@ -72,6 +66,23 @@ module Build
72
66
  # A list of any inputs whose relevant tasks failed:
73
67
  attr :inputs_failed
74
68
 
69
+ # Traverse the graph without performing any real work. Unlike {visit}, this does not
70
+ # wait for inputs to be generated, making it suitable for graph analysis tasks such
71
+ # as visualization where actual files do not need to exist.
72
+ def traverse
73
+ update_inputs_and_outputs
74
+
75
+ @walker.enter(self)
76
+
77
+ update_outputs!
78
+
79
+ @state = :complete
80
+
81
+ @walker.exit(self)
82
+
83
+ return self
84
+ end
85
+
75
86
  # Derived task can override this function to provide appropriate behaviour.
76
87
  def visit
77
88
  update_inputs_and_outputs
@@ -124,10 +135,12 @@ module Build
124
135
  return child_task
125
136
  end
126
137
 
138
+ # @returns [Boolean] whether the task has failed.
127
139
  def failed?
128
140
  @state == :failed
129
141
  end
130
142
 
143
+ # @returns [Boolean] whether the task has completed successfully.
131
144
  def complete?
132
145
  @state == :complete
133
146
  end
@@ -142,18 +155,22 @@ module Build
142
155
  end
143
156
  end
144
157
 
158
+ # Resets the node in the walker if inputs or outputs have changed since the last run.
145
159
  def changed!
146
160
  @walker.delete(@node) if (@inputs.update! or @outputs.update!)
147
161
  end
148
162
 
163
+ # @returns [Array(String)] the list of root directories for all input and output paths.
149
164
  def directories
150
165
  (@inputs.roots + @outputs.roots).collect{|path| path.to_s}
151
166
  end
152
167
 
168
+ # @returns [String] a short human-readable summary of the task.
153
169
  def to_s
154
170
  "#<#{self.class} #{node_string} #{state_string}>"
155
171
  end
156
172
 
173
+ # @returns [String] a detailed human-readable representation including object identity.
157
174
  def inspect
158
175
  "\#<#{self.class}:0x#{self.object_id.to_s(16)} #{node_string} #{state_string}>"
159
176
  end
@@ -210,7 +227,7 @@ module Build
210
227
 
211
228
  # Fail the task with the given error. Any task which is waiting on this task will also fail (eventually).
212
229
  def fail!(error)
213
- @walker.logger&.error(self) {error}
230
+ Console.error(self, "Task failed!", exception: error)
214
231
 
215
232
  @error = error
216
233
  @state = :failed
@@ -1,25 +1,12 @@
1
- # Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
1
+ # frozen_string_literal: true
20
2
 
3
+ # Released under the MIT License.
4
+ # Copyright, 2014-2026, by Samuel Williams.
5
+
6
+ # @namespace
21
7
  module Build
8
+ # @namespace
22
9
  module Graph
23
- VERSION = "2.1.1"
10
+ VERSION = "2.3.0"
24
11
  end
25
12
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2026, by Samuel Williams.
5
+
6
+ module Build
7
+ module Graph
8
+ # Generates Mermaid flowchart visualizations of build graphs.
9
+ class Visualization
10
+ # Convert a path to a valid Mermaid node ID.
11
+ # @parameter path [String] The path to sanitize.
12
+ # @returns [String] A sanitized identifier safe for use in Mermaid diagrams.
13
+ def sanitize_id(path)
14
+ path.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
15
+ end
16
+
17
+ # Generate a Mermaid flowchart diagram for a completed walker.
18
+ # @parameter walker [Walker] The completed walker containing tasks.
19
+ # @returns [String] A Mermaid flowchart diagram in text format.
20
+ def generate(walker)
21
+ lines = ["flowchart LR"]
22
+
23
+ walker.tasks.each do |node, task|
24
+ next unless task.inputs && task.outputs
25
+ next if task.outputs.equal?(:inherit)
26
+
27
+ input_ids = task.inputs.to_a.map{|path| sanitize_id(path)}
28
+ output_ids = task.outputs.to_a.map{|path| sanitize_id(path)}
29
+
30
+ task.inputs.each do |path|
31
+ lines << " #{sanitize_id(path)}[#{path.basename}]"
32
+ end
33
+
34
+ task.outputs.each do |path|
35
+ lines << " #{sanitize_id(path)}[#{path.basename}]"
36
+ end
37
+
38
+ label = node.respond_to?(:title) ? node.title.to_s : nil
39
+
40
+ input_ids.each do |input_id|
41
+ output_ids.each do |output_id|
42
+ if label && !label.empty?
43
+ lines << " #{input_id} -->|#{label}| #{output_id}"
44
+ else
45
+ lines << " #{input_id} --> #{output_id}"
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ return lines.join("\n")
52
+ end
53
+ end
54
+ end
55
+ end