blob: a5a2ffdfdf2c76d86fb78d765d9977245da652ac [file] [log] [blame]
Tom Rini10e47792018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glassc05694f2013-04-03 11:07:16 +00002# Copyright (c) 2013 The Chromium OS Authors.
3#
4# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
5#
Simon Glassc05694f2013-04-03 11:07:16 +00006
7import collections
Simon Glassc05694f2013-04-03 11:07:16 +00008from datetime import datetime, timedelta
9import glob
10import os
11import re
12import Queue
13import shutil
Simon Glass205ac042016-09-18 16:48:37 -060014import signal
Simon Glassc05694f2013-04-03 11:07:16 +000015import string
16import sys
Simon Glassd26e1442016-09-18 16:48:35 -060017import threading
Simon Glassc05694f2013-04-03 11:07:16 +000018import time
19
Simon Glass4a1e88b2014-08-09 15:33:00 -060020import builderthread
Simon Glassc05694f2013-04-03 11:07:16 +000021import command
22import gitutil
23import terminal
Simon Glass4433aa92014-09-05 19:00:07 -060024from terminal import Print
Simon Glassc05694f2013-04-03 11:07:16 +000025import toolchain
26
27
28"""
29Theory of Operation
30
31Please see README for user documentation, and you should be familiar with
32that before trying to make sense of this.
33
34Buildman works by keeping the machine as busy as possible, building different
35commits for different boards on multiple CPUs at once.
36
37The source repo (self.git_dir) contains all the commits to be built. Each
38thread works on a single board at a time. It checks out the first commit,
39configures it for that board, then builds it. Then it checks out the next
40commit and builds it (typically without re-configuring). When it runs out
41of commits, it gets another job from the builder and starts again with that
42board.
43
44Clearly the builder threads could work either way - they could check out a
45commit and then built it for all boards. Using separate directories for each
46commit/board pair they could leave their build product around afterwards
47also.
48
49The intent behind building a single board for multiple commits, is to make
50use of incremental builds. Since each commit is built incrementally from
51the previous one, builds are faster. Reconfiguring for a different board
52removes all intermediate object files.
53
54Many threads can be working at once, but each has its own working directory.
55When a thread finishes a build, it puts the output files into a result
56directory.
57
58The base directory used by buildman is normally '../<branch>', i.e.
59a directory higher than the source repository and named after the branch
60being built.
61
62Within the base directory, we have one subdirectory for each commit. Within
63that is one subdirectory for each board. Within that is the build output for
64that commit/board combination.
65
66Buildman also create working directories for each thread, in a .bm-work/
67subdirectory in the base dir.
68
69As an example, say we are building branch 'us-net' for boards 'sandbox' and
70'seaboard', and say that us-net has two commits. We will have directories
71like this:
72
73us-net/ base directory
74 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
75 sandbox/
76 u-boot.bin
77 seaboard/
78 u-boot.bin
79 02_of_02_g4ed4ebc_net--Check-tftp-comp/
80 sandbox/
81 u-boot.bin
82 seaboard/
83 u-boot.bin
84 .bm-work/
85 00/ working directory for thread 0 (contains source checkout)
86 build/ build output
87 01/ working directory for thread 1
88 build/ build output
89 ...
90u-boot/ source directory
91 .git/ repository
92"""
93
94# Possible build outcomes
95OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
96
Simon Glassd214bef2017-04-12 18:23:26 -060097# Translate a commit subject into a valid filename (and handle unicode)
98trans_valid_chars = string.maketrans('/: ', '---')
99trans_valid_chars = trans_valid_chars.decode('latin-1')
Simon Glassc05694f2013-04-03 11:07:16 +0000100
Simon Glasscde5c302016-11-13 14:25:53 -0700101BASE_CONFIG_FILENAMES = [
102 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
103]
104
105EXTRA_CONFIG_FILENAMES = [
Simon Glassdb17fb82015-02-05 22:06:15 -0700106 '.config', '.config-spl', '.config-tpl',
107 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
108 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
Simon Glassdb17fb82015-02-05 22:06:15 -0700109]
110
Simon Glasscad8abf2015-08-25 21:52:14 -0600111class Config:
112 """Holds information about configuration settings for a board."""
Simon Glasscde5c302016-11-13 14:25:53 -0700113 def __init__(self, config_filename, target):
Simon Glasscad8abf2015-08-25 21:52:14 -0600114 self.target = target
115 self.config = {}
Simon Glasscde5c302016-11-13 14:25:53 -0700116 for fname in config_filename:
Simon Glasscad8abf2015-08-25 21:52:14 -0600117 self.config[fname] = {}
118
119 def Add(self, fname, key, value):
120 self.config[fname][key] = value
121
122 def __hash__(self):
123 val = 0
124 for fname in self.config:
125 for key, value in self.config[fname].iteritems():
126 print key, value
127 val = val ^ hash(key) & hash(value)
128 return val
Simon Glassc05694f2013-04-03 11:07:16 +0000129
Alex Kiernan4059e302018-05-31 04:48:34 +0000130class Environment:
131 """Holds information about environment variables for a board."""
132 def __init__(self, target):
133 self.target = target
134 self.environment = {}
135
136 def Add(self, key, value):
137 self.environment[key] = value
138
Simon Glassc05694f2013-04-03 11:07:16 +0000139class Builder:
140 """Class for building U-Boot for a particular commit.
141
142 Public members: (many should ->private)
Simon Glassc05694f2013-04-03 11:07:16 +0000143 already_done: Number of builds already completed
144 base_dir: Base directory to use for builder
145 checkout: True to check out source, False to skip that step.
146 This is used for testing.
147 col: terminal.Color() object
148 count: Number of commits to build
149 do_make: Method to call to invoke Make
150 fail: Number of builds that failed due to error
151 force_build: Force building even if a build already exists
152 force_config_on_failure: If a commit fails for a board, disable
153 incremental building for the next commit we build for that
154 board, so that we will see all warnings/errors again.
Simon Glass7041c392014-07-13 12:22:31 -0600155 force_build_failures: If a previously-built build (i.e. built on
156 a previous run of buildman) is marked as failed, rebuild it.
Simon Glassc05694f2013-04-03 11:07:16 +0000157 git_dir: Git directory containing source repository
158 last_line_len: Length of the last line we printed (used for erasing
159 it with new progress information)
160 num_jobs: Number of jobs to run at once (passed to make as -j)
161 num_threads: Number of builder threads to run
162 out_queue: Queue of results to process
163 re_make_err: Compiled regular expression for ignore_lines
164 queue: Queue of jobs to run
165 threads: List of active threads
166 toolchains: Toolchains object to use for building
167 upto: Current commit number we are building (0.count-1)
168 warned: Number of builds that produced at least one warning
Simon Glassf3018b7a2014-07-14 17:51:02 -0600169 force_reconfig: Reconfigure U-Boot on each comiit. This disables
170 incremental building, where buildman reconfigures on the first
171 commit for a baord, and then just does an incremental build for
172 the following commits. In fact buildman will reconfigure and
173 retry for any failing commits, so generally the only effect of
174 this option is to slow things down.
Simon Glass38df2e22014-07-14 17:51:03 -0600175 in_tree: Build U-Boot in-tree instead of specifying an output
176 directory separate from the source code. This option is really
177 only useful for testing in-tree builds.
Simon Glassc05694f2013-04-03 11:07:16 +0000178
179 Private members:
180 _base_board_dict: Last-summarised Dict of boards
181 _base_err_lines: Last-summarised list of errors
Simon Glass03749d42014-08-28 09:43:44 -0600182 _base_warn_lines: Last-summarised list of warnings
Simon Glassc05694f2013-04-03 11:07:16 +0000183 _build_period_us: Time taken for a single build (float object).
184 _complete_delay: Expected delay until completion (timedelta)
185 _next_delay_update: Next time we plan to display a progress update
186 (datatime)
187 _show_unknown: Show unknown boards (those not built) in summary
188 _timestamps: List of timestamps for the completion of the last
189 last _timestamp_count builds. Each is a datetime object.
190 _timestamp_count: Number of timestamps to keep in our list.
191 _working_dir: Base working directory containing all threads
192 """
193 class Outcome:
194 """Records a build outcome for a single make invocation
195
196 Public Members:
197 rc: Outcome value (OUTCOME_...)
198 err_lines: List of error lines or [] if none
199 sizes: Dictionary of image size information, keyed by filename
200 - Each value is itself a dictionary containing
201 values for 'text', 'data' and 'bss', being the integer
202 size in bytes of each section.
203 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
204 value is itself a dictionary:
205 key: function name
206 value: Size of function in bytes
Simon Glassdb17fb82015-02-05 22:06:15 -0700207 config: Dictionary keyed by filename - e.g. '.config'. Each
208 value is itself a dictionary:
209 key: config name
210 value: config value
Alex Kiernan4059e302018-05-31 04:48:34 +0000211 environment: Dictionary keyed by environment variable, Each
212 value is the value of environment variable.
Simon Glassc05694f2013-04-03 11:07:16 +0000213 """
Alex Kiernan4059e302018-05-31 04:48:34 +0000214 def __init__(self, rc, err_lines, sizes, func_sizes, config,
215 environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000216 self.rc = rc
217 self.err_lines = err_lines
218 self.sizes = sizes
219 self.func_sizes = func_sizes
Simon Glassdb17fb82015-02-05 22:06:15 -0700220 self.config = config
Alex Kiernan4059e302018-05-31 04:48:34 +0000221 self.environment = environment
Simon Glassc05694f2013-04-03 11:07:16 +0000222
223 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
Simon Glasse87bde12014-12-01 17:33:55 -0700224 gnu_make='make', checkout=True, show_unknown=True, step=1,
Stephen Warren97c96902016-04-11 10:48:44 -0600225 no_subdirs=False, full_path=False, verbose_build=False,
Simon Glass739e8512016-11-13 14:25:51 -0700226 incremental=False, per_board_out_dir=False,
Daniel Schwierzeck20e2ea92018-01-26 16:31:05 +0100227 config_only=False, squash_config_y=False,
228 warnings_as_errors=False):
Simon Glassc05694f2013-04-03 11:07:16 +0000229 """Create a new Builder object
230
231 Args:
232 toolchains: Toolchains object to use for building
233 base_dir: Base directory to use for builder
234 git_dir: Git directory containing source repository
235 num_threads: Number of builder threads to run
236 num_jobs: Number of jobs to run at once (passed to make as -j)
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900237 gnu_make: the command name of GNU Make.
Simon Glassc05694f2013-04-03 11:07:16 +0000238 checkout: True to check out source, False to skip that step.
239 This is used for testing.
240 show_unknown: Show unknown boards (those not built) in summary
241 step: 1 to process every commit, n to process every nth commit
Simon Glassd48a46c2014-12-01 17:34:00 -0700242 no_subdirs: Don't create subdirectories when building current
243 source for a single board
244 full_path: Return the full path in CROSS_COMPILE and don't set
245 PATH
Simon Glass655b6102014-12-01 17:34:07 -0700246 verbose_build: Run build with V=1 and don't use 'make -s'
Stephen Warren97c96902016-04-11 10:48:44 -0600247 incremental: Always perform incremental builds; don't run make
248 mrproper when configuring
249 per_board_out_dir: Build in a separate persistent directory per
250 board rather than a thread-specific directory
Simon Glass739e8512016-11-13 14:25:51 -0700251 config_only: Only configure each build, don't build it
Simon Glasscde5c302016-11-13 14:25:53 -0700252 squash_config_y: Convert CONFIG options with the value 'y' to '1'
Daniel Schwierzeck20e2ea92018-01-26 16:31:05 +0100253 warnings_as_errors: Treat all compiler warnings as errors
Simon Glassc05694f2013-04-03 11:07:16 +0000254 """
255 self.toolchains = toolchains
256 self.base_dir = base_dir
257 self._working_dir = os.path.join(base_dir, '.bm-work')
258 self.threads = []
Simon Glassc05694f2013-04-03 11:07:16 +0000259 self.do_make = self.Make
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900260 self.gnu_make = gnu_make
Simon Glassc05694f2013-04-03 11:07:16 +0000261 self.checkout = checkout
262 self.num_threads = num_threads
263 self.num_jobs = num_jobs
264 self.already_done = 0
265 self.force_build = False
266 self.git_dir = git_dir
267 self._show_unknown = show_unknown
268 self._timestamp_count = 10
269 self._build_period_us = None
270 self._complete_delay = None
271 self._next_delay_update = datetime.now()
272 self.force_config_on_failure = True
Simon Glass7041c392014-07-13 12:22:31 -0600273 self.force_build_failures = False
Simon Glassf3018b7a2014-07-14 17:51:02 -0600274 self.force_reconfig = False
Simon Glassc05694f2013-04-03 11:07:16 +0000275 self._step = step
Simon Glass38df2e22014-07-14 17:51:03 -0600276 self.in_tree = False
Simon Glassbb4dffb2014-08-09 15:33:06 -0600277 self._error_lines = 0
Simon Glasse87bde12014-12-01 17:33:55 -0700278 self.no_subdirs = no_subdirs
Simon Glassd48a46c2014-12-01 17:34:00 -0700279 self.full_path = full_path
Simon Glass655b6102014-12-01 17:34:07 -0700280 self.verbose_build = verbose_build
Simon Glass739e8512016-11-13 14:25:51 -0700281 self.config_only = config_only
Simon Glasscde5c302016-11-13 14:25:53 -0700282 self.squash_config_y = squash_config_y
283 self.config_filenames = BASE_CONFIG_FILENAMES
284 if not self.squash_config_y:
285 self.config_filenames += EXTRA_CONFIG_FILENAMES
Simon Glassc05694f2013-04-03 11:07:16 +0000286
Daniel Schwierzeck20e2ea92018-01-26 16:31:05 +0100287 self.warnings_as_errors = warnings_as_errors
Simon Glassc05694f2013-04-03 11:07:16 +0000288 self.col = terminal.Color()
289
Simon Glass03749d42014-08-28 09:43:44 -0600290 self._re_function = re.compile('(.*): In function.*')
291 self._re_files = re.compile('In file included from.*')
292 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
293 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
294
Simon Glassc05694f2013-04-03 11:07:16 +0000295 self.queue = Queue.Queue()
296 self.out_queue = Queue.Queue()
297 for i in range(self.num_threads):
Stephen Warren97c96902016-04-11 10:48:44 -0600298 t = builderthread.BuilderThread(self, i, incremental,
299 per_board_out_dir)
Simon Glassc05694f2013-04-03 11:07:16 +0000300 t.setDaemon(True)
301 t.start()
302 self.threads.append(t)
303
304 self.last_line_len = 0
Simon Glass4a1e88b2014-08-09 15:33:00 -0600305 t = builderthread.ResultThread(self)
Simon Glassc05694f2013-04-03 11:07:16 +0000306 t.setDaemon(True)
307 t.start()
308 self.threads.append(t)
309
310 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
311 self.re_make_err = re.compile('|'.join(ignore_lines))
312
Simon Glass205ac042016-09-18 16:48:37 -0600313 # Handle existing graceful with SIGINT / Ctrl-C
314 signal.signal(signal.SIGINT, self.signal_handler)
315
Simon Glassc05694f2013-04-03 11:07:16 +0000316 def __del__(self):
317 """Get rid of all threads created by the builder"""
318 for t in self.threads:
319 del t
320
Simon Glass205ac042016-09-18 16:48:37 -0600321 def signal_handler(self, signal, frame):
322 sys.exit(1)
323
Simon Glasseb48bbc2014-08-09 15:33:02 -0600324 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
Simon Glass3394c9f2014-08-28 09:43:43 -0600325 show_detail=False, show_bloat=False,
Alex Kiernan4059e302018-05-31 04:48:34 +0000326 list_error_boards=False, show_config=False,
327 show_environment=False):
Simon Glasseb48bbc2014-08-09 15:33:02 -0600328 """Setup display options for the builder.
329
330 show_errors: True to show summarised error/warning info
331 show_sizes: Show size deltas
332 show_detail: Show detail for each board
333 show_bloat: Show detail for each function
Simon Glass3394c9f2014-08-28 09:43:43 -0600334 list_error_boards: Show the boards which caused each error/warning
Simon Glassdb17fb82015-02-05 22:06:15 -0700335 show_config: Show config deltas
Alex Kiernan4059e302018-05-31 04:48:34 +0000336 show_environment: Show environment deltas
Simon Glasseb48bbc2014-08-09 15:33:02 -0600337 """
338 self._show_errors = show_errors
339 self._show_sizes = show_sizes
340 self._show_detail = show_detail
341 self._show_bloat = show_bloat
Simon Glass3394c9f2014-08-28 09:43:43 -0600342 self._list_error_boards = list_error_boards
Simon Glassdb17fb82015-02-05 22:06:15 -0700343 self._show_config = show_config
Alex Kiernan4059e302018-05-31 04:48:34 +0000344 self._show_environment = show_environment
Simon Glasseb48bbc2014-08-09 15:33:02 -0600345
Simon Glassc05694f2013-04-03 11:07:16 +0000346 def _AddTimestamp(self):
347 """Add a new timestamp to the list and record the build period.
348
349 The build period is the length of time taken to perform a single
350 build (one board, one commit).
351 """
352 now = datetime.now()
353 self._timestamps.append(now)
354 count = len(self._timestamps)
355 delta = self._timestamps[-1] - self._timestamps[0]
356 seconds = delta.total_seconds()
357
358 # If we have enough data, estimate build period (time taken for a
359 # single build) and therefore completion time.
360 if count > 1 and self._next_delay_update < now:
361 self._next_delay_update = now + timedelta(seconds=2)
362 if seconds > 0:
363 self._build_period = float(seconds) / count
364 todo = self.count - self.upto
365 self._complete_delay = timedelta(microseconds=
366 self._build_period * todo * 1000000)
367 # Round it
368 self._complete_delay -= timedelta(
369 microseconds=self._complete_delay.microseconds)
370
371 if seconds > 60:
372 self._timestamps.popleft()
373 count -= 1
374
375 def ClearLine(self, length):
376 """Clear any characters on the current line
377
378 Make way for a new line of length 'length', by outputting enough
379 spaces to clear out the old line. Then remember the new length for
380 next time.
381
382 Args:
383 length: Length of new line, in characters
384 """
385 if length < self.last_line_len:
Simon Glass4433aa92014-09-05 19:00:07 -0600386 Print(' ' * (self.last_line_len - length), newline=False)
387 Print('\r', newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +0000388 self.last_line_len = length
389 sys.stdout.flush()
390
391 def SelectCommit(self, commit, checkout=True):
392 """Checkout the selected commit for this build
393 """
394 self.commit = commit
395 if checkout and self.checkout:
396 gitutil.Checkout(commit.hash)
397
398 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
399 """Run make
400
401 Args:
402 commit: Commit object that is being built
403 brd: Board object that is being built
Roger Meiere0a0e552014-08-20 22:10:29 +0200404 stage: Stage that we are at (mrproper, config, build)
Simon Glassc05694f2013-04-03 11:07:16 +0000405 cwd: Directory where make should be run
406 args: Arguments to pass to make
407 kwargs: Arguments to pass to command.RunPipe()
408 """
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900409 cmd = [self.gnu_make] + list(args)
Simon Glassc05694f2013-04-03 11:07:16 +0000410 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
411 cwd=cwd, raise_on_error=False, **kwargs)
Simon Glass413f91a2015-02-05 22:06:12 -0700412 if self.verbose_build:
413 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
414 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
Simon Glassc05694f2013-04-03 11:07:16 +0000415 return result
416
417 def ProcessResult(self, result):
418 """Process the result of a build, showing progress information
419
420 Args:
Simon Glass78e418e2014-08-09 15:33:03 -0600421 result: A CommandResult object, which indicates the result for
422 a single build
Simon Glassc05694f2013-04-03 11:07:16 +0000423 """
424 col = terminal.Color()
425 if result:
426 target = result.brd.target
427
Simon Glassc05694f2013-04-03 11:07:16 +0000428 self.upto += 1
429 if result.return_code != 0:
430 self.fail += 1
431 elif result.stderr:
432 self.warned += 1
433 if result.already_done:
434 self.already_done += 1
Simon Glass78e418e2014-08-09 15:33:03 -0600435 if self._verbose:
Simon Glass4433aa92014-09-05 19:00:07 -0600436 Print('\r', newline=False)
Simon Glass78e418e2014-08-09 15:33:03 -0600437 self.ClearLine(0)
438 boards_selected = {target : result.brd}
439 self.ResetResultSummary(boards_selected)
440 self.ProduceResultSummary(result.commit_upto, self.commits,
441 boards_selected)
Simon Glassc05694f2013-04-03 11:07:16 +0000442 else:
443 target = '(starting)'
444
445 # Display separate counts for ok, warned and fail
446 ok = self.upto - self.warned - self.fail
447 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
448 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
449 line += self.col.Color(self.col.RED, '%5d' % self.fail)
450
451 name = ' /%-5d ' % self.count
452
453 # Add our current completion time estimate
454 self._AddTimestamp()
455 if self._complete_delay:
456 name += '%s : ' % self._complete_delay
457 # When building all boards for a commit, we can print a commit
458 # progress message.
459 if result and result.commit_upto is None:
460 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
461 self.commit_count)
462
463 name += target
Simon Glass4433aa92014-09-05 19:00:07 -0600464 Print(line + name, newline=False)
Simon Glassb45bd6b2016-11-15 15:32:59 -0700465 length = 16 + len(name)
Simon Glassc05694f2013-04-03 11:07:16 +0000466 self.ClearLine(length)
467
468 def _GetOutputDir(self, commit_upto):
469 """Get the name of the output directory for a commit number
470
471 The output directory is typically .../<branch>/<commit>.
472
473 Args:
474 commit_upto: Commit number to use (0..self.count-1)
475 """
Simon Glasse87bde12014-12-01 17:33:55 -0700476 commit_dir = None
Simon Glassd326ad72014-08-09 15:32:59 -0600477 if self.commits:
478 commit = self.commits[commit_upto]
479 subject = commit.subject.translate(trans_valid_chars)
480 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
481 self.commit_count, commit.hash, subject[:20]))
Simon Glasse87bde12014-12-01 17:33:55 -0700482 elif not self.no_subdirs:
Simon Glassd326ad72014-08-09 15:32:59 -0600483 commit_dir = 'current'
Simon Glasse87bde12014-12-01 17:33:55 -0700484 if not commit_dir:
485 return self.base_dir
486 return os.path.join(self.base_dir, commit_dir)
Simon Glassc05694f2013-04-03 11:07:16 +0000487
488 def GetBuildDir(self, commit_upto, target):
489 """Get the name of the build directory for a commit number
490
491 The build directory is typically .../<branch>/<commit>/<target>.
492
493 Args:
494 commit_upto: Commit number to use (0..self.count-1)
495 target: Target name
496 """
497 output_dir = self._GetOutputDir(commit_upto)
498 return os.path.join(output_dir, target)
499
500 def GetDoneFile(self, commit_upto, target):
501 """Get the name of the done file for a commit number
502
503 Args:
504 commit_upto: Commit number to use (0..self.count-1)
505 target: Target name
506 """
507 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
508
509 def GetSizesFile(self, commit_upto, target):
510 """Get the name of the sizes file for a commit number
511
512 Args:
513 commit_upto: Commit number to use (0..self.count-1)
514 target: Target name
515 """
516 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
517
518 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
519 """Get the name of the funcsizes file for a commit number and ELF file
520
521 Args:
522 commit_upto: Commit number to use (0..self.count-1)
523 target: Target name
524 elf_fname: Filename of elf image
525 """
526 return os.path.join(self.GetBuildDir(commit_upto, target),
527 '%s.sizes' % elf_fname.replace('/', '-'))
528
529 def GetObjdumpFile(self, commit_upto, target, elf_fname):
530 """Get the name of the objdump file for a commit number and ELF file
531
532 Args:
533 commit_upto: Commit number to use (0..self.count-1)
534 target: Target name
535 elf_fname: Filename of elf image
536 """
537 return os.path.join(self.GetBuildDir(commit_upto, target),
538 '%s.objdump' % elf_fname.replace('/', '-'))
539
540 def GetErrFile(self, commit_upto, target):
541 """Get the name of the err file for a commit number
542
543 Args:
544 commit_upto: Commit number to use (0..self.count-1)
545 target: Target name
546 """
547 output_dir = self.GetBuildDir(commit_upto, target)
548 return os.path.join(output_dir, 'err')
549
550 def FilterErrors(self, lines):
551 """Filter out errors in which we have no interest
552
553 We should probably use map().
554
555 Args:
556 lines: List of error lines, each a string
557 Returns:
558 New list with only interesting lines included
559 """
560 out_lines = []
561 for line in lines:
562 if not self.re_make_err.search(line):
563 out_lines.append(line)
564 return out_lines
565
566 def ReadFuncSizes(self, fname, fd):
567 """Read function sizes from the output of 'nm'
568
569 Args:
570 fd: File containing data to read
571 fname: Filename we are reading from (just for errors)
572
573 Returns:
574 Dictionary containing size of each function in bytes, indexed by
575 function name.
576 """
577 sym = {}
578 for line in fd.readlines():
579 try:
580 size, type, name = line[:-1].split()
581 except:
Simon Glass4433aa92014-09-05 19:00:07 -0600582 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
Simon Glassc05694f2013-04-03 11:07:16 +0000583 continue
584 if type in 'tTdDbB':
585 # function names begin with '.' on 64-bit powerpc
586 if '.' in name[1:]:
587 name = 'static.' + name.split('.')[0]
588 sym[name] = sym.get(name, 0) + int(size, 16)
589 return sym
590
Simon Glassdb17fb82015-02-05 22:06:15 -0700591 def _ProcessConfig(self, fname):
592 """Read in a .config, autoconf.mk or autoconf.h file
593
594 This function handles all config file types. It ignores comments and
595 any #defines which don't start with CONFIG_.
596
597 Args:
598 fname: Filename to read
599
600 Returns:
601 Dictionary:
602 key: Config name (e.g. CONFIG_DM)
603 value: Config value (e.g. 1)
604 """
605 config = {}
606 if os.path.exists(fname):
607 with open(fname) as fd:
608 for line in fd:
609 line = line.strip()
610 if line.startswith('#define'):
611 values = line[8:].split(' ', 1)
612 if len(values) > 1:
613 key, value = values
614 else:
615 key = values[0]
Simon Glasscde5c302016-11-13 14:25:53 -0700616 value = '1' if self.squash_config_y else ''
Simon Glassdb17fb82015-02-05 22:06:15 -0700617 if not key.startswith('CONFIG_'):
618 continue
619 elif not line or line[0] in ['#', '*', '/']:
620 continue
621 else:
622 key, value = line.split('=', 1)
Simon Glasscde5c302016-11-13 14:25:53 -0700623 if self.squash_config_y and value == 'y':
624 value = '1'
Simon Glassdb17fb82015-02-05 22:06:15 -0700625 config[key] = value
626 return config
627
Alex Kiernan4059e302018-05-31 04:48:34 +0000628 def _ProcessEnvironment(self, fname):
629 """Read in a uboot.env file
630
631 This function reads in environment variables from a file.
632
633 Args:
634 fname: Filename to read
635
636 Returns:
637 Dictionary:
638 key: environment variable (e.g. bootlimit)
639 value: value of environment variable (e.g. 1)
640 """
641 environment = {}
642 if os.path.exists(fname):
643 with open(fname) as fd:
644 for line in fd.read().split('\0'):
645 try:
646 key, value = line.split('=', 1)
647 environment[key] = value
648 except ValueError:
649 # ignore lines we can't parse
650 pass
651 return environment
652
Simon Glassdb17fb82015-02-05 22:06:15 -0700653 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
Alex Kiernan4059e302018-05-31 04:48:34 +0000654 read_config, read_environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000655 """Work out the outcome of a build.
656
657 Args:
658 commit_upto: Commit number to check (0..n-1)
659 target: Target board to check
660 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700661 read_config: True to read .config and autoconf.h files
Alex Kiernan4059e302018-05-31 04:48:34 +0000662 read_environment: True to read uboot.env files
Simon Glassc05694f2013-04-03 11:07:16 +0000663
664 Returns:
665 Outcome object
666 """
667 done_file = self.GetDoneFile(commit_upto, target)
668 sizes_file = self.GetSizesFile(commit_upto, target)
669 sizes = {}
670 func_sizes = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700671 config = {}
Alex Kiernan4059e302018-05-31 04:48:34 +0000672 environment = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000673 if os.path.exists(done_file):
674 with open(done_file, 'r') as fd:
675 return_code = int(fd.readline())
676 err_lines = []
677 err_file = self.GetErrFile(commit_upto, target)
678 if os.path.exists(err_file):
679 with open(err_file, 'r') as fd:
680 err_lines = self.FilterErrors(fd.readlines())
681
682 # Decide whether the build was ok, failed or created warnings
683 if return_code:
684 rc = OUTCOME_ERROR
685 elif len(err_lines):
686 rc = OUTCOME_WARNING
687 else:
688 rc = OUTCOME_OK
689
690 # Convert size information to our simple format
691 if os.path.exists(sizes_file):
692 with open(sizes_file, 'r') as fd:
693 for line in fd.readlines():
694 values = line.split()
695 rodata = 0
696 if len(values) > 6:
697 rodata = int(values[6], 16)
698 size_dict = {
699 'all' : int(values[0]) + int(values[1]) +
700 int(values[2]),
701 'text' : int(values[0]) - rodata,
702 'data' : int(values[1]),
703 'bss' : int(values[2]),
704 'rodata' : rodata,
705 }
706 sizes[values[5]] = size_dict
707
708 if read_func_sizes:
709 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
710 for fname in glob.glob(pattern):
711 with open(fname, 'r') as fd:
712 dict_name = os.path.basename(fname).replace('.sizes',
713 '')
714 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
715
Simon Glassdb17fb82015-02-05 22:06:15 -0700716 if read_config:
717 output_dir = self.GetBuildDir(commit_upto, target)
Simon Glasscde5c302016-11-13 14:25:53 -0700718 for name in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700719 fname = os.path.join(output_dir, name)
720 config[name] = self._ProcessConfig(fname)
721
Alex Kiernan4059e302018-05-31 04:48:34 +0000722 if read_environment:
723 output_dir = self.GetBuildDir(commit_upto, target)
724 fname = os.path.join(output_dir, 'uboot.env')
725 environment = self._ProcessEnvironment(fname)
726
727 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
728 environment)
Simon Glassc05694f2013-04-03 11:07:16 +0000729
Alex Kiernan4059e302018-05-31 04:48:34 +0000730 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
Simon Glassc05694f2013-04-03 11:07:16 +0000731
Simon Glassdb17fb82015-02-05 22:06:15 -0700732 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
Alex Kiernan4059e302018-05-31 04:48:34 +0000733 read_config, read_environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000734 """Calculate a summary of the results of building a commit.
735
736 Args:
737 board_selected: Dict containing boards to summarise
738 commit_upto: Commit number to summarize (0..self.count-1)
739 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700740 read_config: True to read .config and autoconf.h files
Alex Kiernan4059e302018-05-31 04:48:34 +0000741 read_environment: True to read uboot.env files
Simon Glassc05694f2013-04-03 11:07:16 +0000742
743 Returns:
744 Tuple:
745 Dict containing boards which passed building this commit.
746 keyed by board.target
Simon Glass03749d42014-08-28 09:43:44 -0600747 List containing a summary of error lines
Simon Glass3394c9f2014-08-28 09:43:43 -0600748 Dict keyed by error line, containing a list of the Board
749 objects with that error
Simon Glass03749d42014-08-28 09:43:44 -0600750 List containing a summary of warning lines
751 Dict keyed by error line, containing a list of the Board
752 objects with that warning
Simon Glasscad8abf2015-08-25 21:52:14 -0600753 Dictionary keyed by board.target. Each value is a dictionary:
754 key: filename - e.g. '.config'
Simon Glassdb17fb82015-02-05 22:06:15 -0700755 value is itself a dictionary:
756 key: config name
757 value: config value
Alex Kiernan4059e302018-05-31 04:48:34 +0000758 Dictionary keyed by board.target. Each value is a dictionary:
759 key: environment variable
760 value: value of environment variable
Simon Glassc05694f2013-04-03 11:07:16 +0000761 """
Simon Glass03749d42014-08-28 09:43:44 -0600762 def AddLine(lines_summary, lines_boards, line, board):
763 line = line.rstrip()
764 if line in lines_boards:
765 lines_boards[line].append(board)
766 else:
767 lines_boards[line] = [board]
768 lines_summary.append(line)
769
Simon Glassc05694f2013-04-03 11:07:16 +0000770 board_dict = {}
771 err_lines_summary = []
Simon Glass3394c9f2014-08-28 09:43:43 -0600772 err_lines_boards = {}
Simon Glass03749d42014-08-28 09:43:44 -0600773 warn_lines_summary = []
774 warn_lines_boards = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700775 config = {}
Alex Kiernan4059e302018-05-31 04:48:34 +0000776 environment = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000777
778 for board in boards_selected.itervalues():
779 outcome = self.GetBuildOutcome(commit_upto, board.target,
Alex Kiernan4059e302018-05-31 04:48:34 +0000780 read_func_sizes, read_config,
781 read_environment)
Simon Glassc05694f2013-04-03 11:07:16 +0000782 board_dict[board.target] = outcome
Simon Glass03749d42014-08-28 09:43:44 -0600783 last_func = None
784 last_was_warning = False
785 for line in outcome.err_lines:
786 if line:
787 if (self._re_function.match(line) or
788 self._re_files.match(line)):
789 last_func = line
Simon Glass3394c9f2014-08-28 09:43:43 -0600790 else:
Simon Glass03749d42014-08-28 09:43:44 -0600791 is_warning = self._re_warning.match(line)
792 is_note = self._re_note.match(line)
793 if is_warning or (last_was_warning and is_note):
794 if last_func:
795 AddLine(warn_lines_summary, warn_lines_boards,
796 last_func, board)
797 AddLine(warn_lines_summary, warn_lines_boards,
798 line, board)
799 else:
800 if last_func:
801 AddLine(err_lines_summary, err_lines_boards,
802 last_func, board)
803 AddLine(err_lines_summary, err_lines_boards,
804 line, board)
805 last_was_warning = is_warning
806 last_func = None
Simon Glasscde5c302016-11-13 14:25:53 -0700807 tconfig = Config(self.config_filenames, board.target)
808 for fname in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700809 if outcome.config:
810 for key, value in outcome.config[fname].iteritems():
Simon Glasscad8abf2015-08-25 21:52:14 -0600811 tconfig.Add(fname, key, value)
812 config[board.target] = tconfig
Simon Glassdb17fb82015-02-05 22:06:15 -0700813
Alex Kiernan4059e302018-05-31 04:48:34 +0000814 tenvironment = Environment(board.target)
815 if outcome.environment:
816 for key, value in outcome.environment.iteritems():
817 tenvironment.Add(key, value)
818 environment[board.target] = tenvironment
819
Simon Glass03749d42014-08-28 09:43:44 -0600820 return (board_dict, err_lines_summary, err_lines_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +0000821 warn_lines_summary, warn_lines_boards, config, environment)
Simon Glassc05694f2013-04-03 11:07:16 +0000822
823 def AddOutcome(self, board_dict, arch_list, changes, char, color):
824 """Add an output to our list of outcomes for each architecture
825
826 This simple function adds failing boards (changes) to the
827 relevant architecture string, so we can print the results out
828 sorted by architecture.
829
830 Args:
831 board_dict: Dict containing all boards
832 arch_list: Dict keyed by arch name. Value is a string containing
833 a list of board names which failed for that arch.
834 changes: List of boards to add to arch_list
835 color: terminal.Colour object
836 """
837 done_arch = {}
838 for target in changes:
839 if target in board_dict:
840 arch = board_dict[target].arch
841 else:
842 arch = 'unknown'
843 str = self.col.Color(color, ' ' + target)
844 if not arch in done_arch:
Simon Glass26f72372015-02-05 22:06:11 -0700845 str = ' %s %s' % (self.col.Color(color, char), str)
Simon Glassc05694f2013-04-03 11:07:16 +0000846 done_arch[arch] = True
847 if not arch in arch_list:
848 arch_list[arch] = str
849 else:
850 arch_list[arch] += str
851
852
853 def ColourNum(self, num):
854 color = self.col.RED if num > 0 else self.col.GREEN
855 if num == 0:
856 return '0'
857 return self.col.Color(color, str(num))
858
859 def ResetResultSummary(self, board_selected):
860 """Reset the results summary ready for use.
861
862 Set up the base board list to be all those selected, and set the
863 error lines to empty.
864
865 Following this, calls to PrintResultSummary() will use this
866 information to work out what has changed.
867
868 Args:
869 board_selected: Dict containing boards to summarise, keyed by
870 board.target
871 """
872 self._base_board_dict = {}
873 for board in board_selected:
Alex Kiernan4059e302018-05-31 04:48:34 +0000874 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
875 {})
Simon Glassc05694f2013-04-03 11:07:16 +0000876 self._base_err_lines = []
Simon Glass03749d42014-08-28 09:43:44 -0600877 self._base_warn_lines = []
878 self._base_err_line_boards = {}
879 self._base_warn_line_boards = {}
Simon Glasscad8abf2015-08-25 21:52:14 -0600880 self._base_config = None
Alex Kiernan4059e302018-05-31 04:48:34 +0000881 self._base_environment = None
Simon Glassc05694f2013-04-03 11:07:16 +0000882
883 def PrintFuncSizeDetail(self, fname, old, new):
884 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
885 delta, common = [], {}
886
887 for a in old:
888 if a in new:
889 common[a] = 1
890
891 for name in old:
892 if name not in common:
893 remove += 1
894 down += old[name]
895 delta.append([-old[name], name])
896
897 for name in new:
898 if name not in common:
899 add += 1
900 up += new[name]
901 delta.append([new[name], name])
902
903 for name in common:
904 diff = new.get(name, 0) - old.get(name, 0)
905 if diff > 0:
906 grow, up = grow + 1, up + diff
907 elif diff < 0:
908 shrink, down = shrink + 1, down - diff
909 delta.append([diff, name])
910
911 delta.sort()
912 delta.reverse()
913
914 args = [add, -remove, grow, -shrink, up, -down, up - down]
Tom Rini0b48cd62017-05-22 13:48:52 -0400915 if max(args) == 0 and min(args) == 0:
Simon Glassc05694f2013-04-03 11:07:16 +0000916 return
917 args = [self.ColourNum(x) for x in args]
918 indent = ' ' * 15
Simon Glass4433aa92014-09-05 19:00:07 -0600919 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
920 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
921 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
922 'delta'))
Simon Glassc05694f2013-04-03 11:07:16 +0000923 for diff, name in delta:
924 if diff:
925 color = self.col.RED if diff > 0 else self.col.GREEN
926 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
927 old.get(name, '-'), new.get(name,'-'), diff)
Simon Glass4433aa92014-09-05 19:00:07 -0600928 Print(msg, colour=color)
Simon Glassc05694f2013-04-03 11:07:16 +0000929
930
931 def PrintSizeDetail(self, target_list, show_bloat):
932 """Show details size information for each board
933
934 Args:
935 target_list: List of targets, each a dict containing:
936 'target': Target name
937 'total_diff': Total difference in bytes across all areas
938 <part_name>: Difference for that part
939 show_bloat: Show detail for each function
940 """
941 targets_by_diff = sorted(target_list, reverse=True,
942 key=lambda x: x['_total_diff'])
943 for result in targets_by_diff:
944 printed_target = False
945 for name in sorted(result):
946 diff = result[name]
947 if name.startswith('_'):
948 continue
949 if diff != 0:
950 color = self.col.RED if diff > 0 else self.col.GREEN
951 msg = ' %s %+d' % (name, diff)
952 if not printed_target:
Simon Glass4433aa92014-09-05 19:00:07 -0600953 Print('%10s %-15s:' % ('', result['_target']),
954 newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +0000955 printed_target = True
Simon Glass4433aa92014-09-05 19:00:07 -0600956 Print(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +0000957 if printed_target:
Simon Glass4433aa92014-09-05 19:00:07 -0600958 Print()
Simon Glassc05694f2013-04-03 11:07:16 +0000959 if show_bloat:
960 target = result['_target']
961 outcome = result['_outcome']
962 base_outcome = self._base_board_dict[target]
963 for fname in outcome.func_sizes:
964 self.PrintFuncSizeDetail(fname,
965 base_outcome.func_sizes[fname],
966 outcome.func_sizes[fname])
967
968
969 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
970 show_bloat):
971 """Print a summary of image sizes broken down by section.
972
973 The summary takes the form of one line per architecture. The
974 line contains deltas for each of the sections (+ means the section
975 got bigger, - means smaller). The nunmbers are the average number
976 of bytes that a board in this section increased by.
977
978 For example:
979 powerpc: (622 boards) text -0.0
980 arm: (285 boards) text -0.0
981 nds32: (3 boards) text -8.0
982
983 Args:
984 board_selected: Dict containing boards to summarise, keyed by
985 board.target
986 board_dict: Dict containing boards for which we built this
987 commit, keyed by board.target. The value is an Outcome object.
988 show_detail: Show detail for each board
989 show_bloat: Show detail for each function
990 """
991 arch_list = {}
992 arch_count = {}
993
994 # Calculate changes in size for different image parts
995 # The previous sizes are in Board.sizes, for each board
996 for target in board_dict:
997 if target not in board_selected:
998 continue
999 base_sizes = self._base_board_dict[target].sizes
1000 outcome = board_dict[target]
1001 sizes = outcome.sizes
1002
1003 # Loop through the list of images, creating a dict of size
1004 # changes for each image/part. We end up with something like
1005 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1006 # which means that U-Boot data increased by 5 bytes and SPL
1007 # text decreased by 4.
1008 err = {'_target' : target}
1009 for image in sizes:
1010 if image in base_sizes:
1011 base_image = base_sizes[image]
1012 # Loop through the text, data, bss parts
1013 for part in sorted(sizes[image]):
1014 diff = sizes[image][part] - base_image[part]
1015 col = None
1016 if diff:
1017 if image == 'u-boot':
1018 name = part
1019 else:
1020 name = image + ':' + part
1021 err[name] = diff
1022 arch = board_selected[target].arch
1023 if not arch in arch_count:
1024 arch_count[arch] = 1
1025 else:
1026 arch_count[arch] += 1
1027 if not sizes:
1028 pass # Only add to our list when we have some stats
1029 elif not arch in arch_list:
1030 arch_list[arch] = [err]
1031 else:
1032 arch_list[arch].append(err)
1033
1034 # We now have a list of image size changes sorted by arch
1035 # Print out a summary of these
1036 for arch, target_list in arch_list.iteritems():
1037 # Get total difference for each type
1038 totals = {}
1039 for result in target_list:
1040 total = 0
1041 for name, diff in result.iteritems():
1042 if name.startswith('_'):
1043 continue
1044 total += diff
1045 if name in totals:
1046 totals[name] += diff
1047 else:
1048 totals[name] = diff
1049 result['_total_diff'] = total
1050 result['_outcome'] = board_dict[result['_target']]
1051
1052 count = len(target_list)
1053 printed_arch = False
1054 for name in sorted(totals):
1055 diff = totals[name]
1056 if diff:
1057 # Display the average difference in this name for this
1058 # architecture
1059 avg_diff = float(diff) / count
1060 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1061 msg = ' %s %+1.1f' % (name, avg_diff)
1062 if not printed_arch:
Simon Glass4433aa92014-09-05 19:00:07 -06001063 Print('%10s: (for %d/%d boards)' % (arch, count,
1064 arch_count[arch]), newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001065 printed_arch = True
Simon Glass4433aa92014-09-05 19:00:07 -06001066 Print(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001067
1068 if printed_arch:
Simon Glass4433aa92014-09-05 19:00:07 -06001069 Print()
Simon Glassc05694f2013-04-03 11:07:16 +00001070 if show_detail:
1071 self.PrintSizeDetail(target_list, show_bloat)
1072
1073
1074 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glass03749d42014-08-28 09:43:44 -06001075 err_line_boards, warn_lines, warn_line_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +00001076 config, environment, show_sizes, show_detail,
1077 show_bloat, show_config, show_environment):
Simon Glassc05694f2013-04-03 11:07:16 +00001078 """Compare results with the base results and display delta.
1079
1080 Only boards mentioned in board_selected will be considered. This
1081 function is intended to be called repeatedly with the results of
1082 each commit. It therefore shows a 'diff' between what it saw in
1083 the last call and what it sees now.
1084
1085 Args:
1086 board_selected: Dict containing boards to summarise, keyed by
1087 board.target
1088 board_dict: Dict containing boards for which we built this
1089 commit, keyed by board.target. The value is an Outcome object.
1090 err_lines: A list of errors for this commit, or [] if there is
1091 none, or we don't want to print errors
Simon Glass3394c9f2014-08-28 09:43:43 -06001092 err_line_boards: Dict keyed by error line, containing a list of
1093 the Board objects with that error
Simon Glass03749d42014-08-28 09:43:44 -06001094 warn_lines: A list of warnings for this commit, or [] if there is
1095 none, or we don't want to print errors
1096 warn_line_boards: Dict keyed by warning line, containing a list of
1097 the Board objects with that warning
Simon Glassdb17fb82015-02-05 22:06:15 -07001098 config: Dictionary keyed by filename - e.g. '.config'. Each
1099 value is itself a dictionary:
1100 key: config name
1101 value: config value
Alex Kiernan4059e302018-05-31 04:48:34 +00001102 environment: Dictionary keyed by environment variable, Each
1103 value is the value of environment variable.
Simon Glassc05694f2013-04-03 11:07:16 +00001104 show_sizes: Show image size deltas
1105 show_detail: Show detail for each board
1106 show_bloat: Show detail for each function
Simon Glassdb17fb82015-02-05 22:06:15 -07001107 show_config: Show config changes
Alex Kiernan4059e302018-05-31 04:48:34 +00001108 show_environment: Show environment changes
Simon Glassc05694f2013-04-03 11:07:16 +00001109 """
Simon Glass03749d42014-08-28 09:43:44 -06001110 def _BoardList(line, line_boards):
Simon Glass3394c9f2014-08-28 09:43:43 -06001111 """Helper function to get a line of boards containing a line
1112
1113 Args:
1114 line: Error line to search for
1115 Return:
1116 String containing a list of boards with that error line, or
1117 '' if the user has not requested such a list
1118 """
1119 if self._list_error_boards:
1120 names = []
Simon Glass03749d42014-08-28 09:43:44 -06001121 for board in line_boards[line]:
Simon Glass85620bb2014-10-16 01:05:55 -06001122 if not board.target in names:
1123 names.append(board.target)
Simon Glass3394c9f2014-08-28 09:43:43 -06001124 names_str = '(%s) ' % ','.join(names)
1125 else:
1126 names_str = ''
1127 return names_str
1128
Simon Glass03749d42014-08-28 09:43:44 -06001129 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1130 char):
1131 better_lines = []
1132 worse_lines = []
1133 for line in lines:
1134 if line not in base_lines:
1135 worse_lines.append(char + '+' +
1136 _BoardList(line, line_boards) + line)
1137 for line in base_lines:
1138 if line not in lines:
1139 better_lines.append(char + '-' +
1140 _BoardList(line, base_line_boards) + line)
1141 return better_lines, worse_lines
1142
Simon Glassdb17fb82015-02-05 22:06:15 -07001143 def _CalcConfig(delta, name, config):
1144 """Calculate configuration changes
1145
1146 Args:
1147 delta: Type of the delta, e.g. '+'
1148 name: name of the file which changed (e.g. .config)
1149 config: configuration change dictionary
1150 key: config name
1151 value: config value
1152 Returns:
1153 String containing the configuration changes which can be
1154 printed
1155 """
1156 out = ''
1157 for key in sorted(config.keys()):
1158 out += '%s=%s ' % (key, config[key])
Simon Glasscad8abf2015-08-25 21:52:14 -06001159 return '%s %s: %s' % (delta, name, out)
Simon Glassdb17fb82015-02-05 22:06:15 -07001160
Simon Glasscad8abf2015-08-25 21:52:14 -06001161 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1162 """Add changes in configuration to a list
Simon Glassdb17fb82015-02-05 22:06:15 -07001163
1164 Args:
Simon Glasscad8abf2015-08-25 21:52:14 -06001165 lines: list to add to
1166 name: config file name
Simon Glassdb17fb82015-02-05 22:06:15 -07001167 config_plus: configurations added, dictionary
1168 key: config name
1169 value: config value
1170 config_minus: configurations removed, dictionary
1171 key: config name
1172 value: config value
1173 config_change: configurations changed, dictionary
1174 key: config name
1175 value: config value
1176 """
1177 if config_plus:
Simon Glasscad8abf2015-08-25 21:52:14 -06001178 lines.append(_CalcConfig('+', name, config_plus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001179 if config_minus:
Simon Glasscad8abf2015-08-25 21:52:14 -06001180 lines.append(_CalcConfig('-', name, config_minus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001181 if config_change:
Simon Glasscad8abf2015-08-25 21:52:14 -06001182 lines.append(_CalcConfig('c', name, config_change))
1183
1184 def _OutputConfigInfo(lines):
1185 for line in lines:
1186 if not line:
1187 continue
1188 if line[0] == '+':
1189 col = self.col.GREEN
1190 elif line[0] == '-':
1191 col = self.col.RED
1192 elif line[0] == 'c':
1193 col = self.col.YELLOW
1194 Print(' ' + line, newline=True, colour=col)
1195
Simon Glassdb17fb82015-02-05 22:06:15 -07001196
Simon Glassc05694f2013-04-03 11:07:16 +00001197 better = [] # List of boards fixed since last commit
1198 worse = [] # List of new broken boards since last commit
1199 new = [] # List of boards that didn't exist last time
1200 unknown = [] # List of boards that were not built
1201
1202 for target in board_dict:
1203 if target not in board_selected:
1204 continue
1205
1206 # If the board was built last time, add its outcome to a list
1207 if target in self._base_board_dict:
1208 base_outcome = self._base_board_dict[target].rc
1209 outcome = board_dict[target]
1210 if outcome.rc == OUTCOME_UNKNOWN:
1211 unknown.append(target)
1212 elif outcome.rc < base_outcome:
1213 better.append(target)
1214 elif outcome.rc > base_outcome:
1215 worse.append(target)
1216 else:
1217 new.append(target)
1218
1219 # Get a list of errors that have appeared, and disappeared
Simon Glass03749d42014-08-28 09:43:44 -06001220 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1221 self._base_err_line_boards, err_lines, err_line_boards, '')
1222 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1223 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassc05694f2013-04-03 11:07:16 +00001224
1225 # Display results by arch
Simon Glass03749d42014-08-28 09:43:44 -06001226 if (better or worse or unknown or new or worse_err or better_err
1227 or worse_warn or better_warn):
Simon Glassc05694f2013-04-03 11:07:16 +00001228 arch_list = {}
1229 self.AddOutcome(board_selected, arch_list, better, '',
1230 self.col.GREEN)
1231 self.AddOutcome(board_selected, arch_list, worse, '+',
1232 self.col.RED)
1233 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1234 if self._show_unknown:
1235 self.AddOutcome(board_selected, arch_list, unknown, '?',
1236 self.col.MAGENTA)
1237 for arch, target_list in arch_list.iteritems():
Simon Glass4433aa92014-09-05 19:00:07 -06001238 Print('%10s: %s' % (arch, target_list))
Simon Glassbb4dffb2014-08-09 15:33:06 -06001239 self._error_lines += 1
Simon Glassc05694f2013-04-03 11:07:16 +00001240 if better_err:
Simon Glass4433aa92014-09-05 19:00:07 -06001241 Print('\n'.join(better_err), colour=self.col.GREEN)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001242 self._error_lines += 1
Simon Glassc05694f2013-04-03 11:07:16 +00001243 if worse_err:
Simon Glass4433aa92014-09-05 19:00:07 -06001244 Print('\n'.join(worse_err), colour=self.col.RED)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001245 self._error_lines += 1
Simon Glass03749d42014-08-28 09:43:44 -06001246 if better_warn:
Simon Glass4433aa92014-09-05 19:00:07 -06001247 Print('\n'.join(better_warn), colour=self.col.CYAN)
Simon Glass03749d42014-08-28 09:43:44 -06001248 self._error_lines += 1
1249 if worse_warn:
Simon Glass4433aa92014-09-05 19:00:07 -06001250 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
Simon Glass03749d42014-08-28 09:43:44 -06001251 self._error_lines += 1
Simon Glassc05694f2013-04-03 11:07:16 +00001252
1253 if show_sizes:
1254 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1255 show_bloat)
1256
Alex Kiernan4059e302018-05-31 04:48:34 +00001257 if show_environment and self._base_environment:
1258 lines = []
1259
1260 for target in board_dict:
1261 if target not in board_selected:
1262 continue
1263
1264 tbase = self._base_environment[target]
1265 tenvironment = environment[target]
1266 environment_plus = {}
1267 environment_minus = {}
1268 environment_change = {}
1269 base = tbase.environment
1270 for key, value in tenvironment.environment.iteritems():
1271 if key not in base:
1272 environment_plus[key] = value
1273 for key, value in base.iteritems():
1274 if key not in tenvironment.environment:
1275 environment_minus[key] = value
1276 for key, value in base.iteritems():
1277 new_value = tenvironment.environment.get(key)
1278 if new_value and value != new_value:
1279 desc = '%s -> %s' % (value, new_value)
1280 environment_change[key] = desc
1281
1282 _AddConfig(lines, target, environment_plus, environment_minus,
1283 environment_change)
1284
1285 _OutputConfigInfo(lines)
1286
Simon Glasscad8abf2015-08-25 21:52:14 -06001287 if show_config and self._base_config:
1288 summary = {}
1289 arch_config_plus = {}
1290 arch_config_minus = {}
1291 arch_config_change = {}
1292 arch_list = []
1293
1294 for target in board_dict:
1295 if target not in board_selected:
1296 continue
1297 arch = board_selected[target].arch
1298 if arch not in arch_list:
1299 arch_list.append(arch)
1300
1301 for arch in arch_list:
1302 arch_config_plus[arch] = {}
1303 arch_config_minus[arch] = {}
1304 arch_config_change[arch] = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001305 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001306 arch_config_plus[arch][name] = {}
1307 arch_config_minus[arch][name] = {}
1308 arch_config_change[arch][name] = {}
1309
1310 for target in board_dict:
1311 if target not in board_selected:
Simon Glassdb17fb82015-02-05 22:06:15 -07001312 continue
Simon Glasscad8abf2015-08-25 21:52:14 -06001313
1314 arch = board_selected[target].arch
1315
1316 all_config_plus = {}
1317 all_config_minus = {}
1318 all_config_change = {}
1319 tbase = self._base_config[target]
1320 tconfig = config[target]
1321 lines = []
Simon Glasscde5c302016-11-13 14:25:53 -07001322 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001323 if not tconfig.config[name]:
1324 continue
1325 config_plus = {}
1326 config_minus = {}
1327 config_change = {}
1328 base = tbase.config[name]
1329 for key, value in tconfig.config[name].iteritems():
1330 if key not in base:
1331 config_plus[key] = value
1332 all_config_plus[key] = value
1333 for key, value in base.iteritems():
1334 if key not in tconfig.config[name]:
1335 config_minus[key] = value
1336 all_config_minus[key] = value
1337 for key, value in base.iteritems():
1338 new_value = tconfig.config.get(key)
1339 if new_value and value != new_value:
1340 desc = '%s -> %s' % (value, new_value)
1341 config_change[key] = desc
1342 all_config_change[key] = desc
1343
1344 arch_config_plus[arch][name].update(config_plus)
1345 arch_config_minus[arch][name].update(config_minus)
1346 arch_config_change[arch][name].update(config_change)
1347
1348 _AddConfig(lines, name, config_plus, config_minus,
1349 config_change)
1350 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1351 all_config_change)
1352 summary[target] = '\n'.join(lines)
1353
1354 lines_by_target = {}
1355 for target, lines in summary.iteritems():
1356 if lines in lines_by_target:
1357 lines_by_target[lines].append(target)
1358 else:
1359 lines_by_target[lines] = [target]
1360
1361 for arch in arch_list:
1362 lines = []
1363 all_plus = {}
1364 all_minus = {}
1365 all_change = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001366 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001367 all_plus.update(arch_config_plus[arch][name])
1368 all_minus.update(arch_config_minus[arch][name])
1369 all_change.update(arch_config_change[arch][name])
1370 _AddConfig(lines, name, arch_config_plus[arch][name],
1371 arch_config_minus[arch][name],
1372 arch_config_change[arch][name])
1373 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1374 #arch_summary[target] = '\n'.join(lines)
1375 if lines:
1376 Print('%s:' % arch)
1377 _OutputConfigInfo(lines)
1378
1379 for lines, targets in lines_by_target.iteritems():
1380 if not lines:
1381 continue
1382 Print('%s :' % ' '.join(sorted(targets)))
1383 _OutputConfigInfo(lines.split('\n'))
1384
Simon Glassdb17fb82015-02-05 22:06:15 -07001385
Simon Glassc05694f2013-04-03 11:07:16 +00001386 # Save our updated information for the next call to this function
1387 self._base_board_dict = board_dict
1388 self._base_err_lines = err_lines
Simon Glass03749d42014-08-28 09:43:44 -06001389 self._base_warn_lines = warn_lines
1390 self._base_err_line_boards = err_line_boards
1391 self._base_warn_line_boards = warn_line_boards
Simon Glassdb17fb82015-02-05 22:06:15 -07001392 self._base_config = config
Alex Kiernan4059e302018-05-31 04:48:34 +00001393 self._base_environment = environment
Simon Glassc05694f2013-04-03 11:07:16 +00001394
1395 # Get a list of boards that did not get built, if needed
1396 not_built = []
1397 for board in board_selected:
1398 if not board in board_dict:
1399 not_built.append(board)
1400 if not_built:
Simon Glass4433aa92014-09-05 19:00:07 -06001401 Print("Boards not built (%d): %s" % (len(not_built),
1402 ', '.join(not_built)))
Simon Glassc05694f2013-04-03 11:07:16 +00001403
Simon Glasseb48bbc2014-08-09 15:33:02 -06001404 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glass03749d42014-08-28 09:43:44 -06001405 (board_dict, err_lines, err_line_boards, warn_lines,
Alex Kiernan4059e302018-05-31 04:48:34 +00001406 warn_line_boards, config, environment) = self.GetResultSummary(
Simon Glass3394c9f2014-08-28 09:43:43 -06001407 board_selected, commit_upto,
Simon Glassdb17fb82015-02-05 22:06:15 -07001408 read_func_sizes=self._show_bloat,
Alex Kiernan4059e302018-05-31 04:48:34 +00001409 read_config=self._show_config,
1410 read_environment=self._show_environment)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001411 if commits:
1412 msg = '%02d: %s' % (commit_upto + 1,
1413 commits[commit_upto].subject)
Simon Glass4433aa92014-09-05 19:00:07 -06001414 Print(msg, colour=self.col.BLUE)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001415 self.PrintResultSummary(board_selected, board_dict,
Simon Glass3394c9f2014-08-28 09:43:43 -06001416 err_lines if self._show_errors else [], err_line_boards,
Simon Glass03749d42014-08-28 09:43:44 -06001417 warn_lines if self._show_errors else [], warn_line_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +00001418 config, environment, self._show_sizes, self._show_detail,
1419 self._show_bloat, self._show_config, self._show_environment)
Simon Glassc05694f2013-04-03 11:07:16 +00001420
Simon Glasseb48bbc2014-08-09 15:33:02 -06001421 def ShowSummary(self, commits, board_selected):
Simon Glassc05694f2013-04-03 11:07:16 +00001422 """Show a build summary for U-Boot for a given board list.
1423
1424 Reset the result summary, then repeatedly call GetResultSummary on
1425 each commit's results, then display the differences we see.
1426
1427 Args:
1428 commit: Commit objects to summarise
1429 board_selected: Dict containing boards to summarise
Simon Glassc05694f2013-04-03 11:07:16 +00001430 """
Simon Glassd326ad72014-08-09 15:32:59 -06001431 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001432 self.commits = commits
1433 self.ResetResultSummary(board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001434 self._error_lines = 0
Simon Glassc05694f2013-04-03 11:07:16 +00001435
1436 for commit_upto in range(0, self.commit_count, self._step):
Simon Glasseb48bbc2014-08-09 15:33:02 -06001437 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001438 if not self._error_lines:
Simon Glass4433aa92014-09-05 19:00:07 -06001439 Print('(no errors to report)', colour=self.col.GREEN)
Simon Glassc05694f2013-04-03 11:07:16 +00001440
1441
1442 def SetupBuild(self, board_selected, commits):
1443 """Set up ready to start a build.
1444
1445 Args:
1446 board_selected: Selected boards to build
1447 commits: Selected commits to build
1448 """
1449 # First work out how many commits we will build
Simon Glassd326ad72014-08-09 15:32:59 -06001450 count = (self.commit_count + self._step - 1) / self._step
Simon Glassc05694f2013-04-03 11:07:16 +00001451 self.count = len(board_selected) * count
1452 self.upto = self.warned = self.fail = 0
1453 self._timestamps = collections.deque()
1454
Simon Glassc05694f2013-04-03 11:07:16 +00001455 def GetThreadDir(self, thread_num):
1456 """Get the directory path to the working dir for a thread.
1457
1458 Args:
1459 thread_num: Number of thread to check.
1460 """
1461 return os.path.join(self._working_dir, '%02d' % thread_num)
1462
Simon Glassd326ad72014-08-09 15:32:59 -06001463 def _PrepareThread(self, thread_num, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001464 """Prepare the working directory for a thread.
1465
1466 This clones or fetches the repo into the thread's work directory.
1467
1468 Args:
1469 thread_num: Thread number (0, 1, ...)
Simon Glassd326ad72014-08-09 15:32:59 -06001470 setup_git: True to set up a git repo clone
Simon Glassc05694f2013-04-03 11:07:16 +00001471 """
1472 thread_dir = self.GetThreadDir(thread_num)
Simon Glass4a1e88b2014-08-09 15:33:00 -06001473 builderthread.Mkdir(thread_dir)
Simon Glassc05694f2013-04-03 11:07:16 +00001474 git_dir = os.path.join(thread_dir, '.git')
1475
1476 # Clone the repo if it doesn't already exist
1477 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1478 # we have a private index but uses the origin repo's contents?
Simon Glassd326ad72014-08-09 15:32:59 -06001479 if setup_git and self.git_dir:
Simon Glassc05694f2013-04-03 11:07:16 +00001480 src_dir = os.path.abspath(self.git_dir)
1481 if os.path.exists(git_dir):
1482 gitutil.Fetch(git_dir, thread_dir)
1483 else:
Simon Glass2e2a6e62016-09-18 16:48:31 -06001484 Print('\rCloning repo for thread %d' % thread_num,
1485 newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001486 gitutil.Clone(src_dir, thread_dir)
Simon Glass2e2a6e62016-09-18 16:48:31 -06001487 Print('\r%s\r' % (' ' * 30), newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001488
Simon Glassd326ad72014-08-09 15:32:59 -06001489 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001490 """Prepare the working directory for use.
1491
1492 Set up the git repo for each thread.
1493
1494 Args:
1495 max_threads: Maximum number of threads we expect to need.
Simon Glassd326ad72014-08-09 15:32:59 -06001496 setup_git: True to set up a git repo clone
Simon Glassc05694f2013-04-03 11:07:16 +00001497 """
Simon Glass4a1e88b2014-08-09 15:33:00 -06001498 builderthread.Mkdir(self._working_dir)
Simon Glassc05694f2013-04-03 11:07:16 +00001499 for thread in range(max_threads):
Simon Glassd326ad72014-08-09 15:32:59 -06001500 self._PrepareThread(thread, setup_git)
Simon Glassc05694f2013-04-03 11:07:16 +00001501
1502 def _PrepareOutputSpace(self):
1503 """Get the output directories ready to receive files.
1504
1505 We delete any output directories which look like ones we need to
1506 create. Having left over directories is confusing when the user wants
1507 to check the output manually.
1508 """
Simon Glassb9a9b7c2014-12-01 17:33:53 -07001509 if not self.commits:
1510 return
Simon Glassc05694f2013-04-03 11:07:16 +00001511 dir_list = []
1512 for commit_upto in range(self.commit_count):
1513 dir_list.append(self._GetOutputDir(commit_upto))
1514
Simon Glass83cb6cc2016-09-18 16:48:32 -06001515 to_remove = []
Simon Glassc05694f2013-04-03 11:07:16 +00001516 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1517 if dirname not in dir_list:
Simon Glass83cb6cc2016-09-18 16:48:32 -06001518 to_remove.append(dirname)
1519 if to_remove:
1520 Print('Removing %d old build directories' % len(to_remove),
1521 newline=False)
1522 for dirname in to_remove:
Simon Glassc05694f2013-04-03 11:07:16 +00001523 shutil.rmtree(dirname)
1524
Simon Glass78e418e2014-08-09 15:33:03 -06001525 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassc05694f2013-04-03 11:07:16 +00001526 """Build all commits for a list of boards
1527
1528 Args:
1529 commits: List of commits to be build, each a Commit object
1530 boards_selected: Dict of selected boards, key is target name,
1531 value is Board object
Simon Glassc05694f2013-04-03 11:07:16 +00001532 keep_outputs: True to save build output files
Simon Glass78e418e2014-08-09 15:33:03 -06001533 verbose: Display build results as they are completed
Simon Glassc2f91072014-08-28 09:43:39 -06001534 Returns:
1535 Tuple containing:
1536 - number of boards that failed to build
1537 - number of boards that issued warnings
Simon Glassc05694f2013-04-03 11:07:16 +00001538 """
Simon Glassd326ad72014-08-09 15:32:59 -06001539 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001540 self.commits = commits
Simon Glass78e418e2014-08-09 15:33:03 -06001541 self._verbose = verbose
Simon Glassc05694f2013-04-03 11:07:16 +00001542
1543 self.ResetResultSummary(board_selected)
Thierry Reding336d5bd2014-08-19 10:22:39 +02001544 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassd326ad72014-08-09 15:32:59 -06001545 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1546 commits is not None)
Simon Glassc05694f2013-04-03 11:07:16 +00001547 self._PrepareOutputSpace()
Simon Glasse0f23602016-09-18 16:48:33 -06001548 Print('\rStarting build...', newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001549 self.SetupBuild(board_selected, commits)
1550 self.ProcessResult(None)
1551
1552 # Create jobs to build all commits for each board
1553 for brd in board_selected.itervalues():
Simon Glass4a1e88b2014-08-09 15:33:00 -06001554 job = builderthread.BuilderJob()
Simon Glassc05694f2013-04-03 11:07:16 +00001555 job.board = brd
1556 job.commits = commits
1557 job.keep_outputs = keep_outputs
1558 job.step = self._step
1559 self.queue.put(job)
1560
Simon Glassd26e1442016-09-18 16:48:35 -06001561 term = threading.Thread(target=self.queue.join)
1562 term.setDaemon(True)
1563 term.start()
1564 while term.isAlive():
1565 term.join(100)
Simon Glassc05694f2013-04-03 11:07:16 +00001566
1567 # Wait until we have processed all output
1568 self.out_queue.join()
Simon Glass4433aa92014-09-05 19:00:07 -06001569 Print()
Simon Glassc05694f2013-04-03 11:07:16 +00001570 self.ClearLine(0)
Simon Glassc2f91072014-08-28 09:43:39 -06001571 return (self.fail, self.warned)