arake 0.0.1 → 0.0.2
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.
- data/lib/arake.rb +104 -30
- data/spec/arake_spec.rb +397 -0
- metadata +3 -2
data/lib/arake.rb
CHANGED
@@ -8,56 +8,69 @@ require 'watchr'
|
|
8
8
|
|
9
9
|
module ARake
|
10
10
|
class Application
|
11
|
-
attr_accessor :accumulated_args
|
12
|
-
|
13
11
|
def initialize(top_level_self)
|
12
|
+
@_rake = nil
|
13
|
+
@_watchr = nil
|
14
|
+
@_watchr_script = nil
|
14
15
|
@top_level_self = top_level_self
|
15
|
-
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
+
def run
|
19
|
+
original_Rake_application = Rake.application
|
20
|
+
begin
|
21
|
+
Rake.application = rake
|
22
|
+
|
23
|
+
Rake.application.run
|
24
|
+
watchr.run
|
25
|
+
ensure
|
26
|
+
Rake.application = original_Rake_application
|
27
|
+
end
|
18
28
|
end
|
19
29
|
|
20
|
-
|
21
|
-
a = self
|
22
|
-
(class << @top_level_self; self; end).class_eval do
|
23
|
-
include Rake::TaskManager
|
30
|
+
# Misc.
|
24
31
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
32
|
+
def rake
|
33
|
+
@_rake ||= CustomRakeAppliation.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def rake_tasks
|
37
|
+
rake.tasks
|
31
38
|
end
|
32
39
|
|
33
|
-
def
|
34
|
-
@
|
40
|
+
def watchr
|
41
|
+
@_watchr ||= _create_custom_watchr
|
35
42
|
end
|
36
43
|
|
37
|
-
def
|
44
|
+
def _create_custom_watchr
|
45
|
+
Watchr::Controller.new(watchr_script, Watchr.handler.new)
|
46
|
+
end
|
47
|
+
|
48
|
+
def watchr_rules
|
49
|
+
watchr_script.rules
|
50
|
+
end
|
51
|
+
|
52
|
+
def watchr_script
|
53
|
+
@_watchr_script ||= _create_custom_watchr_script
|
54
|
+
end
|
55
|
+
|
56
|
+
def _create_custom_watchr_script
|
38
57
|
a = self
|
39
58
|
s = Watchr::Script.new
|
40
59
|
(class << s; self; end).class_eval do
|
41
60
|
define_method :parse! do
|
42
61
|
@ec.instance_eval do
|
43
|
-
a.
|
44
|
-
|
45
|
-
watch "^#{Regexp.escape
|
62
|
+
a.rake_tasks.each do |t|
|
63
|
+
t.prerequisites.each do |p|
|
64
|
+
watch "^#{Regexp.escape p}$" do
|
65
|
+
a.rake.reenable_all_tasks
|
66
|
+
a.rake.invoke_root_tasks_of(t)
|
67
|
+
end
|
46
68
|
end
|
47
69
|
end
|
48
70
|
end
|
49
71
|
end
|
50
72
|
end
|
51
|
-
|
52
|
-
end
|
53
|
-
|
54
|
-
def load_rakefiles
|
55
|
-
CustomRakeAppliation.new.run
|
56
|
-
end
|
57
|
-
|
58
|
-
def run
|
59
|
-
load_rakefiles
|
60
|
-
create_custom_watchr.run
|
73
|
+
s
|
61
74
|
end
|
62
75
|
end
|
63
76
|
|
@@ -70,6 +83,67 @@ module ARake
|
|
70
83
|
# run whenever dependents are updated.
|
71
84
|
end
|
72
85
|
end
|
86
|
+
|
87
|
+
def reenable_all_tasks
|
88
|
+
tasks.each do |t|
|
89
|
+
t.reenable
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def invoke_root_tasks_of(task)
|
94
|
+
root_tasks_of(task).each do |t|
|
95
|
+
t.invoke
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def root_tasks_of(task)
|
100
|
+
Misc.root_tasks_of task, tasks
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
module Misc
|
105
|
+
def self.root_tasks_of(task, tasks)
|
106
|
+
tree_from_task(task, pt_table_from_tasks(tasks)).leaves
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.pt_table_from_tasks(tasks) # prerequisite-to-target table
|
110
|
+
h = Hash.new {|h, name| h[name] = []}
|
111
|
+
tasks.each do |target_task|
|
112
|
+
target_task.prerequisites.each do |prerequisite|
|
113
|
+
h[prerequisite.to_s].push target_task
|
114
|
+
end
|
115
|
+
end
|
116
|
+
h
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.tree_from_task(task, table = Hash.new {|h, key| h[key] = []})
|
120
|
+
t = Tree.new
|
121
|
+
t.value = task
|
122
|
+
t.subtrees = table[task.to_s].map {|x| tree_from_task x, table}
|
123
|
+
t
|
124
|
+
end
|
125
|
+
|
126
|
+
class Tree
|
127
|
+
attr_accessor :value
|
128
|
+
attr_accessor :subtrees
|
129
|
+
|
130
|
+
def initialize(value = nil, subtrees = [])
|
131
|
+
@value = value
|
132
|
+
@subtrees = subtrees
|
133
|
+
end
|
134
|
+
|
135
|
+
def leaf?
|
136
|
+
subtrees.empty?
|
137
|
+
end
|
138
|
+
|
139
|
+
def leaves
|
140
|
+
if leaf?
|
141
|
+
[value]
|
142
|
+
else
|
143
|
+
subtrees.map{|s| s.leaves}.inject :+
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
73
147
|
end
|
74
148
|
end
|
75
149
|
|
data/spec/arake_spec.rb
ADDED
@@ -0,0 +1,397 @@
|
|
1
|
+
#!/usr/bin/env rspec
|
2
|
+
|
3
|
+
require 'arake'
|
4
|
+
require 'stringio'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
# $ mount
|
8
|
+
# /dev/disk0s2 on / (hfs, local, journaled)
|
9
|
+
# devfs on /dev (devfs, local)
|
10
|
+
# fdesc on /dev (fdesc, union)
|
11
|
+
# map -hosts on /net (autofs, automounted)
|
12
|
+
# map auto_home on /home (autofs, automounted)
|
13
|
+
#
|
14
|
+
# $ ruby -e 'p (Dir.glob "/a", File::FNM_CASEFOLD)'
|
15
|
+
# -e:1:in `glob': invalid byte sequence in US-ASCII (ArgumentError)
|
16
|
+
# from -e:1:in `<main>'
|
17
|
+
#
|
18
|
+
# $ ruby --version
|
19
|
+
# ruby 1.9.2p180 (2011-02-18 revision 30909) [i386-darwin9.8.0]
|
20
|
+
File::FNM_CASEFOLD = 0
|
21
|
+
|
22
|
+
def with_argv(*args, &block)
|
23
|
+
original_ARGV = ARGV.dup
|
24
|
+
|
25
|
+
ARGV.clear
|
26
|
+
ARGV.push *args
|
27
|
+
|
28
|
+
block.call
|
29
|
+
|
30
|
+
ARGV.clear
|
31
|
+
ARGV.push *original_ARGV
|
32
|
+
end
|
33
|
+
|
34
|
+
def with_rakefile(rakefile_content, &block)
|
35
|
+
Dir.mktmpdir do |d|
|
36
|
+
rakefile = "#{d}/Rakefile"
|
37
|
+
File.open rakefile, 'w' do |io|
|
38
|
+
io.write rakefile_content
|
39
|
+
end
|
40
|
+
with_argv *['-f', rakefile] do
|
41
|
+
block.call
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def with_rake_application(rake_application, &block)
|
47
|
+
original_Rake_application = Rake.application
|
48
|
+
begin
|
49
|
+
Rake.application = rake_application
|
50
|
+
block.call
|
51
|
+
ensure
|
52
|
+
Rake.application = original_Rake_application
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def with_rake(rake_application, rakefile_content, &block)
|
57
|
+
with_rakefile rakefile_content do
|
58
|
+
with_rake_application rake_application do
|
59
|
+
block.call
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def redirect(stdout = $stdout, &block)
|
65
|
+
_stdout = $stdout
|
66
|
+
begin
|
67
|
+
$stdout = stdout
|
68
|
+
block.call
|
69
|
+
ensure
|
70
|
+
$stdout = _stdout
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
top_level_self = self
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
|
79
|
+
describe ARake::CustomRakeAppliation do
|
80
|
+
it 'should read rakefiles but should not run any specified targets' do
|
81
|
+
rakefile_content = <<-"END"
|
82
|
+
p 'outer#{self.object_id}'
|
83
|
+
task :default do
|
84
|
+
p 'inner#{self.object_id}'
|
85
|
+
end
|
86
|
+
END
|
87
|
+
with_rake ARake::CustomRakeAppliation.new, rakefile_content do
|
88
|
+
s = String.new
|
89
|
+
redirect (StringIO.new s) do
|
90
|
+
Rake.application.run
|
91
|
+
end
|
92
|
+
|
93
|
+
(s.index "outer#{self.object_id}").should_not be_nil
|
94
|
+
(s.index "inner#{self.object_id}").should be_nil
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
describe ARake::Application do
|
103
|
+
def re(s)
|
104
|
+
"^#{Regexp.escape s}$"
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'should not affect the default Rake.application' do
|
108
|
+
oa = Rake.application
|
109
|
+
a = ARake::Application.new top_level_self
|
110
|
+
with_rake_application a.rake do
|
111
|
+
oa.tasks.should be_empty
|
112
|
+
a.rake_tasks.should be_empty
|
113
|
+
|
114
|
+
block = Proc.new {}
|
115
|
+
top_level_self.instance_eval do
|
116
|
+
task 'bar1'
|
117
|
+
task 'bar2'
|
118
|
+
task 'baz1'
|
119
|
+
task 'baz2'
|
120
|
+
task 'foo1' => ['bar1', 'baz1'], &block
|
121
|
+
task 'foo2' => ['bar2', 'baz2'], &block
|
122
|
+
end
|
123
|
+
|
124
|
+
oa.tasks.should be_empty
|
125
|
+
a.rake_tasks.should_not be_empty
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'should be able to refer tasks' do
|
130
|
+
a = ARake::Application.new top_level_self
|
131
|
+
with_rake_application a.rake do
|
132
|
+
a.rake_tasks.should be_empty
|
133
|
+
|
134
|
+
block = Proc.new {}
|
135
|
+
top_level_self.instance_eval do
|
136
|
+
task 'bar1'
|
137
|
+
task 'bar2'
|
138
|
+
task 'baz1'
|
139
|
+
task 'baz2'
|
140
|
+
task 'foo1' => ['bar1', 'baz1'], &block
|
141
|
+
task 'foo2' => ['bar2', 'baz2'], &block
|
142
|
+
end
|
143
|
+
|
144
|
+
a.rake_tasks.should_not be_empty
|
145
|
+
a.rake_tasks[-2].to_s.should eql 'foo1'
|
146
|
+
a.rake_tasks[-2].prerequisites.should eql ['bar1', 'baz1']
|
147
|
+
a.rake_tasks[-2].actions.should eql [block]
|
148
|
+
a.rake_tasks[-1].to_s.should eql 'foo2'
|
149
|
+
a.rake_tasks[-1].prerequisites.should eql ['bar2', 'baz2']
|
150
|
+
a.rake_tasks[-1].actions.should eql [block]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'should define a watch rule for each dependent' do
|
155
|
+
a = ARake::Application.new top_level_self
|
156
|
+
with_rake_application a.rake do
|
157
|
+
a.rake_tasks.should be_empty
|
158
|
+
a.watchr_rules.should be_empty
|
159
|
+
|
160
|
+
block = Proc.new {}
|
161
|
+
top_level_self.instance_eval do
|
162
|
+
task 'bar1'
|
163
|
+
task 'bar2'
|
164
|
+
task 'baz1'
|
165
|
+
task 'baz2'
|
166
|
+
task 'foo1' => ['bar1', 'baz1'], &block
|
167
|
+
task 'foo2' => ['bar2', 'baz2'], &block
|
168
|
+
end
|
169
|
+
a.watchr_script.parse!
|
170
|
+
|
171
|
+
a.rake_tasks.should_not be_empty
|
172
|
+
a.watchr_rules.should_not be_empty
|
173
|
+
a.watchr_rules[0].pattern.should eql re('bar1')
|
174
|
+
a.watchr_rules[1].pattern.should eql re('baz1')
|
175
|
+
a.watchr_rules[2].pattern.should eql re('bar2')
|
176
|
+
a.watchr_rules[3].pattern.should eql re('baz2')
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'should invoke a rake task with proper parameters' do
|
181
|
+
a = ARake::Application.new top_level_self
|
182
|
+
with_rake_application a.rake do
|
183
|
+
passed_task = nil
|
184
|
+
block = Proc.new {|task| passed_task = task}
|
185
|
+
top_level_self.instance_eval do
|
186
|
+
task 'bar'
|
187
|
+
task 'foo' => 'bar', &block
|
188
|
+
end
|
189
|
+
a.watchr_script.parse!
|
190
|
+
r = a.watchr_rules[0]
|
191
|
+
|
192
|
+
r.pattern.should eql re('bar')
|
193
|
+
passed_task.should be_nil
|
194
|
+
|
195
|
+
r.action.call
|
196
|
+
t = a.rake_tasks[1]
|
197
|
+
|
198
|
+
t.to_s.should eql 'foo'
|
199
|
+
passed_task.should equal t
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'should execute rake tasks even if they are already executed before' do
|
204
|
+
a = ARake::Application.new top_level_self
|
205
|
+
with_rake_application a.rake do
|
206
|
+
call_count = 0
|
207
|
+
block = Proc.new {call_count += 1}
|
208
|
+
top_level_self.instance_eval do
|
209
|
+
task :dependent
|
210
|
+
task :target => :dependent, &block
|
211
|
+
end
|
212
|
+
a.watchr_script.parse!
|
213
|
+
r = a.watchr_rules[0]
|
214
|
+
|
215
|
+
r.pattern.should eql re(:dependent)
|
216
|
+
call_count.should eql 0
|
217
|
+
|
218
|
+
r.action.call
|
219
|
+
|
220
|
+
call_count.should eql 1
|
221
|
+
|
222
|
+
r.action.call
|
223
|
+
|
224
|
+
call_count.should eql 2
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'should execute chained tasks properly' do
|
229
|
+
a = ARake::Application.new top_level_self
|
230
|
+
with_rake_application a.rake do
|
231
|
+
calling_history = []
|
232
|
+
block = Proc.new {|t| calling_history.push t.to_s}
|
233
|
+
top_level_self.instance_eval do
|
234
|
+
# t1a t1b t1c
|
235
|
+
# |\ | |
|
236
|
+
# | \ | |
|
237
|
+
# | \| |
|
238
|
+
# t2a t2b t2c
|
239
|
+
# | | /|
|
240
|
+
# | | / |
|
241
|
+
# | |/ |
|
242
|
+
# t3a t3b t3c
|
243
|
+
task :t1a => [:t2a, :t2b], &block
|
244
|
+
task :t1b => [:t2b], &block
|
245
|
+
task :t1c => [:t2c], &block
|
246
|
+
task :t2a => [:t3a], &block
|
247
|
+
task :t2b => [:t3b], &block
|
248
|
+
task :t2c => [:t3b, :t3c], &block
|
249
|
+
task :t3a => [], &block
|
250
|
+
task :t3b => [], &block
|
251
|
+
task :t3c => [], &block
|
252
|
+
end
|
253
|
+
a.watchr_script.parse!
|
254
|
+
rules_table = Hash.new {|h, key| h[key] = []}
|
255
|
+
a.watchr_rules.each do |r|
|
256
|
+
rules_table[r.pattern].push r
|
257
|
+
end
|
258
|
+
def rules_table.trigger(task_name)
|
259
|
+
self[task_name].each do |r|
|
260
|
+
r.action.call
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
rules_table.keys.sort.should eql [
|
265
|
+
re(:t2a),
|
266
|
+
re(:t2b),
|
267
|
+
re(:t2c),
|
268
|
+
re(:t3a),
|
269
|
+
re(:t3b),
|
270
|
+
re(:t3c),
|
271
|
+
]
|
272
|
+
calling_history.should be_empty
|
273
|
+
|
274
|
+
calling_history.clear
|
275
|
+
rules_table.trigger re(:t3c)
|
276
|
+
|
277
|
+
calling_history.sort.should eql %w[t1c t2c t3b t3c]
|
278
|
+
|
279
|
+
calling_history.clear
|
280
|
+
rules_table.trigger re(:t2c)
|
281
|
+
|
282
|
+
calling_history.sort.should eql %w[t1c t2c t3b t3c]
|
283
|
+
|
284
|
+
calling_history.clear
|
285
|
+
rules_table.trigger re(:t3b)
|
286
|
+
|
287
|
+
calling_history.sort.should eql %w[
|
288
|
+
t1a t1b t1c t2a t2b t2c t3a t3b t3b t3c
|
289
|
+
]
|
290
|
+
|
291
|
+
calling_history.clear
|
292
|
+
rules_table.trigger re(:t2b)
|
293
|
+
|
294
|
+
calling_history.sort.should eql %w[t1a t1b t2a t2b t2b t3a t3b t3b]
|
295
|
+
|
296
|
+
calling_history.clear
|
297
|
+
rules_table.trigger re(:t3a)
|
298
|
+
|
299
|
+
calling_history.sort.should eql %w[t1a t2a t2b t3a t3b]
|
300
|
+
|
301
|
+
calling_history.clear
|
302
|
+
rules_table.trigger re(:t2a)
|
303
|
+
|
304
|
+
calling_history.sort.should eql %w[t1a t2a t2b t3a t3b]
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
|
310
|
+
|
311
|
+
|
312
|
+
describe ARake::Misc do
|
313
|
+
Tree = ARake::Misc::Tree
|
314
|
+
|
315
|
+
it 'should create an empty instance' do
|
316
|
+
t = Tree.new
|
317
|
+
t.value.should be_nil
|
318
|
+
t.subtrees.should be_empty
|
319
|
+
|
320
|
+
t.leaf?.should be_true
|
321
|
+
t.leaves.should eql [nil]
|
322
|
+
end
|
323
|
+
|
324
|
+
it 'should return values of leaves' do
|
325
|
+
t = Tree.new(
|
326
|
+
1,
|
327
|
+
[
|
328
|
+
Tree.new(2),
|
329
|
+
Tree.new(
|
330
|
+
3,
|
331
|
+
[Tree.new(4)]
|
332
|
+
),
|
333
|
+
Tree.new(5),
|
334
|
+
]
|
335
|
+
)
|
336
|
+
|
337
|
+
t.leaves.should eql [2, 4, 5]
|
338
|
+
end
|
339
|
+
|
340
|
+
it 'should return a tree of tasks based on their dependencies' do
|
341
|
+
def task(name, *prerequisites)
|
342
|
+
t = Rake::Task.new name, Rake::Application.new
|
343
|
+
t.enhance prerequisites
|
344
|
+
t
|
345
|
+
end
|
346
|
+
|
347
|
+
# t1a t1b t1c
|
348
|
+
# \ /| |
|
349
|
+
# X | |
|
350
|
+
# / \| |
|
351
|
+
# t2a t2b t2c
|
352
|
+
# | | /|
|
353
|
+
# | | / |
|
354
|
+
# | |/ |
|
355
|
+
# t3a t3b t3c
|
356
|
+
# |
|
357
|
+
# |
|
358
|
+
# |
|
359
|
+
# t4b
|
360
|
+
t4b = task 't4b'
|
361
|
+
t3c = task 't3c'
|
362
|
+
t3b = task 't3b', t4b
|
363
|
+
t3a = task 't3a'
|
364
|
+
t2c = task 't2c', t3b, t3c
|
365
|
+
t2b = task 't2b', t3b
|
366
|
+
t2a = task 't2a', t3a
|
367
|
+
t1c = task 't1c', t2c
|
368
|
+
t1b = task 't1b', t2a, t2b
|
369
|
+
t1a = task 't1a', t2b
|
370
|
+
|
371
|
+
tasks = [
|
372
|
+
t1a,
|
373
|
+
t1b,
|
374
|
+
t1c,
|
375
|
+
t2a,
|
376
|
+
t2b,
|
377
|
+
t2c,
|
378
|
+
t3a,
|
379
|
+
t3b,
|
380
|
+
t3c,
|
381
|
+
t4b,
|
382
|
+
]
|
383
|
+
|
384
|
+
ARake::Misc.root_tasks_of(t1a, tasks).should eql [t1a]
|
385
|
+
ARake::Misc.root_tasks_of(t1b, tasks).should eql [t1b]
|
386
|
+
ARake::Misc.root_tasks_of(t1c, tasks).should eql [t1c]
|
387
|
+
ARake::Misc.root_tasks_of(t2a, tasks).should eql [t1b]
|
388
|
+
ARake::Misc.root_tasks_of(t2b, tasks).should eql [t1a, t1b]
|
389
|
+
ARake::Misc.root_tasks_of(t2c, tasks).should eql [t1c]
|
390
|
+
ARake::Misc.root_tasks_of(t3a, tasks).should eql [t1b]
|
391
|
+
ARake::Misc.root_tasks_of(t3b, tasks).should eql [t1a, t1b, t1c]
|
392
|
+
ARake::Misc.root_tasks_of(t3c, tasks).should eql [t1c]
|
393
|
+
ARake::Misc.root_tasks_of(t4b, tasks).should eql [t1a, t1b, t1c]
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
__END__
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: arake
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.2
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Kana Natsuno
|
@@ -10,7 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-05-
|
13
|
+
date: 2011-05-06 00:00:00 +09:00
|
14
14
|
default_executable:
|
15
15
|
dependencies:
|
16
16
|
- !ruby/object:Gem::Dependency
|
@@ -59,6 +59,7 @@ files:
|
|
59
59
|
- arake.gemspec
|
60
60
|
- bin/arake
|
61
61
|
- lib/arake.rb
|
62
|
+
- spec/arake_spec.rb
|
62
63
|
has_rdoc: true
|
63
64
|
homepage: http://github.com/kana/ruby-arake
|
64
65
|
licenses: []
|