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.
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Build
22
22
  module Graph
23
- VERSION = "0.3.6"
23
+ VERSION = "0.3.7"
24
24
  end
25
25
  end
@@ -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(controller, &task)
30
- @controller = controller
31
- @task = task
32
+ def initialize(&block)
33
+ # Node -> Task mapping.
34
+ @tasks = {}
32
35
 
33
- # The number of nodes we have touched:
34
- @count = 0
36
+ @update = block
35
37
 
36
38
  @outputs = {}
37
- @dirty = Set.new
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
- @failed = Set.new
43
+ @failed_tasks = []
44
+ @failed_outputs = Set.new
57
45
 
58
- # The number of failed nodes:
59
- @failures = 0
46
+ @monitor = Files::Monitor.new
60
47
  end
61
48
 
62
- attr :controller
63
- attr :task
49
+ attr :tasks # {Node => Task}
64
50
 
65
51
  attr :outputs
66
52
 
67
- attr_accessor :count
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
- # A list of outputs which have failed to generate:
73
- attr :failed
61
+ attr :monitor
74
62
 
75
- def failed?
76
- @failures > 0
63
+ def update(nodes)
64
+ Array(nodes).each do |node|
65
+ @update.call(self, node)
66
+ end
77
67
  end
78
68
 
79
- def task(*arguments)
80
- @task.call(self, *arguments)
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
- failed = false
86
-
89
+
90
+ paths = paths.collect(&:to_s)
91
+
87
92
  paths.each do |path|
88
- if @outputs.include? path
89
- @outputs[path] << edge
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
- edge.wait || failed
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 @dirty.include?(child)
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
- edge.wait
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 node.failed?
122
- @failed += node.outputs
123
- @failures += 1
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
- node.outputs.each do |path|
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(node)}
161
+ edges.each{|edge| edge.traverse(task)}
130
162
  end
131
163
  end
132
-
133
- # Trigger the parent nodes:
134
- if parents = @parents.delete(node)
135
- parents.each{|edge| edge.traverse(node)}
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
- # If all inputs were good, we can update the node.
172
- unless any_inputs_failed?
173
- begin
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 exit
185
- if @error || any_child_failed? || any_inputs_failed?
186
- @node.fail!
187
- elsif wet?
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
- @walker.count += 1
194
- end
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
- # The graph node is created once, so a graph has a fixed number of nodes, which store per-vertex state and connectivity.
31
- class Node < Build::Graph::Node
32
- include Build::Files
33
-
34
- def initialize(controller, inputs = Paths::NONE, outputs = Paths::NONE, &update)
35
- @update = update
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
- super(controller, inputs, outputs)
38
- end
39
-
40
- def apply!(scope)
41
- scope.instance_eval(&@update)
36
+ @block = block
42
37
  end
43
38
 
44
- # This ensures that enclosed nodes are run if they are dirty. The top level node has no inputs or outputs by default, so children who become dirty wouldn't mark it as dirty and thus wouldn't be run.
45
- def requires_update?
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
- # The task is the context in which a vertex is updated. Because nodes may initially create other nodes, it is also responsible for looking up and creating new nodes.
55
- class Task < Build::Graph::Task
56
- include Build::Files
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
- @group = group
49
+ node = ProcessNode.new(inputs, outputs, block)
50
+
51
+ self.invoke(node)
62
52
  end
63
53
 
64
54
  def wet?
65
- @group and @node.dirty?
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 Rainbow(arguments.join(' ')).blue
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
- def visit
96
- super do
97
- @node.apply!(self)
98
- end
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
- node = nil
87
+ group = Process::Group.new
145
88
 
146
- controller = Controller.new do
147
- node = process test_glob, listing_output do
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
- expect(controller.top).to_not be nil
153
- expect(node).to_not be nil
103
+ walker.update(top)
104
+ group.wait
154
105
 
155
- controller.update!
106
+ first_modified_time = listing_output.first.modified_time
156
107
 
157
- mtime = listing_output.first.mtime
108
+ walker.update(top)
109
+ group.wait
158
110
 
159
- # Ensure the mtime will change even if the granularity of the filesystem is 1 second:
160
- sleep(1)
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
- controller.update!
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
- # The output file shouldn't have been changed because already exists and the input files haven't changed either:
165
- expect(listing_output.first.mtime).to be == mtime
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
- # FileUtils.touch(code_glob.first)
136
+ group = Process::Group.new
176
137
 
177
- controller = Controller.new do
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, Paths::NONE do
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 = controller.update!
210
- expect(walker).to be_kind_of Build::Graph::Walker
180
+ walker.update(top)
181
+ group.wait
211
182
 
212
183
  expect(program_path).to be_exist
213
- expect(code_glob.first.mtime).to be <= program_path.mtime
184
+ expect(code_glob.first.modified_time).to be <= program_path.modified_time
214
185
  end
215
186
  end
216
187
  end