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 +4 -4
- data/.travis.yml +9 -6
- data/README.md +32 -8
- data/build-graph.gemspec +2 -2
- data/lib/build/graph.rb +1 -1
- data/lib/build/graph/call_stack.rb +51 -0
- data/lib/build/graph/edge.rb +1 -2
- data/lib/build/graph/task.rb +23 -3
- data/lib/build/graph/version.rb +1 -1
- data/lib/build/graph/walker.rb +47 -18
- data/spec/build/graph/build_test.rb +3 -2
- data/spec/build/graph/call_stack_spec.rb +42 -0
- data/{lib/build/graph/error.rb → spec/build/graph/edge_spec.rb} +15 -14
- data/spec/build/graph/graph_spec.rb +33 -27
- data/spec/build/graph/process_graph.rb +10 -6
- data/spec/build/graph/program/Benchmark.cpp +3 -3
- data/spec/build/graph/program/DictionarySort.h +58 -58
- data/spec/build/graph/program/ParallelMergeSort.h +53 -53
- data/spec/build/graph/program/main.cpp +21 -21
- data/spec/build/graph/walker_spec.rb +4 -6
- metadata +12 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7dcba8fbc1368e6fd3a2c4117d6a787609b4127e
|
4
|
+
data.tar.gz: 4984ebb6012624af504492ce5ad5597b471f362b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5e34905fd1150c7d69c77be9292505a7f67f08c8b46924047971a2af18665c8bbf8f9c0d8a8349e0eec2891410b443a45005ce9b9b27ae1e58cb56d37ed81baa
|
7
|
+
data.tar.gz: 471ef8ad632e75fcc1048d497aabbe5959a86e9dd226edf6014ee70c371eee66fbd9592704920b217fe55aa9d4711aaeb9d0e0c0706e142868445baae6561517
|
data/.travis.yml
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
language: ruby
|
2
|
-
|
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.
|
6
|
-
[![Code Climate](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
|
-
|
13
|
+
gem 'build-graph'
|
14
14
|
|
15
15
|
And then execute:
|
16
16
|
|
17
|
-
|
17
|
+
$ bundle
|
18
18
|
|
19
19
|
Or install it yourself as:
|
20
20
|
|
21
|
-
|
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
|
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.
|
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
|
26
|
-
spec.add_dependency "build-files", "~> 1.0.
|
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/
|
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
|
+
|
data/lib/build/graph/edge.rb
CHANGED
@@ -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
|
|
data/lib/build/graph/task.rb
CHANGED
@@ -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!(
|
87
|
+
fail!(InputsFailed)
|
73
88
|
end
|
74
89
|
|
75
90
|
unless wait_for_children?
|
76
|
-
fail!(
|
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.
|
data/lib/build/graph/version.rb
CHANGED
data/lib/build/graph/walker.rb
CHANGED
@@ -24,7 +24,8 @@ require 'logger'
|
|
24
24
|
require_relative 'task'
|
25
25
|
require_relative 'node'
|
26
26
|
require_relative 'edge'
|
27
|
-
|
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
|
-
|
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
|
-
|
83
|
-
|
84
|
-
@
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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{"
|
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{"
|
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{"
|
218
|
+
@logger.debug{"Delete #{node}"}
|
190
219
|
|
191
220
|
if task = @tasks.delete(node)
|
192
221
|
@monitor.delete(task)
|