autobuild 1.17.0 → 1.21.0
Sign up to get free protection for your applications and to get access to all the features.
- 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?
|