dake 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/Gemfile +6 -0
- data/LICENSE +21 -0
- data/README.md +36 -0
- data/Rakefile +17 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/dake.gemspec +51 -0
- data/exe/dake +170 -0
- data/lib/dake/analyzer.rb +387 -0
- data/lib/dake/database.rb +49 -0
- data/lib/dake/executor.rb +211 -0
- data/lib/dake/parser.rb +300 -0
- data/lib/dake/protocol.rb +141 -0
- data/lib/dake/resolver.rb +222 -0
- data/lib/dake/scheme.rb +85 -0
- data/lib/dake/version.rb +3 -0
- data/lib/dake.rb +13 -0
- data/vim/ftdetect/dake.vim +9 -0
- data/vim/syntax/dake.vim +97 -0
- metadata +217 -0
@@ -0,0 +1,387 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'open3'
|
3
|
+
|
4
|
+
# the data struct needed by the executor
|
5
|
+
# note that this is not nessesarily the complete graph,
|
6
|
+
# the graph is only used to produce the given targets
|
7
|
+
DepGraph = Struct.new(
|
8
|
+
:succ_step, # a dict maps each step in the DepGraph to the steps depend on it
|
9
|
+
:dep_step, # a dict maps each step in the DepGraph to the steps it depends on
|
10
|
+
:step_list, # a list of steps represents one sequential execution order
|
11
|
+
:root_step, # a set of steps that hos no dependant
|
12
|
+
:leaf_step, # a set of steps that has no prerequisite
|
13
|
+
:need_rebuild # a set of steps in step_list which should be executed to update their targets
|
14
|
+
)
|
15
|
+
|
16
|
+
class DakeAnalyzer
|
17
|
+
attr_reader :workflow, :variable_dict, :method_dict, :included_files
|
18
|
+
attr_reader :tag_target_dict, :file_target_dict
|
19
|
+
attr_reader :tag_template_dict, :file_template_dict, :step_template_dict
|
20
|
+
|
21
|
+
def initialize(workflow, inclusion_stack, env={})
|
22
|
+
@workflow = workflow
|
23
|
+
@inclusion_stack = inclusion_stack
|
24
|
+
@included_files = inclusion_stack.to_set
|
25
|
+
@variable_dict = env.dup
|
26
|
+
@method_dict = {}
|
27
|
+
@tag_target_dict = {}
|
28
|
+
@file_target_dict = {}
|
29
|
+
@tag_template_dict = {}
|
30
|
+
@file_template_dict = {}
|
31
|
+
@step_template_dict = {}
|
32
|
+
end
|
33
|
+
|
34
|
+
def analyze_option(step) # method is the same as step
|
35
|
+
step.option_dict = {}
|
36
|
+
step.options.each do |option|
|
37
|
+
name = option.name.to_s
|
38
|
+
if option.value.is_a? Parslet::Slice
|
39
|
+
value = option.value.to_s
|
40
|
+
value = 'true' if value == '+'
|
41
|
+
value = 'false' if value == '-'
|
42
|
+
else
|
43
|
+
value = text_eval(option.value, step.src_file, step.context)
|
44
|
+
end
|
45
|
+
if step.option_dict[name]
|
46
|
+
line, column = option.name.line_and_column
|
47
|
+
raise "Option `#{name}' in #{step.src_file} at #{line}:#{column} has already been set."
|
48
|
+
else
|
49
|
+
if name == 'protocol' and not DakeProtocol::ProtocolDict.keys.include? value
|
50
|
+
line, column = option.value.line_and_column
|
51
|
+
raise "Protocol `#{value}' in #{step.src_file} at #{line}:#{column} is not supported."
|
52
|
+
end
|
53
|
+
# TODO: should have more option check
|
54
|
+
step.option_dict[name] = value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
step.option_dict['protocol'] = 'shell' unless step.option_dict['protocol']
|
58
|
+
end
|
59
|
+
|
60
|
+
def analyze_method(meth)
|
61
|
+
if @method_dict[meth.name.to_s]
|
62
|
+
line, column = @method_dict[meth.name.to_s].name.line_and_column
|
63
|
+
raise "Method `#{meth.name.to_s}' has already beed defined in " +
|
64
|
+
"#{@method_dict[meth.name.to_s].src_file} at #{line}:#{column}"
|
65
|
+
end
|
66
|
+
meth.context = @variable_dict.dup
|
67
|
+
analyze_option(meth)
|
68
|
+
@method_dict[meth.name.to_s] = meth
|
69
|
+
end
|
70
|
+
|
71
|
+
def analyze_scheme(name, step, line, column)
|
72
|
+
scheme_part, path_part = name.match(/(\w+:)?(.*)/).to_a[1, 2]
|
73
|
+
scheme_cls = DakeScheme::SchemeDict[scheme_part ? scheme_part : 'local:']
|
74
|
+
if step
|
75
|
+
raise "Scheme `#{scheme_part}' in #{step.src_file} at #{line}:#{column} is not supported." unless scheme_cls
|
76
|
+
scheme_cls.new(scheme_part, path_part, step)
|
77
|
+
else # for user input target name
|
78
|
+
raise "Scheme `#{scheme_part}' in #{name} is not supported." unless scheme_cls
|
79
|
+
dummy_step = Step.new([], [], [], {}, nil, nil, @variable_dict, nil, nil)
|
80
|
+
scheme_cls.new(scheme_part, path_part, dummy_step)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def analyze_file(file, type, step)
|
85
|
+
line, column = text_line_and_column(file.name)
|
86
|
+
if file.flag == '?' and type == :targets
|
87
|
+
file_name = text_eval(file.name)
|
88
|
+
raise "Output file `#{file_name}' in #{step.src_file} at #{line}:#{column} should not be optional."
|
89
|
+
end
|
90
|
+
if file.tag
|
91
|
+
tag_name = text_eval(file.name, step.src_file, step.context)
|
92
|
+
if file.regex
|
93
|
+
if type == :prerequisites
|
94
|
+
raise "Pattern `#{file_name}' in #{step.src_file} at #{line}:#{column} cannot be used in input file list."
|
95
|
+
end
|
96
|
+
file.scheme = DakeScheme::Regex.new('^', tag_name, step)
|
97
|
+
if type == :targets
|
98
|
+
@tag_template_dict[file.scheme.path] ||= []
|
99
|
+
@tag_template_dict[file.scheme.path] << step
|
100
|
+
end
|
101
|
+
else
|
102
|
+
file.scheme = DakeScheme::Tag.new('@', tag_name, step)
|
103
|
+
if type == :targets
|
104
|
+
@tag_target_dict[file.scheme.path] ||= []
|
105
|
+
@tag_target_dict[file.scheme.path] << step
|
106
|
+
end
|
107
|
+
end
|
108
|
+
return [file]
|
109
|
+
else
|
110
|
+
# there maybe more than one file if a file list is generated in command substitution
|
111
|
+
file_names = text_eval(file.name, step.src_file, step.context).split("\n")
|
112
|
+
files = file_names.map do |file_name|
|
113
|
+
if file.regex
|
114
|
+
scheme = DakeScheme::Regex.new('^', file_name, step)
|
115
|
+
else
|
116
|
+
scheme = analyze_scheme(file_name, step, line, column)
|
117
|
+
end
|
118
|
+
if file_names.length == 1
|
119
|
+
file_path = scheme.path
|
120
|
+
if type == :targets
|
121
|
+
if file.regex
|
122
|
+
if @file_template_dict[file_path]
|
123
|
+
raise "Output pattern `#{file_name}' in #{step.src_file} at #{line}:#{column} appears in more than one step."
|
124
|
+
else
|
125
|
+
@file_template_dict[file_path] = step
|
126
|
+
end
|
127
|
+
else
|
128
|
+
if @file_target_dict[file_path]
|
129
|
+
raise "Output file `#{file_name}' in #{step.src_file} at #{line}:#{column} appears in more than one step."
|
130
|
+
else
|
131
|
+
@file_target_dict[file_path] = step
|
132
|
+
end
|
133
|
+
end
|
134
|
+
else
|
135
|
+
if file.regex
|
136
|
+
raise "Pattern `#{file_name}' in #{step.src_file} at #{line}:#{column} cannot be used in input file list."
|
137
|
+
end
|
138
|
+
end
|
139
|
+
else
|
140
|
+
# Generated file list should not be used in targets
|
141
|
+
if type == :targets
|
142
|
+
raise "File list `#{file_name}' in #{step.src_file} at #{line}:#{column} cannot be used as targets."
|
143
|
+
end
|
144
|
+
end
|
145
|
+
newfile = file.dup
|
146
|
+
newfile.scheme = scheme
|
147
|
+
newfile
|
148
|
+
end
|
149
|
+
return files
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def analyze_step(step)
|
154
|
+
step.context = @variable_dict.dup
|
155
|
+
# the analysis of prerequisites is deferred to the resolve phase
|
156
|
+
step.targets.map! { |file| analyze_file(file, :targets, step) }.flatten!
|
157
|
+
end
|
158
|
+
|
159
|
+
def analyze_condition(condition)
|
160
|
+
case condition.cond
|
161
|
+
when EqCond
|
162
|
+
lhs = text_eval condition.cond.eq_lhs
|
163
|
+
rhs = text_eval condition.cond.eq_rhs
|
164
|
+
if condition.not
|
165
|
+
truth = lhs != rhs
|
166
|
+
else
|
167
|
+
truth = lhs == rhs
|
168
|
+
end
|
169
|
+
when DefCond
|
170
|
+
var_name = condition.cond.var_name.to_s
|
171
|
+
if condition.not
|
172
|
+
truth = (not @variable_dict.has_key? var_name)
|
173
|
+
else
|
174
|
+
truth = @variable_dict.has_key? var_name
|
175
|
+
end
|
176
|
+
end
|
177
|
+
truth ? condition.if_body : condition.else_body
|
178
|
+
end
|
179
|
+
|
180
|
+
def analyze
|
181
|
+
@workflow.tasks.each do |task|
|
182
|
+
case task
|
183
|
+
when VariableDef
|
184
|
+
var_name = task.var_name.to_s
|
185
|
+
var_value = text_eval(task.var_value)
|
186
|
+
if task.type == :assign
|
187
|
+
@variable_dict[var_name] = var_value
|
188
|
+
elsif not @variable_dict[var_name]
|
189
|
+
@variable_dict[var_name] = var_value
|
190
|
+
end
|
191
|
+
when Step
|
192
|
+
analyze_step(task)
|
193
|
+
when StepMethod
|
194
|
+
analyze_method(task)
|
195
|
+
when Workflow, Inclusion, Condition
|
196
|
+
case task
|
197
|
+
when Inclusion
|
198
|
+
file_names = text_eval(task.files).split("\n")
|
199
|
+
file_names.each do |file_name|
|
200
|
+
src_dirname = File.dirname(@inclusion_stack[-1])
|
201
|
+
path = file_name.start_with?('/') ? file_name : File.absolute_path(file_name, src_dirname)
|
202
|
+
if @included_files.include? path
|
203
|
+
line, column = text_line_and_column task.files
|
204
|
+
raise "Cyclical inclusion detected in #{task.src_file} at #{line}:#{column}"
|
205
|
+
else
|
206
|
+
@inclusion_stack.push path
|
207
|
+
end
|
208
|
+
begin
|
209
|
+
tree = DakeParser.new.parse(File.read(path))
|
210
|
+
rescue Parslet::ParseFailed => failure
|
211
|
+
line, column = text_line_and_column task.files
|
212
|
+
STDERR.puts "Failed parsing #{path} included from #{task.src_file} at #{line}:#{column}"
|
213
|
+
raise failure.message
|
214
|
+
end
|
215
|
+
workflow = DakeTransform.new.apply(tree, src_file: path)
|
216
|
+
if task.type == :include or task.type == :call
|
217
|
+
sub_workflow = DakeAnalyzer.new(workflow, @inclusion_stack, @variable_dict).analyze
|
218
|
+
else
|
219
|
+
sub_workflow = DakeAnalyzer.new(workflow, @inclusion_stack, 'BASE' => File.dirname(path)).analyze
|
220
|
+
end
|
221
|
+
@inclusion_stack.pop
|
222
|
+
end
|
223
|
+
when Condition
|
224
|
+
body = analyze_condition(task)
|
225
|
+
next unless body
|
226
|
+
sub_workflow = DakeAnalyzer.new(body, @inclusion_stack, @variable_dict).analyze
|
227
|
+
when Workflow # for scope
|
228
|
+
sub_workflow = DakeAnalyzer.new(task, @inclusion_stack, @variable_dict).analyze
|
229
|
+
end
|
230
|
+
@tag_target_dict.merge! sub_workflow.tag_target_dict do |tag, step_list1, step_list2|
|
231
|
+
step_list1 + step_list2
|
232
|
+
end
|
233
|
+
@file_target_dict.merge! sub_workflow.file_target_dict do |file, step1, step2|
|
234
|
+
file1 = step1.targets.find { |target| target.scheme.path == file }
|
235
|
+
file2 = step2.targets.find { |target| target.scheme.path == file }
|
236
|
+
line1, column1 = text_line_and_column file1.name
|
237
|
+
line2, column2 = text_line_and_column file2.name
|
238
|
+
file2_name = file2.scheme.path
|
239
|
+
raise "Output file `#{file2_name}' defined in #{step2.src_file} at #{line2}:#{column2} " +
|
240
|
+
"was previously defined in #{step1.src_file} at #{line1}:#{column1}."
|
241
|
+
end
|
242
|
+
@file_template_dict.merge! sub_workflow.file_template_dict do |file, (scheme1, step1), (scheme2, step2)|
|
243
|
+
file1 = step1.targets.find { |target| target.scheme.path == file }
|
244
|
+
file2 = step2.targets.find { |target| target.scheme.path == file }
|
245
|
+
line1, column1 = text_line_and_column file1.name
|
246
|
+
line2, column2 = text_line_and_column file2.name
|
247
|
+
file2_name = file2.scheme.path
|
248
|
+
raise "Output pattern `#{file2_name}' defined in #{step2.src_file} at #{line2}:#{column2} " +
|
249
|
+
"was previously defined in #{step1.src_file} at #{line1}:#{column1}."
|
250
|
+
end
|
251
|
+
if task.is_a? Condition or (task.is_a? Inclusion and task.type == :include)
|
252
|
+
@variable_dict.merge! sub_workflow.variable_dict
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
self
|
257
|
+
end
|
258
|
+
|
259
|
+
def step_line_and_column(step)
|
260
|
+
return step.name.line_and_column if step.is_a? StepMethod
|
261
|
+
first_target = step.targets[0]
|
262
|
+
if first_target.name.is_a? Parslet::Slice
|
263
|
+
first_target.name.line_and_column
|
264
|
+
else
|
265
|
+
text_line_and_column(first_target.name)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def option_line_and_column(options, option_name)
|
270
|
+
options.each do |option|
|
271
|
+
next unless option.name.to_s == option_name
|
272
|
+
if option.value.is_a? Parslet::Slice
|
273
|
+
return option.value.line_and_column
|
274
|
+
else
|
275
|
+
return text_line_and_column option.value
|
276
|
+
end
|
277
|
+
end
|
278
|
+
nil
|
279
|
+
end
|
280
|
+
|
281
|
+
def text_line_and_column(text)
|
282
|
+
first_item = text.items[0]
|
283
|
+
case first_item
|
284
|
+
when Chars then first_item.string.line_and_column
|
285
|
+
when VariableSub
|
286
|
+
line, column = first_item.var_name.line_and_column
|
287
|
+
[line, column - 2]
|
288
|
+
when CommandSub
|
289
|
+
line, column = text_line_and_column first_item.cmd_text
|
290
|
+
[line, column - 2]
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def print_target(target, tag, target_step, dep_graph = nil, pad_stack = [])
|
295
|
+
pad_prefix = ''
|
296
|
+
unless pad_stack.empty?
|
297
|
+
pad_prefix += pad_stack.reduce('') do |str, pad|
|
298
|
+
next str + '│ ' if pad == '├'
|
299
|
+
next str + ' ' if pad == '└'
|
300
|
+
end
|
301
|
+
end
|
302
|
+
pwd = Pathname.new(Dir.pwd)
|
303
|
+
target_path = if tag then '@' + target
|
304
|
+
elsif target.start_with? '/'
|
305
|
+
Pathname.new(target).relative_path_from(pwd)
|
306
|
+
else
|
307
|
+
target
|
308
|
+
end
|
309
|
+
if target_step
|
310
|
+
workflow_path = Pathname.new(target_step.src_file).relative_path_from(pwd)
|
311
|
+
line, column = step_line_and_column(target_step)
|
312
|
+
|
313
|
+
pad = (pad_stack.empty? ? '' : pad_prefix[0...-4] + pad_stack.last + '── ')
|
314
|
+
puts pad.colorize(:magenta) +
|
315
|
+
"#{target_path}".colorize((tag ? :light_blue : :light_white)) +
|
316
|
+
" in " +
|
317
|
+
"#{workflow_path} ".colorize(:green) +
|
318
|
+
"#{line}:#{column} ".colorize(:yellow)
|
319
|
+
unless target_step.doc_str.empty?
|
320
|
+
pad = (pad_stack.empty? ? '│ ' : pad_prefix + '│ ')
|
321
|
+
target_step.doc_str.each do |doc|
|
322
|
+
if target_step.prerequisites.empty? or not dep_graph
|
323
|
+
puts (pad[0...-2] + (doc == target_step.doc_str.last ? '\_' : '│ ') + doc.string.to_s).colorize(:magenta)
|
324
|
+
else
|
325
|
+
puts (pad + doc.string.to_s).colorize(:magenta)
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
else
|
330
|
+
pad = (pad_stack.empty? ? '' : pad_prefix[0...-4] + pad_stack.last + '── ')
|
331
|
+
puts pad.colorize(:magenta) +
|
332
|
+
"#{target_path}".colorize((tag ? :light_blue : :light_white)) +
|
333
|
+
" (No step) "
|
334
|
+
end
|
335
|
+
|
336
|
+
if target_step and dep_graph
|
337
|
+
dep_steps = dep_graph.dep_step[target_step]
|
338
|
+
pairs = target_step.prerequisites.each_with_object([]) do |prereq, pairs|
|
339
|
+
steps = dep_steps.find_all { |dep_step| dep_step.targets.any? { |tar| tar.scheme.path == prereq.scheme.path } }
|
340
|
+
if steps.empty?
|
341
|
+
pairs << [prereq, nil]
|
342
|
+
else
|
343
|
+
steps.each { |step| pairs << [prereq, step]}
|
344
|
+
end
|
345
|
+
end
|
346
|
+
while pair = pairs.pop
|
347
|
+
pad_stack << (pairs.empty? ? '└' : '├')
|
348
|
+
prerequisite, step = pair
|
349
|
+
print_target(prerequisite.scheme.path, prerequisite.tag, step, dep_graph, pad_stack)
|
350
|
+
pad_stack.pop
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# src_file is needed by prepare_command in execution phase
|
356
|
+
# because @inclusion_stack will have only root workflow by then
|
357
|
+
def text_eval(text, src_file=@inclusion_stack[-1], context=@variable_dict, skip=0)
|
358
|
+
text.items[skip..-1].reduce('') do |string, item|
|
359
|
+
case item
|
360
|
+
when Chars
|
361
|
+
string.concat item.string.to_s
|
362
|
+
when VariableSub
|
363
|
+
var_name = item.var_name.to_s
|
364
|
+
var_value = context[var_name]
|
365
|
+
if var_value
|
366
|
+
string.concat var_value
|
367
|
+
else
|
368
|
+
line, column = item.var_name.line_and_column
|
369
|
+
raise "Variable `#{var_name}' in #{src_file} at #{line}:#{column} is not defined."
|
370
|
+
end
|
371
|
+
when CommandSub
|
372
|
+
command = text_eval(item.cmd_text, src_file, context)
|
373
|
+
stdout, stderr, status = Open3.capture3(command, :chdir=>@variable_dict['BASE'])
|
374
|
+
if status.success?
|
375
|
+
string.concat stdout.chomp
|
376
|
+
else
|
377
|
+
line, column = text_line_and_column item.cmd_text
|
378
|
+
raise "Command `#{command}' in #{src_file} at #{line}:#{column} failed " +
|
379
|
+
"with EXIT_STATUS:#{status.exitstatus} and STDERR:\n#{stderr}"
|
380
|
+
end
|
381
|
+
else # For Escaped Char
|
382
|
+
string.concat item.to_s
|
383
|
+
end
|
384
|
+
string
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'git'
|
2
|
+
# require "sqlite3"
|
3
|
+
|
4
|
+
class DakeDB
|
5
|
+
attr_reader :database_path, :database_file
|
6
|
+
def initialize(path)
|
7
|
+
workflow_path = File.dirname(path)
|
8
|
+
@database_path = workflow_path + '/.dake'
|
9
|
+
@database_file = database_path + '/step_history.db'
|
10
|
+
|
11
|
+
FileUtils.mkdir(@database_path) unless File.exist? @database_path
|
12
|
+
# @db = SQLite3::Database.new database_file
|
13
|
+
# @db.execute <<-SQL
|
14
|
+
# create table if not exists step_history (
|
15
|
+
# id int unsigned auto_increment primary key,
|
16
|
+
# step_sha1 binary(20) not null
|
17
|
+
# target varchar(50),
|
18
|
+
# process_id big int(50),
|
19
|
+
# start_time varchar(5),
|
20
|
+
# end_time varchar(5),
|
21
|
+
# ip_address binary(4)
|
22
|
+
# );
|
23
|
+
# create table if not exists step_target (
|
24
|
+
# id int unsigned auto_increment primary key,
|
25
|
+
# target varchar(1024),
|
26
|
+
# type char(50)
|
27
|
+
# );
|
28
|
+
# SQL
|
29
|
+
|
30
|
+
# git_opts = {
|
31
|
+
# repository: database_path + '/.git',
|
32
|
+
# index: database_path + '/.git/index',
|
33
|
+
# log: Logger.new(File.open(database_path + '/git.log', 'w+'))
|
34
|
+
# }
|
35
|
+
|
36
|
+
# if File.exist? database_path + '/.git'
|
37
|
+
# @git = Git.open(workflow_path, git_opts)
|
38
|
+
# else
|
39
|
+
# @git = Git.init(workflow_path, git_opts)
|
40
|
+
# @git.config('user.name', 'Dake User')
|
41
|
+
# @git.config('user.email', 'email@email.com')
|
42
|
+
# File.open(database_path + '/.gitignore', 'w') do |f|
|
43
|
+
# f.puts File.basename('.dake')
|
44
|
+
# end
|
45
|
+
# @git.add(database_path + '/.gitignore')
|
46
|
+
# @git.commit('init commit')
|
47
|
+
# end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
require 'concurrent'
|
3
|
+
|
4
|
+
class DakeExecutor
|
5
|
+
def initialize(analyzer, dake_db, dep_graph, jobs)
|
6
|
+
@analyzer = analyzer
|
7
|
+
@dake_db = dake_db
|
8
|
+
@dep_graph = dep_graph
|
9
|
+
@complete_dep_steps = Hash.new(0)
|
10
|
+
@async = (jobs ? true : false)
|
11
|
+
@pool = Concurrent::ThreadPoolExecutor.new(
|
12
|
+
min_threads: 1,
|
13
|
+
max_threads: jobs,
|
14
|
+
max_queue: 0 # unbounded work queue
|
15
|
+
) if @async
|
16
|
+
end
|
17
|
+
|
18
|
+
def execute(dry_run=false, log=false)
|
19
|
+
if @dep_graph.need_rebuild.empty?
|
20
|
+
STDERR.puts "Nothing to be done.".colorize(:green)
|
21
|
+
return
|
22
|
+
end
|
23
|
+
if @async
|
24
|
+
dep_map = Hash.new
|
25
|
+
@dep_graph.dep_step.each do |step, dep_set|
|
26
|
+
next if dep_set.empty?
|
27
|
+
dep_map[step] = dep_set.dup
|
28
|
+
end
|
29
|
+
|
30
|
+
queue = Queue.new
|
31
|
+
error_queue = Queue.new
|
32
|
+
error_steps = Set.new
|
33
|
+
|
34
|
+
error_thr = Thread.new do
|
35
|
+
while error = error_queue.deq
|
36
|
+
if error.is_a? Exception
|
37
|
+
STDERR.puts "#{error.class}: #{error.message}".colorize(:red)
|
38
|
+
STDERR.puts "Continue to execute other Step(s)".colorize(:red)
|
39
|
+
STDERR.puts "To Force Quitting: Press Ctrl + C".colorize(:red)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
lock = Concurrent::ReadWriteLock.new
|
45
|
+
@dep_graph.leaf_step.each { |step| queue << step }
|
46
|
+
|
47
|
+
while next_step = queue.deq
|
48
|
+
@pool.post(next_step) do |step|
|
49
|
+
lock.acquire_read_lock
|
50
|
+
error_step = error_steps.include? step
|
51
|
+
lock.release_read_lock
|
52
|
+
if error_step
|
53
|
+
line, column = @analyzer.step_line_and_column step
|
54
|
+
msg = "Step(#{step.object_id}) defined in #{step.src_file} at #{line}:#{column} " +
|
55
|
+
"skipped due to prerequisite step(s) error."
|
56
|
+
error_queue << Exception.new(msg)
|
57
|
+
else
|
58
|
+
execute_step(step, dry_run, log) if @dep_graph.need_rebuild.include? step
|
59
|
+
end
|
60
|
+
lock.acquire_write_lock
|
61
|
+
dep_map.delete step
|
62
|
+
if dep_map.empty?
|
63
|
+
queue.close
|
64
|
+
else
|
65
|
+
@dep_graph.succ_step[step].each do |succ|
|
66
|
+
dep_map[succ].delete step
|
67
|
+
if dep_map[succ].empty?
|
68
|
+
queue << succ
|
69
|
+
elsif dep_map[succ].all? { |dep_step| error_steps.include? dep_step }
|
70
|
+
error_steps << succ
|
71
|
+
queue << succ
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
lock.release_write_lock
|
76
|
+
rescue Exception => e
|
77
|
+
error_queue << e
|
78
|
+
lock.acquire_write_lock
|
79
|
+
error_steps << step
|
80
|
+
dep_map.delete step
|
81
|
+
if dep_map.empty?
|
82
|
+
queue.close
|
83
|
+
else
|
84
|
+
@dep_graph.succ_step[step].each do |succ|
|
85
|
+
if dep_map[succ].all? { |dep_step| error_steps.include? dep_step }
|
86
|
+
error_steps << succ
|
87
|
+
queue << succ
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
lock.release_write_lock
|
92
|
+
end
|
93
|
+
end
|
94
|
+
@pool.shutdown
|
95
|
+
@pool.wait_for_termination
|
96
|
+
queue.close
|
97
|
+
error_queue.close
|
98
|
+
error_thr.join
|
99
|
+
raise "Failed to execute some step(s)" unless error_steps.empty?
|
100
|
+
else
|
101
|
+
@dep_graph.step_list.each do |step|
|
102
|
+
execute_step(step, dry_run, log) if @dep_graph.need_rebuild.include? step
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def execute_step(step, dry_run, log)
|
108
|
+
prepare_step(step)
|
109
|
+
protocol = step.option_dict['protocol']
|
110
|
+
protocol ||= 'shell'
|
111
|
+
|
112
|
+
line, column = @analyzer.step_line_and_column step
|
113
|
+
proto = DakeProtocol::ProtocolDict[protocol].new(step, @analyzer, @dake_db, dry_run)
|
114
|
+
STDERR.puts ("[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] Running #{protocol} step(#{step.object_id}) defined in " +
|
115
|
+
"#{step.src_file} at #{line}:#{column}").colorize(:green)
|
116
|
+
STDERR.puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] step(#{step.object_id}) Script in #{proto.script_file}".colorize(:green) unless dry_run
|
117
|
+
step.targets.each do |target|
|
118
|
+
next if target.scheme.is_a? DakeScheme::Regex
|
119
|
+
if target.scheme.is_a? DakeScheme::Tag
|
120
|
+
STDERR.puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] step(#{step.object_id}) Producing ".colorize(:green) +
|
121
|
+
"@#{target.scheme.path}".colorize(:light_blue)
|
122
|
+
else
|
123
|
+
STDERR.puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] step(#{step.object_id}) Producing ".colorize(:green) +
|
124
|
+
"#{target.scheme.path}".colorize(:light_white)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
STDERR.puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] step(#{step.object_id}) STDOUT in #{proto.script_stdout}".colorize(:green) if log and not dry_run
|
128
|
+
STDERR.puts "[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] step(#{step.object_id}) STDERR in #{proto.script_stderr}".colorize(:green) if log and not dry_run
|
129
|
+
|
130
|
+
if dry_run
|
131
|
+
puts step.cmd_text
|
132
|
+
else
|
133
|
+
proto.execute_step(log)
|
134
|
+
end
|
135
|
+
|
136
|
+
STDERR.puts ("[#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] Complete #{protocol} step(#{step.object_id}) defined in " +
|
137
|
+
"#{step.src_file} at #{line}:#{column}").colorize(:green)
|
138
|
+
end
|
139
|
+
|
140
|
+
def prepare_step(step)
|
141
|
+
context = step.context.merge!({'OUTPUTN' => 0, 'OUTPUTS' => [], 'INPUTN' => 0, 'INPUTS' => []})
|
142
|
+
step.targets.reject { |f| [DakeScheme::Tag, DakeScheme::Regex].include? f.scheme.class }.each_with_index do |output, n|
|
143
|
+
name = output.scheme.path
|
144
|
+
context["OUTPUT"] = name if n == 0
|
145
|
+
context["OUTPUT#{n}"] = name
|
146
|
+
context["OUTPUTS"] << name
|
147
|
+
context["OUTPUTN"] += 1
|
148
|
+
end
|
149
|
+
context['OUTPUTN'] = context['OUTPUTN'].to_s
|
150
|
+
context['OUTPUTS'] = context['OUTPUTS'].join(" ")
|
151
|
+
step.prerequisites.reject { |s| s.tag }.each_with_index do |input, n|
|
152
|
+
name = input.scheme.path
|
153
|
+
context["INPUT"] = name if n == 0
|
154
|
+
context["INPUT#{n}"] = name
|
155
|
+
context["INPUTS"] << name
|
156
|
+
context["INPUTN"] += 1
|
157
|
+
end
|
158
|
+
context['INPUTN'] = context['INPUTN'].to_s
|
159
|
+
context['INPUTS'] = context['INPUTS'].join(" ")
|
160
|
+
@analyzer.analyze_option(step)
|
161
|
+
step.cmd_text = prepare_command(step)
|
162
|
+
end
|
163
|
+
|
164
|
+
# command preparation is intentionally deferred to execution phase to
|
165
|
+
# accelerate the analysis phase of big workflow file
|
166
|
+
def prepare_command(step, context={})
|
167
|
+
mixin_method = (step.option_dict['method'] ? step.option_dict['method'] : nil)
|
168
|
+
method_mode = (step.option_dict['method_mode'] ? step.option_dict['method_mode'] : 'prepend')
|
169
|
+
if mixin_method
|
170
|
+
meth = @analyzer.method_dict[mixin_method]
|
171
|
+
unless meth
|
172
|
+
line, column = @analyzer.option_line_and_column(step.options, 'method')
|
173
|
+
raise "Method `#{mixin_method}' used in #{step.src_file} at #{line}:#{column} is not defined."
|
174
|
+
end
|
175
|
+
@analyzer.analyze_option(meth)
|
176
|
+
unless step.option_dict['protocol'] == meth.option_dict['protocol']
|
177
|
+
line, column = @analyzer.option_line_and_column(step.options, 'protocol')
|
178
|
+
line, column = @analyzer.step_line_and_column(step) unless line
|
179
|
+
meth_line, meth_column = meth.name.line_and_column
|
180
|
+
raise "Method `#{mixin_method}' defined in #{meth.src_file} at #{meth_line}:#{meth_column} " +
|
181
|
+
"uses protocol `#{meth.option_dict['protocol']}', which is incompatible with protocol " +
|
182
|
+
"`#{step.option_dict['protocol']}' used in #{step.src_file} at #{line}:#{column}."
|
183
|
+
end
|
184
|
+
if method_mode == 'replace'
|
185
|
+
return prepare_command(meth, step.context)
|
186
|
+
else
|
187
|
+
method_text = prepare_command(meth, step.context)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
cmd_text = ''
|
191
|
+
cmd_text << method_text if mixin_method and method_mode == 'prepend'
|
192
|
+
first_indent = step.commands[0].items[0].to_s unless step.commands.empty?
|
193
|
+
step.commands.each do |command|
|
194
|
+
indent = command.items[0]
|
195
|
+
if not indent.to_s.start_with? first_indent
|
196
|
+
line, column = indent.line_and_column
|
197
|
+
raise "Incompatible indentation in #{step.src_file} at #{line}:#{column}."
|
198
|
+
else
|
199
|
+
indentation = indent.to_s[first_indent.length..-1]
|
200
|
+
cmd_text << indentation + @analyzer.text_eval(command, step.src_file, step.context.merge(context), 1) + "\n"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
cmd_text << method_text if mixin_method and method_mode == 'append'
|
204
|
+
if cmd_text == ''
|
205
|
+
line, column = @analyzer.step_line_and_column step
|
206
|
+
step_meth = step.is_a?(StepMethod) ? 'Method' : 'Step'
|
207
|
+
raise "#{step_meth} defined in #{step.src_file} at #{line}:#{column} has no commands."
|
208
|
+
end
|
209
|
+
cmd_text
|
210
|
+
end
|
211
|
+
end
|