blob: 078ddf070d3d151558d8f0feec1f210162f99908 [file] [log] [blame]
Tom Rini83d290c2018-05-06 17:58:06 -04001# SPDX-License-Identifier: GPL-2.0+
Simon Glassfc3fe1c2013-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 Glassfc3fe1c2013-04-03 11:07:16 +00006
7import collections
Simon Glassfc3fe1c2013-04-03 11:07:16 +00008from datetime import datetime, timedelta
9import glob
10import os
11import re
Simon Glassc05aa032019-10-31 07:42:53 -060012import queue
Simon Glassfc3fe1c2013-04-03 11:07:16 +000013import shutil
Simon Glass2f256642016-09-18 16:48:37 -060014import signal
Simon Glassfc3fe1c2013-04-03 11:07:16 +000015import string
16import sys
Simon Glassd436e382016-09-18 16:48:35 -060017import threading
Simon Glassfc3fe1c2013-04-03 11:07:16 +000018import time
19
Simon Glass0ede00f2020-04-17 18:09:02 -060020from buildman import builderthread
21from buildman import toolchain
Simon Glassbf776672020-04-17 18:09:04 -060022from patman import command
23from patman import gitutil
24from patman import terminal
Simon Glass098b10f2022-01-29 14:14:18 -070025from patman.terminal import tprint
Simon Glassfc3fe1c2013-04-03 11:07:16 +000026
Simon Glass7bf83a52021-10-19 21:43:24 -060027# This indicates an new int or hex Kconfig property with no default
28# It hangs the build since the 'conf' tool cannot proceed without valid input.
29#
30# We get a repeat sequence of something like this:
31# >>
32# Break things (BREAK_ME) [] (NEW)
33# Error in reading or end of file.
34# <<
35# which indicates that BREAK_ME has an empty default
36RE_NO_DEFAULT = re.compile(b'\((\w+)\) \[] \(NEW\)')
37
Simon Glassfc3fe1c2013-04-03 11:07:16 +000038"""
39Theory of Operation
40
41Please see README for user documentation, and you should be familiar with
42that before trying to make sense of this.
43
44Buildman works by keeping the machine as busy as possible, building different
45commits for different boards on multiple CPUs at once.
46
47The source repo (self.git_dir) contains all the commits to be built. Each
48thread works on a single board at a time. It checks out the first commit,
49configures it for that board, then builds it. Then it checks out the next
50commit and builds it (typically without re-configuring). When it runs out
51of commits, it gets another job from the builder and starts again with that
52board.
53
54Clearly the builder threads could work either way - they could check out a
55commit and then built it for all boards. Using separate directories for each
56commit/board pair they could leave their build product around afterwards
57also.
58
59The intent behind building a single board for multiple commits, is to make
60use of incremental builds. Since each commit is built incrementally from
61the previous one, builds are faster. Reconfiguring for a different board
62removes all intermediate object files.
63
64Many threads can be working at once, but each has its own working directory.
65When a thread finishes a build, it puts the output files into a result
66directory.
67
68The base directory used by buildman is normally '../<branch>', i.e.
69a directory higher than the source repository and named after the branch
70being built.
71
72Within the base directory, we have one subdirectory for each commit. Within
73that is one subdirectory for each board. Within that is the build output for
74that commit/board combination.
75
76Buildman also create working directories for each thread, in a .bm-work/
77subdirectory in the base dir.
78
79As an example, say we are building branch 'us-net' for boards 'sandbox' and
80'seaboard', and say that us-net has two commits. We will have directories
81like this:
82
83us-net/ base directory
Ovidiu Panait7664b032020-05-15 09:30:12 +030084 01_g4ed4ebc_net--Add-tftp-speed-/
Simon Glassfc3fe1c2013-04-03 11:07:16 +000085 sandbox/
86 u-boot.bin
87 seaboard/
88 u-boot.bin
Ovidiu Panait7664b032020-05-15 09:30:12 +030089 02_g4ed4ebc_net--Check-tftp-comp/
Simon Glassfc3fe1c2013-04-03 11:07:16 +000090 sandbox/
91 u-boot.bin
92 seaboard/
93 u-boot.bin
94 .bm-work/
95 00/ working directory for thread 0 (contains source checkout)
96 build/ build output
97 01/ working directory for thread 1
98 build/ build output
99 ...
100u-boot/ source directory
101 .git/ repository
102"""
103
Simon Glass35d696d2020-04-09 15:08:36 -0600104"""Holds information about a particular error line we are outputing
105
106 char: Character representation: '+': error, '-': fixed error, 'w+': warning,
107 'w-' = fixed warning
108 boards: List of Board objects which have line in the error/warning output
109 errline: The text of the error line
110"""
111ErrLine = collections.namedtuple('ErrLine', 'char,boards,errline')
112
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000113# Possible build outcomes
Simon Glassc05aa032019-10-31 07:42:53 -0600114OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4))
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000115
Simon Glass9a6d2e22017-04-12 18:23:26 -0600116# Translate a commit subject into a valid filename (and handle unicode)
Simon Glassc05aa032019-10-31 07:42:53 -0600117trans_valid_chars = str.maketrans('/: ', '---')
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000118
Simon Glassb464f8e2016-11-13 14:25:53 -0700119BASE_CONFIG_FILENAMES = [
120 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
121]
122
123EXTRA_CONFIG_FILENAMES = [
Simon Glass843312d2015-02-05 22:06:15 -0700124 '.config', '.config-spl', '.config-tpl',
125 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
126 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
Simon Glass843312d2015-02-05 22:06:15 -0700127]
128
Simon Glass8270e3c2015-08-25 21:52:14 -0600129class Config:
130 """Holds information about configuration settings for a board."""
Simon Glassb464f8e2016-11-13 14:25:53 -0700131 def __init__(self, config_filename, target):
Simon Glass8270e3c2015-08-25 21:52:14 -0600132 self.target = target
133 self.config = {}
Simon Glassb464f8e2016-11-13 14:25:53 -0700134 for fname in config_filename:
Simon Glass8270e3c2015-08-25 21:52:14 -0600135 self.config[fname] = {}
136
137 def Add(self, fname, key, value):
138 self.config[fname][key] = value
139
140 def __hash__(self):
141 val = 0
142 for fname in self.config:
Simon Glassc05aa032019-10-31 07:42:53 -0600143 for key, value in self.config[fname].items():
144 print(key, value)
Simon Glass8270e3c2015-08-25 21:52:14 -0600145 val = val ^ hash(key) & hash(value)
146 return val
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000147
Alex Kiernan48ae4122018-05-31 04:48:34 +0000148class Environment:
149 """Holds information about environment variables for a board."""
150 def __init__(self, target):
151 self.target = target
152 self.environment = {}
153
154 def Add(self, key, value):
155 self.environment[key] = value
156
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000157class Builder:
158 """Class for building U-Boot for a particular commit.
159
160 Public members: (many should ->private)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000161 already_done: Number of builds already completed
162 base_dir: Base directory to use for builder
163 checkout: True to check out source, False to skip that step.
164 This is used for testing.
165 col: terminal.Color() object
166 count: Number of commits to build
167 do_make: Method to call to invoke Make
168 fail: Number of builds that failed due to error
169 force_build: Force building even if a build already exists
170 force_config_on_failure: If a commit fails for a board, disable
171 incremental building for the next commit we build for that
172 board, so that we will see all warnings/errors again.
Simon Glass4266dc22014-07-13 12:22:31 -0600173 force_build_failures: If a previously-built build (i.e. built on
174 a previous run of buildman) is marked as failed, rebuild it.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000175 git_dir: Git directory containing source repository
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000176 num_jobs: Number of jobs to run at once (passed to make as -j)
177 num_threads: Number of builder threads to run
178 out_queue: Queue of results to process
179 re_make_err: Compiled regular expression for ignore_lines
180 queue: Queue of jobs to run
181 threads: List of active threads
182 toolchains: Toolchains object to use for building
183 upto: Current commit number we are building (0.count-1)
184 warned: Number of builds that produced at least one warning
Simon Glass97e91522014-07-14 17:51:02 -0600185 force_reconfig: Reconfigure U-Boot on each comiit. This disables
186 incremental building, where buildman reconfigures on the first
187 commit for a baord, and then just does an incremental build for
188 the following commits. In fact buildman will reconfigure and
189 retry for any failing commits, so generally the only effect of
190 this option is to slow things down.
Simon Glass189a4962014-07-14 17:51:03 -0600191 in_tree: Build U-Boot in-tree instead of specifying an output
192 directory separate from the source code. This option is really
193 only useful for testing in-tree builds.
Simon Glassd829f122020-03-18 09:42:42 -0600194 work_in_output: Use the output directory as the work directory and
195 don't write to a separate output directory.
Simon Glass8116c782021-04-11 16:27:27 +1200196 thread_exceptions: List of exceptions raised by thread jobs
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000197
198 Private members:
199 _base_board_dict: Last-summarised Dict of boards
200 _base_err_lines: Last-summarised list of errors
Simon Glasse30965d2014-08-28 09:43:44 -0600201 _base_warn_lines: Last-summarised list of warnings
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000202 _build_period_us: Time taken for a single build (float object).
203 _complete_delay: Expected delay until completion (timedelta)
204 _next_delay_update: Next time we plan to display a progress update
205 (datatime)
206 _show_unknown: Show unknown boards (those not built) in summary
Simon Glass7b33f212020-04-09 15:08:47 -0600207 _start_time: Start time for the build
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000208 _timestamps: List of timestamps for the completion of the last
209 last _timestamp_count builds. Each is a datetime object.
210 _timestamp_count: Number of timestamps to keep in our list.
211 _working_dir: Base working directory containing all threads
Simon Glassb82492b2021-01-30 22:17:46 -0700212 _single_builder: BuilderThread object for the singer builder, if
213 threading is not being used
Simon Glass7bf83a52021-10-19 21:43:24 -0600214 _terminated: Thread was terminated due to an error
215 _restarting_config: True if 'Restart config' is detected in output
Simon Glassae1a09f2022-07-11 19:03:56 -0600216 _ide: Produce output suitable for an Integrated Development Environment,
217 i.e. dont emit progress information and put errors/warnings on stderr
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000218 """
219 class Outcome:
220 """Records a build outcome for a single make invocation
221
222 Public Members:
223 rc: Outcome value (OUTCOME_...)
224 err_lines: List of error lines or [] if none
225 sizes: Dictionary of image size information, keyed by filename
226 - Each value is itself a dictionary containing
227 values for 'text', 'data' and 'bss', being the integer
228 size in bytes of each section.
229 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
230 value is itself a dictionary:
231 key: function name
232 value: Size of function in bytes
Simon Glass843312d2015-02-05 22:06:15 -0700233 config: Dictionary keyed by filename - e.g. '.config'. Each
234 value is itself a dictionary:
235 key: config name
236 value: config value
Alex Kiernan48ae4122018-05-31 04:48:34 +0000237 environment: Dictionary keyed by environment variable, Each
238 value is the value of environment variable.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000239 """
Alex Kiernan48ae4122018-05-31 04:48:34 +0000240 def __init__(self, rc, err_lines, sizes, func_sizes, config,
241 environment):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000242 self.rc = rc
243 self.err_lines = err_lines
244 self.sizes = sizes
245 self.func_sizes = func_sizes
Simon Glass843312d2015-02-05 22:06:15 -0700246 self.config = config
Alex Kiernan48ae4122018-05-31 04:48:34 +0000247 self.environment = environment
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000248
249 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
Simon Glass5971ab52014-12-01 17:33:55 -0700250 gnu_make='make', checkout=True, show_unknown=True, step=1,
Stephen Warrenf79f1e02016-04-11 10:48:44 -0600251 no_subdirs=False, full_path=False, verbose_build=False,
Simon Glasseb70a2c2020-04-09 15:08:51 -0600252 mrproper=False, per_board_out_dir=False,
Daniel Schwierzeck2371d1b2018-01-26 16:31:05 +0100253 config_only=False, squash_config_y=False,
Simon Glass8116c782021-04-11 16:27:27 +1200254 warnings_as_errors=False, work_in_output=False,
Simon Glass2b4806e2022-01-22 05:07:33 -0700255 test_thread_exceptions=False, adjust_cfg=None):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000256 """Create a new Builder object
257
258 Args:
259 toolchains: Toolchains object to use for building
260 base_dir: Base directory to use for builder
261 git_dir: Git directory containing source repository
262 num_threads: Number of builder threads to run
263 num_jobs: Number of jobs to run at once (passed to make as -j)
Masahiro Yamada99796922014-07-22 11:19:09 +0900264 gnu_make: the command name of GNU Make.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000265 checkout: True to check out source, False to skip that step.
266 This is used for testing.
267 show_unknown: Show unknown boards (those not built) in summary
268 step: 1 to process every commit, n to process every nth commit
Simon Glassbb1501f2014-12-01 17:34:00 -0700269 no_subdirs: Don't create subdirectories when building current
270 source for a single board
271 full_path: Return the full path in CROSS_COMPILE and don't set
272 PATH
Simon Glassd2ce6582014-12-01 17:34:07 -0700273 verbose_build: Run build with V=1 and don't use 'make -s'
Simon Glasseb70a2c2020-04-09 15:08:51 -0600274 mrproper: Always run 'make mrproper' when configuring
Stephen Warrenf79f1e02016-04-11 10:48:44 -0600275 per_board_out_dir: Build in a separate persistent directory per
276 board rather than a thread-specific directory
Simon Glassb50113f2016-11-13 14:25:51 -0700277 config_only: Only configure each build, don't build it
Simon Glassb464f8e2016-11-13 14:25:53 -0700278 squash_config_y: Convert CONFIG options with the value 'y' to '1'
Daniel Schwierzeck2371d1b2018-01-26 16:31:05 +0100279 warnings_as_errors: Treat all compiler warnings as errors
Simon Glassd829f122020-03-18 09:42:42 -0600280 work_in_output: Use the output directory as the work directory and
281 don't write to a separate output directory.
Simon Glass8116c782021-04-11 16:27:27 +1200282 test_thread_exceptions: Uses for tests only, True to make the
283 threads raise an exception instead of reporting their result.
284 This simulates a failure in the code somewhere
Simon Glass2b4806e2022-01-22 05:07:33 -0700285 adjust_cfg_list (list of str): List of changes to make to .config
286 file before building. Each is one of (where C is the config
287 option with or without the CONFIG_ prefix)
288
289 C to enable C
290 ~C to disable C
291 C=val to set the value of C (val must have quotes if C is
292 a string Kconfig
293
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000294 """
295 self.toolchains = toolchains
296 self.base_dir = base_dir
Simon Glassd829f122020-03-18 09:42:42 -0600297 if work_in_output:
298 self._working_dir = base_dir
299 else:
300 self._working_dir = os.path.join(base_dir, '.bm-work')
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000301 self.threads = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000302 self.do_make = self.Make
Masahiro Yamada99796922014-07-22 11:19:09 +0900303 self.gnu_make = gnu_make
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000304 self.checkout = checkout
305 self.num_threads = num_threads
306 self.num_jobs = num_jobs
307 self.already_done = 0
308 self.force_build = False
309 self.git_dir = git_dir
310 self._show_unknown = show_unknown
311 self._timestamp_count = 10
312 self._build_period_us = None
313 self._complete_delay = None
314 self._next_delay_update = datetime.now()
Simon Glass7b33f212020-04-09 15:08:47 -0600315 self._start_time = datetime.now()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000316 self.force_config_on_failure = True
Simon Glass4266dc22014-07-13 12:22:31 -0600317 self.force_build_failures = False
Simon Glass97e91522014-07-14 17:51:02 -0600318 self.force_reconfig = False
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000319 self._step = step
Simon Glass189a4962014-07-14 17:51:03 -0600320 self.in_tree = False
Simon Glass28370c12014-08-09 15:33:06 -0600321 self._error_lines = 0
Simon Glass5971ab52014-12-01 17:33:55 -0700322 self.no_subdirs = no_subdirs
Simon Glassbb1501f2014-12-01 17:34:00 -0700323 self.full_path = full_path
Simon Glassd2ce6582014-12-01 17:34:07 -0700324 self.verbose_build = verbose_build
Simon Glassb50113f2016-11-13 14:25:51 -0700325 self.config_only = config_only
Simon Glassb464f8e2016-11-13 14:25:53 -0700326 self.squash_config_y = squash_config_y
327 self.config_filenames = BASE_CONFIG_FILENAMES
Simon Glassd829f122020-03-18 09:42:42 -0600328 self.work_in_output = work_in_output
Simon Glass2b4806e2022-01-22 05:07:33 -0700329 self.adjust_cfg = adjust_cfg
Simon Glassae1a09f2022-07-11 19:03:56 -0600330 self._ide = False
Simon Glass2b4806e2022-01-22 05:07:33 -0700331
Simon Glassb464f8e2016-11-13 14:25:53 -0700332 if not self.squash_config_y:
333 self.config_filenames += EXTRA_CONFIG_FILENAMES
Simon Glass7bf83a52021-10-19 21:43:24 -0600334 self._terminated = False
335 self._restarting_config = False
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000336
Daniel Schwierzeck2371d1b2018-01-26 16:31:05 +0100337 self.warnings_as_errors = warnings_as_errors
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000338 self.col = terminal.Color()
339
Simon Glasse30965d2014-08-28 09:43:44 -0600340 self._re_function = re.compile('(.*): In function.*')
341 self._re_files = re.compile('In file included from.*')
342 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
Simon Glass2d483332018-11-06 16:02:11 -0700343 self._re_dtb_warning = re.compile('(.*): Warning .*')
Simon Glasse30965d2014-08-28 09:43:44 -0600344 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
Simon Glass113a8a52020-04-09 15:08:53 -0600345 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
346 re.MULTILINE | re.DOTALL)
Simon Glasse30965d2014-08-28 09:43:44 -0600347
Simon Glass8116c782021-04-11 16:27:27 +1200348 self.thread_exceptions = []
349 self.test_thread_exceptions = test_thread_exceptions
Simon Glassb82492b2021-01-30 22:17:46 -0700350 if self.num_threads:
351 self._single_builder = None
352 self.queue = queue.Queue()
353 self.out_queue = queue.Queue()
354 for i in range(self.num_threads):
Simon Glass8116c782021-04-11 16:27:27 +1200355 t = builderthread.BuilderThread(
356 self, i, mrproper, per_board_out_dir,
357 test_exception=test_thread_exceptions)
Simon Glassb82492b2021-01-30 22:17:46 -0700358 t.setDaemon(True)
359 t.start()
360 self.threads.append(t)
361
362 t = builderthread.ResultThread(self)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000363 t.setDaemon(True)
364 t.start()
365 self.threads.append(t)
Simon Glassb82492b2021-01-30 22:17:46 -0700366 else:
367 self._single_builder = builderthread.BuilderThread(
368 self, -1, mrproper, per_board_out_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000369
370 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
371 self.re_make_err = re.compile('|'.join(ignore_lines))
372
Simon Glass2f256642016-09-18 16:48:37 -0600373 # Handle existing graceful with SIGINT / Ctrl-C
374 signal.signal(signal.SIGINT, self.signal_handler)
375
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000376 def __del__(self):
377 """Get rid of all threads created by the builder"""
378 for t in self.threads:
379 del t
380
Simon Glass2f256642016-09-18 16:48:37 -0600381 def signal_handler(self, signal, frame):
382 sys.exit(1)
383
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600384 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
Simon Glassed966652014-08-28 09:43:43 -0600385 show_detail=False, show_bloat=False,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000386 list_error_boards=False, show_config=False,
Simon Glass113a8a52020-04-09 15:08:53 -0600387 show_environment=False, filter_dtb_warnings=False,
Simon Glassae1a09f2022-07-11 19:03:56 -0600388 filter_migration_warnings=False, ide=False):
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600389 """Setup display options for the builder.
390
Simon Glass174592b2020-04-09 15:08:52 -0600391 Args:
392 show_errors: True to show summarised error/warning info
393 show_sizes: Show size deltas
394 show_detail: Show size delta detail for each board if show_sizes
395 show_bloat: Show detail for each function
396 list_error_boards: Show the boards which caused each error/warning
397 show_config: Show config deltas
398 show_environment: Show environment deltas
399 filter_dtb_warnings: Filter out any warnings from the device-tree
400 compiler
Simon Glass113a8a52020-04-09 15:08:53 -0600401 filter_migration_warnings: Filter out any warnings about migrating
402 a board to driver model
Simon Glassae1a09f2022-07-11 19:03:56 -0600403 ide: Create output that can be parsed by an IDE. There is no '+' prefix on
404 error lines and output on stderr stays on stderr.
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600405 """
406 self._show_errors = show_errors
407 self._show_sizes = show_sizes
408 self._show_detail = show_detail
409 self._show_bloat = show_bloat
Simon Glassed966652014-08-28 09:43:43 -0600410 self._list_error_boards = list_error_boards
Simon Glass843312d2015-02-05 22:06:15 -0700411 self._show_config = show_config
Alex Kiernan48ae4122018-05-31 04:48:34 +0000412 self._show_environment = show_environment
Simon Glass174592b2020-04-09 15:08:52 -0600413 self._filter_dtb_warnings = filter_dtb_warnings
Simon Glass113a8a52020-04-09 15:08:53 -0600414 self._filter_migration_warnings = filter_migration_warnings
Simon Glassae1a09f2022-07-11 19:03:56 -0600415 self._ide = ide
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600416
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000417 def _AddTimestamp(self):
418 """Add a new timestamp to the list and record the build period.
419
420 The build period is the length of time taken to perform a single
421 build (one board, one commit).
422 """
423 now = datetime.now()
424 self._timestamps.append(now)
425 count = len(self._timestamps)
426 delta = self._timestamps[-1] - self._timestamps[0]
427 seconds = delta.total_seconds()
428
429 # If we have enough data, estimate build period (time taken for a
430 # single build) and therefore completion time.
431 if count > 1 and self._next_delay_update < now:
432 self._next_delay_update = now + timedelta(seconds=2)
433 if seconds > 0:
434 self._build_period = float(seconds) / count
435 todo = self.count - self.upto
436 self._complete_delay = timedelta(microseconds=
437 self._build_period * todo * 1000000)
438 # Round it
439 self._complete_delay -= timedelta(
440 microseconds=self._complete_delay.microseconds)
441
442 if seconds > 60:
443 self._timestamps.popleft()
444 count -= 1
445
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000446 def SelectCommit(self, commit, checkout=True):
447 """Checkout the selected commit for this build
448 """
449 self.commit = commit
450 if checkout and self.checkout:
Simon Glass0157b182022-01-29 14:14:11 -0700451 gitutil.checkout(commit.hash)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000452
453 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
454 """Run make
455
456 Args:
457 commit: Commit object that is being built
458 brd: Board object that is being built
Roger Meierfd18a892014-08-20 22:10:29 +0200459 stage: Stage that we are at (mrproper, config, build)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000460 cwd: Directory where make should be run
461 args: Arguments to pass to make
Simon Glassd9800692022-01-29 14:14:05 -0700462 kwargs: Arguments to pass to command.run_pipe()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000463 """
Simon Glass7bf83a52021-10-19 21:43:24 -0600464
465 def check_output(stream, data):
466 if b'Restart config' in data:
467 self._restarting_config = True
468
469 # If we see 'Restart config' following by multiple errors
470 if self._restarting_config:
471 m = RE_NO_DEFAULT.findall(data)
472
473 # Number of occurences of each Kconfig item
474 multiple = [m.count(val) for val in set(m)]
475
476 # If any of them occur more than once, we have a loop
477 if [val for val in multiple if val > 1]:
478 self._terminated = True
479 return True
480 return False
481
482 self._restarting_config = False
483 self._terminated = False
Masahiro Yamada99796922014-07-22 11:19:09 +0900484 cmd = [self.gnu_make] + list(args)
Simon Glassd9800692022-01-29 14:14:05 -0700485 result = command.run_pipe([cmd], capture=True, capture_stderr=True,
Simon Glass7bf83a52021-10-19 21:43:24 -0600486 cwd=cwd, raise_on_error=False, infile='/dev/null',
487 output_func=check_output, **kwargs)
488
489 if self._terminated:
490 # Try to be helpful
491 result.stderr += '(** did you define an int/hex Kconfig with no default? **)'
492
Simon Glass40f11fc2015-02-05 22:06:12 -0700493 if self.verbose_build:
494 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
495 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000496 return result
497
498 def ProcessResult(self, result):
499 """Process the result of a build, showing progress information
500
501 Args:
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600502 result: A CommandResult object, which indicates the result for
503 a single build
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000504 """
505 col = terminal.Color()
506 if result:
507 target = result.brd.target
508
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000509 self.upto += 1
510 if result.return_code != 0:
511 self.fail += 1
512 elif result.stderr:
513 self.warned += 1
514 if result.already_done:
515 self.already_done += 1
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600516 if self._verbose:
Simon Glass098b10f2022-01-29 14:14:18 -0700517 terminal.print_clear()
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600518 boards_selected = {target : result.brd}
519 self.ResetResultSummary(boards_selected)
520 self.ProduceResultSummary(result.commit_upto, self.commits,
521 boards_selected)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000522 else:
523 target = '(starting)'
524
525 # Display separate counts for ok, warned and fail
526 ok = self.upto - self.warned - self.fail
Simon Glass252ac582022-01-29 14:14:17 -0700527 line = '\r' + self.col.build(self.col.GREEN, '%5d' % ok)
528 line += self.col.build(self.col.YELLOW, '%5d' % self.warned)
529 line += self.col.build(self.col.RED, '%5d' % self.fail)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000530
Simon Glass6eb76ca2020-04-09 15:08:45 -0600531 line += ' /%-5d ' % self.count
532 remaining = self.count - self.upto
533 if remaining:
Simon Glass252ac582022-01-29 14:14:17 -0700534 line += self.col.build(self.col.MAGENTA, ' -%-5d ' % remaining)
Simon Glass6eb76ca2020-04-09 15:08:45 -0600535 else:
536 line += ' ' * 8
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000537
538 # Add our current completion time estimate
539 self._AddTimestamp()
540 if self._complete_delay:
Simon Glass6eb76ca2020-04-09 15:08:45 -0600541 line += '%s : ' % self._complete_delay
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000542
Simon Glass6eb76ca2020-04-09 15:08:45 -0600543 line += target
Simon Glassae1a09f2022-07-11 19:03:56 -0600544 if not self._ide:
545 terminal.print_clear()
546 tprint(line, newline=False, limit_to_line=True)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000547
548 def _GetOutputDir(self, commit_upto):
549 """Get the name of the output directory for a commit number
550
551 The output directory is typically .../<branch>/<commit>.
552
553 Args:
554 commit_upto: Commit number to use (0..self.count-1)
555 """
Simon Glass60b285f2020-04-17 17:51:34 -0600556 if self.work_in_output:
557 return self._working_dir
558
Simon Glass5971ab52014-12-01 17:33:55 -0700559 commit_dir = None
Simon Glassfea58582014-08-09 15:32:59 -0600560 if self.commits:
561 commit = self.commits[commit_upto]
562 subject = commit.subject.translate(trans_valid_chars)
Simon Glass925f6ad2020-03-18 09:42:45 -0600563 # See _GetOutputSpaceRemovals() which parses this name
Ovidiu Panait7664b032020-05-15 09:30:12 +0300564 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
565 commit.hash, subject[:20]))
Simon Glass5971ab52014-12-01 17:33:55 -0700566 elif not self.no_subdirs:
Simon Glassfea58582014-08-09 15:32:59 -0600567 commit_dir = 'current'
Simon Glass5971ab52014-12-01 17:33:55 -0700568 if not commit_dir:
569 return self.base_dir
570 return os.path.join(self.base_dir, commit_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000571
572 def GetBuildDir(self, commit_upto, target):
573 """Get the name of the build directory for a commit number
574
575 The build directory is typically .../<branch>/<commit>/<target>.
576
577 Args:
578 commit_upto: Commit number to use (0..self.count-1)
579 target: Target name
580 """
581 output_dir = self._GetOutputDir(commit_upto)
Simon Glass60b285f2020-04-17 17:51:34 -0600582 if self.work_in_output:
583 return output_dir
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000584 return os.path.join(output_dir, target)
585
586 def GetDoneFile(self, commit_upto, target):
587 """Get the name of the done file for a commit number
588
589 Args:
590 commit_upto: Commit number to use (0..self.count-1)
591 target: Target name
592 """
593 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
594
595 def GetSizesFile(self, commit_upto, target):
596 """Get the name of the sizes file for a commit number
597
598 Args:
599 commit_upto: Commit number to use (0..self.count-1)
600 target: Target name
601 """
602 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
603
604 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
605 """Get the name of the funcsizes file for a commit number and ELF file
606
607 Args:
608 commit_upto: Commit number to use (0..self.count-1)
609 target: Target name
610 elf_fname: Filename of elf image
611 """
612 return os.path.join(self.GetBuildDir(commit_upto, target),
613 '%s.sizes' % elf_fname.replace('/', '-'))
614
615 def GetObjdumpFile(self, commit_upto, target, elf_fname):
616 """Get the name of the objdump file for a commit number and ELF file
617
618 Args:
619 commit_upto: Commit number to use (0..self.count-1)
620 target: Target name
621 elf_fname: Filename of elf image
622 """
623 return os.path.join(self.GetBuildDir(commit_upto, target),
624 '%s.objdump' % elf_fname.replace('/', '-'))
625
626 def GetErrFile(self, commit_upto, target):
627 """Get the name of the err file for a commit number
628
629 Args:
630 commit_upto: Commit number to use (0..self.count-1)
631 target: Target name
632 """
633 output_dir = self.GetBuildDir(commit_upto, target)
634 return os.path.join(output_dir, 'err')
635
636 def FilterErrors(self, lines):
637 """Filter out errors in which we have no interest
638
639 We should probably use map().
640
641 Args:
642 lines: List of error lines, each a string
643 Returns:
644 New list with only interesting lines included
645 """
646 out_lines = []
Simon Glass113a8a52020-04-09 15:08:53 -0600647 if self._filter_migration_warnings:
648 text = '\n'.join(lines)
649 text = self._re_migration_warning.sub('', text)
650 lines = text.splitlines()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000651 for line in lines:
Simon Glass174592b2020-04-09 15:08:52 -0600652 if self.re_make_err.search(line):
653 continue
654 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
655 continue
656 out_lines.append(line)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000657 return out_lines
658
659 def ReadFuncSizes(self, fname, fd):
660 """Read function sizes from the output of 'nm'
661
662 Args:
663 fd: File containing data to read
664 fname: Filename we are reading from (just for errors)
665
666 Returns:
667 Dictionary containing size of each function in bytes, indexed by
668 function name.
669 """
670 sym = {}
671 for line in fd.readlines():
672 try:
Tom Rinid08c38c2019-12-06 15:31:31 -0500673 if line.strip():
674 size, type, name = line[:-1].split()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000675 except:
Simon Glass098b10f2022-01-29 14:14:18 -0700676 tprint("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000677 continue
678 if type in 'tTdDbB':
679 # function names begin with '.' on 64-bit powerpc
680 if '.' in name[1:]:
681 name = 'static.' + name.split('.')[0]
682 sym[name] = sym.get(name, 0) + int(size, 16)
683 return sym
684
Simon Glass843312d2015-02-05 22:06:15 -0700685 def _ProcessConfig(self, fname):
686 """Read in a .config, autoconf.mk or autoconf.h file
687
688 This function handles all config file types. It ignores comments and
689 any #defines which don't start with CONFIG_.
690
691 Args:
692 fname: Filename to read
693
694 Returns:
695 Dictionary:
696 key: Config name (e.g. CONFIG_DM)
697 value: Config value (e.g. 1)
698 """
699 config = {}
700 if os.path.exists(fname):
701 with open(fname) as fd:
702 for line in fd:
703 line = line.strip()
704 if line.startswith('#define'):
705 values = line[8:].split(' ', 1)
706 if len(values) > 1:
707 key, value = values
708 else:
709 key = values[0]
Simon Glassb464f8e2016-11-13 14:25:53 -0700710 value = '1' if self.squash_config_y else ''
Simon Glass843312d2015-02-05 22:06:15 -0700711 if not key.startswith('CONFIG_'):
712 continue
713 elif not line or line[0] in ['#', '*', '/']:
714 continue
715 else:
716 key, value = line.split('=', 1)
Simon Glassb464f8e2016-11-13 14:25:53 -0700717 if self.squash_config_y and value == 'y':
718 value = '1'
Simon Glass843312d2015-02-05 22:06:15 -0700719 config[key] = value
720 return config
721
Alex Kiernan48ae4122018-05-31 04:48:34 +0000722 def _ProcessEnvironment(self, fname):
723 """Read in a uboot.env file
724
725 This function reads in environment variables from a file.
726
727 Args:
728 fname: Filename to read
729
730 Returns:
731 Dictionary:
732 key: environment variable (e.g. bootlimit)
733 value: value of environment variable (e.g. 1)
734 """
735 environment = {}
736 if os.path.exists(fname):
737 with open(fname) as fd:
738 for line in fd.read().split('\0'):
739 try:
740 key, value = line.split('=', 1)
741 environment[key] = value
742 except ValueError:
743 # ignore lines we can't parse
744 pass
745 return environment
746
Simon Glass843312d2015-02-05 22:06:15 -0700747 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000748 read_config, read_environment):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000749 """Work out the outcome of a build.
750
751 Args:
752 commit_upto: Commit number to check (0..n-1)
753 target: Target board to check
754 read_func_sizes: True to read function size information
Simon Glass843312d2015-02-05 22:06:15 -0700755 read_config: True to read .config and autoconf.h files
Alex Kiernan48ae4122018-05-31 04:48:34 +0000756 read_environment: True to read uboot.env files
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000757
758 Returns:
759 Outcome object
760 """
761 done_file = self.GetDoneFile(commit_upto, target)
762 sizes_file = self.GetSizesFile(commit_upto, target)
763 sizes = {}
764 func_sizes = {}
Simon Glass843312d2015-02-05 22:06:15 -0700765 config = {}
Alex Kiernan48ae4122018-05-31 04:48:34 +0000766 environment = {}
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000767 if os.path.exists(done_file):
768 with open(done_file, 'r') as fd:
Simon Glass347ea0b2019-04-26 19:02:23 -0600769 try:
770 return_code = int(fd.readline())
771 except ValueError:
772 # The file may be empty due to running out of disk space.
773 # Try a rebuild
774 return_code = 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000775 err_lines = []
776 err_file = self.GetErrFile(commit_upto, target)
777 if os.path.exists(err_file):
778 with open(err_file, 'r') as fd:
779 err_lines = self.FilterErrors(fd.readlines())
780
781 # Decide whether the build was ok, failed or created warnings
782 if return_code:
783 rc = OUTCOME_ERROR
784 elif len(err_lines):
785 rc = OUTCOME_WARNING
786 else:
787 rc = OUTCOME_OK
788
789 # Convert size information to our simple format
790 if os.path.exists(sizes_file):
791 with open(sizes_file, 'r') as fd:
792 for line in fd.readlines():
793 values = line.split()
794 rodata = 0
795 if len(values) > 6:
796 rodata = int(values[6], 16)
797 size_dict = {
798 'all' : int(values[0]) + int(values[1]) +
799 int(values[2]),
800 'text' : int(values[0]) - rodata,
801 'data' : int(values[1]),
802 'bss' : int(values[2]),
803 'rodata' : rodata,
804 }
805 sizes[values[5]] = size_dict
806
807 if read_func_sizes:
808 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
809 for fname in glob.glob(pattern):
810 with open(fname, 'r') as fd:
811 dict_name = os.path.basename(fname).replace('.sizes',
812 '')
813 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
814
Simon Glass843312d2015-02-05 22:06:15 -0700815 if read_config:
816 output_dir = self.GetBuildDir(commit_upto, target)
Simon Glassb464f8e2016-11-13 14:25:53 -0700817 for name in self.config_filenames:
Simon Glass843312d2015-02-05 22:06:15 -0700818 fname = os.path.join(output_dir, name)
819 config[name] = self._ProcessConfig(fname)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000820
Alex Kiernan48ae4122018-05-31 04:48:34 +0000821 if read_environment:
822 output_dir = self.GetBuildDir(commit_upto, target)
823 fname = os.path.join(output_dir, 'uboot.env')
824 environment = self._ProcessEnvironment(fname)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000825
Alex Kiernan48ae4122018-05-31 04:48:34 +0000826 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
827 environment)
828
829 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
Simon Glass843312d2015-02-05 22:06:15 -0700830
831 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000832 read_config, read_environment):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000833 """Calculate a summary of the results of building a commit.
834
835 Args:
836 board_selected: Dict containing boards to summarise
837 commit_upto: Commit number to summarize (0..self.count-1)
838 read_func_sizes: True to read function size information
Simon Glass843312d2015-02-05 22:06:15 -0700839 read_config: True to read .config and autoconf.h files
Alex Kiernan48ae4122018-05-31 04:48:34 +0000840 read_environment: True to read uboot.env files
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000841
842 Returns:
843 Tuple:
Simon Glassae1a09f2022-07-11 19:03:56 -0600844 Dict containing boards which built this commit:
845 key: board.target
846 value: Builder.Outcome object
Simon Glasse30965d2014-08-28 09:43:44 -0600847 List containing a summary of error lines
Simon Glassed966652014-08-28 09:43:43 -0600848 Dict keyed by error line, containing a list of the Board
849 objects with that error
Simon Glasse30965d2014-08-28 09:43:44 -0600850 List containing a summary of warning lines
851 Dict keyed by error line, containing a list of the Board
852 objects with that warning
Simon Glass8270e3c2015-08-25 21:52:14 -0600853 Dictionary keyed by board.target. Each value is a dictionary:
854 key: filename - e.g. '.config'
Simon Glass843312d2015-02-05 22:06:15 -0700855 value is itself a dictionary:
856 key: config name
857 value: config value
Alex Kiernan48ae4122018-05-31 04:48:34 +0000858 Dictionary keyed by board.target. Each value is a dictionary:
859 key: environment variable
860 value: value of environment variable
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000861 """
Simon Glasse30965d2014-08-28 09:43:44 -0600862 def AddLine(lines_summary, lines_boards, line, board):
863 line = line.rstrip()
864 if line in lines_boards:
865 lines_boards[line].append(board)
866 else:
867 lines_boards[line] = [board]
868 lines_summary.append(line)
869
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000870 board_dict = {}
871 err_lines_summary = []
Simon Glassed966652014-08-28 09:43:43 -0600872 err_lines_boards = {}
Simon Glasse30965d2014-08-28 09:43:44 -0600873 warn_lines_summary = []
874 warn_lines_boards = {}
Simon Glass843312d2015-02-05 22:06:15 -0700875 config = {}
Alex Kiernan48ae4122018-05-31 04:48:34 +0000876 environment = {}
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000877
Simon Glassc05aa032019-10-31 07:42:53 -0600878 for board in boards_selected.values():
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000879 outcome = self.GetBuildOutcome(commit_upto, board.target,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000880 read_func_sizes, read_config,
881 read_environment)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000882 board_dict[board.target] = outcome
Simon Glasse30965d2014-08-28 09:43:44 -0600883 last_func = None
884 last_was_warning = False
885 for line in outcome.err_lines:
886 if line:
887 if (self._re_function.match(line) or
888 self._re_files.match(line)):
889 last_func = line
Simon Glassed966652014-08-28 09:43:43 -0600890 else:
Simon Glass2d483332018-11-06 16:02:11 -0700891 is_warning = (self._re_warning.match(line) or
892 self._re_dtb_warning.match(line))
Simon Glasse30965d2014-08-28 09:43:44 -0600893 is_note = self._re_note.match(line)
894 if is_warning or (last_was_warning and is_note):
895 if last_func:
896 AddLine(warn_lines_summary, warn_lines_boards,
897 last_func, board)
898 AddLine(warn_lines_summary, warn_lines_boards,
899 line, board)
900 else:
901 if last_func:
902 AddLine(err_lines_summary, err_lines_boards,
903 last_func, board)
904 AddLine(err_lines_summary, err_lines_boards,
905 line, board)
906 last_was_warning = is_warning
907 last_func = None
Simon Glassb464f8e2016-11-13 14:25:53 -0700908 tconfig = Config(self.config_filenames, board.target)
909 for fname in self.config_filenames:
Simon Glass843312d2015-02-05 22:06:15 -0700910 if outcome.config:
Simon Glassc05aa032019-10-31 07:42:53 -0600911 for key, value in outcome.config[fname].items():
Simon Glass8270e3c2015-08-25 21:52:14 -0600912 tconfig.Add(fname, key, value)
913 config[board.target] = tconfig
Simon Glass843312d2015-02-05 22:06:15 -0700914
Alex Kiernan48ae4122018-05-31 04:48:34 +0000915 tenvironment = Environment(board.target)
916 if outcome.environment:
Simon Glassc05aa032019-10-31 07:42:53 -0600917 for key, value in outcome.environment.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +0000918 tenvironment.Add(key, value)
919 environment[board.target] = tenvironment
920
Simon Glasse30965d2014-08-28 09:43:44 -0600921 return (board_dict, err_lines_summary, err_lines_boards,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000922 warn_lines_summary, warn_lines_boards, config, environment)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000923
924 def AddOutcome(self, board_dict, arch_list, changes, char, color):
925 """Add an output to our list of outcomes for each architecture
926
927 This simple function adds failing boards (changes) to the
928 relevant architecture string, so we can print the results out
929 sorted by architecture.
930
931 Args:
932 board_dict: Dict containing all boards
933 arch_list: Dict keyed by arch name. Value is a string containing
934 a list of board names which failed for that arch.
935 changes: List of boards to add to arch_list
936 color: terminal.Colour object
937 """
938 done_arch = {}
939 for target in changes:
940 if target in board_dict:
941 arch = board_dict[target].arch
942 else:
943 arch = 'unknown'
Simon Glass252ac582022-01-29 14:14:17 -0700944 str = self.col.build(color, ' ' + target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000945 if not arch in done_arch:
Simon Glass252ac582022-01-29 14:14:17 -0700946 str = ' %s %s' % (self.col.build(color, char), str)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000947 done_arch[arch] = True
948 if not arch in arch_list:
949 arch_list[arch] = str
950 else:
951 arch_list[arch] += str
952
953
954 def ColourNum(self, num):
955 color = self.col.RED if num > 0 else self.col.GREEN
956 if num == 0:
957 return '0'
Simon Glass252ac582022-01-29 14:14:17 -0700958 return self.col.build(color, str(num))
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000959
960 def ResetResultSummary(self, board_selected):
961 """Reset the results summary ready for use.
962
963 Set up the base board list to be all those selected, and set the
964 error lines to empty.
965
966 Following this, calls to PrintResultSummary() will use this
967 information to work out what has changed.
968
969 Args:
970 board_selected: Dict containing boards to summarise, keyed by
971 board.target
972 """
973 self._base_board_dict = {}
974 for board in board_selected:
Alex Kiernan48ae4122018-05-31 04:48:34 +0000975 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
976 {})
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000977 self._base_err_lines = []
Simon Glasse30965d2014-08-28 09:43:44 -0600978 self._base_warn_lines = []
979 self._base_err_line_boards = {}
980 self._base_warn_line_boards = {}
Simon Glass8270e3c2015-08-25 21:52:14 -0600981 self._base_config = None
Alex Kiernan48ae4122018-05-31 04:48:34 +0000982 self._base_environment = None
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000983
984 def PrintFuncSizeDetail(self, fname, old, new):
985 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
986 delta, common = [], {}
987
988 for a in old:
989 if a in new:
990 common[a] = 1
991
992 for name in old:
993 if name not in common:
994 remove += 1
995 down += old[name]
996 delta.append([-old[name], name])
997
998 for name in new:
999 if name not in common:
1000 add += 1
1001 up += new[name]
1002 delta.append([new[name], name])
1003
1004 for name in common:
1005 diff = new.get(name, 0) - old.get(name, 0)
1006 if diff > 0:
1007 grow, up = grow + 1, up + diff
1008 elif diff < 0:
1009 shrink, down = shrink + 1, down - diff
1010 delta.append([diff, name])
1011
1012 delta.sort()
1013 delta.reverse()
1014
1015 args = [add, -remove, grow, -shrink, up, -down, up - down]
Tom Rinid5686a62017-05-22 13:48:52 -04001016 if max(args) == 0 and min(args) == 0:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001017 return
1018 args = [self.ColourNum(x) for x in args]
1019 indent = ' ' * 15
Simon Glass098b10f2022-01-29 14:14:18 -07001020 tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
Simon Glass252ac582022-01-29 14:14:17 -07001021 tuple([indent, self.col.build(self.col.YELLOW, fname)] + args))
Simon Glass098b10f2022-01-29 14:14:18 -07001022 tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
Simon Glass4653a882014-09-05 19:00:07 -06001023 'delta'))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001024 for diff, name in delta:
1025 if diff:
1026 color = self.col.RED if diff > 0 else self.col.GREEN
1027 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1028 old.get(name, '-'), new.get(name,'-'), diff)
Simon Glass098b10f2022-01-29 14:14:18 -07001029 tprint(msg, colour=color)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001030
1031
1032 def PrintSizeDetail(self, target_list, show_bloat):
1033 """Show details size information for each board
1034
1035 Args:
1036 target_list: List of targets, each a dict containing:
1037 'target': Target name
1038 'total_diff': Total difference in bytes across all areas
1039 <part_name>: Difference for that part
1040 show_bloat: Show detail for each function
1041 """
1042 targets_by_diff = sorted(target_list, reverse=True,
1043 key=lambda x: x['_total_diff'])
1044 for result in targets_by_diff:
1045 printed_target = False
1046 for name in sorted(result):
1047 diff = result[name]
1048 if name.startswith('_'):
1049 continue
1050 if diff != 0:
1051 color = self.col.RED if diff > 0 else self.col.GREEN
1052 msg = ' %s %+d' % (name, diff)
1053 if not printed_target:
Simon Glass098b10f2022-01-29 14:14:18 -07001054 tprint('%10s %-15s:' % ('', result['_target']),
Simon Glass4653a882014-09-05 19:00:07 -06001055 newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001056 printed_target = True
Simon Glass098b10f2022-01-29 14:14:18 -07001057 tprint(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001058 if printed_target:
Simon Glass098b10f2022-01-29 14:14:18 -07001059 tprint()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001060 if show_bloat:
1061 target = result['_target']
1062 outcome = result['_outcome']
1063 base_outcome = self._base_board_dict[target]
1064 for fname in outcome.func_sizes:
1065 self.PrintFuncSizeDetail(fname,
1066 base_outcome.func_sizes[fname],
1067 outcome.func_sizes[fname])
1068
1069
1070 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
1071 show_bloat):
1072 """Print a summary of image sizes broken down by section.
1073
1074 The summary takes the form of one line per architecture. The
1075 line contains deltas for each of the sections (+ means the section
Flavio Suligoi9de5c392020-01-29 09:56:05 +01001076 got bigger, - means smaller). The numbers are the average number
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001077 of bytes that a board in this section increased by.
1078
1079 For example:
1080 powerpc: (622 boards) text -0.0
1081 arm: (285 boards) text -0.0
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001082
1083 Args:
1084 board_selected: Dict containing boards to summarise, keyed by
1085 board.target
1086 board_dict: Dict containing boards for which we built this
1087 commit, keyed by board.target. The value is an Outcome object.
Simon Glassf9c094b2020-03-18 09:42:43 -06001088 show_detail: Show size delta detail for each board
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001089 show_bloat: Show detail for each function
1090 """
1091 arch_list = {}
1092 arch_count = {}
1093
1094 # Calculate changes in size for different image parts
1095 # The previous sizes are in Board.sizes, for each board
1096 for target in board_dict:
1097 if target not in board_selected:
1098 continue
1099 base_sizes = self._base_board_dict[target].sizes
1100 outcome = board_dict[target]
1101 sizes = outcome.sizes
1102
1103 # Loop through the list of images, creating a dict of size
1104 # changes for each image/part. We end up with something like
1105 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1106 # which means that U-Boot data increased by 5 bytes and SPL
1107 # text decreased by 4.
1108 err = {'_target' : target}
1109 for image in sizes:
1110 if image in base_sizes:
1111 base_image = base_sizes[image]
1112 # Loop through the text, data, bss parts
1113 for part in sorted(sizes[image]):
1114 diff = sizes[image][part] - base_image[part]
1115 col = None
1116 if diff:
1117 if image == 'u-boot':
1118 name = part
1119 else:
1120 name = image + ':' + part
1121 err[name] = diff
1122 arch = board_selected[target].arch
1123 if not arch in arch_count:
1124 arch_count[arch] = 1
1125 else:
1126 arch_count[arch] += 1
1127 if not sizes:
1128 pass # Only add to our list when we have some stats
1129 elif not arch in arch_list:
1130 arch_list[arch] = [err]
1131 else:
1132 arch_list[arch].append(err)
1133
1134 # We now have a list of image size changes sorted by arch
1135 # Print out a summary of these
Simon Glassc05aa032019-10-31 07:42:53 -06001136 for arch, target_list in arch_list.items():
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001137 # Get total difference for each type
1138 totals = {}
1139 for result in target_list:
1140 total = 0
Simon Glassc05aa032019-10-31 07:42:53 -06001141 for name, diff in result.items():
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001142 if name.startswith('_'):
1143 continue
1144 total += diff
1145 if name in totals:
1146 totals[name] += diff
1147 else:
1148 totals[name] = diff
1149 result['_total_diff'] = total
1150 result['_outcome'] = board_dict[result['_target']]
1151
1152 count = len(target_list)
1153 printed_arch = False
1154 for name in sorted(totals):
1155 diff = totals[name]
1156 if diff:
1157 # Display the average difference in this name for this
1158 # architecture
1159 avg_diff = float(diff) / count
1160 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1161 msg = ' %s %+1.1f' % (name, avg_diff)
1162 if not printed_arch:
Simon Glass098b10f2022-01-29 14:14:18 -07001163 tprint('%10s: (for %d/%d boards)' % (arch, count,
Simon Glass4653a882014-09-05 19:00:07 -06001164 arch_count[arch]), newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001165 printed_arch = True
Simon Glass098b10f2022-01-29 14:14:18 -07001166 tprint(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001167
1168 if printed_arch:
Simon Glass098b10f2022-01-29 14:14:18 -07001169 tprint()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001170 if show_detail:
1171 self.PrintSizeDetail(target_list, show_bloat)
1172
1173
1174 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glasse30965d2014-08-28 09:43:44 -06001175 err_line_boards, warn_lines, warn_line_boards,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001176 config, environment, show_sizes, show_detail,
1177 show_bloat, show_config, show_environment):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001178 """Compare results with the base results and display delta.
1179
1180 Only boards mentioned in board_selected will be considered. This
1181 function is intended to be called repeatedly with the results of
1182 each commit. It therefore shows a 'diff' between what it saw in
1183 the last call and what it sees now.
1184
1185 Args:
1186 board_selected: Dict containing boards to summarise, keyed by
1187 board.target
1188 board_dict: Dict containing boards for which we built this
1189 commit, keyed by board.target. The value is an Outcome object.
1190 err_lines: A list of errors for this commit, or [] if there is
1191 none, or we don't want to print errors
Simon Glassed966652014-08-28 09:43:43 -06001192 err_line_boards: Dict keyed by error line, containing a list of
1193 the Board objects with that error
Simon Glasse30965d2014-08-28 09:43:44 -06001194 warn_lines: A list of warnings for this commit, or [] if there is
1195 none, or we don't want to print errors
1196 warn_line_boards: Dict keyed by warning line, containing a list of
1197 the Board objects with that warning
Simon Glass843312d2015-02-05 22:06:15 -07001198 config: Dictionary keyed by filename - e.g. '.config'. Each
1199 value is itself a dictionary:
1200 key: config name
1201 value: config value
Alex Kiernan48ae4122018-05-31 04:48:34 +00001202 environment: Dictionary keyed by environment variable, Each
1203 value is the value of environment variable.
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001204 show_sizes: Show image size deltas
Simon Glassf9c094b2020-03-18 09:42:43 -06001205 show_detail: Show size delta detail for each board if show_sizes
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001206 show_bloat: Show detail for each function
Simon Glass843312d2015-02-05 22:06:15 -07001207 show_config: Show config changes
Alex Kiernan48ae4122018-05-31 04:48:34 +00001208 show_environment: Show environment changes
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001209 """
Simon Glasse30965d2014-08-28 09:43:44 -06001210 def _BoardList(line, line_boards):
Simon Glassed966652014-08-28 09:43:43 -06001211 """Helper function to get a line of boards containing a line
1212
1213 Args:
1214 line: Error line to search for
Simon Glass35d696d2020-04-09 15:08:36 -06001215 line_boards: boards to search, each a Board
Simon Glassed966652014-08-28 09:43:43 -06001216 Return:
Simon Glass35d696d2020-04-09 15:08:36 -06001217 List of boards with that error line, or [] if the user has not
1218 requested such a list
Simon Glassed966652014-08-28 09:43:43 -06001219 """
Simon Glass35d696d2020-04-09 15:08:36 -06001220 boards = []
1221 board_set = set()
Simon Glassed966652014-08-28 09:43:43 -06001222 if self._list_error_boards:
Simon Glasse30965d2014-08-28 09:43:44 -06001223 for board in line_boards[line]:
Simon Glass35d696d2020-04-09 15:08:36 -06001224 if not board in board_set:
1225 boards.append(board)
1226 board_set.add(board)
1227 return boards
Simon Glassed966652014-08-28 09:43:43 -06001228
Simon Glasse30965d2014-08-28 09:43:44 -06001229 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1230 char):
Simon Glass35d696d2020-04-09 15:08:36 -06001231 """Calculate the required output based on changes in errors
1232
1233 Args:
1234 base_lines: List of errors/warnings for previous commit
1235 base_line_boards: Dict keyed by error line, containing a list
1236 of the Board objects with that error in the previous commit
1237 lines: List of errors/warning for this commit, each a str
1238 line_boards: Dict keyed by error line, containing a list
1239 of the Board objects with that error in this commit
1240 char: Character representing error ('') or warning ('w'). The
1241 broken ('+') or fixed ('-') characters are added in this
1242 function
1243
1244 Returns:
1245 Tuple
1246 List of ErrLine objects for 'better' lines
1247 List of ErrLine objects for 'worse' lines
1248 """
Simon Glasse30965d2014-08-28 09:43:44 -06001249 better_lines = []
1250 worse_lines = []
1251 for line in lines:
1252 if line not in base_lines:
Simon Glass35d696d2020-04-09 15:08:36 -06001253 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1254 line)
1255 worse_lines.append(errline)
Simon Glasse30965d2014-08-28 09:43:44 -06001256 for line in base_lines:
1257 if line not in lines:
Simon Glass35d696d2020-04-09 15:08:36 -06001258 errline = ErrLine(char + '-',
1259 _BoardList(line, base_line_boards), line)
1260 better_lines.append(errline)
Simon Glasse30965d2014-08-28 09:43:44 -06001261 return better_lines, worse_lines
1262
Simon Glass843312d2015-02-05 22:06:15 -07001263 def _CalcConfig(delta, name, config):
1264 """Calculate configuration changes
1265
1266 Args:
1267 delta: Type of the delta, e.g. '+'
1268 name: name of the file which changed (e.g. .config)
1269 config: configuration change dictionary
1270 key: config name
1271 value: config value
1272 Returns:
1273 String containing the configuration changes which can be
1274 printed
1275 """
1276 out = ''
1277 for key in sorted(config.keys()):
1278 out += '%s=%s ' % (key, config[key])
Simon Glass8270e3c2015-08-25 21:52:14 -06001279 return '%s %s: %s' % (delta, name, out)
Simon Glass843312d2015-02-05 22:06:15 -07001280
Simon Glass8270e3c2015-08-25 21:52:14 -06001281 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1282 """Add changes in configuration to a list
Simon Glass843312d2015-02-05 22:06:15 -07001283
1284 Args:
Simon Glass8270e3c2015-08-25 21:52:14 -06001285 lines: list to add to
1286 name: config file name
Simon Glass843312d2015-02-05 22:06:15 -07001287 config_plus: configurations added, dictionary
1288 key: config name
1289 value: config value
1290 config_minus: configurations removed, dictionary
1291 key: config name
1292 value: config value
1293 config_change: configurations changed, dictionary
1294 key: config name
1295 value: config value
1296 """
1297 if config_plus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001298 lines.append(_CalcConfig('+', name, config_plus))
Simon Glass843312d2015-02-05 22:06:15 -07001299 if config_minus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001300 lines.append(_CalcConfig('-', name, config_minus))
Simon Glass843312d2015-02-05 22:06:15 -07001301 if config_change:
Simon Glass8270e3c2015-08-25 21:52:14 -06001302 lines.append(_CalcConfig('c', name, config_change))
1303
1304 def _OutputConfigInfo(lines):
1305 for line in lines:
1306 if not line:
1307 continue
1308 if line[0] == '+':
1309 col = self.col.GREEN
1310 elif line[0] == '-':
1311 col = self.col.RED
1312 elif line[0] == 'c':
1313 col = self.col.YELLOW
Simon Glass098b10f2022-01-29 14:14:18 -07001314 tprint(' ' + line, newline=True, colour=col)
Simon Glass8270e3c2015-08-25 21:52:14 -06001315
Simon Glassb206d872020-04-09 15:08:28 -06001316 def _OutputErrLines(err_lines, colour):
1317 """Output the line of error/warning lines, if not empty
1318
1319 Also increments self._error_lines if err_lines not empty
1320
1321 Args:
Simon Glass35d696d2020-04-09 15:08:36 -06001322 err_lines: List of ErrLine objects, each an error or warning
1323 line, possibly including a list of boards with that
1324 error/warning
Simon Glassb206d872020-04-09 15:08:28 -06001325 colour: Colour to use for output
1326 """
1327 if err_lines:
Simon Glass8c9a2672020-04-09 15:08:37 -06001328 out_list = []
Simon Glass35d696d2020-04-09 15:08:36 -06001329 for line in err_lines:
1330 boards = ''
1331 names = [board.target for board in line.boards]
Simon Glass9ef0ceb2020-04-09 15:08:38 -06001332 board_str = ' '.join(names) if names else ''
Simon Glass8c9a2672020-04-09 15:08:37 -06001333 if board_str:
Simon Glass252ac582022-01-29 14:14:17 -07001334 out = self.col.build(colour, line.char + '(')
1335 out += self.col.build(self.col.MAGENTA, board_str,
Simon Glass8c9a2672020-04-09 15:08:37 -06001336 bright=False)
Simon Glass252ac582022-01-29 14:14:17 -07001337 out += self.col.build(colour, ') %s' % line.errline)
Simon Glass8c9a2672020-04-09 15:08:37 -06001338 else:
Simon Glass252ac582022-01-29 14:14:17 -07001339 out = self.col.build(colour, line.char + line.errline)
Simon Glass8c9a2672020-04-09 15:08:37 -06001340 out_list.append(out)
Simon Glass098b10f2022-01-29 14:14:18 -07001341 tprint('\n'.join(out_list))
Simon Glassb206d872020-04-09 15:08:28 -06001342 self._error_lines += 1
1343
Simon Glass843312d2015-02-05 22:06:15 -07001344
Simon Glass4cf2b222018-11-06 16:02:12 -07001345 ok_boards = [] # List of boards fixed since last commit
Simon Glass6af71012018-11-06 16:02:13 -07001346 warn_boards = [] # List of boards with warnings since last commit
Simon Glass4cf2b222018-11-06 16:02:12 -07001347 err_boards = [] # List of new broken boards since last commit
1348 new_boards = [] # List of boards that didn't exist last time
1349 unknown_boards = [] # List of boards that were not built
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001350
1351 for target in board_dict:
1352 if target not in board_selected:
1353 continue
1354
1355 # If the board was built last time, add its outcome to a list
1356 if target in self._base_board_dict:
1357 base_outcome = self._base_board_dict[target].rc
1358 outcome = board_dict[target]
1359 if outcome.rc == OUTCOME_UNKNOWN:
Simon Glass4cf2b222018-11-06 16:02:12 -07001360 unknown_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001361 elif outcome.rc < base_outcome:
Simon Glass6af71012018-11-06 16:02:13 -07001362 if outcome.rc == OUTCOME_WARNING:
1363 warn_boards.append(target)
1364 else:
1365 ok_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001366 elif outcome.rc > base_outcome:
Simon Glass6af71012018-11-06 16:02:13 -07001367 if outcome.rc == OUTCOME_WARNING:
1368 warn_boards.append(target)
1369 else:
1370 err_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001371 else:
Simon Glass4cf2b222018-11-06 16:02:12 -07001372 new_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001373
Simon Glassb206d872020-04-09 15:08:28 -06001374 # Get a list of errors and warnings that have appeared, and disappeared
Simon Glasse30965d2014-08-28 09:43:44 -06001375 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1376 self._base_err_line_boards, err_lines, err_line_boards, '')
1377 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1378 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001379
Simon Glassae1a09f2022-07-11 19:03:56 -06001380 # For the IDE mode, print out all the output
1381 if self._ide:
1382 outcome = board_dict[target]
1383 for line in outcome.err_lines:
1384 sys.stderr.write(line)
1385
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001386 # Display results by arch
Simon Glassae1a09f2022-07-11 19:03:56 -06001387 elif any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
Simon Glass6af71012018-11-06 16:02:13 -07001388 worse_err, better_err, worse_warn, better_warn)):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001389 arch_list = {}
Simon Glass4cf2b222018-11-06 16:02:12 -07001390 self.AddOutcome(board_selected, arch_list, ok_boards, '',
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001391 self.col.GREEN)
Simon Glass6af71012018-11-06 16:02:13 -07001392 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1393 self.col.YELLOW)
Simon Glass4cf2b222018-11-06 16:02:12 -07001394 self.AddOutcome(board_selected, arch_list, err_boards, '+',
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001395 self.col.RED)
Simon Glass4cf2b222018-11-06 16:02:12 -07001396 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001397 if self._show_unknown:
Simon Glass4cf2b222018-11-06 16:02:12 -07001398 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001399 self.col.MAGENTA)
Simon Glassc05aa032019-10-31 07:42:53 -06001400 for arch, target_list in arch_list.items():
Simon Glass098b10f2022-01-29 14:14:18 -07001401 tprint('%10s: %s' % (arch, target_list))
Simon Glass28370c12014-08-09 15:33:06 -06001402 self._error_lines += 1
Simon Glassb206d872020-04-09 15:08:28 -06001403 _OutputErrLines(better_err, colour=self.col.GREEN)
1404 _OutputErrLines(worse_err, colour=self.col.RED)
1405 _OutputErrLines(better_warn, colour=self.col.CYAN)
Simon Glass5627bd92020-04-09 15:08:35 -06001406 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001407
1408 if show_sizes:
1409 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1410 show_bloat)
1411
Alex Kiernan48ae4122018-05-31 04:48:34 +00001412 if show_environment and self._base_environment:
1413 lines = []
1414
1415 for target in board_dict:
1416 if target not in board_selected:
1417 continue
1418
1419 tbase = self._base_environment[target]
1420 tenvironment = environment[target]
1421 environment_plus = {}
1422 environment_minus = {}
1423 environment_change = {}
1424 base = tbase.environment
Simon Glassc05aa032019-10-31 07:42:53 -06001425 for key, value in tenvironment.environment.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +00001426 if key not in base:
1427 environment_plus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001428 for key, value in base.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +00001429 if key not in tenvironment.environment:
1430 environment_minus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001431 for key, value in base.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +00001432 new_value = tenvironment.environment.get(key)
1433 if new_value and value != new_value:
1434 desc = '%s -> %s' % (value, new_value)
1435 environment_change[key] = desc
1436
1437 _AddConfig(lines, target, environment_plus, environment_minus,
1438 environment_change)
1439
1440 _OutputConfigInfo(lines)
1441
Simon Glass8270e3c2015-08-25 21:52:14 -06001442 if show_config and self._base_config:
1443 summary = {}
1444 arch_config_plus = {}
1445 arch_config_minus = {}
1446 arch_config_change = {}
1447 arch_list = []
1448
1449 for target in board_dict:
1450 if target not in board_selected:
Simon Glass843312d2015-02-05 22:06:15 -07001451 continue
Simon Glass8270e3c2015-08-25 21:52:14 -06001452 arch = board_selected[target].arch
1453 if arch not in arch_list:
1454 arch_list.append(arch)
1455
1456 for arch in arch_list:
1457 arch_config_plus[arch] = {}
1458 arch_config_minus[arch] = {}
1459 arch_config_change[arch] = {}
Simon Glassb464f8e2016-11-13 14:25:53 -07001460 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001461 arch_config_plus[arch][name] = {}
1462 arch_config_minus[arch][name] = {}
1463 arch_config_change[arch][name] = {}
1464
1465 for target in board_dict:
1466 if target not in board_selected:
1467 continue
1468
1469 arch = board_selected[target].arch
1470
1471 all_config_plus = {}
1472 all_config_minus = {}
1473 all_config_change = {}
1474 tbase = self._base_config[target]
1475 tconfig = config[target]
1476 lines = []
Simon Glassb464f8e2016-11-13 14:25:53 -07001477 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001478 if not tconfig.config[name]:
1479 continue
1480 config_plus = {}
1481 config_minus = {}
1482 config_change = {}
1483 base = tbase.config[name]
Simon Glassc05aa032019-10-31 07:42:53 -06001484 for key, value in tconfig.config[name].items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001485 if key not in base:
1486 config_plus[key] = value
1487 all_config_plus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001488 for key, value in base.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001489 if key not in tconfig.config[name]:
1490 config_minus[key] = value
1491 all_config_minus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001492 for key, value in base.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001493 new_value = tconfig.config.get(key)
1494 if new_value and value != new_value:
1495 desc = '%s -> %s' % (value, new_value)
1496 config_change[key] = desc
1497 all_config_change[key] = desc
1498
1499 arch_config_plus[arch][name].update(config_plus)
1500 arch_config_minus[arch][name].update(config_minus)
1501 arch_config_change[arch][name].update(config_change)
1502
1503 _AddConfig(lines, name, config_plus, config_minus,
1504 config_change)
1505 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1506 all_config_change)
1507 summary[target] = '\n'.join(lines)
1508
1509 lines_by_target = {}
Simon Glassc05aa032019-10-31 07:42:53 -06001510 for target, lines in summary.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001511 if lines in lines_by_target:
1512 lines_by_target[lines].append(target)
1513 else:
1514 lines_by_target[lines] = [target]
1515
1516 for arch in arch_list:
1517 lines = []
1518 all_plus = {}
1519 all_minus = {}
1520 all_change = {}
Simon Glassb464f8e2016-11-13 14:25:53 -07001521 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001522 all_plus.update(arch_config_plus[arch][name])
1523 all_minus.update(arch_config_minus[arch][name])
1524 all_change.update(arch_config_change[arch][name])
1525 _AddConfig(lines, name, arch_config_plus[arch][name],
1526 arch_config_minus[arch][name],
1527 arch_config_change[arch][name])
1528 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1529 #arch_summary[target] = '\n'.join(lines)
1530 if lines:
Simon Glass098b10f2022-01-29 14:14:18 -07001531 tprint('%s:' % arch)
Simon Glass8270e3c2015-08-25 21:52:14 -06001532 _OutputConfigInfo(lines)
1533
Simon Glassc05aa032019-10-31 07:42:53 -06001534 for lines, targets in lines_by_target.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001535 if not lines:
1536 continue
Simon Glass098b10f2022-01-29 14:14:18 -07001537 tprint('%s :' % ' '.join(sorted(targets)))
Simon Glass8270e3c2015-08-25 21:52:14 -06001538 _OutputConfigInfo(lines.split('\n'))
1539
Simon Glass843312d2015-02-05 22:06:15 -07001540
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001541 # Save our updated information for the next call to this function
1542 self._base_board_dict = board_dict
1543 self._base_err_lines = err_lines
Simon Glasse30965d2014-08-28 09:43:44 -06001544 self._base_warn_lines = warn_lines
1545 self._base_err_line_boards = err_line_boards
1546 self._base_warn_line_boards = warn_line_boards
Simon Glass843312d2015-02-05 22:06:15 -07001547 self._base_config = config
Alex Kiernan48ae4122018-05-31 04:48:34 +00001548 self._base_environment = environment
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001549
1550 # Get a list of boards that did not get built, if needed
1551 not_built = []
1552 for board in board_selected:
1553 if not board in board_dict:
1554 not_built.append(board)
1555 if not_built:
Simon Glass098b10f2022-01-29 14:14:18 -07001556 tprint("Boards not built (%d): %s" % (len(not_built),
Simon Glass4653a882014-09-05 19:00:07 -06001557 ', '.join(not_built)))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001558
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001559 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glasse30965d2014-08-28 09:43:44 -06001560 (board_dict, err_lines, err_line_boards, warn_lines,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001561 warn_line_boards, config, environment) = self.GetResultSummary(
Simon Glassed966652014-08-28 09:43:43 -06001562 board_selected, commit_upto,
Simon Glass843312d2015-02-05 22:06:15 -07001563 read_func_sizes=self._show_bloat,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001564 read_config=self._show_config,
1565 read_environment=self._show_environment)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001566 if commits:
1567 msg = '%02d: %s' % (commit_upto + 1,
1568 commits[commit_upto].subject)
Simon Glass098b10f2022-01-29 14:14:18 -07001569 tprint(msg, colour=self.col.BLUE)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001570 self.PrintResultSummary(board_selected, board_dict,
Simon Glassed966652014-08-28 09:43:43 -06001571 err_lines if self._show_errors else [], err_line_boards,
Simon Glasse30965d2014-08-28 09:43:44 -06001572 warn_lines if self._show_errors else [], warn_line_boards,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001573 config, environment, self._show_sizes, self._show_detail,
1574 self._show_bloat, self._show_config, self._show_environment)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001575
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001576 def ShowSummary(self, commits, board_selected):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001577 """Show a build summary for U-Boot for a given board list.
1578
1579 Reset the result summary, then repeatedly call GetResultSummary on
1580 each commit's results, then display the differences we see.
1581
1582 Args:
1583 commit: Commit objects to summarise
1584 board_selected: Dict containing boards to summarise
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001585 """
Simon Glassfea58582014-08-09 15:32:59 -06001586 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001587 self.commits = commits
1588 self.ResetResultSummary(board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001589 self._error_lines = 0
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001590
1591 for commit_upto in range(0, self.commit_count, self._step):
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001592 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001593 if not self._error_lines:
Simon Glass098b10f2022-01-29 14:14:18 -07001594 tprint('(no errors to report)', colour=self.col.GREEN)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001595
1596
1597 def SetupBuild(self, board_selected, commits):
1598 """Set up ready to start a build.
1599
1600 Args:
1601 board_selected: Selected boards to build
1602 commits: Selected commits to build
1603 """
1604 # First work out how many commits we will build
Simon Glassc05aa032019-10-31 07:42:53 -06001605 count = (self.commit_count + self._step - 1) // self._step
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001606 self.count = len(board_selected) * count
1607 self.upto = self.warned = self.fail = 0
1608 self._timestamps = collections.deque()
1609
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001610 def GetThreadDir(self, thread_num):
1611 """Get the directory path to the working dir for a thread.
1612
1613 Args:
Simon Glassb82492b2021-01-30 22:17:46 -07001614 thread_num: Number of thread to check (-1 for main process, which
1615 is treated as 0)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001616 """
Simon Glassd829f122020-03-18 09:42:42 -06001617 if self.work_in_output:
1618 return self._working_dir
Simon Glassb82492b2021-01-30 22:17:46 -07001619 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001620
Simon Glassfea58582014-08-09 15:32:59 -06001621 def _PrepareThread(self, thread_num, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001622 """Prepare the working directory for a thread.
1623
1624 This clones or fetches the repo into the thread's work directory.
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001625 Optionally, it can create a linked working tree of the repo in the
1626 thread's work directory instead.
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001627
1628 Args:
1629 thread_num: Thread number (0, 1, ...)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001630 setup_git:
1631 'clone' to set up a git clone
1632 'worktree' to set up a git worktree
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001633 """
1634 thread_dir = self.GetThreadDir(thread_num)
Simon Glass190064b2014-08-09 15:33:00 -06001635 builderthread.Mkdir(thread_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001636 git_dir = os.path.join(thread_dir, '.git')
1637
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001638 # Create a worktree or a git repo clone for this thread if it
1639 # doesn't already exist
Simon Glassfea58582014-08-09 15:32:59 -06001640 if setup_git and self.git_dir:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001641 src_dir = os.path.abspath(self.git_dir)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001642 if os.path.isdir(git_dir):
1643 # This is a clone of the src_dir repo, we can keep using
1644 # it but need to fetch from src_dir.
Simon Glass098b10f2022-01-29 14:14:18 -07001645 tprint('\rFetching repo for thread %d' % thread_num,
Simon Glass212c0b82020-04-09 15:08:43 -06001646 newline=False)
Simon Glass0157b182022-01-29 14:14:11 -07001647 gitutil.fetch(git_dir, thread_dir)
Simon Glass098b10f2022-01-29 14:14:18 -07001648 terminal.print_clear()
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001649 elif os.path.isfile(git_dir):
1650 # This is a worktree of the src_dir repo, we don't need to
1651 # create it again or update it in any way.
1652 pass
1653 elif os.path.exists(git_dir):
1654 # Don't know what could trigger this, but we probably
1655 # can't create a git worktree/clone here.
1656 raise ValueError('Git dir %s exists, but is not a file '
1657 'or a directory.' % git_dir)
1658 elif setup_git == 'worktree':
Simon Glass098b10f2022-01-29 14:14:18 -07001659 tprint('\rChecking out worktree for thread %d' % thread_num,
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001660 newline=False)
Simon Glass0157b182022-01-29 14:14:11 -07001661 gitutil.add_worktree(src_dir, thread_dir)
Simon Glass098b10f2022-01-29 14:14:18 -07001662 terminal.print_clear()
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001663 elif setup_git == 'clone' or setup_git == True:
Simon Glass098b10f2022-01-29 14:14:18 -07001664 tprint('\rCloning repo for thread %d' % thread_num,
Simon Glass21f0eb32016-09-18 16:48:31 -06001665 newline=False)
Simon Glass0157b182022-01-29 14:14:11 -07001666 gitutil.clone(src_dir, thread_dir)
Simon Glass098b10f2022-01-29 14:14:18 -07001667 terminal.print_clear()
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001668 else:
1669 raise ValueError("Can't setup git repo with %s." % setup_git)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001670
Simon Glassfea58582014-08-09 15:32:59 -06001671 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001672 """Prepare the working directory for use.
1673
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001674 Set up the git repo for each thread. Creates a linked working tree
1675 if git-worktree is available, or clones the repo if it isn't.
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001676
1677 Args:
Simon Glassb82492b2021-01-30 22:17:46 -07001678 max_threads: Maximum number of threads we expect to need. If 0 then
1679 1 is set up, since the main process still needs somewhere to
1680 work
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001681 setup_git: True to set up a git worktree or a git clone
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001682 """
Simon Glass190064b2014-08-09 15:33:00 -06001683 builderthread.Mkdir(self._working_dir)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001684 if setup_git and self.git_dir:
1685 src_dir = os.path.abspath(self.git_dir)
Simon Glass0157b182022-01-29 14:14:11 -07001686 if gitutil.check_worktree_is_available(src_dir):
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001687 setup_git = 'worktree'
1688 # If we previously added a worktree but the directory for it
1689 # got deleted, we need to prune its files from the repo so
1690 # that we can check out another in its place.
Simon Glass0157b182022-01-29 14:14:11 -07001691 gitutil.prune_worktrees(src_dir)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001692 else:
1693 setup_git = 'clone'
Simon Glassb82492b2021-01-30 22:17:46 -07001694
1695 # Always do at least one thread
1696 for thread in range(max(max_threads, 1)):
Simon Glassfea58582014-08-09 15:32:59 -06001697 self._PrepareThread(thread, setup_git)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001698
Simon Glass925f6ad2020-03-18 09:42:45 -06001699 def _GetOutputSpaceRemovals(self):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001700 """Get the output directories ready to receive files.
1701
Simon Glass925f6ad2020-03-18 09:42:45 -06001702 Figure out what needs to be deleted in the output directory before it
1703 can be used. We only delete old buildman directories which have the
1704 expected name pattern. See _GetOutputDir().
1705
1706 Returns:
1707 List of full paths of directories to remove
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001708 """
Simon Glass1a915672014-12-01 17:33:53 -07001709 if not self.commits:
1710 return
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001711 dir_list = []
1712 for commit_upto in range(self.commit_count):
1713 dir_list.append(self._GetOutputDir(commit_upto))
1714
Simon Glassb222abe2016-09-18 16:48:32 -06001715 to_remove = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001716 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1717 if dirname not in dir_list:
Simon Glass925f6ad2020-03-18 09:42:45 -06001718 leaf = dirname[len(self.base_dir) + 1:]
Ovidiu Panait7664b032020-05-15 09:30:12 +03001719 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
Simon Glass925f6ad2020-03-18 09:42:45 -06001720 if m:
1721 to_remove.append(dirname)
1722 return to_remove
1723
1724 def _PrepareOutputSpace(self):
1725 """Get the output directories ready to receive files.
1726
1727 We delete any output directories which look like ones we need to
1728 create. Having left over directories is confusing when the user wants
1729 to check the output manually.
1730 """
1731 to_remove = self._GetOutputSpaceRemovals()
Simon Glassb222abe2016-09-18 16:48:32 -06001732 if to_remove:
Simon Glass098b10f2022-01-29 14:14:18 -07001733 tprint('Removing %d old build directories...' % len(to_remove),
Simon Glassb222abe2016-09-18 16:48:32 -06001734 newline=False)
1735 for dirname in to_remove:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001736 shutil.rmtree(dirname)
Simon Glass098b10f2022-01-29 14:14:18 -07001737 terminal.print_clear()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001738
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001739 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001740 """Build all commits for a list of boards
1741
1742 Args:
1743 commits: List of commits to be build, each a Commit object
1744 boards_selected: Dict of selected boards, key is target name,
1745 value is Board object
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001746 keep_outputs: True to save build output files
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001747 verbose: Display build results as they are completed
Simon Glass2c3deb92014-08-28 09:43:39 -06001748 Returns:
1749 Tuple containing:
1750 - number of boards that failed to build
1751 - number of boards that issued warnings
Simon Glass8116c782021-04-11 16:27:27 +12001752 - list of thread exceptions raised
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001753 """
Simon Glassfea58582014-08-09 15:32:59 -06001754 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001755 self.commits = commits
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001756 self._verbose = verbose
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001757
1758 self.ResetResultSummary(board_selected)
Thierry Redingf3d015c2014-08-19 10:22:39 +02001759 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassfea58582014-08-09 15:32:59 -06001760 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1761 commits is not None)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001762 self._PrepareOutputSpace()
Simon Glassae1a09f2022-07-11 19:03:56 -06001763 if not self._ide:
1764 tprint('\rStarting build...', newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001765 self.SetupBuild(board_selected, commits)
1766 self.ProcessResult(None)
Simon Glass8116c782021-04-11 16:27:27 +12001767 self.thread_exceptions = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001768 # Create jobs to build all commits for each board
Simon Glassc05aa032019-10-31 07:42:53 -06001769 for brd in board_selected.values():
Simon Glass190064b2014-08-09 15:33:00 -06001770 job = builderthread.BuilderJob()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001771 job.board = brd
1772 job.commits = commits
1773 job.keep_outputs = keep_outputs
Simon Glassd829f122020-03-18 09:42:42 -06001774 job.work_in_output = self.work_in_output
Simon Glass2b4806e2022-01-22 05:07:33 -07001775 job.adjust_cfg = self.adjust_cfg
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001776 job.step = self._step
Simon Glassb82492b2021-01-30 22:17:46 -07001777 if self.num_threads:
1778 self.queue.put(job)
1779 else:
Simon Glassac053352022-02-11 13:23:19 -07001780 self._single_builder.RunJob(job)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001781
Simon Glassb82492b2021-01-30 22:17:46 -07001782 if self.num_threads:
1783 term = threading.Thread(target=self.queue.join)
1784 term.setDaemon(True)
1785 term.start()
1786 while term.is_alive():
1787 term.join(100)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001788
Simon Glassb82492b2021-01-30 22:17:46 -07001789 # Wait until we have processed all output
1790 self.out_queue.join()
Simon Glassae1a09f2022-07-11 19:03:56 -06001791 if not self._ide:
1792 tprint()
Simon Glass7b33f212020-04-09 15:08:47 -06001793
Simon Glassae1a09f2022-07-11 19:03:56 -06001794 msg = 'Completed: %d total built' % self.count
1795 if self.already_done:
1796 msg += ' (%d previously' % self.already_done
1797 if self.already_done != self.count:
1798 msg += ', %d newly' % (self.count - self.already_done)
1799 msg += ')'
1800 duration = datetime.now() - self._start_time
1801 if duration > timedelta(microseconds=1000000):
1802 if duration.microseconds >= 500000:
1803 duration = duration + timedelta(seconds=1)
1804 duration = duration - timedelta(microseconds=duration.microseconds)
1805 rate = float(self.count) / duration.total_seconds()
1806 msg += ', duration %s, rate %1.2f' % (duration, rate)
1807 tprint(msg)
1808 if self.thread_exceptions:
1809 tprint('Failed: %d thread exceptions' % len(self.thread_exceptions),
1810 colour=self.col.RED)
Simon Glass7b33f212020-04-09 15:08:47 -06001811
Simon Glass8116c782021-04-11 16:27:27 +12001812 return (self.fail, self.warned, self.thread_exceptions)