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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d39ed0195a0d9781587caca742e099f8d06d3387
4
- data.tar.gz: bffbd32d48639c06da1ae2d02f41d54bee2d7b99
3
+ metadata.gz: 9e451eb353fae615423fa48f9f20a2890fcc6157
4
+ data.tar.gz: 78c36724ea7dc989d38c4b908fc9b9d5a36db7da
5
5
  SHA512:
6
- metadata.gz: 4fcd3f3bbdf5e1ae87e3f5a7ad7ecec4c6bb27ee68b19bcb545acc656be1b740de901774f069927c3fa0e95418b43a919728e7579829412fdc0b22f1ae969205
7
- data.tar.gz: d46d6eb6978817802cb270626cd8cc5f3bf36f7b7c2d65f6809f921ff4bcb6bdc8cefccc970baba730cf3c75f4c3350385b53035fc33d0e73282ecaac6aae486
6
+ metadata.gz: 80e50b90964b646412353ad187c787a4d40e47e4b11498bfe9f3c2df3e4c4f338b8f1b1b8909dc855204f3e8f26652735e3db9e6d6d8b706852dbeb3fbb7b5d4
7
+ data.tar.gz: 67c5ce3bdf104256332a94ddd12becb2516d95941a3d697bdf6d753d6b4262d64e33fab17487be0b570ebb8f6d51650ffc967fd2af9b451b46dcb3d105121015
data/README.md CHANGED
@@ -22,7 +22,25 @@ Or install it yourself as:
22
22
 
23
23
  ## Usage
24
24
 
25
- TODO: Write usage instructions here
25
+ A build graph is an abstract set of `[input, process, output]` nodes. A node executes it's proces within the context of a `Task` which represents a specific set of inputs and outputs and is managed within a `Walker` that walks over graph nodes, regenerating tasks where required. If inputs or outputs change (i.e. become dirty), the task is destroyed and regenerated.
26
+
27
+ A `Walker` is used to traverse the build graph once. As it walks over the graph it builds a set of `Edge` relationships between nodes and only traverses relationships which are complete `Walker#wait_on_paths`. Parent nodes also wait until all their children are complete `Walker#wait_on_nodes` It also keeps track of failures `Walker#failed?` and fails all dependencies of a node.
28
+
29
+ A `Task` is instantiated once per node when traversing the graph. The task represents a specific process being applied to the graph, e.g. build, clean, etc. It is responsible for actually performing any real actions and providing the methods to do so. A `Task` contains all details about the specific graph state at that point in time, e.g. `Task#children` and updating the node state in `Task#exit`. Statistics on the build graph are also captured through `Task` and `Walker`, e.g. number of nodes visited, etc.
30
+
31
+ ### Inputs and Outputs
32
+
33
+ Inputs to a node should be all on-disk state and any additional parameters which cause it's behavior to produce different results.
34
+
35
+ Outputs from a node should be all files that are generated directly by the processes within the node and sometimes it's children.
36
+
37
+ ### Dirty Propagation
38
+
39
+ A `Node` has a set of `#inputs` and `#outputs` but these are abstract. A `Task`, at the time of execution, captures it's inputs and outputs and these may be monitored for changes in real time. The simplest way to cause a task to regenerate is to simply remove it from the existing graph and it will be regenerated.
40
+
41
+ File changes are currently detected using `File::mtime` as this is generally a good trade off between efficiency and accuracy.
42
+
43
+ When a task is marked as dirty, it also marks all it's outputs as being dirty, which in cause could mark other tasks as dirty. This is the mechanism for which dirtiness propagates through the graph. The walker should only have to traverse the graph once to build it completely. If multiple updates are required (i.e. buidling one part of the graph implicitly dirties another part of the graph), the specification of the graph is incomplete and this may lead to problems within the build graph.
26
44
 
27
45
  ## Contributing
28
46
 
data/build-graph.gemspec CHANGED
@@ -23,13 +23,13 @@ Gem::Specification.new do |spec|
23
23
  spec.required_ruby_version = '>= 2.0'
24
24
 
25
25
  spec.add_dependency "process-group", "~> 0.2.1"
26
- spec.add_dependency "build-files", "~> 0.3.0"
26
+ spec.add_dependency "build-files", "~> 0.3.3"
27
27
 
28
28
  spec.add_dependency "system"
29
29
  spec.add_dependency "rainbow", "~> 2.0.0"
