autobuild 1.17.0 → 1.21.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 +4 -4
- data/.rubocop.yml +107 -0
- data/.travis.yml +3 -2
- data/Gemfile +2 -1
- data/Rakefile +1 -4
- data/autobuild.gemspec +18 -13
- data/bin/autobuild +4 -3
- data/lib/autobuild.rb +4 -5
- data/lib/autobuild/build_logfile.rb +6 -4
- data/lib/autobuild/config.rb +104 -41
- data/lib/autobuild/configurable.rb +32 -18
- data/lib/autobuild/environment.rb +126 -120
- data/lib/autobuild/exceptions.rb +48 -31
- data/lib/autobuild/import/archive.rb +134 -82
- data/lib/autobuild/import/cvs.rb +28 -24
- data/lib/autobuild/import/darcs.rb +13 -16
- data/lib/autobuild/import/git-lfs.rb +37 -30
- data/lib/autobuild/import/git.rb +246 -182
- data/lib/autobuild/import/hg.rb +23 -18
- data/lib/autobuild/import/svn.rb +48 -29
- data/lib/autobuild/importer.rb +534 -499
- data/lib/autobuild/mail_reporter.rb +77 -77
- data/lib/autobuild/package.rb +200 -122
- data/lib/autobuild/packages/autotools.rb +47 -42
- data/lib/autobuild/packages/cmake.rb +77 -65
- data/lib/autobuild/packages/dummy.rb +9 -8
- data/lib/autobuild/packages/genom.rb +1 -1
- data/lib/autobuild/packages/gnumake.rb +74 -31
- data/lib/autobuild/packages/import.rb +2 -6
- data/lib/autobuild/packages/orogen.rb +32 -31
- data/lib/autobuild/packages/pkgconfig.rb +2 -2
- data/lib/autobuild/packages/python.rb +12 -8
- data/lib/autobuild/packages/ruby.rb +22 -17
- data/lib/autobuild/parallel.rb +50 -46
- data/lib/autobuild/pkgconfig.rb +25 -13
- data/lib/autobuild/progress_display.rb +149 -64
- data/lib/autobuild/rake_task_extension.rb +12 -7
- data/lib/autobuild/reporting.rb +51 -26
- data/lib/autobuild/subcommand.rb +72 -65
- data/lib/autobuild/test.rb +9 -7
- data/lib/autobuild/test_utility.rb +12 -10
- data/lib/autobuild/timestamps.rb +28 -23
- data/lib/autobuild/tools.rb +17 -16
- data/lib/autobuild/utility.rb +67 -23
- data/lib/autobuild/version.rb +1 -1
- metadata +53 -37
data/lib/autobuild/parallel.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
require 'thread'
|
2
|
-
|
3
1
|
module Autobuild
|
4
2
|
# This is a rewrite of the Rake task invocation code to use parallelism
|
5
3
|
#
|
@@ -23,9 +21,11 @@ def initialize(level)
|
|
23
21
|
wio.fcntl(Fcntl::F_SETFD, 0)
|
24
22
|
put(level)
|
25
23
|
end
|
24
|
+
|
26
25
|
def get(token_count = 1)
|
27
26
|
@rio.read(token_count)
|
28
27
|
end
|
28
|
+
|
29
29
|
def put(token_count = 1)
|
30
30
|
@wio.write(" " * token_count)
|
31
31
|
end
|
@@ -36,7 +36,6 @@ def initialize(level = Autobuild.parallel_build_level)
|
|
36
36
|
@available_workers = Array.new
|
37
37
|
@finished_workers = Queue.new
|
38
38
|
@workers = Array.new
|
39
|
-
|
40
39
|
end
|
41
40
|
|
42
41
|
def wait_for_worker_to_end(state)
|
@@ -46,9 +45,12 @@ def wait_for_worker_to_end(state)
|
|
46
45
|
if error
|
47
46
|
if available_workers.size != workers.size
|
48
47
|
if finished_task.respond_to?(:package) && finished_task.package
|
49
|
-
Autobuild.error "got an error processing
|
48
|
+
Autobuild.error "got an error processing "\
|
49
|
+
"#{finished_task.package.name}, "\
|
50
|
+
"waiting for pending jobs to end"
|
50
51
|
else
|
51
|
-
Autobuild.error "got an error doing parallel processing,
|
52
|
+
Autobuild.error "got an error doing parallel processing, "\
|
53
|
+
"waiting for pending jobs to end"
|
52
54
|
end
|
53
55
|
end
|
54
56
|
begin
|
@@ -61,16 +63,14 @@ def wait_for_worker_to_end(state)
|
|
61
63
|
state.process_finished_task(finished_task)
|
62
64
|
end
|
63
65
|
|
64
|
-
def discover_dependencies(all_tasks, reverse_dependencies,
|
65
|
-
if
|
66
|
-
|
67
|
-
end
|
66
|
+
def discover_dependencies(all_tasks, reverse_dependencies, task)
|
67
|
+
return if task.already_invoked?
|
68
|
+
return if all_tasks.include?(task) # already discovered or being discovered
|
68
69
|
|
69
|
-
|
70
|
-
all_tasks << t
|
70
|
+
all_tasks << task
|
71
71
|
|
72
|
-
|
73
|
-
reverse_dependencies[dep_t] <<
|
72
|
+
task.prerequisite_tasks.each do |dep_t|
|
73
|
+
reverse_dependencies[dep_t] << task
|
74
74
|
discover_dependencies(all_tasks, reverse_dependencies, dep_t)
|
75
75
|
end
|
76
76
|
end
|
@@ -83,8 +83,9 @@ class ProcessingState
|
|
83
83
|
attr_reader :queue
|
84
84
|
attr_reader :priorities
|
85
85
|
|
86
|
-
def initialize(reverse_dependencies)
|
86
|
+
def initialize(reverse_dependencies, completion_callback: proc { })
|
87
87
|
@reverse_dependencies = reverse_dependencies
|
88
|
+
@completion_callback = completion_callback
|
88
89
|
@processed = Set.new
|
89
90
|
@active_tasks = Set.new
|
90
91
|
@priorities = Hash.new
|
@@ -101,7 +102,7 @@ def push(task, base_priority = 1)
|
|
101
102
|
end
|
102
103
|
|
103
104
|
def find_task
|
104
|
-
if task = queue.
|
105
|
+
if (task = queue.min_by { |_t, p| p })
|
105
106
|
priorities[task.first] = task.last
|
106
107
|
task.first
|
107
108
|
end
|
@@ -143,16 +144,19 @@ def process_finished_task(task)
|
|
143
144
|
push(candidate, priorities[task])
|
144
145
|
end
|
145
146
|
end
|
147
|
+
|
148
|
+
@completion_callback.call(task)
|
146
149
|
end
|
147
150
|
|
148
151
|
def trivial_task?(task)
|
149
|
-
(task.kind_of?(Autobuild::SourceTreeTask) ||
|
152
|
+
(task.kind_of?(Autobuild::SourceTreeTask) ||
|
153
|
+
task.kind_of?(Rake::FileTask)) && task.actions.empty?
|
150
154
|
end
|
151
155
|
end
|
152
156
|
|
153
157
|
# Invokes the provided tasks. Unlike the rake code, this is a toplevel
|
154
158
|
# algorithm that does not use recursion
|
155
|
-
def invoke_parallel(required_tasks)
|
159
|
+
def invoke_parallel(required_tasks, completion_callback: proc { })
|
156
160
|
tasks = Set.new
|
157
161
|
reverse_dependencies = Hash.new { |h, k| h[k] = Set.new }
|
158
162
|
required_tasks.each do |t|
|
@@ -161,13 +165,12 @@ def invoke_parallel(required_tasks)
|
|
161
165
|
# The queue is the set of tasks for which all prerequisites have
|
162
166
|
# been successfully executed (or where not needed). I.e. it is the
|
163
167
|
# set of tasks that can be queued for execution.
|
164
|
-
state = ProcessingState.new(reverse_dependencies
|
168
|
+
state = ProcessingState.new(reverse_dependencies,
|
169
|
+
completion_callback: completion_callback)
|
165
170
|
tasks.each do |t|
|
166
|
-
if state.ready?(t)
|
167
|
-
state.push(t)
|
168
|
-
end
|
171
|
+
state.push(t) if state.ready?(t)
|
169
172
|
end
|
170
|
-
|
173
|
+
|
171
174
|
# Build a reverse dependency graph (i.e. a mapping from a task to
|
172
175
|
# the tasks that depend on it)
|
173
176
|
|
@@ -175,9 +178,9 @@ def invoke_parallel(required_tasks)
|
|
175
178
|
# topological sort since we would then have to scan all tasks each
|
176
179
|
# time for tasks that have no currently running prerequisites
|
177
180
|
|
178
|
-
|
181
|
+
loop do
|
179
182
|
pending_task = state.pop
|
180
|
-
|
183
|
+
unless pending_task
|
181
184
|
# If we have pending workers, wait for one to be finished
|
182
185
|
# until either they are all finished or the queue is not
|
183
186
|
# empty anymore
|
@@ -186,17 +189,19 @@ def invoke_parallel(required_tasks)
|
|
186
189
|
pending_task = state.pop
|
187
190
|
end
|
188
191
|
|
189
|
-
if !pending_task && available_workers.size == workers.size
|
190
|
-
break
|
191
|
-
end
|
192
|
+
break if !pending_task && available_workers.size == workers.size
|
192
193
|
end
|
193
194
|
|
194
|
-
|
195
|
-
|
195
|
+
bypass_task = pending_task.disabled? ||
|
196
|
+
pending_task.already_invoked? ||
|
197
|
+
!pending_task.needed?
|
198
|
+
|
199
|
+
if bypass_task
|
200
|
+
pending_task.already_invoked = true
|
196
201
|
state.process_finished_task(pending_task)
|
197
202
|
next
|
198
|
-
elsif
|
199
|
-
pending_task
|
203
|
+
elsif state.trivial_task?(pending_task)
|
204
|
+
Worker.execute_task(pending_task)
|
200
205
|
state.process_finished_task(pending_task)
|
201
206
|
next
|
202
207
|
end
|
@@ -204,9 +209,7 @@ def invoke_parallel(required_tasks)
|
|
204
209
|
# Get a job server token
|
205
210
|
job_server.get
|
206
211
|
|
207
|
-
|
208
|
-
wait_for_worker_to_end(state)
|
209
|
-
end
|
212
|
+
wait_for_worker_to_end(state) until finished_workers.empty?
|
210
213
|
|
211
214
|
# We do have a job server token, so we are allowed to allocate a
|
212
215
|
# new worker if none are available
|
@@ -222,9 +225,9 @@ def invoke_parallel(required_tasks)
|
|
222
225
|
end
|
223
226
|
|
224
227
|
not_processed = tasks.find_all { |t| !t.already_invoked? }
|
225
|
-
|
228
|
+
unless not_processed.empty?
|
226
229
|
cycle = resolve_cycle(tasks, not_processed, reverse_dependencies)
|
227
|
-
raise "cycle in task graph: #{cycle.map(&:name).sort.join(
|
230
|
+
raise "cycle in task graph: #{cycle.map(&:name).sort.join(', ')}"
|
228
231
|
end
|
229
232
|
end
|
230
233
|
|
@@ -232,7 +235,7 @@ def resolve_cycle(all_tasks, tasks, reverse_dependencies)
|
|
232
235
|
cycle = tasks.dup
|
233
236
|
chain = []
|
234
237
|
next_task = tasks.first
|
235
|
-
|
238
|
+
loop do
|
236
239
|
task = next_task
|
237
240
|
chain << task
|
238
241
|
tasks.delete(next_task)
|
@@ -244,10 +247,12 @@ def resolve_cycle(all_tasks, tasks, reverse_dependencies)
|
|
244
247
|
true
|
245
248
|
end
|
246
249
|
end
|
247
|
-
|
248
|
-
Autobuild.fatal "parallel processing stopped prematurely,
|
249
|
-
|
250
|
-
Autobuild.fatal "
|
250
|
+
unless next_task
|
251
|
+
Autobuild.fatal "parallel processing stopped prematurely, "\
|
252
|
+
"but no cycle is present in the remaining tasks"
|
253
|
+
Autobuild.fatal "remaining tasks: #{cycle.map(&:name).join(', ')}"
|
254
|
+
Autobuild.fatal "known dependencies at initialization time that "\
|
255
|
+
"could block the processing of the remaining tasks"
|
251
256
|
reverse_dependencies.each do |parent_task, parents|
|
252
257
|
if cycle.include?(parent_task)
|
253
258
|
parents.each do |p|
|
@@ -255,13 +260,14 @@ def resolve_cycle(all_tasks, tasks, reverse_dependencies)
|
|
255
260
|
end
|
256
261
|
end
|
257
262
|
end
|
258
|
-
Autobuild.fatal "known dependencies right now that could block
|
263
|
+
Autobuild.fatal "known dependencies right now that could block "\
|
264
|
+
"the processing of the remaining tasks"
|
259
265
|
all_tasks.each do |p|
|
260
266
|
(cycle & p.prerequisite_tasks).each do |t|
|
261
267
|
Autobuild.fatal " #{p}: #{t}"
|
262
268
|
end
|
263
269
|
end
|
264
|
-
raise "failed to resolve cycle in #{cycle.map(&:name).join(
|
270
|
+
raise "failed to resolve cycle in #{cycle.map(&:name).join(', ')}"
|
265
271
|
end
|
266
272
|
end
|
267
273
|
chain
|
@@ -301,7 +307,7 @@ def do_task(task)
|
|
301
307
|
end
|
302
308
|
|
303
309
|
def last_result
|
304
|
-
|
310
|
+
[@last_finished_task, @last_error]
|
305
311
|
end
|
306
312
|
|
307
313
|
def queue(task)
|
@@ -321,5 +327,3 @@ class << self
|
|
321
327
|
attr_accessor :parallel_task_manager
|
322
328
|
end
|
323
329
|
end
|
324
|
-
|
325
|
-
|
data/lib/autobuild/pkgconfig.rb
CHANGED
@@ -2,10 +2,16 @@
|
|
2
2
|
class PkgConfig
|
3
3
|
class NotFound < RuntimeError
|
4
4
|
attr_reader :name
|
5
|
-
|
6
|
-
def
|
5
|
+
|
6
|
+
def initialize(name)
|
7
|
+
@name = name
|
8
|
+
end
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
"#{name} is not available to pkg-config"
|
12
|
+
end
|
7
13
|
end
|
8
|
-
|
14
|
+
|
9
15
|
# The module name
|
10
16
|
attr_reader :name
|
11
17
|
# The module version
|
@@ -14,30 +20,36 @@ def to_s; "#{name} is not available to pkg-config" end
|
|
14
20
|
# Create a PkgConfig object for the package +name+
|
15
21
|
# Raises PkgConfig::NotFound if the module does not exist
|
16
22
|
def initialize(name)
|
17
|
-
|
18
|
-
raise NotFound.new(name)
|
23
|
+
unless system("pkg-config --exists #{name}")
|
24
|
+
raise NotFound.new(name), "pkg-config package '#{name}' not found"
|
19
25
|
end
|
20
|
-
|
26
|
+
|
21
27
|
@name = name
|
22
28
|
@version = `pkg-config --modversion #{name}`.chomp.strip
|
23
29
|
@actions = Hash.new
|
24
30
|
@variables = Hash.new
|
25
31
|
end
|
26
32
|
|
27
|
-
ACTIONS = %w
|
28
|
-
|
33
|
+
ACTIONS = %w[cflags cflags-only-I cflags-only-other
|
34
|
+
libs libs-only-L libs-only-l libs-only-other static].freeze
|
29
35
|
ACTIONS.each do |action|
|
30
|
-
define_method(action.
|
36
|
+
define_method(action.tr('-', '_')) do
|
31
37
|
@actions[action] ||= `pkg-config --#{action} #{name}`.chomp.strip
|
32
38
|
end
|
33
39
|
end
|
34
40
|
|
41
|
+
def respond_to_missing?(varname, _include_all)
|
42
|
+
varname =~ /^\w+$/
|
43
|
+
end
|
44
|
+
|
35
45
|
def method_missing(varname, *args, &proc)
|
36
46
|
if args.empty?
|
37
|
-
@variables[varname]
|
38
|
-
|
39
|
-
|
47
|
+
unless (value = @variables[varname])
|
48
|
+
value = `pkg-config --variable=#{varname} #{name}`.chomp.strip
|
49
|
+
@variables[varname] = value
|
50
|
+
end
|
51
|
+
return value
|
40
52
|
end
|
53
|
+
super
|
41
54
|
end
|
42
55
|
end
|
43
|
-
|
@@ -1,51 +1,106 @@
|
|
1
|
+
require "concurrent/atomic/atomic_boolean"
|
2
|
+
require "concurrent/array"
|
3
|
+
|
1
4
|
module Autobuild
|
2
5
|
# Management of the progress display
|
3
6
|
class ProgressDisplay
|
4
7
|
def initialize(io, color: ::Autobuild.method(:color))
|
5
8
|
@io = io
|
6
|
-
#@cursor = Blank.new
|
7
9
|
@cursor = TTY::Cursor
|
8
10
|
@last_formatted_progress = []
|
9
|
-
@progress_messages =
|
11
|
+
@progress_messages = Concurrent::Array.new
|
10
12
|
|
11
13
|
@silent = false
|
12
14
|
@color = color
|
13
|
-
@progress_enabled = true
|
14
15
|
@display_lock = Mutex.new
|
16
|
+
|
17
|
+
@next_progress_display = Time.at(0)
|
18
|
+
@progress_mode = :single_line
|
19
|
+
@progress_period = 0.1
|
20
|
+
|
21
|
+
@message_queue = Queue.new
|
22
|
+
@forced_progress_display = Concurrent::AtomicBoolean.new(false)
|
15
23
|
end
|
16
24
|
|
17
|
-
|
25
|
+
def synchronize(&block)
|
26
|
+
result = @display_lock.synchronize(&block)
|
27
|
+
refresh_display
|
28
|
+
result
|
29
|
+
end
|
30
|
+
|
31
|
+
# Set the minimum time between two progress messages
|
32
|
+
#
|
33
|
+
# @see period
|
34
|
+
def progress_period=(period)
|
35
|
+
@progress_period = Float(period)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Minimum time between two progress displays
|
39
|
+
#
|
40
|
+
# This does not affect normal messages
|
41
|
+
#
|
42
|
+
# @return [Float]
|
43
|
+
attr_reader :progress_period
|
44
|
+
|
45
|
+
# Valid progress modes
|
46
|
+
#
|
47
|
+
# @see progress_mode=
|
48
|
+
PROGRESS_MODES = %I[single_line newline off]
|
49
|
+
|
50
|
+
# Sets how progress messages will be displayed
|
51
|
+
#
|
52
|
+
# @param [String] the new mode. Can be either 'single_line', where a
|
53
|
+
# progress message replaces the last one, 'newline' which displays
|
54
|
+
# each on a new line or 'off' to disable progress messages altogether
|
55
|
+
def progress_mode=(mode)
|
56
|
+
mode = mode.to_sym
|
57
|
+
unless PROGRESS_MODES.include?(mode)
|
58
|
+
raise ArgumentError,
|
59
|
+
"#{mode} is not a valid mode, expected one of "\
|
60
|
+
"#{PROGRESS_MODES.join(", ")}"
|
61
|
+
end
|
62
|
+
@progress_mode = mode
|
63
|
+
end
|
64
|
+
|
65
|
+
# Return the current display mode
|
66
|
+
#
|
67
|
+
# @return [Symbol]
|
68
|
+
# @see mode=
|
69
|
+
attr_reader :progress_mode
|
18
70
|
|
19
71
|
def silent?
|
20
72
|
@silent
|
21
73
|
end
|
22
74
|
|
75
|
+
def silent=(flag)
|
76
|
+
@silent = flag
|
77
|
+
end
|
78
|
+
|
23
79
|
def silent
|
24
|
-
|
80
|
+
silent = @silent
|
81
|
+
@silent = true
|
25
82
|
yield
|
26
83
|
ensure
|
27
84
|
@silent = silent
|
28
85
|
end
|
29
86
|
|
30
|
-
|
87
|
+
# @deprecated use progress_mode= instead
|
88
|
+
def progress_enabled=(flag)
|
89
|
+
self.progress_mode = flag ? :single_line : :off
|
90
|
+
end
|
31
91
|
|
92
|
+
# Whether progress messages will be displayed at all
|
32
93
|
def progress_enabled?
|
33
|
-
!@silent && @
|
94
|
+
!@silent && (@progress_mode != :off)
|
34
95
|
end
|
35
96
|
|
36
97
|
def message(message, *args, io: @io, force: false)
|
37
98
|
return if silent? && !force
|
38
99
|
|
39
|
-
if args.last.respond_to?(:to_io)
|
40
|
-
|
41
|
-
end
|
100
|
+
io = args.pop if args.last.respond_to?(:to_io)
|
101
|
+
@message_queue << [message, args, io]
|
42
102
|
|
43
|
-
|
44
|
-
io.print "#{@cursor.column(1)}#{@cursor.clear_screen_down}#{@color.call(message, *args)}\n"
|
45
|
-
io.flush if @io != io
|
46
|
-
display_progress
|
47
|
-
@io.flush
|
48
|
-
end
|
103
|
+
refresh_display
|
49
104
|
end
|
50
105
|
|
51
106
|
def progress_start(key, *args, done_message: nil)
|
@@ -54,13 +109,13 @@ def progress_start(key, *args, done_message: nil)
|
|
54
109
|
formatted_message = @color.call(*args)
|
55
110
|
@progress_messages << [key, formatted_message]
|
56
111
|
if progress_enabled?
|
57
|
-
@
|
58
|
-
display_progress
|
59
|
-
end
|
112
|
+
@forced_progress_display.make_true
|
60
113
|
else
|
61
114
|
message " #{formatted_message}"
|
62
115
|
end
|
63
116
|
|
117
|
+
refresh_display
|
118
|
+
|
64
119
|
if block_given?
|
65
120
|
begin
|
66
121
|
result = yield
|
@@ -74,76 +129,101 @@ def progress_start(key, *args, done_message: nil)
|
|
74
129
|
end
|
75
130
|
|
76
131
|
def progress(key, *args)
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
[msg_key, msg]
|
85
|
-
end
|
132
|
+
found = false
|
133
|
+
@progress_messages.map! do |msg_key, msg|
|
134
|
+
if msg_key == key
|
135
|
+
found = true
|
136
|
+
[msg_key, @color.call(*args)]
|
137
|
+
else
|
138
|
+
[msg_key, msg]
|
86
139
|
end
|
87
|
-
@progress_messages << [key, @color.call(*args)] unless found
|
88
|
-
display_progress
|
89
140
|
end
|
141
|
+
@progress_messages << [key, @color.call(*args)] unless found
|
142
|
+
|
143
|
+
refresh_display
|
90
144
|
end
|
91
145
|
|
92
146
|
def progress_done(key, display_last = true, message: nil)
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
if
|
97
|
-
|
98
|
-
message = msg
|
99
|
-
end
|
100
|
-
true
|
101
|
-
end
|
147
|
+
current_size = @progress_messages.size
|
148
|
+
@progress_messages.delete_if do |msg_key, msg|
|
149
|
+
if msg_key == key
|
150
|
+
message = msg if display_last && !message
|
151
|
+
true
|
102
152
|
end
|
103
|
-
current_size != @progress_messages.size
|
104
153
|
end
|
154
|
+
changed = current_size != @progress_messages.size
|
105
155
|
|
106
156
|
if changed
|
107
157
|
if message
|
108
158
|
message(" #{message}")
|
109
|
-
# Note: message
|
159
|
+
# Note: message updates the display already
|
110
160
|
else
|
111
|
-
|
112
|
-
display_progress
|
113
|
-
end
|
161
|
+
refresh_display
|
114
162
|
end
|
115
163
|
true
|
116
164
|
end
|
117
165
|
end
|
118
166
|
|
167
|
+
def refresh_display
|
168
|
+
return unless @display_lock.try_lock
|
169
|
+
|
170
|
+
begin
|
171
|
+
refresh_display_under_lock
|
172
|
+
ensure
|
173
|
+
@display_lock.unlock
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def refresh_display_under_lock
|
178
|
+
# Display queued messages
|
179
|
+
until @message_queue.empty?
|
180
|
+
message, args, io = @message_queue.pop
|
181
|
+
if @progress_mode == :single_line
|
182
|
+
io.print @cursor.clear_screen_down
|
183
|
+
end
|
184
|
+
io.puts @color.call(message, *args)
|
185
|
+
|
186
|
+
io.flush if @io != io
|
187
|
+
end
|
188
|
+
|
189
|
+
# And re-display the progress
|
190
|
+
display_progress(consider_period: @forced_progress_display.false?)
|
191
|
+
@forced_progress_display.make_false
|
192
|
+
@io.flush
|
193
|
+
end
|
119
194
|
|
120
|
-
def display_progress
|
195
|
+
def display_progress(consider_period: true)
|
121
196
|
return unless progress_enabled?
|
197
|
+
return if consider_period && (@next_progress_display > Time.now)
|
122
198
|
|
123
|
-
formatted = format_grouped_messages(
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
199
|
+
formatted = format_grouped_messages(
|
200
|
+
@progress_messages.map(&:last),
|
201
|
+
indent: " "
|
202
|
+
)
|
203
|
+
if @progress_mode == :newline
|
204
|
+
@io.print formatted.join("\n")
|
205
|
+
@io.print "\n"
|
128
206
|
else
|
207
|
+
@io.print @cursor.clear_screen_down
|
208
|
+
@io.print formatted.join("\n")
|
209
|
+
@io.print @cursor.up(formatted.size - 1) if formatted.size > 1
|
129
210
|
@io.print @cursor.column(0)
|
130
211
|
end
|
131
212
|
@io.flush
|
213
|
+
@next_progress_display = Time.now + @progress_period
|
132
214
|
end
|
133
215
|
|
134
216
|
def find_common_prefix(msg, other_msg)
|
135
|
-
msg = msg.split(
|
136
|
-
other_msg = other_msg.split(
|
217
|
+
msg = msg.split(' ')
|
218
|
+
other_msg = other_msg.split(' ')
|
137
219
|
msg.each_with_index do |token, idx|
|
138
220
|
if other_msg[idx] != token
|
139
221
|
prefix = msg[0..(idx - 1)].join(" ")
|
140
|
-
|
141
|
-
prefix << " "
|
142
|
-
end
|
222
|
+
prefix << ' ' unless prefix.empty?
|
143
223
|
return prefix
|
144
224
|
end
|
145
225
|
end
|
146
|
-
|
226
|
+
msg.join(' ')
|
147
227
|
end
|
148
228
|
|
149
229
|
def group_messages(messages)
|
@@ -152,19 +232,22 @@ def group_messages(messages)
|
|
152
232
|
groups = Array.new
|
153
233
|
groups << ["", (0...messages.size)]
|
154
234
|
messages.each_with_index do |msg, idx|
|
155
|
-
prefix
|
235
|
+
prefix = nil
|
236
|
+
grouping = false
|
156
237
|
messages[(idx + 1)..-1].each_with_index do |other_msg, other_idx|
|
157
238
|
other_idx += idx + 1
|
158
239
|
prefix ||= find_common_prefix(msg, other_msg)
|
159
|
-
break
|
240
|
+
break unless other_msg.start_with?(prefix)
|
160
241
|
|
161
242
|
if grouping
|
162
243
|
break if prefix != groups.last[0]
|
244
|
+
|
163
245
|
groups.last[1] << other_idx
|
164
246
|
else
|
165
247
|
current_prefix, current_group = groups.last
|
166
|
-
if prefix.size > current_prefix.size # create a new group
|
167
|
-
|
248
|
+
if prefix.size > current_prefix.size # create a new group
|
249
|
+
group_end_index = [idx - 1, current_group.last].min
|
250
|
+
groups.last[1] = (current_group.first..group_end_index)
|
168
251
|
groups << [prefix, [idx, other_idx]]
|
169
252
|
grouping = true
|
170
253
|
else break
|
@@ -179,13 +262,14 @@ def group_messages(messages)
|
|
179
262
|
groups.map do |prefix, indexes|
|
180
263
|
indexes = indexes.to_a
|
181
264
|
next if indexes.empty?
|
265
|
+
|
182
266
|
range = (prefix.size)..-1
|
183
267
|
[prefix, indexes.map { |i| messages[i][range] }]
|
184
268
|
end.compact
|
185
269
|
end
|
186
270
|
|
187
|
-
def format_grouped_messages(
|
188
|
-
groups = group_messages(
|
271
|
+
def format_grouped_messages(raw_messages, indent: " ", width: TTY::Screen.width)
|
272
|
+
groups = group_messages(raw_messages)
|
189
273
|
groups.each_with_object([]) do |(prefix, messages), lines|
|
190
274
|
if prefix.empty?
|
191
275
|
lines.concat(messages.map { |m| "#{indent}#{m.strip}" })
|
@@ -197,10 +281,11 @@ def format_grouped_messages(messages, indent: " ", width: TTY::Screen.width)
|
|
197
281
|
msg = messages.shift.strip
|
198
282
|
margin = messages.empty? ? 1 : 2
|
199
283
|
if lines.last.size + margin + msg.size > width
|
200
|
-
lines << ""
|
284
|
+
lines.last << ","
|
285
|
+
lines << +""
|
201
286
|
lines.last << indent << indent << msg
|
202
287
|
else
|
203
|
-
lines.last << " " << msg
|
288
|
+
lines.last << ", " << msg
|
204
289
|
end
|
205
290
|
end
|
206
291
|
lines.last << "," unless messages.empty?
|