build-graph 1.0.3 → 1.0.4

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: 2fe664d73ec6b69811e09e9afc08106e751fe154
4
- data.tar.gz: 6f9d78571bdd3000fdb171e70855a0da1881cd33
3
+ metadata.gz: 7dcba8fbc1368e6fd3a2c4117d6a787609b4127e
4
+ data.tar.gz: 4984ebb6012624af504492ce5ad5597b471f362b
5
5
  SHA512:
6
- metadata.gz: 04c4b26479fe93ca729f7aa18b996138550f32b9d5214667ea85d7ab29453c847fd65bd703d10ed6b1d4083434eb81ef9f64d42c0cc2b633702d9e6cd4a1ee68
7
- data.tar.gz: df5b79fd7d44d134170e78e0b7d032fe8ba2322c83f5b3c3ab07cfc4e05b4636d4af4763510f5f49485cd729dca15ab4d6cdc1341ec2858c555ca19c176380d5
6
+ metadata.gz: 5e34905fd1150c7d69c77be9292505a7f67f08c8b46924047971a2af18665c8bbf8f9c0d8a8349e0eec2891410b443a45005ce9b9b27ae1e58cb56d37ed81baa
7
+ data.tar.gz: 471ef8ad632e75fcc1048d497aabbe5959a86e9dd226edf6014ee70c371eee66fbd9592704920b217fe55aa9d4711aaeb9d0e0c0706e142868445baae6561517
data/.travis.yml CHANGED
@@ -1,9 +1,5 @@
1
1
  language: ruby
2
- compiler: clang
3
- before_install:
4
- - sudo add-apt-repository --yes ppa:ubuntu-toolchain-r/test
5
- - sudo apt-get -qq update
6
- - sudo apt-get -qq install libstdc++-4.8-dev
2
+ sudo: false
7
3
  rvm:
8
4
  - 2.0.0
9
5
  - 2.1.8
@@ -13,6 +9,13 @@ rvm:
13
9
  - rbx-2
14
10
  env: COVERAGE=true
15
11
  matrix:
12
+ fast_finish: true
16
13
  allow_failures:
14
+ - rvm: ruby-head
17
15
  - rvm: "rbx-2"
18
-
16
+ addons:
17
+ apt:
18
+ sources:
19
+ - ubuntu-toolchain-r-test
20
+ packages:
21
+ - libstdc++-4.8-dev
data/README.md CHANGED
@@ -2,27 +2,27 @@
2
2
 
3
3
  Build::Graph is a framework for build systems, with specific functionality for dealing with file based processes.
4
4
 
