build-graph 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,218 @@
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 System
23
+ # Represents a specific file on disk with a specific mtime.
24
+ class FileTime
25
+ include Comparable
26
+
27
+ def initialize(path, time)
28
+ @path = path
29
+ @time = time
30
+ end
31
+
32
+ attr :path
33
+ attr :time
34
+
35
+ def <=> other
36
+ @time <=> other.time
37
+ end
38
+ end
39
+
40
+ class State
41
+ def initialize(files)
42
+ raise ArgumentError.new("Invalid files list: #{files}") unless Files::List === files
43
+
44
+ @files = files
45
+
46
+ @times = {}
47
+
48
+ update!
49
+ end
50
+
51
+ attr :files
52
+
53
+ attr :added
54
+ attr :removed
55
+ attr :changed
56
+ attr :missing
57
+
58
+ attr :times
59
+
60
+ def update!
61
+ last_times = @times
62
+ @times = {}
63
+
64
+ @added = []
65
+ @removed = []
66
+ @changed = []
67
+ @missing = []
68
+
69
+ file_times = []
70
+
71
+ @files.each do |path|
72
+ if File.exist?(path)
73
+ modified_time = File.mtime(path)
74
+
75
+ if last_time = last_times.delete(path)
76
+ # Path was valid last update:
77
+ if modified_time != last_time
78
+ @changed << path
79
+ end
80
+ else
81
+ # Path didn't exist before:
82
+ @added << path
83
+ end
84
+
85
+ @times[path] = modified_time
86
+
87
+ unless File.directory?(path)
88
+ file_times << FileTime.new(path, modified_time)
89
+ end
90
+ else
91
+ @missing << path
92
+ end
93
+ end
94
+
95
+ @removed = last_times.keys
96
+
97
+ @oldest_time = file_times.min
98
+ @newest_time = file_times.max
99
+
100
+ return @added.size > 0 || @changed.size > 0 || @removed.size > 0
101
+ end
102
+
103
+ attr :oldest_time
104
+ attr :newest_time
105
+
106
+ attr :missing
107
+
108
+ def missing?
109
+ !@missing.empty?
110
+ end
111
+
112
+ # Outputs is a list of full paths and must not include any patterns/globs.
113
+ def intersects?(outputs)
114
+ @files.intersects?(outputs)
115
+ end
116
+
117
+ def empty?
118
+ @files.to_a.empty?
119
+ end
120
+ end
121
+
122
+ class IOState
123
+ def initialize(inputs, outputs)
124
+ @input_state = State.new(inputs)
125
+ @output_state = State.new(outputs)
126
+ end
127
+
128
+ attr :input_state
129
+ attr :output_state
130
+
131
+ # Output is dirty if files are missing or if latest input is older than any output.
132
+ def dirty?
133
+ if @output_state.missing?
134
+ # puts "Output file missing: #{output_state.missing.inspect}"
135
+
136
+ return true
137
+ end
138
+
139
+ # If there are no inputs, we are always clean as long as outputs exist:
140
+ # if @input_state.empty?
141
+ # return false
142
+ # end
143
+
144
+ oldest_output_time = @output_state.oldest_time
145
+ newest_input_time = @input_state.newest_time
146
+
147
+ if newest_input_time and oldest_output_time
148
+ # if newest_input_time > oldest_output_time
149
+ # puts "Out of date file: #{newest_input_time.inspect} > #{oldest_output_time.inspect}"
150
+ # end
151
+
152
+ return newest_input_time > oldest_output_time
153
+ end
154
+
155
+ # puts "Missing file dates: #{newest_input_time.inspect} < #{oldest_output_time.inspect}"
156
+
157
+ return true
158
+ end
159
+
160
+ def fresh?
161
+ not dirty?
162
+ end
163
+
164
+ def files
165
+ @input_state.files + @output_state.files
166
+ end
167
+
168
+ def added
169
+ @input_state.added + @output_state.added
170
+ end
171
+
172
+ def removed
173
+ @input_state.removed + @output_state.removed
174
+ end
175
+
176
+ def changed
177
+ @input_state.changed + @output_state.changed
178
+ end
179
+
180
+ def update!
181
+ input_changed = @input_state.update!
182
+ output_changed = @output_state.update!
183
+
184
+ input_changed or output_changed
185
+ end
186
+
187
+ def intersects?(outputs)
188
+ @input_state.intersects?(outputs) or @output_state.intersects?(outputs)
189
+ end
190
+ end
191
+
192
+ class Handle
193
+ def initialize(monitor, files, &block)
194
+ @monitor = monitor
195
+ @state = State.new(files)
196
+ @on_changed = block
197
+ end
198
+
199
+ attr :monitor
200
+
201
+ def commit!
202
+ @state.update!
203
+ end
204
+
205
+ def directories
206
+ @state.files.roots
207
+ end
208
+
209
+ def remove!
210
+ monitor.delete(self)
211
+ end
212
+
213
+ def changed!
214
+ @on_changed.call(@state) if @state.update!
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,3 @@
1
+ module Build
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,175 @@
1
+
2
+ require 'set'
3
+
4
+ require 'build/error'
5
+
6
+ module Build
7
+ # A walker walks over a graph and applies a task to each node.
8
+ class Walker
9
+ def initialize(graph, &task)
10
+ @graph = graph
11
+ @task = task
12
+
13
+ @count = 0
14
+
15
+ @outputs = {}
16
+ @dirty = Set.new
17
+
18
+ # Generate a list of dirty outputs, possibly a subset, if the build graph might generate additional nodes:
19
+ @graph.nodes.each do |key, node|
20
+ # For a given child, a list of any parents waiting on it.
21
+ if node.dirty?
22
+ @dirty << node
23
+
24
+ @outputs[node] = []
25
+
26
+ node.outputs.each do |output|
27
+ @outputs[output] = []
28
+ end
29
+ end
30
+ end
31
+
32
+ @parents = {}
33
+
34
+ # Failed output paths:
35
+ @failed = Set.new
36
+ end
37
+
38
+ attr :graph
39
+ attr :output
40
+
41
+ attr_accessor :count
42
+
43
+ attr :dirty
44
+ attr :parents
45
+
46
+ def task(*arguments)
47
+ @task.call(self, *arguments)
48
+ end
49
+
50
+ def wait_on_paths(paths)
51
+ edge = Edge.new
52
+ failed = false
53
+
54
+ paths.each do |path|
55
+ if @outputs.include? path
56
+ @outputs[path] << edge
57
+
58
+ edge.increment!
59
+ end
60
+
61
+ if !failed and @failed.include?(path)
62
+ failed = true
63
+ end
64
+ end
65
+
66
+ edge.wait || failed
67
+ end
68
+
69
+ def wait_for_nodes(children)
70
+ edge = Edge.new
71
+
72
+ children.each do |child|
73
+ if @dirty.include?(child)
74
+ edge.increment!
75
+
76
+ @parents[child] ||= []
77
+ @parents[child] << edge
78
+ end
79
+ end
80
+
81
+ edge.wait
82
+ end
83
+
84
+ def exit(node)
85
+ @dirty.delete(node)
86
+
87
+ # Fail outputs if the node failed:
88
+ @failed += node.outputs if node.failed?
89
+
90
+ # Clean the node's outputs:
91
+ node.outputs.each do |path|
92
+ if edges = @outputs.delete(path)
93
+ edges.each{|edge| edge.traverse(node)}
94
+ end
95
+ end
96
+
97
+ # Trigger the parent nodes:
98
+ if parents = @parents.delete(node)
99
+ parents.each{|edge| edge.traverse(node)}
100
+ end
101
+ end
102
+ end
103
+
104
+ # A task is a specific process and scope applied to a graph node.
105
+ class Task
106
+ def initialize(graph, walker, node)
107
+ @graph = graph
108
+ @node = node
109
+ @walker = walker
110
+
111
+ # If the execution of the node fails, this is where we save the error:
112
+ @error = nil
113
+
114
+ @children = []
115
+ end
116
+
117
+ attr :children
118
+
119
+ def inputs
120
+ @node.inputs
121
+ end
122
+
123
+ def outputs
124
+ @node.outputs
125
+ end
126
+
127
+ # Derived task should override this function to provide appropriate behaviour.
128
+ def visit
129
+ wait_for_inputs
130
+
131
+ # If all inputs were good, we can update the node.
132
+ unless any_inputs_failed?
133
+ begin
134
+ #self.instance_eval(&update)
135
+ yield
136
+ rescue TransientError => error
137
+ $stderr.puts Rainbow("Error: #{error.inspect}").red
138
+ @error = error
139
+ end
140
+ end
141
+
142
+ wait_for_children
143
+ end
144
+
145
+ def exit
146
+ if @error || any_child_failed? || any_inputs_failed?
147
+ @node.fail!
148
+ elsif @pool
149
+ @node.clean!
150
+ end
151
+
152
+ @walker.exit(@node)
153
+
154
+ @walker.count += 1
155
+ end
156
+
157
+ protected
158
+ def wait_for_inputs
159
+ # Wait on any inputs, returns whether any inputs failed:
160
+ @inputs_failed = @walker.wait_on_paths(@node.inputs)
161
+ end
162
+
163
+ def wait_for_children
164
+ @walker.wait_for_nodes(@children)
165
+ end
166
+
167
+ def any_child_failed?
168
+ @children.any?{|child| child.failed?}
169
+ end
170
+
171
+ def any_inputs_failed?
172
+ @inputs_failed
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,63 @@
1
+ # Copyright, 2012, 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
+ require 'test/unit'
22
+
23
+ require 'build/files'
24
+
25
+ class TestFiles < Test::Unit::TestCase
26
+ def test_inclusion
27
+ # Glob all test files:
28
+ glob = Build::Files::Glob.new(__dir__, "*.rb")
29
+
30
+ assert glob.count > 0
31
+
32
+ # Should include this file:
33
+ assert glob.include?(__FILE__)
34
+
35
+ # Glob should intersect self:
36
+ assert glob.intersects?(glob)
37
+ end
38
+
39
+ def test_composites
40
+ lib = File.join(__dir__, "../lib")
41
+
42
+ test_glob = Build::Files::Glob.new(__dir__, "*.rb")
43
+ lib_glob = Build::Files::Glob.new(lib, "*.rb")
44
+
45
+ both = test_glob + lib_glob
46
+
47
+ # List#roots is the generic accessor for Lists
48
+ assert both.roots.include? test_glob.root
49
+
50
+ # The composite should include both:
51
+ assert both.include?(__FILE__)
52
+ end
53
+
54
+ def test_roots
55
+ test_glob = Build::Files::Glob.new(__dir__, "*.rb")
56
+
57
+ # Despite returning a String:
58
+ assert test_glob.first.kind_of? String
59
+
60
+ # We actually return a subclass which includes the root portion:
61
+ assert_equal __dir__, test_glob.first.root
62
+ end
63
+ end