build-graph 0.3.6 → 0.3.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +19 -1
- data/build-graph.gemspec +2 -2
- data/lib/build/graph/controller.rb +9 -4
- data/lib/build/graph/edge.rb +27 -12
- data/lib/build/graph/node.rb +41 -112
- data/lib/build/graph/task.rb +168 -0
- data/lib/build/graph/version.rb +1 -1
- data/lib/build/graph/walker.rb +109 -135
- data/spec/build/graph/graph_spec.rb +74 -103
- data/spec/build/graph/inherit_spec.rb +56 -0
- data/spec/build/graph/node_spec.rb +68 -0
- data/spec/build/graph/task_spec.rb +66 -0
- data/spec/build/graph/walker_spec.rb +93 -0
- metadata +15 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9e451eb353fae615423fa48f9f20a2890fcc6157
|
4
|
+
data.tar.gz: 78c36724ea7dc989d38c4b908fc9b9d5a36db7da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80e50b90964b646412353ad187c787a4d40e47e4b11498bfe9f3c2df3e4c4f338b8f1b1b8909dc855204f3e8f26652735e3db9e6d6d8b706852dbeb3fbb7b5d4
|
7
|
+
data.tar.gz: 67c5ce3bdf104256332a94ddd12becb2516d95941a3d697bdf6d753d6b4262d64e33fab17487be0b570ebb8f6d51650ffc967fd2af9b451b46dcb3d105121015
|
data/README.md
CHANGED
@@ -22,7 +22,25 @@ Or install it yourself as:
|
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
25
|
-
|
25
|
+
A build graph is an abstract set of `[input, process, output]` nodes. A node executes it's proces 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 task is destroyed and regenerated.
|
26
|
+
|
27
|
+
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.
|
28
|
+
|
29
|
+
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.
|
30
|
+
|
31
|
+
### Inputs and Outputs
|
32
|
+
|
33
|
+
Inputs to a node should be all on-disk state and any additional parameters which cause it's behavior to produce different results.
|
34
|
+
|
35
|
+
Outputs from a node should be all files that are generated directly by the processes within the node and sometimes it's children.
|
36
|
+
|
37
|
+
### Dirty Propagation
|
38
|
+
|
39
|
+
A `Node` has a set of `#inputs` and `#outputs` but these are abstract. A `Task`, at the time of execution, captures it's inputs and outputs and these may be monitored for changes in real time. The simplest way to cause a task to regenerate is to simply remove it from the existing graph and it will be regenerated.
|
40
|
+
|
41
|
+
File changes are currently detected using `File::mtime` as this is generally a good trade off between efficiency and accuracy.
|
42
|
+
|
43
|
+
When a task is marked as dirty, it also marks all it's outputs as being dirty, which in cause could mark other tasks as dirty. This is the mechanism for 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. buidling 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.
|
26
44
|
|
27
45
|
## Contributing
|
28
46
|
|
data/build-graph.gemspec
CHANGED
@@ -23,13 +23,13 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.required_ruby_version = '>= 2.0'
|
24
24
|
|
25
25
|
spec.add_dependency "process-group", "~> 0.2.1"
|
26
|
-
spec.add_dependency "build-files", "~> 0.3.
|
26
|
+
spec.add_dependency "build-files", "~> 0.3.3"
|
27
27
|
|
28
28
|
spec.add_dependency "system"
|
29
29
|
spec.add_dependency "rainbow", "~> 2.0.0"
|
30
30
|
|
31
31
|
spec.add_development_dependency "bundler", "~> 1.3"
|
32
32
|
spec.add_development_dependency "rspec", "~> 3.0.0"
|
33
|
-
spec.add_development_dependency "build-makefile", "~> 0.
|
33
|
+
spec.add_development_dependency "build-makefile", "~> 0.3.0"
|
34
34
|
spec.add_development_dependency "rake"
|
35
35
|
end
|
@@ -28,12 +28,12 @@ require_relative 'edge'
|
|
28
28
|
module Build
|
29
29
|
module Graph
|
30
30
|
# The top level graph controller is responsible for managing build graph state.
|
31
|
-
class Controller
|
31
|
+
class Controller
|
32
32
|
def initialize
|
33
33
|
super
|
34
|
-
|
34
|
+
|
35
35
|
@nodes = {}
|
36
|
-
|
36
|
+
|
37
37
|
build_graph!
|
38
38
|
end
|
39
39
|
|
@@ -58,7 +58,7 @@ module Build
|
|
58
58
|
|
59
59
|
walker = walk do |walker, node|
|
60
60
|
nodes << node
|
61
|
-
|
61
|
+
|
62
62
|
yield walker, node
|
63
63
|
end
|
64
64
|
|
@@ -97,6 +97,11 @@ module Build
|
|
97
97
|
|
98
98
|
return walker
|
99
99
|
end
|
100
|
+
|
101
|
+
# What to do when a task has a trasient failure:
|
102
|
+
def task_failure(error, task)
|
103
|
+
$stderr.puts Rainbow("Error: #{error.inspect}").red
|
104
|
+
end
|
100
105
|
end
|
101
106
|
end
|
102
107
|
end
|
data/lib/build/graph/edge.rb
CHANGED
@@ -31,45 +31,60 @@ module Build
|
|
31
31
|
|
32
32
|
# The number of inputs we are waiting for:
|
33
33
|
@count = count
|
34
|
+
@vertices = 0
|
34
35
|
|
35
36
|
@failed = []
|
36
37
|
end
|
37
|
-
|
38
|
+
|
38
39
|
attr :failed
|
39
|
-
|
40
|
+
|
40
41
|
attr :fiber
|
41
42
|
attr :count
|
42
43
|
|
43
|
-
# Wait until all inputs to the edge have been traversed.
|
44
|
+
# Wait until all inputs to the edge have been traversed. Returns false if failed?
|
44
45
|
def wait
|
45
46
|
if @count > 0
|
46
47
|
Fiber.yield
|
47
48
|
end
|
48
|
-
|
49
|
-
|
49
|
+
|
50
|
+
succeeded?
|
50
51
|
end
|
51
|
-
|
52
|
+
|
52
53
|
attr :failed
|
53
|
-
|
54
|
+
|
54
55
|
def failed?
|
55
56
|
@failed.size != 0
|
56
57
|
end
|
57
58
|
|
59
|
+
def succeeded?
|
60
|
+
@failed.size == 0
|
61
|
+
end
|
62
|
+
|
58
63
|
# Traverse the edge, mark the edge as failed if the source was also failed.
|
59
|
-
def traverse(
|
64
|
+
def traverse(task)
|
60
65
|
@count -= 1
|
61
|
-
|
62
|
-
if
|
63
|
-
|
66
|
+
|
67
|
+
# The entire edge fails if any individual task fails.
|
68
|
+
if task.failed?
|
69
|
+
@failed << task
|
64
70
|
end
|
65
|
-
|
71
|
+
|
66
72
|
if @count == 0
|
67
73
|
@fiber.resume
|
68
74
|
end
|
69
75
|
end
|
70
76
|
|
77
|
+
def skip!(task)
|
78
|
+
@vertices += 1
|
79
|
+
|
80
|
+
if task.failed?
|
81
|
+
@failed << task
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
71
85
|
# Increase the number of traversals we are waiting for.
|
72
86
|
def increment!
|
87
|
+
@vertices += 1
|
73
88
|
@count += 1
|
74
89
|
end
|
75
90
|
end
|
data/lib/build/graph/node.rb
CHANGED
@@ -19,140 +19,69 @@
|
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
21
|
require 'build/files/state'
|
22
|
+
require 'build/files'
|
23
|
+
|
24
|
+
require 'rainbow'
|
22
25
|
|
23
26
|
module Build
|
24
27
|
module Graph
|
28
|
+
# This is essentialy a immutable key:
|
25
29
|
class Node
|
26
|
-
def initialize(
|
27
|
-
@controller = controller
|
28
|
-
|
29
|
-
@state = Files::IOState.new(inputs, outputs)
|
30
|
-
|
31
|
-
@status = :unknown
|
32
|
-
@fiber = nil
|
33
|
-
|
30
|
+
def initialize(inputs, outputs, process)
|
34
31
|
# These are immutable - rather than change them, create a new node:
|
35
32
|
@inputs = inputs
|
36
33
|
@outputs = outputs
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
def eql?(other)
|
42
|
-
other.kind_of?(self.class) and @inputs.eql?(other.inputs) and @outputs.eql?(other.outputs)
|
43
|
-
end
|
44
|
-
|
45
|
-
def hash
|
46
|
-
[@inputs, @outputs].hash
|
47
|
-
end
|
48
|
-
|
49
|
-
def directories
|
50
|
-
@state.files.roots
|
51
|
-
end
|
52
|
-
|
53
|
-
def remove!
|
54
|
-
@controller.delete(self)
|
55
|
-
end
|
56
|
-
|
57
|
-
# It is possible this function is called unnecessarily. The state check confirms whether a change occurred or not.
|
58
|
-
def changed!(outputs = [])
|
59
|
-
# Don't do anything if we are already dirty.
|
60
|
-
return if dirty?
|
61
|
-
|
62
|
-
if @state.intersects?(outputs) || @state.update!
|
63
|
-
# puts "** Dirty: #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect}"
|
64
|
-
|
65
|
-
# Could possibly use unknown status here.
|
66
|
-
@status = :dirty
|
67
|
-
|
68
|
-
# If this node changes, we force all other nodes which depend on this node to be dirty.
|
69
|
-
@controller.update(directories, @outputs)
|
70
|
-
end
|
34
|
+
|
35
|
+
# Represents an abstract process, e.g. a name or a function.
|
36
|
+
@process = process
|
71
37
|
end
|
72
38
|
|
73
39
|
attr :inputs
|
74
40
|
attr :outputs
|
41
|
+
attr :process
|
75
42
|
|
76
|
-
#
|
77
|
-
|
43
|
+
# Nodes that inherit outputs are special in the sense that outputs are not available until all child nodes have been evaluated.
|
44
|
+
def inherit_outputs?
|
45
|
+
@outputs == :inherit
|
46
|
+
end
|
78
47
|
|
79
|
-
#
|
80
|
-
|
81
|
-
|
82
|
-
def unknown?
|
83
|
-
@status == :unknown
|
48
|
+
# This computes the most recent modified time for all inputs.
|
49
|
+
def modified_time
|
50
|
+
modified_time = @inputs.map{|path| path.modified_time}.max
|
84
51
|
end
|
85
|
-
|
52
|
+
|
53
|
+
# This is a canonical dirty function. All outputs must exist and must be newer than all inputs. This function is not efficient, in the sense that it must query all files on disk for last modified time.
|
86
54
|
def dirty?
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
def failed?
|
103
|
-
@status == :failed
|
55
|
+
if inherit_outputs?
|
56
|
+
return true
|
57
|
+
else
|
58
|
+
# Dirty if any outputs don't exist:
|
59
|
+
return true if @outputs.any?{|path| !path.exist?}
|
60
|
+
|
61
|
+
# Dirty if input modified after any output:
|
62
|
+
input_modified_time = self.modified_time
|
63
|
+
|
64
|
+
# Outputs should always be more recent than their inputs:
|
65
|
+
return true if @outputs.any?{|output_path| output_path.modified_time < input_modified_time}
|
66
|
+
end
|
67
|
+
|
68
|
+
return false
|
104
69
|
end
|
105
|
-
|
106
|
-
def
|
107
|
-
@
|
70
|
+
|
71
|
+
def eql?(other)
|
72
|
+
other.kind_of?(self.class) and @inputs.eql?(other.inputs) and @outputs.eql?(other.outputs) and @process.eql?(other.process)
|
108
73
|
end
|
109
|
-
|
110
|
-
# If we are in the initial state, we need to check if the outputs are fresh.
|
111
|
-
def update_status!
|
112
|
-
#puts "Update status: #{@inputs.inspect} -> #{@outputs.inspect} (status=#{@status} @fiber=#{@fiber.inspect}) @status=#{@status} @state.fresh?=#{@state.fresh?}"
|
113
74
|
|
114
|
-
|
115
|
-
|
116
|
-
unless @state.fresh?
|
117
|
-
changed!(self.inputs)
|
118
|
-
else
|
119
|
-
@status = :clean
|
120
|
-
end
|
121
|
-
end
|
75
|
+
def hash
|
76
|
+
[@inputs, @outputs, @process].hash
|
122
77
|
end
|
123
|
-
|
78
|
+
|
124
79
|
def inspect
|
125
|
-
"<#{
|
126
|
-
end
|
127
|
-
|
128
|
-
def requires_update?
|
129
|
-
not clean?
|
80
|
+
"<#{self.class.name} #{@inputs.inspect} => #{@outputs.inspect} by #{@process.inspect}>"
|
130
81
|
end
|
131
|
-
|
132
|
-
# Perform some actions to update this node, returns when completed, and the node is no longer dirty.
|
133
|
-
def update!(walker)
|
134
|
-
#puts "Walking #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect} (dirty=#{dirty?} @fiber=#{@fiber.inspect})"
|
135
82
|
|
136
|
-
|
137
|
-
|
138
|
-
# puts "Beginning: #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect}"
|
139
|
-
|
140
|
-
@fiber = Fiber.new do
|
141
|
-
task = walker.task(self)
|
142
|
-
|
143
|
-
task.visit
|
144
|
-
|
145
|
-
# Commit changes:
|
146
|
-
# puts "** Committing: #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect}"
|
147
|
-
|
148
|
-
@state.update!
|
149
|
-
@fiber = nil
|
150
|
-
|
151
|
-
task.exit
|
152
|
-
end
|
153
|
-
|
154
|
-
@fiber.resume
|
155
|
-
end
|
83
|
+
def self.top(inputs = Files::Paths::NONE, outputs = :inherit, &block)
|
84
|
+
self.new(inputs, outputs, block)
|
156
85
|
end
|
157
86
|
end
|
158
87
|
end
|
@@ -0,0 +1,168 @@
|
|
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.
|
20
|
+
|
21
|
+
module Build
|
22
|
+
module Graph
|
23
|
+
class Task
|
24
|
+
def initialize(walker, node)
|
25
|
+
@walker = walker
|
26
|
+
|
27
|
+
@walker.tasks[node] = self
|
28
|
+
|
29
|
+
@node = node
|
30
|
+
|
31
|
+
# If the execution of the node fails, this is where we save the error:
|
32
|
+
@error = nil
|
33
|
+
|
34
|
+
@children = []
|
35
|
+
|
36
|
+
@state = nil
|
37
|
+
|
38
|
+
@inputs_failed = false
|
39
|
+
end
|
40
|
+
|
41
|
+
attr :inputs
|
42
|
+
attr :outputs
|
43
|
+
|
44
|
+
attr :children
|
45
|
+
attr :state
|
46
|
+
|
47
|
+
attr :walker
|
48
|
+
|
49
|
+
attr :node
|
50
|
+
|
51
|
+
# A list of any inputs whose relevant tasks failed:
|
52
|
+
attr :inputs_failed
|
53
|
+
|
54
|
+
# Derived task should override this function to provide appropriate behaviour.
|
55
|
+
def visit
|
56
|
+
update_inputs_and_outputs
|
57
|
+
|
58
|
+
# Inforn the walker a new task is being generated for this node:
|
59
|
+
@walker.enter(self)
|
60
|
+
|
61
|
+
@fiber = Fiber.new do
|
62
|
+
# If all inputs were good, we can update the node.
|
63
|
+
if wait_for_inputs?
|
64
|
+
begin
|
65
|
+
yield
|
66
|
+
rescue TransientError => error
|
67
|
+
fail!(error)
|
68
|
+
end
|
69
|
+
else
|
70
|
+
fail!(:inputs)
|
71
|
+
end
|
72
|
+
|
73
|
+
unless wait_for_children?
|
74
|
+
fail!(:children)
|
75
|
+
end
|
76
|
+
|
77
|
+
update_outputs
|
78
|
+
|
79
|
+
@state ||= :complete
|
80
|
+
|
81
|
+
@walker.exit(self)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Schedule the work, hopefully synchronously:
|
85
|
+
@fiber.resume
|
86
|
+
|
87
|
+
# This allows the child task to be passed back to the parent when it is first invoked.
|
88
|
+
return self
|
89
|
+
end
|
90
|
+
|
91
|
+
def invoke(node)
|
92
|
+
child_task = @walker.call(node)
|
93
|
+
|
94
|
+
raise ArgumentError.new("Invalid child task") unless child_task
|
95
|
+
|
96
|
+
@children << child_task
|
97
|
+
end
|
98
|
+
|
99
|
+
def failed?
|
100
|
+
@state == :failed
|
101
|
+
end
|
102
|
+
|
103
|
+
def complete?
|
104
|
+
@state == :complete
|
105
|
+
end
|
106
|
+
|
107
|
+
# Returns true if the outputs of the task are out of date w.r.t. the inputs.
|
108
|
+
# Currently, does not take into account if the input is a glob and files have been added.
|
109
|
+
def dirty?
|
110
|
+
@outputs.dirty?(@inputs)
|
111
|
+
end
|
112
|
+
|
113
|
+
def changed!
|
114
|
+
@walker.delete(@node)
|
115
|
+
end
|
116
|
+
|
117
|
+
def directories
|
118
|
+
@inputs.roots + @outputs.roots
|
119
|
+
end
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
"<#{self.class}:#{'0x%X' % self.object_id} #{@node.inspect} #{@state}>"
|
123
|
+
end
|
124
|
+
|
125
|
+
attr :error
|
126
|
+
attr :state
|
127
|
+
|
128
|
+
protected
|
129
|
+
def update_inputs_and_outputs
|
130
|
+
# If @node.inputs is a glob, this part of the process converts the glob into an actual list of files.
|
131
|
+
@inputs = Files::State.new(@node.inputs)
|
132
|
+
|
133
|
+
unless @node.inherit_outputs?
|
134
|
+
@outputs = Files::State.new(@node.outputs)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def children_outputs
|
139
|
+
@children.collect(&:outputs).inject(Files::Paths::NONE, &:+)
|
140
|
+
end
|
141
|
+
|
142
|
+
def update_outputs
|
143
|
+
if @node.inherit_outputs?
|
144
|
+
@outputs = Files::State.new(self.children_outputs)
|
145
|
+
else
|
146
|
+
# After the task has finished, we update the output states:
|
147
|
+
@outputs.update!
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def fail!(error)
|
152
|
+
@error = error
|
153
|
+
@state = :failed
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns false if any input failed.
|
157
|
+
def wait_for_inputs?
|
158
|
+
# Wait on any inputs, returns whether any inputs failed:
|
159
|
+
@walker.wait_on_paths(@inputs)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Returns false if any child failed.
|
163
|
+
def wait_for_children?
|
164
|
+
@walker.wait_for_children(self, @children)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|