5
- [![Build Status](https://secure.travis-ci.org/ioquatix/build-graph.png)](http://travis-ci.org/ioquatix/build-graph)
6
- [![Code Climate](https://codeclimate.com/github/ioquatix/build-graph.png)](https://codeclimate.com/github/ioquatix/build-graph)
5
+ [![Build Status](https://secure.travis-ci.org/ioquatix/build-graph.svg)](http://travis-ci.org/ioquatix/build-graph)
6
+ [![Code Climate](https://codeclimate.com/github/ioquatix/build-graph.svg)](https://codeclimate.com/github/ioquatix/build-graph)
7
7
  [![Coverage Status](https://coveralls.io/repos/ioquatix/build-graph/badge.svg)](https://coveralls.io/r/ioquatix/build-graph)
8
8
 
9
9
  ## Installation
10
10
 
11
11
  Add this line to your application's Gemfile:
12
12
 
13
- gem 'build-graph'
13
+ gem 'build-graph'
14
14
 
15
15
  And then execute:
16
16
 
17
- $ bundle
17
+ $ bundle
18
18
 
19
19
  Or install it yourself as:
20
20
 
21
- $ gem install build-graph
21
+ $ gem install build-graph
22
22
 
23
23
  ## Usage
24
24
 
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.
25
+ A build graph is an abstract set of `[input, process, output]` nodes. A node executes it's process 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 old task is nullified.
26
26
 
27
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
28
 
@@ -36,12 +36,36 @@ Outputs from a node should be all files that are generated directly by the proce
36
36
 
37
37
  ### Dirty Propagation
38
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.
39
+ A `Node` has a set of `#inputs` and `#outputs` but these are abstract. For example, `#outputs` could be `:inherit` which means that the node symbolically has all the outputs of all it's direct children. A `Task`, at the time of execution, captures it's inputs and outputs and these may be monitored for changes in real time.
40
40
 
41
41
  File changes are currently detected using `File::mtime` as this is generally a good trade off between efficiency and accuracy.
42
42
 
43
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.
44
44
 
45
+ ### Example Graph
46
+
47
+ target("Library/UnitTest", [] -> :inherit) do
48
+ library([UnitTest.cpp] -> UnitTest.a) do
49
+ compile([UnitTest.cpp] -> UnitTest.o)
50
+ link([UnitTest.o] -> libUnitTest.a)
51
+ end
52
+
53
+ copy headers: [UnitTest.hpp]
54
+
55
+ # Outputs become libUnitTest.a and UnitTest.hpp
56
+ end
57
+
58
+ target("Executable/UnitTest", [] -> :inherit) do
59
+ depends("Library/UnitTest")
60
+
61
+ executable(main.cpp -> UnitTest) do
62
+ compile(main.cpp -> main.o)
63
+ link([main.o, libUnitTest.a] -> UnitTest)
64
+ end
65
+
66
+ # Outputs become UnitTest
67
+ end
68
+
45
69
  ## Contributing
46
70
 
47
71
  1. Fork it
@@ -54,7 +78,7 @@ When a task is marked as dirty, it also marks all it's outputs as being dirty, w
54
78
 
55
79
  Released under the MIT license.
56
80
 
57
- Copyright, 2012, 2014, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
81
+ Copyright, 2012, 2014, 2016, by [Samuel G. D. Williams](http://www.codeotaku.com/samuel-williams).
58
82
 
59
83
  Permission is hereby granted, free of charge, to any person obtaining a copy
60
84
  of this software and associated documentation files (the "Software"), to deal
data/build-graph.gemspec CHANGED
@@ -22,8 +22,8 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.required_ruby_version = '>= 2.0'
24
24
 
25
- spec.add_dependency "process-group", "~> 1.0.1"
26
- spec.add_dependency "build-files", "~> 1.0.2"
25
+ spec.add_dependency "process-group", "~> 1.1.0"
26
+ spec.add_dependency "build-files", "~> 1.0.3"
27
27
 
28
28
  spec.add_development_dependency "build-makefile", "~> 1.0.0"
29
29
 
data/lib/build/graph.rb CHANGED
@@ -18,7 +18,7 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require_relative 'graph/error'
21
+ require_relative 'graph/task'
22
22
  require_relative 'graph/node'
23
23
  require_relative 'graph/walker'
24
24
  require_relative 'graph/edge'
@@ -0,0 +1,51 @@
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
+ # A call stack contains frames to track state during nested invocations.
24
+ class CallStack
25
+ def initialize
26
+ # Saves state if supplied to #call, which is useful for top level state:
27
+ @frames = [{}.freeze]
28
+ end
29
+
30
+ # All stack frames which had state associated with them.
31
+ attr :frames
32
+
33
+ # Yield with the given state, merged with any prior state.
34
+ def with(state)
35
+ if state and !state.empty?
36
+ @frames << @frames.last.merge(state).freeze
37
+ yield
38
+ @frames.pop
39
+ else
40
+ yield
41
+ end
42
+ end
43
+
44
+ # The current stack frame state.
45
+ def last
46
+ @frames.last
47
+ end
48
+ end
49
+ end
50
+ end
51
+
@@ -18,8 +18,6 @@
18
18
  # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
19
  # THE SOFTWARE.
20
20
 
21
- require_relative 'error'
22
-
23
21
  require 'fiber'
24
22
 
25
23
  module Build
@@ -72,6 +70,7 @@ module Build
72
70
  end
73
71
  end
74
72
 
73
+ # This is called in the case that a parent fails to complete because a child task has failed.
75
74
  def skip!(task)
76
75
  @vertices += 1
77
76
 
@@ -20,6 +20,21 @@
20
20
 
21
21
  module Build
22
22
  module Graph
23
+ class TransientError < StandardError
24
+ end
25
+
26
+ module ChildrenFailed
27
+ def self.to_s
28
+ "Children tasks failed!"
29
+ end
30
+ end
31
+
32
+ module InputsFailed
33
+ def self.to_s
34
+ "Tasks generating inputs failed!"
35
+ end
36
+ end
37
+
23
38
  class Task
24
39
  def initialize(walker, node)
25
40
  @walker = walker
@@ -69,11 +84,11 @@ module Build
69
84
  fail!(error)
70
85
  end
71
86
  else
72
- fail!(:inputs)
87
+ fail!(InputsFailed)
73
88
  end
74
89
 
75
90
  unless wait_for_children?
76
- fail!(:children)
91
+ fail!(ChildrenFailed)
77
92
  end
78
93
 
79
94
  update_outputs
@@ -148,6 +163,11 @@ module Build
148
163
  end
149
164
 
150
165
  def fail!(error)
166
+ if logger = @walker.logger
167
+ logger.error("Task #{self} failed: #{error}")
168
+ logger.debug(error) if error.kind_of?(Exception)
169
+ end
170
+
151
171
  @error = error
152
172
  @state = :failed
153
173
  end
@@ -155,7 +175,7 @@ module Build
155
175
  # Returns false if any input failed.
156
176
  def wait_for_inputs?
157
177
  # Wait on any inputs, returns whether any inputs failed:
158
- @walker.wait_on_paths(@inputs)
178
+ @walker.wait_on_paths(self, @inputs)
159
179
  end
160
180
 
161
181
  # Returns false if any child failed.
@@ -20,6 +20,6 @@
20
20
 
21
21
  module Build
22
22
  module Graph
23
- VERSION = "1.0.3"
23
+ VERSION = "1.0.4"
24
24
  end
25
25
  end
@@ -24,7 +24,8 @@ require 'logger'
24
24
  require_relative 'task'
25
25
  require_relative 'node'
26
26
  require_relative 'edge'
27
- require_relative 'error'
27
+
28
+ require_relative 'call_stack'
28
29
 
29
30
  module Build
30
31
  module Graph
@@ -46,9 +47,10 @@ module Build
46
47
 
47
48
  @update = block
48
49
 
50
+ # A list of paths which are currently being generated by tasks:
49
51
  @outputs = {}
50
52
 
51
- @parents = {}
53
+ @parents = Hash.new{|h,k| h[k] = []}
52
54
 
53
55
  # Failed output paths:
54
56
  @failed_tasks = []
@@ -56,10 +58,17 @@ module Build
56
58
 
57
59
  @logger = logger || Logger.new(nil)
58
60
  @monitor = Files::Monitor.new(logger: @logger)
61
+
62
+ @call_stack = CallStack.new
59
63
  end
60
64
 
61
- attr :tasks # {Node => Task}
65
+ # Primarily for debugging from within Task
66
+ attr :logger
62
67
 
68
+ # An Array of all instantiated tasks.
69
+ attr :tasks
70
+
71
+ # An Array of transient outputs which are currently being generated.
63
72
  attr :outputs
64
73
 
65
74
  attr :failed_tasks
@@ -78,15 +87,17 @@ module Build
78
87
  end
79
88
  end
80
89
 
81
- def call(node)
82
- # We try to fetch the task if it has already been invoked, otherwise we create a new task.
83
- @tasks.fetch(node) do
84
- @logger.debug{"Update: #{node}"}
85
-
86
- @update.call(self, node)
87
-
88
- # This should now be defined:
89
- @tasks[node]
90
+ def call(node, state = nil)
91
+ @call_stack.with(state) do
92
+ # We try to fetch the task if it has already been invoked, otherwise we create a new task.
93
+ @tasks.fetch(node) do
94
+ @logger.debug{"Update: #{node}"}
95
+
96
+ @update.call(self, node, @call_stack.last)
97
+
98
+ # This should now be defined:
99
+ return @tasks[node]
100
+ end
90
101
  end
91
102
  end
92
103
 
@@ -94,7 +105,7 @@ module Build
94
105
  @failed_tasks.size > 0
95
106
  end
96
107
 
97
- def wait_on_paths(paths)
108
+ def wait_on_paths(task, paths)
98
109
  # If there are no paths, we are done:
99
110
  return true if paths.count == 0
100
111
 
@@ -102,6 +113,7 @@ module Build
102
113
  edge = Edge.new
103
114
 
104
115
  paths = paths.collect(&:to_s)
116
+ @logger.debug{"Task #{task} is waiting on paths #{paths}"}
105
117
 
106
118
  paths.each do |path|
107
119
  # Is there a task generating this output?
@@ -110,6 +122,11 @@ module Build
110
122
  outputs << edge
111
123
  edge.increment!
112
124
  end
125
+
126
+ # What should we do about paths which haven't been registered as outputs?
127
+ # Either they exist - or they don't.
128
+ # If they exist, it means they are probably static inputs of the build graph.
129
+ # If they don't, it might be an error, or it might be deliberate.
113
130
  end
114
131
 
115
132
  failed = paths.any?{|path| @failed_outputs.include? path}
@@ -125,6 +142,8 @@ module Build
125
142
  # If there are no children like this, then done:
126
143
  return true if children.size == 0
127
144
 
145
+ @logger.debug{"Task #{parent} is waiting on #{children.count} children"}
146
+
128
147
  # Otherwise, construct an edge to track state changes:
129
148
  edge = Edge.new
130
149
 
@@ -135,7 +154,6 @@ module Build
135
154
  # We are waiting for this child to finish:
136
155
  edge.increment!
137
156
 
138
- @parents[child.node] ||= []
139
157
  @parents[child.node] << edge
140
158
  end
141
159
  end
@@ -144,12 +162,15 @@ module Build
144
162
  end
145
163
 
146
164
  def enter(task)
147
- @logger.debug{"--> #{task.node.process}"}
165
+ @logger.debug{"Walker entering: #{task.node.process}"}
148
166
 
149
167
  @tasks[task.node] = task
150
168
 
151
- # 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.
169
+ # 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 by design and is not a problem in practice.
170
+
152
171
  if outputs = task.outputs
172
+ @logger.debug{"Task will generate outputs: #{outputs.to_a.collect(&:to_s).inspect}"}
173
+
153
174
  outputs.each do |path|
154
175
  @outputs[path.to_s] = []
155
176
  end
@@ -157,7 +178,7 @@ module Build
157
178
  end
158
179
 
159
180
  def exit(task)
160
- @logger.debug{"<-- #{task.node.process}"}
181
+ @logger.debug{"Walker exiting: #{task.node.process}, task #{task.failed? ? 'failed' : 'succeeded'}"}
161
182
 
162
183
  # Fail outputs if the node failed:
163
184
  if task.failed?
@@ -172,6 +193,14 @@ module Build
172
193
  task.outputs.each do |path|
173
194
  path = path.to_s
174
195
 
196
+ if logger.debug?
197
+ if task.failed?
198
+ @logger.debug "\tFile failed: #{path}"
199
+ else
200
+ @logger.debug "\tFile available: #{path}"
201
+ end
202
+ end
203
+
175
204
  if edges = @outputs.delete(path)
176
205
  edges.each{|edge| edge.traverse(task)}
177
206
  end
@@ -186,7 +215,7 @@ module Build
186
215
  end
187
216
 
188
217
  def delete(node)
189
- @logger.debug{">-< #{node.process}"}
218
+ @logger.debug{"Delete #{node}"}
190
219
 
191
220
  if task = @tasks.delete(node)
192
221
  @monitor.delete(task)
@@ -57,8 +57,9 @@ viz = Graphviz::Graph.new
57
57
  viz.attributes[:rankdir] = 'LR'
58
58
 
59
59
  walker.run do
60
- walker.update(top)
61
- group.wait
60
+ group.wait do
61
+ walker.update(top)
62
+ end
62
63
 
63
64
  walker.tasks.each do |node, task|
64
65
  input_nodes = []