30
30
 
31
31
  spec.add_development_dependency "bundler", "~> 1.3"
32
32
  spec.add_development_dependency "rspec", "~> 3.0.0"
33
- spec.add_development_dependency "build-makefile", "~> 0.2.0"
33
+ spec.add_development_dependency "build-makefile", "~> 0.3.0"
34
34
  spec.add_development_dependency "rake"
35
35
  end
@@ -28,12 +28,12 @@ require_relative 'edge'
28
28
  module Build
29
29
  module Graph
30
30
  # The top level graph controller is responsible for managing build graph state.
31
- class Controller < Files::Monitor
31
+ class Controller
32
32
  def initialize
33
33
  super
34
-
34
+
35
35
  @nodes = {}
36
-
36
+
37
37
  build_graph!
38
38
  end
39
39
 
@@ -58,7 +58,7 @@ module Build
58
58
 
59
59
  walker = walk do |walker, node|
60
60
  nodes << node
61
-
61
+
62
62
  yield walker, node
63
63
  end
64
64
 
@@ -97,6 +97,11 @@ module Build
97
97
 
98
98
  return walker
99
99
  end
100
+
101
+ # What to do when a task has a trasient failure:
102
+ def task_failure(error, task)
103
+ $stderr.puts Rainbow("Error: #{error.inspect}").red
104
+ end
100
105
  end
101
106
  end
102
107
  end
@@ -31,45 +31,60 @@ module Build
31
31
 
32
32
  # The number of inputs we are waiting for:
33
33
  @count = count
34
+ @vertices = 0
34
35
 
35
36
  @failed = []
36
37
  end
37
-
38
+
38
39
  attr :failed
39
-
40
+
40
41
  attr :fiber
41
42
  attr :count
42
43
 
43
- # Wait until all inputs to the edge have been traversed.
44
+ # Wait until all inputs to the edge have been traversed. Returns false if failed?
44
45
  def wait
45
46
  if @count > 0
46
47
  Fiber.yield
47
48
  end
48
-
49
- failed?
49
+
50
+ succeeded?
50
51
  end
51
-
52
+
52
53
  attr :failed
53
-
54
+
54
55
  def failed?
55
56
  @failed.size != 0
56
57
  end
57
58
 
59
+ def succeeded?
60
+ @failed.size == 0
61
+ end
62
+
58
63
  # Traverse the edge, mark the edge as failed if the source was also failed.
59
- def traverse(node)
64
+ def traverse(task)
60
65
  @count -= 1
61
-
62
- if node.failed?
63
- @failed << node
66
+
67
+ # The entire edge fails if any individual task fails.
68
+ if task.failed?
69
+ @failed << task
64
70
  end
65
-
71
+
66
72
  if @count == 0
67
73
  @fiber.resume
68
74
  end
69
75
  end
70
76
 
77
+ def skip!(task)
78
+ @vertices += 1
79
+
80
+ if task.failed?
81
+ @failed << task
82
+ end
83
+ end
84
+
71
85
  # Increase the number of traversals we are waiting for.
72
86
  def increment!
87
+ @vertices += 1
73
88
  @count += 1
74
89
  end
75
90
  end
@@ -19,140 +19,69 @@
19
19
  # THE SOFTWARE.
20
20
 
21
21
  require 'build/files/state'
22
+ require 'build/files'
23
+
24
+ require 'rainbow'
22
25
 
23
26
  module Build
24
27
  module Graph
28
+ # This is essentialy a immutable key:
25
29
  class Node
26
- def initialize(controller, inputs, outputs)
27
- @controller = controller
28
-
29
- @state = Files::IOState.new(inputs, outputs)
30
-
31
- @status = :unknown
32
- @fiber = nil
33
-
30
+ def initialize(inputs, outputs, process)
34
31
  # These are immutable - rather than change them, create a new node:
35
32
  @inputs = inputs
36
33
  @outputs = outputs
