build-graph 0.1.0 → 0.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.
@@ -1,123 +0,0 @@
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
- require 'set'
22
-
23
- require 'build/files/state'
24
-
25
- module Build
26
- module Files
27
- class Monitor
28
- def initialize
29
- @directories = Hash.new { |hash, key| hash[key] = Set.new }
30
-
31
- @updated = false
32
- end
33
-
34
- attr :updated
35
-
36
- # Notify the monitor that files in these directories have changed.
37
- def update(directories, *args)
38
- directories.each do |directory|
39
- # directory = File.realpath(directory)
40
-
41
- @directories[directory].each do |handle|
42
- handle.changed!(*args)
43
- end
44
- end
45
- end
46
-
47
- def roots
48
- @directories.keys
49
- end
50
-
51
- def delete(handle)
52
- handle.directories.each do |directory|
53
- @directories[directory].delete(handle)
54
-
55
- # Remove the entire record if there are no handles:
56
- if @directories[directory].size == 0
57
- @directories.delete(directory)
58
-
59
- @updated = true
60
- end
61
- end
62
- end
63
-
64
- def track_changes(files, &block)
65
- handle = Handle.new(self, files, &block)
66
-
67
- add(handle)
68
- end
69
-
70
- def add(handle)
71
- handle.directories.each do |directory|
72
- @directories[directory] << handle
73
-
74
- # We just added the first handle:
75
- if @directories[directory].size == 1
76
- # If the handle already existed, this might trigger unnecessarily.
77
- @updated = true
78
- end
79
- end
80
-
81
- handle
82
- end
83
- end
84
-
85
- def self.run_with_fsevent(monitor, options = {}, &block)
86
- require 'rb-fsevent'
87
-
88
- fsevent ||= FSEvent.new
89
-
90
- catch(:interrupt) do
91
- while true
92
- fsevent.watch monitor.roots do |directories|
93
- monitor.update(directories)
94
-
95
- yield
96
-
97
- if monitor.updated
98
- fsevent.stop
99
- end
100
- end
101
-
102
- fsevent.run
103
- end
104
- end
105
- end
106
-
107
- def self.run_with_polling(monitor, options = {}, &block)
108
- catch(:interrupt) do
109
- while true
110
- monitor.update(monitor.roots)
111
-
112
- yield
113
-
114
- sleep(options[:latency] || 5.0)
115
- end
116
- end
117
- end
118
-
119
- def self.run(monitor, options = {}, &block)
120
- run_with_polling(monitor, options, &block)
121
- end
122
- end
123
- end
@@ -1,218 +0,0 @@
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 Files
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
data/lib/build/node.rb DELETED
@@ -1,134 +0,0 @@
1
-
2
- require 'build/files/state'
3
-
4
- module Build
5
- class Node
6
- def initialize(graph, inputs, outputs)
7
- @graph = graph
8
-
9
- @state = Files::IOState.new(inputs, outputs)
10
-
11
- @status = :unknown
12
- @fiber = nil
13
-
14
- # These are immutable - rather than change them, create a new node:
15
- @inputs = inputs
16
- @outputs = outputs
17
-
18
- @graph.add(self)
19
- end
20
-
21
- def eql?(other)
22
- other.kind_of?(self.class) and @inputs.eql?(other.inputs) and @outputs.eql?(other.outputs)
23
- end
24
-
25
- def hash
26
- [@inputs, @outputs].hash
27
- end
28
-
29
- def directories
30
- @state.files.roots
31
- end
32
-
33
- def remove!
34
- @graph.delete(self)
35
- end
36
-
37
- # It is possible this function is called unnecessarily. The state check confirms whether a change occurred or not.
38
- def changed!(outputs = [])
39
- # Don't do anything if we are already dirty.
40
- return if dirty?
41
-
42
- if @state.intersects?(outputs) || @state.update!
43
- # puts "changed: inputs=#{inputs} #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect}"
44
-
45
- # Could possibly use unknown status here.
46
- @status = :dirty
47
-
48
- # If this node changes, we force all other nodes which depend on this node to be dirty.
49
- @graph.update(directories, @outputs)
50
- end
51
- end
52
-
53
- attr :inputs
54
- attr :outputs
55
-
56
- attr :state
57
- attr :status
58
-
59
- def unknown?
60
- @status == :unknown
61
- end
62
-
63
- def dirty?
64
- @status == :dirty
65
- end
66
-
67
- def clean?
68
- @status == :clean
69
- end
70
-
71
- def clean!
72
- @status = :clean
73
- end
74
-
75
- def fail!
76
- @status = :failed
77
- end
78
-
79
- def failed?
80
- @status == :failed
81
- end
82
-
83
- def updating?
84
- @fiber != nil
85
- end
86
-
87
- # If we are in the initial state, we need to check if the outputs are fresh.
88
- def update_status!
89
- # puts "Update status: #{@inputs.inspect} -> #{@outputs.inspect} (status=#{@status} @fiber=#{@fiber.inspect}) @status=#{@status} @state.fresh?=#{@state.fresh?}"
90
-
91
- if @status == :unknown
92
- # This could be improved - only stale files should be reported, instead we report all.
93
- unless @state.fresh?
94
- changed!(self.inputs)
95
- else
96
- @status = :clean
97
- end
98
- end
99
- end
100
-
101
- def inspect
102
- "<#{dirty? ? '*' : ''}inputs=#{inputs.inspect} outputs=#{outputs.inspect} fiber=#{@fiber.inspect} fresh=#{@state.fresh?}>"
103
- end
104
-
105
- def requires_update?
106
- not clean?
107
- end
108
-
109
- # Perform some actions to update this node, returns when completed, and the node is no longer dirty.
110
- def update!(walker)
111
- # puts "Walking #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect} (dirty=#{dirty?} @fiber=#{@fiber.inspect})"
112
-
113
- # If a fiber already exists, this node is in the process of updating.
114
- if requires_update? and @fiber == nil
115
- # puts "Beginning: #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect}"
116
-
117
- @fiber = Fiber.new do
118
- task = walker.task(self)
119
-
120
- task.visit
121
-
122
- # Commit changes:
123
- # puts "Committing: #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect}"
124
- @state.update!
125
- @fiber = nil
126
-
127
- task.exit
128
- end
129
-
130
- @fiber.resume
131
- end
132
- end
133
- end
134
- end