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
data/lib/build/graph/version.rb
CHANGED
data/lib/build/graph/walker.rb
CHANGED
@@ -20,195 +20,169 @@
|
|
20
20
|
|
21
21
|
require 'set'
|
22
22
|
|
23
|
+
require_relative 'task'
|
24
|
+
require_relative 'node'
|
25
|
+
require_relative 'edge'
|
23
26
|
require_relative 'error'
|
24
27
|
|
25
28
|
module Build
|
26
29
|
module Graph
|
27
30
|
# A walker walks over a graph and applies a task to each node.
|
28
31
|
class Walker
|
29
|
-
def initialize(
|
30
|
-
|
31
|
-
@
|
32
|
+
def initialize(&block)
|
33
|
+
# Node -> Task mapping.
|
34
|
+
@tasks = {}
|
32
35
|
|
33
|
-
|
34
|
-
@count = 0
|
36
|
+
@update = block
|
35
37
|
|
36
38
|
@outputs = {}
|
37
|
-
|
38
|
-
|
39
|
-
# Generate a list of dirty outputs, possibly a subset, if the build graph might generate additional nodes:
|
40
|
-
@controller.nodes.each do |key, node|
|
41
|
-
# For a given child, a list of any parents waiting on it.
|
42
|
-
if node.dirty?
|
43
|
-
@dirty << node
|
44
|
-
|
45
|
-
@outputs[node] = []
|
46
|
-
|
47
|
-
node.outputs.each do |output|
|
48
|
-
@outputs[output] = []
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
39
|
+
|
53
40
|
@parents = {}
|
54
|
-
|
41
|
+
|
55
42
|
# Failed output paths:
|
56
|
-
@
|
43
|
+
@failed_tasks = []
|
44
|
+
@failed_outputs = Set.new
|
57
45
|
|
58
|
-
|
59
|
-
@failures = 0
|
46
|
+
@monitor = Files::Monitor.new
|
60
47
|
end
|
61
48
|
|
62
|
-
attr :
|
63
|
-
attr :task
|
49
|
+
attr :tasks # {Node => Task}
|
64
50
|
|
65
51
|
attr :outputs
|
66
52
|
|
67
|
-
|
53
|
+
attr :failed_tasks
|
54
|
+
attr :failed_outputs
|
55
|
+
|
56
|
+
attr :count
|
68
57
|
attr :dirty
|
69
58
|
|
70
59
|
attr :parents
|
71
60
|
|
72
|
-
|
73
|
-
attr :failed
|
61
|
+
attr :monitor
|
74
62
|
|
75
|
-
def
|
76
|
-
|
63
|
+
def update(nodes)
|
64
|
+
Array(nodes).each do |node|
|
65
|
+
@update.call(self, node)
|
66
|
+
end
|
77
67
|
end
|
78
68
|
|
79
|
-
def
|
80
|
-
|
69
|
+
def call(node)
|
70
|
+
# We try to fetch the task if it has already been invoked, otherwise we create a new task.
|
71
|
+
@tasks.fetch(node) do
|
72
|
+
@update.call(self, node)
|
73
|
+
|
74
|
+
# This should now be defined:
|
75
|
+
@tasks[node]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def failed?
|
80
|
+
@failed_tasks.size > 0
|
81
81
|
end
|
82
82
|
|
83
83
|
def wait_on_paths(paths)
|
84
|
+
# If there are no paths, we are done:
|
85
|
+
return true if paths.count == 0
|
86
|
+
|
87
|
+
# We create a new directed hyper-graph edge which waits for all paths to be ready (or failed):
|
84
88
|
edge = Edge.new
|
85
|
-
|
86
|
-
|
89
|
+
|
90
|
+
paths = paths.collect(&:to_s)
|
91
|
+
|
87
92
|
paths.each do |path|
|
88
|
-
|
89
|
-
|
90
|
-
|
93
|
+
# Is there a task generating this output?
|
94
|
+
if outputs = @outputs[path]
|
95
|
+
# When the output is ready, trigger this edge:
|
96
|
+
outputs << edge
|
91
97
|
edge.increment!
|
92
98
|
end
|
93
|
-
|
94
|
-
if !failed and @failed.include?(path)
|
95
|
-
failed = true
|
96
|
-
end
|
97
99
|
end
|
98
|
-
|
99
|
-
|
100
|
+
|
101
|
+
failed = paths.any?{|path| @failed_outputs.include? path}
|
102
|
+
|
103
|
+
return edge.wait && !failed
|
100
104
|
end
|
101
|
-
|
102
|
-
def wait_for_nodes(children)
|
103
|
-
edge = Edge.new
|
104
105
|
|
106
|
+
# A parent task only completes once all it's children are complete.
|
107
|
+
def wait_for_children(parent, children)
|
108
|
+
# Consider only incomplete/failed children:
|
109
|
+
children = children.select{|child| !child.complete?}
|
110
|
+
|
111
|
+
# If there are no children like this, then done:
|
112
|
+
return true if children.size == 0
|
113
|
+
|
114
|
+
# Otherwise, construct an edge to track state changes:
|
115
|
+
edge = Edge.new
|
116
|
+
|
105
117
|
children.each do |child|
|
106
|
-
if
|
118
|
+
if child.failed?
|
119
|
+
edge.skip!(child)
|
120
|
+
else
|
121
|
+
# We are waiting for this child to finish:
|
107
122
|
edge.increment!
|
108
|
-
|
109
|
-
@parents[child] ||= []
|
110
|
-
@parents[child] << edge
|
123
|
+
|
124
|
+
@parents[child.node] ||= []
|
125
|
+
@parents[child.node] << edge
|
111
126
|
end
|
112
127
|
end
|
128
|
+
|
129
|
+
return edge.wait
|
130
|
+
end
|
113
131
|
|
114
|
-
|
132
|
+
def enter(task)
|
133
|
+
# puts "--> #{task.node.process}"
|
134
|
+
@tasks[task.node] = task
|
135
|
+
|
136
|
+
# In order to wait on outputs, they must be known before entering the task. This might seem odd, but unless we know outputs are being generated, waiting for them to complete is impossible - unless this was somehow specified ahead of time. The implications of this logic is that all tasks must be sequential in terms of output -> input chaning. This is not a problem in practice.
|
137
|
+
if outputs = task.outputs
|
138
|
+
outputs.each do |path|
|
139
|
+
@outputs[path.to_s] = []
|
140
|
+
end
|
141
|
+
end
|
115
142
|
end
|
116
|
-
|
117
|
-
def exit(node)
|
118
|
-
@dirty.delete(node)
|
119
143
|
|
144
|
+
def exit(task)
|
145
|
+
# puts "<-- #{task.node.process}"
|
146
|
+
|
120
147
|
# Fail outputs if the node failed:
|
121
|
-
if
|
122
|
-
@
|
123
|
-
|
148
|
+
if task.failed?
|
149
|
+
@failed_tasks << task
|
150
|
+
|
151
|
+
if task.outputs
|
152
|
+
@failed_outputs += task.outputs.collect{|path| path.to_s}
|
153
|
+
end
|
124
154
|
end
|
125
155
|
|
126
156
|
# Clean the node's outputs:
|
127
|
-
|
157
|
+
task.outputs.each do |path|
|
158
|
+
path = path.to_s
|
159
|
+
|
128
160
|
if edges = @outputs.delete(path)
|
129
|
-
edges.each{|edge| edge.traverse(
|
161
|
+
edges.each{|edge| edge.traverse(task)}
|
130
162
|
end
|
131
163
|
end
|
132
|
-
|
133
|
-
#
|
134
|
-
if parents = @parents.delete(node)
|
135
|
-
parents.each{|edge| edge.traverse(
|
164
|
+
|
165
|
+
# Notify the parent nodes that the child is done:
|
166
|
+
if parents = @parents.delete(task.node)
|
167
|
+
parents.each{|edge| edge.traverse(task)}
|
136
168
|
end
|
169
|
+
|
170
|
+
@monitor.add(task)
|
137
171
|
end
|
138
|
-
end
|
139
|
-
|
140
|
-
# A task is a specific process and scope applied to a graph node.
|
141
|
-
class Task
|
142
|
-
def initialize(controller, walker, node)
|
143
|
-
@controller = controller
|
144
|
-
@node = node
|
145
|
-
@walker = walker
|
146
|
-
|
147
|
-
# If the execution of the node fails, this is where we save the error:
|
148
|
-
@error = nil
|
149
|
-
|
150
|
-
@children = []
|
151
|
-
end
|
152
|
-
|
153
|
-
attr :children
|
154
|
-
|
155
|
-
def inputs
|
156
|
-
@node.inputs
|
157
|
-
end
|
158
|
-
|
159
|
-
def outputs
|
160
|
-
@node.outputs
|
161
|
-
end
|
162
|
-
|
163
|
-
def wet?
|
164
|
-
@node.dirty?
|
165
|
-
end
|
166
|
-
|
167
|
-
# Derived task should override this function to provide appropriate behaviour.
|
168
|
-
def visit
|
169
|
-
wait_for_inputs
|
170
172
|
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
yield
|
175
|
-
rescue TransientError => error
|
176
|
-
$stderr.puts Rainbow("Error: #{error.inspect}").red
|
177
|
-
@error = error
|
178
|
-
end
|
173
|
+
def delete(node)
|
174
|
+
if task = @tasks.delete(node)
|
175
|
+
@monitor.delete(task)
|
179
176
|
end
|
180
|
-
|
181
|
-
wait_for_children
|
182
177
|
end
|
183
|
-
|
184
|
-
def
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
@node.clean!
|
189
|
-
end
|
190
|
-
|
191
|
-
@walker.exit(@node)
|
178
|
+
|
179
|
+
def clear_failed
|
180
|
+
@failed_tasks.each do |task|
|
181
|
+
self.delete(task.node)
|
182
|
+
end if @failed_tasks
|
192
183
|
|
193
|
-
@
|
194
|
-
|
195
|
-
|
196
|
-
protected
|
197
|
-
def wait_for_inputs
|
198
|
-
# Wait on any inputs, returns whether any inputs failed:
|
199
|
-
@inputs_failed = @walker.wait_on_paths(@node.inputs)
|
200
|
-
end
|
201
|
-
|
202
|
-
def wait_for_children
|
203
|
-
@walker.wait_for_nodes(@children)
|
204
|
-
end
|
205
|
-
|
206
|
-
def any_child_failed?
|
207
|
-
@children.any?{|child| child.failed?}
|
208
|
-
end
|
209
|
-
|
210
|
-
def any_inputs_failed?
|
211
|
-
@inputs_failed
|
184
|
+
@failed_tasks = []
|
185
|
+
@failed_outputs = Set.new
|
212
186
|
end
|
213
187
|
end
|
214
188
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
#!/usr/bin/env rspec
|
1
2
|
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
3
|
#
|
3
4
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
@@ -18,72 +19,45 @@
|
|
18
19
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
20
|
# THE SOFTWARE.
|
20
21
|
|
21
|
-
require 'build/graph'
|
22
|
-
require 'build/files'
|
22
|
+
require 'build/graph/walker'
|
23
23
|
require 'build/makefile'
|
24
24
|
|
25
25
|
require 'process/group'
|
26
26
|
require 'fileutils'
|
27
|
-
require 'rainbow'
|
28
27
|
|
29
28
|
module Build::Graph::GraphSpec
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
def initialize(
|
35
|
-
|
29
|
+
include Build::Graph
|
30
|
+
include Build::Files
|
31
|
+
|
32
|
+
class ProcessNode < Node
|
33
|
+
def initialize(inputs, outputs, block)
|
34
|
+
super(inputs, outputs, block.source_location)
|
36
35
|
|
37
|
-
|
38
|
-
end
|
39
|
-
|
40
|
-
def apply!(scope)
|
41
|
-
scope.instance_eval(&@update)
|
36
|
+
@block = block
|
42
37
|
end
|
43
38
|
|
44
|
-
|
45
|
-
|
46
|
-
if outputs.count == 0
|
47
|
-
return true
|
48
|
-
else
|
49
|
-
super
|
50
|
-
end
|
39
|
+
def evaluate(context)
|
40
|
+
context.instance_eval(&@block)
|
51
41
|
end
|
52
42
|
end
|
53
43
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
def initialize(controller, walker, node, group = nil)
|
59
|
-
super(controller, walker, node)
|
44
|
+
class ProcessTask < Task
|
45
|
+
def process(inputs, outputs = :inherit, &block)
|
46
|
+
inputs = Build::Files::List.coerce(inputs)
|
47
|
+
outputs = Build::Files::List.coerce(outputs) unless outputs.kind_of? Symbol
|
60
48
|
|
61
|
-
|
49
|
+
node = ProcessNode.new(inputs, outputs, block)
|
50
|
+
|
51
|
+
self.invoke(node)
|
62
52
|
end
|
63
53
|
|
64
54
|
def wet?
|
65
|
-
@group
|
66
|
-
end
|
67
|
-
|
68
|
-
def process(inputs, outputs, &block)
|
69
|
-
inputs = Build::Files::List.coerce(inputs)
|
70
|
-
outputs = Build::Files::List.coerce(outputs)
|
71
|
-
|
72
|
-
child_node = @controller.nodes.fetch([inputs, outputs]) do |key|
|
73
|
-
@controller.nodes[key] = Node.new(@controller, inputs, outputs, &block)
|
74
|
-
end
|
75
|
-
|
76
|
-
@children << child_node
|
77
|
-
|
78
|
-
# State saved in update!
|
79
|
-
child_node.update!(@walker)
|
80
|
-
|
81
|
-
return child_node
|
55
|
+
@group != nil
|
82
56
|
end
|
83
57
|
|
84
58
|
def run(*arguments)
|
85
59
|
if wet?
|
86
|
-
puts
|
60
|
+
puts "\t[run] #{arguments.join(' ')}"
|
87
61
|
status = @group.spawn(*arguments)
|
88
62
|
|
89
63
|
if status != 0
|
@@ -92,48 +66,17 @@ module Build::Graph::GraphSpec
|
|
92
66
|
end
|
93
67
|
end
|
94
68
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
69
|
+
# This function is called to finish the invocation of the task within the graph.
|
70
|
+
# There are two possible ways this function can generally proceed.
|
71
|
+
# 1/ The node this task is running for is clean, and thus no actual processing needs to take place, but children should probably be executed.
|
72
|
+
# 2/ The node this task is running for is dirty, and the execution of commands should work as expected.
|
73
|
+
def update(group = nil)
|
74
|
+
@group = group if @node.dirty?
|
75
|
+
|
76
|
+
@node.evaluate(self)
|
99
77
|
end
|
100
78
|
end
|
101
79
|
|
102
|
-
# The controller contains all graph nodes and is responsible for executing tasks on the graph.
|
103
|
-
class Controller < Build::Graph::Controller
|
104
|
-
def initialize(&block)
|
105
|
-
@top = Node.new(self, &block)
|
106
|
-
|
107
|
-
super()
|
108
|
-
end
|
109
|
-
|
110
|
-
attr_accessor :top
|
111
|
-
|
112
|
-
def traverse!(walker)
|
113
|
-
@top.update!(walker)
|
114
|
-
end
|
115
|
-
|
116
|
-
def build_graph!
|
117
|
-
super do |walker, node|
|
118
|
-
Task.new(self, walker, node)
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
def update!
|
123
|
-
group = Process::Group.new
|
124
|
-
|
125
|
-
walker = super do |walker, node|
|
126
|
-
Task.new(self, walker, node, group)
|
127
|
-
end
|
128
|
-
|
129
|
-
group.wait
|
130
|
-
|
131
|
-
return walker
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
include Build::Files
|
136
|
-
|
137
80
|
describe Build::Graph do
|
138
81
|
it "shouldn't update mtime" do
|
139
82
|
test_glob = Glob.new(__dir__, "*.rb")
|
@@ -141,28 +84,46 @@ module Build::Graph::GraphSpec
|
|
141
84
|
|
142
85
|
FileUtils.rm_f listing_output.to_a
|
143
86
|
|
144
|
-
|
87
|
+
group = Process::Group.new
|
145
88
|
|
146
|
-
|
147
|
-
|
89
|
+
walker = Walker.new do |walker, node|
|
90
|
+
task = ProcessTask.new(walker, node)
|
91
|
+
|
92
|
+
task.visit do
|
93
|
+
task.update(group)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
top = ProcessNode.top do
|
98
|
+
process test_glob, listing_output do
|
148
99
|
run("ls", "-la", *inputs, :out => outputs.first.for_writing)
|
149
100
|
end
|
150
101
|
end
|
151
102
|
|
152
|
-
|
153
|
-
|
103
|
+
walker.update(top)
|
104
|
+
group.wait
|
154
105
|
|
155
|
-
|
106
|
+
first_modified_time = listing_output.first.modified_time
|
156
107
|
|
157
|
-
|
108
|
+
walker.update(top)
|
109
|
+
group.wait
|
158
110
|
|
159
|
-
#
|
160
|
-
|
111
|
+
# The output file shouldn't have been changed because already exists and the input files haven't changed either:
|
112
|
+
second_modified_time = listing_output.first.modified_time
|
161
113
|
|
162
|
-
|
114
|
+
# The granularity of mtime on some systems is a bit weird:
|
115
|
+
expect(second_modified_time.to_f).to be_within(0.001).of(first_modified_time.to_f)
|
163
116
|
|
164
|
-
|
165
|
-
|
117
|
+
FileUtils.rm_f listing_output.to_a
|
118
|
+
walker.monitor.update(listing_output.roots)
|
119
|
+
|
120
|
+
# The granularity of modification times isn't that great, so we use >= below.
|
121
|
+
# sleep 1
|
122
|
+
|
123
|
+
walker.update(top)
|
124
|
+
group.wait
|
125
|
+
|
126
|
+
expect(listing_output.first.modified_time).to be >= first_modified_time
|
166
127
|
|
167
128
|
FileUtils.rm_f listing_output.to_a
|
168
129
|
end
|
@@ -172,9 +133,19 @@ module Build::Graph::GraphSpec
|
|
172
133
|
code_glob = Glob.new(program_root, "*.cpp")
|
173
134
|
program_path = Path.join(program_root, "dictionary-sort")
|
174
135
|
|
175
|
-
|
136
|
+
group = Process::Group.new
|
176
137
|
|
177
|
-
|
138
|
+
walker = Walker.new do |walker, node|
|
139
|
+
task = ProcessTask.new(walker, node)
|
140
|
+
|
141
|
+
task.visit do
|
142
|
+
task.update(group)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
#FileUtils.touch(code_glob.first)
|
147
|
+
|
148
|
+
top = ProcessNode.top do
|
178
149
|
process code_glob, program_path do
|
179
150
|
object_files = inputs.with(extension: ".o") do |input_path, output_path|
|
180
151
|
depfile_path = input_path + ".d"
|
@@ -201,16 +172,16 @@ module Build::Graph::GraphSpec
|
|
201
172
|
end
|
202
173
|
end
|
203
174
|
|
204
|
-
process program_path
|
175
|
+
process program_path do
|
205
176
|
run("./" + program_path.relative_path, chdir: program_path.root)
|
206
177
|
end
|
207
178
|
end
|
208
179
|
|
209
|
-
walker
|
210
|
-
|
180
|
+
walker.update(top)
|
181
|
+
group.wait
|
211
182
|
|
212
183
|
expect(program_path).to be_exist
|
213
|
-
expect(code_glob.first.
|
184
|
+
expect(code_glob.first.modified_time).to be <= program_path.modified_time
|
214
185
|
end
|
215
186
|
end
|
216
187
|
end
|