37
-
38
- @controller.add(self)
39
- end
40
-
41
- def eql?(other)
42
- other.kind_of?(self.class) and @inputs.eql?(other.inputs) and @outputs.eql?(other.outputs)
43
- end
44
-
45
- def hash
46
- [@inputs, @outputs].hash
47
- end
48
-
49
- def directories
50
- @state.files.roots
51
- end
52
-
53
- def remove!
54
- @controller.delete(self)
55
- end
56
-
57
- # It is possible this function is called unnecessarily. The state check confirms whether a change occurred or not.
58
- def changed!(outputs = [])
59
- # Don't do anything if we are already dirty.
60
- return if dirty?
61
-
62
- if @state.intersects?(outputs) || @state.update!
63
- # puts "** Dirty: #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect}"
64
-
65
- # Could possibly use unknown status here.
66
- @status = :dirty
67
-
68
- # If this node changes, we force all other nodes which depend on this node to be dirty.
69
- @controller.update(directories, @outputs)
70
- end
34
+
35
+ # Represents an abstract process, e.g. a name or a function.
36
+ @process = process
71
37
  end
72
38
 
73
39
  attr :inputs
74
40
  attr :outputs
41
+ attr :process
75
42
 
76
- # The IOState for this node.
77
- attr :state
43
+ # Nodes that inherit outputs are special in the sense that outputs are not available until all child nodes have been evaluated.
44
+ def inherit_outputs?
45
+ @outputs == :inherit
46
+ end
78
47
 
79
- # The status of this node.
80
- attr :status
81
-
82
- def unknown?
83
- @status == :unknown
48
+ # This computes the most recent modified time for all inputs.
49
+ def modified_time
50
+ modified_time = @inputs.map{|path| path.modified_time}.max
84
51
  end
85
-
52
+
53
+ # This is a canonical dirty function. All outputs must exist and must be newer than all inputs. This function is not efficient, in the sense that it must query all files on disk for last modified time.
86
54
  def dirty?
87
- @status == :dirty
88
- end
89
-
90
- def clean?
91
- @status == :clean
92
- end
93
-
94
- def clean!
95
- @status = :clean
96
- end
97
-
98
- def fail!
99
- @status = :failed
100
- end
101
-
102
- def failed?
103
- @status == :failed
55
+ if inherit_outputs?
56
+ return true
57
+ else
58
+ # Dirty if any outputs don't exist:
59
+ return true if @outputs.any?{|path| !path.exist?}
60
+
61
+ # Dirty if input modified after any output:
62
+ input_modified_time = self.modified_time
63
+
64
+ # Outputs should always be more recent than their inputs:
65
+ return true if @outputs.any?{|output_path| output_path.modified_time < input_modified_time}
66
+ end
67
+
68
+ return false
104
69
  end
105
-
106
- def updating?
107
- @fiber != nil
70
+
71
+ def eql?(other)
72
+ other.kind_of?(self.class) and @inputs.eql?(other.inputs) and @outputs.eql?(other.outputs) and @process.eql?(other.process)
108
73
  end
109
-
110
- # If we are in the initial state, we need to check if the outputs are fresh.
111
- def update_status!
112
- #puts "Update status: #{@inputs.inspect} -> #{@outputs.inspect} (status=#{@status} @fiber=#{@fiber.inspect}) @status=#{@status} @state.fresh?=#{@state.fresh?}"
113
74
 
114
- if @status == :unknown
115
- # This could be improved - only stale files should be reported, instead we report all.
116
- unless @state.fresh?
117
- changed!(self.inputs)
118
- else
119
- @status = :clean
120
- end
121
- end
75
+ def hash
76
+ [@inputs, @outputs, @process].hash
122
77
  end
123
-
78
+
124
79
  def inspect
125
- "<#{dirty? ? '*' : ''}inputs=#{inputs.inspect} outputs=#{outputs.inspect} fiber=#{@fiber.inspect} fresh=#{@state.fresh?}>"
126
- end
127
-
128
- def requires_update?
129
- not clean?
80
+ "<#{self.class.name} #{@inputs.inspect} => #{@outputs.inspect} by #{@process.inspect}>"
130
81
  end
131
-
132
- # Perform some actions to update this node, returns when completed, and the node is no longer dirty.
133
- def update!(walker)
134
- #puts "Walking #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect} (dirty=#{dirty?} @fiber=#{@fiber.inspect})"
135
82
 
136
- # If a fiber already exists, this node is in the process of updating.
137
- if requires_update? and @fiber == nil
138
- # puts "Beginning: #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect}"
139
-
140
- @fiber = Fiber.new do
141
- task = walker.task(self)
142
-
143
- task.visit
144
-
145
- # Commit changes:
146
- # puts "** Committing: #{@inputs.to_a.inspect} -> #{@outputs.to_a.inspect}"
147
-
148
- @state.update!
149
- @fiber = nil
150
-
151
- task.exit
152
- end
153
-
154
- @fiber.resume
155
- end
83
+ def self.top(inputs = Files::Paths::NONE, outputs = :inherit, &block)
84
+ self.new(inputs, outputs, block)
156
85
  end
