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,75 @@
1
+
2
+ require 'build/system/monitor'
3
+ require 'build/system/pool'
4
+
5
+ require 'build/error'
6
+ require 'build/node'
7
+ require 'build/walker'
8
+ require 'build/edge'
9
+
10
+ module Build
11
+ class Graph < System::Monitor
12
+ def initialize
13
+ super
14
+
15
+ @nodes = {}
16
+
17
+ build_graph!
18
+ end
19
+
20
+ attr :nodes
21
+
22
+ # You need to override this to traverse the top nodes as required:
23
+ def traverse!(walker)
24
+ #Array(top).each do |node|
25
+ # node.update!(walker)
26
+ #end
27
+ end
28
+
29
+ def walk(&block)
30
+ Walker.new(self, &block)
31
+ end
32
+
33
+ def build_graph!
34
+ # We build the graph without doing any actual execution:
35
+ nodes = []
36
+
37
+ walker = walk do |walker, node|
38
+ nodes << node
39
+
40
+ yield walker, node
41
+ end
42
+
43
+ traverse! walker
44
+
45
+ # We should update the status of all nodes in the graph once we've traversed the graph.
46
+ nodes.each do |node|
47
+ node.update_status!
48
+ end
49
+ end
50
+
51
+ def update_with_log
52
+ puts Rainbow("*** Graph update traversal ***").green
53
+
54
+ start_time = Time.now
55
+
56
+ walker = update!
57
+ ensure
58
+ end_time = Time.now
59
+ elapsed_time = end_time - start_time
60
+
61
+ $stdout.flush
62
+ $stderr.puts Rainbow("Graph Update Time: %0.3fs" % elapsed_time).magenta
63
+ end
64
+
65
+ def update!
66
+ walker = walk do |walker, node|
67
+ yield walker, node
68
+ end
69
+
70
+ traverse! walker
71
+
72
+ return walker
73
+ end
74
+ end
75
+ end
data/lib/build/node.rb ADDED
@@ -0,0 +1,134 @@
1
+
2
+ require 'build/system/state'
3
+
4
+ module Build
5
+ class Node
6
+ def initialize(graph, inputs, outputs)
7
+ @graph = graph
8
+
9
+ @state = System::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
@@ -0,0 +1,123 @@
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/system/state'
24
+
25
+ module Build
26
+ module System
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
@@ -0,0 +1,116 @@
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 'rainbow'
22
+ require 'system'
23
+ require 'fiber'
24
+
25
+ module Build
26
+ module System
27
+ # A pool is a group of tasks which can be run asynchrnously using fibers. Someone must call #wait to ensure that all fibers eventuall resume.
28
+ class Pool
29
+ def self.processor_count
30
+ ::System::CPU.count
31
+ end
32
+
33
+ class Command
34
+ def initialize(arguments, options, fiber = Fiber.current)
35
+ @arguments = arguments
36
+ @options = options
37
+
38
+ @fiber = fiber
39
+ end
40
+
41
+ attr :arguments
42
+ attr :options
43
+
44
+ def run(options = {})
45
+ puts Rainbow("Running #{@arguments.inspect} options: #{@options.merge(options).inspect}").blue
46
+
47
+ Process.spawn(*@arguments, @options.merge(options))
48
+ end
49
+
50
+ def resume(*arguments)
51
+ @fiber.resume(*arguments)
52
+ end
53
+ end
54
+
55
+ def initialize(options = {})
56
+ @commands = []
57
+ @limit = options[:limit] || Pool.processor_count
58
+
59
+ @running = {}
60
+ @fiber = nil
61
+
62
+ @pgid = true
63
+ end
64
+
65
+ attr :running
66
+
67
+ def run(*arguments)
68
+ options = Hash === arguments.last ? arguments.pop : {}
69
+ arguments = arguments.flatten.collect &:to_s
70
+
71
+ @commands << Command.new(arguments, options)
72
+
73
+ schedule!
74
+
75
+ Fiber.yield
76
+ end
77
+
78
+ def schedule!
79
+ while @running.size < @limit and @commands.size > 0
80
+ command = @commands.shift
81
+
82
+ if @running.size == 0
83
+ pid = command.run(:pgroup => true)
84
+ @pgid = Process.getpgid(pid)
85
+ else
86
+ pid = command.run(:pgroup => @pgid)
87
+ end
88
+
89
+ @running[pid] = command
90
+ end
91
+ end
92
+
93
+ def wait
94
+ while @running.size > 0
95
+ # Wait for processes in this group:
96
+ pid, status = Process.wait2(-@pgid)
97
+
98
+ command = @running.delete(pid)
99
+
100
+ schedule!
101
+
102
+ command.resume(status)
103
+ end
104
+ end
105
+ end
106
+
107
+ module FakePool
108
+ def self.wait
109
+ end
110
+
111
+ def self.run(*arguments)
112
+ 0
113
+ end
114
+ end
115
+ end
116
+ end