157
86
  end
158
87
  end
@@ -0,0 +1,168 @@
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 Graph
23
+ class Task
24
+ def initialize(walker, node)
25
+ @walker = walker
26
+
27
+ @walker.tasks[node] = self
28
+
29
+ @node = node
30
+
31
+ # If the execution of the node fails, this is where we save the error:
32
+ @error = nil
33
+
34
+ @children = []
35
+
36
+ @state = nil
37
+
38
+ @inputs_failed = false
39
+ end
40
+
41
+ attr :inputs
42
+ attr :outputs
43
+
44
+ attr :children
45
+ attr :state
46
+
47
+ attr :walker
48
+
49
+ attr :node
50
+
51
+ # A list of any inputs whose relevant tasks failed:
52
+ attr :inputs_failed
53
+
54
+ # Derived task should override this function to provide appropriate behaviour.
55
+ def visit
56
+ update_inputs_and_outputs
57
+
58
+ # Inforn the walker a new task is being generated for this node:
59
+ @walker.enter(self)
60
+
61
+ @fiber = Fiber.new do
62
+ # If all inputs were good, we can update the node.
63
+ if wait_for_inputs?
64
+ begin
65
+ yield
66
+ rescue TransientError => error
67
+ fail!(error)
68
+ end
69
+ else
70
+ fail!(:inputs)
71
+ end
72
+
73
+ unless wait_for_children?
74
+ fail!(:children)
75
+ end
76
+
77
+ update_outputs
78
+
79
+ @state ||= :complete
80
+
81
+ @walker.exit(self)
82
+ end
83
+
84
+ # Schedule the work, hopefully synchronously:
85
+ @fiber.resume
86
+
87
+ # This allows the child task to be passed back to the parent when it is first invoked.
88
+ return self
89
+ end
90
+
91
+ def invoke(node)
92
+ child_task = @walker.call(node)
93
+
94
+ raise ArgumentError.new("Invalid child task") unless child_task
95
+
96
+ @children << child_task
97
+ end
98
+
99
+ def failed?
100
+ @state == :failed
101
+ end
102
+
103
+ def complete?
104
+ @state == :complete
105
+ end
106
+
107
+ # Returns true if the outputs of the task are out of date w.r.t. the inputs.
108
+ # Currently, does not take into account if the input is a glob and files have been added.
109
+ def dirty?
110
+ @outputs.dirty?(@inputs)
111
+ end
112
+
113
+ def changed!
114
+ @walker.delete(@node)
115
+ end
116
+
117
+ def directories
118
+ @inputs.roots + @outputs.roots
119
+ end
120
+
121
+ def inspect
122
+ "<#{self.class}:#{'0x%X' % self.object_id} #{@node.inspect} #{@state}>"
123
+ end
124
+
125
+ attr :error
126
+ attr :state
127
+
128
+ protected
129
+ def update_inputs_and_outputs
130
+ # If @node.inputs is a glob, this part of the process converts the glob into an actual list of files.
131
+ @inputs = Files::State.new(@node.inputs)
132
+
133
+ unless @node.inherit_outputs?
134
+ @outputs = Files::State.new(@node.outputs)
135
+ end
136
+ end
137
+
138
+ def children_outputs
139
+ @children.collect(&:outputs).inject(Files::Paths::NONE, &:+)
140
+ end
141
+
142
+ def update_outputs
143
+ if @node.inherit_outputs?
144
+ @outputs = Files::State.new(self.children_outputs)
145
+ else
146
+ # After the task has finished, we update the output states:
147
+ @outputs.update!
148
+ end
149
+ end
150
+
151
+ def fail!(error)
152
+ @error = error
153
+ @state = :failed
154
+ end
155
+
156
+ # Returns false if any input failed.
157
+ def wait_for_inputs?
158
+ # Wait on any inputs, returns whether any inputs failed:
159
+ @walker.wait_on_paths(@inputs)
160
+ end
161
+
162
+ # Returns false if any child failed.
163
+ def wait_for_children?
164
+ @walker.wait_for_children(self, @children)
165
+ end
166
+ end
167
+ end
168
+ end