blob: c2a69027f885b74a82d19644fbf0b59f07eebef9 [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
Simon Glassc78ed662019-10-31 07:42:53 -060012import queue
Simon Glassc05694f2013-04-03 11:07:16 +000013import 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 Glassf0d9c102020-04-17 18:09:02 -060020from buildman import builderthread
21from buildman import toolchain
Simon Glassa997ea52020-04-17 18:09:04 -060022from patman import command
23from patman import gitutil
24from patman import terminal
Simon Glass02811582022-01-29 14:14:18 -070025from patman.terminal import tprint
Simon Glassc05694f2013-04-03 11:07:16 +000026
Simon Glass146b6022021-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 Glassc05694f2013-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 Panaitee8e9cb2020-05-15 09:30:12 +030084 01_g4ed4ebc_net--Add-tftp-speed-/
Simon Glassc05694f2013-04-03 11:07:16 +000085 sandbox/
86 u-boot.bin
87 seaboard/
88 u-boot.bin
Ovidiu Panaitee8e9cb2020-05-15 09:30:12 +030089 02_g4ed4ebc_net--Check-tftp-comp/
Simon Glassc05694f2013-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 Glassde0fefc2020-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"""
Simon Glass5df45222022-07-11 19:04:00 -0600111ErrLine = collections.namedtuple('ErrLine', 'char,brds,errline')
Simon Glassde0fefc2020-04-09 15:08:36 -0600112
Simon Glassc05694f2013-04-03 11:07:16 +0000113# Possible build outcomes
Simon Glassc78ed662019-10-31 07:42:53 -0600114OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4))
Simon Glassc05694f2013-04-03 11:07:16 +0000115
Simon Glassd214bef2017-04-12 18:23:26 -0600116# Translate a commit subject into a valid filename (and handle unicode)
Simon Glassc78ed662019-10-31 07:42:53 -0600117trans_valid_chars = str.maketrans('/: ', '---')
Simon Glassc05694f2013-04-03 11:07:16 +0000118
Simon Glasscde5c302016-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 Glassdb17fb82015-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 Glassdb17fb82015-02-05 22:06:15 -0700127]
128
Simon Glasscad8abf2015-08-25 21:52:14 -0600129class Config:
130 """Holds information about configuration settings for a board."""
Simon Glasscde5c302016-11-13 14:25:53 -0700131 def __init__(self, config_filename, target):
Simon Glasscad8abf2015-08-25 21:52:14 -0600132 self.target = target
133 self.config = {}
Simon Glasscde5c302016-11-13 14:25:53 -0700134 for fname in config_filename:
Simon Glasscad8abf2015-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 Glassc78ed662019-10-31 07:42:53 -0600143 for key, value in self.config[fname].items():
144 print(key, value)
Simon Glasscad8abf2015-08-25 21:52:14 -0600145 val = val ^ hash(key) & hash(value)
146 return val
Simon Glassc05694f2013-04-03 11:07:16 +0000147
Alex Kiernan4059e302018-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 Glassc05694f2013-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 Glassc05694f2013-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 Glass7041c392014-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 Glassc05694f2013-04-03 11:07:16 +0000175 git_dir: Git directory containing source repository
Simon Glassc05694f2013-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 Glassf3018b7a2014-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 Glass38df2e22014-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 Glassb6eb8cf2020-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 Glass9bf9a722021-04-11 16:27:27 +1200196 thread_exceptions: List of exceptions raised by thread jobs
Simon Glassc05694f2013-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 Glass03749d42014-08-28 09:43:44 -0600201 _base_warn_lines: Last-summarised list of warnings
Simon Glassc05694f2013-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 Glass726ae812020-04-09 15:08:47 -0600207 _start_time: Start time for the build
Simon Glassc05694f2013-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 Glassc635d892021-01-30 22:17:46 -0700212 _single_builder: BuilderThread object for the singer builder, if
213 threading is not being used
Simon Glass146b6022021-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 Glass6c435622022-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 Glassc05694f2013-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 Glassdb17fb82015-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 Kiernan4059e302018-05-31 04:48:34 +0000237 environment: Dictionary keyed by environment variable, Each
238 value is the value of environment variable.
Simon Glassc05694f2013-04-03 11:07:16 +0000239 """
Alex Kiernan4059e302018-05-31 04:48:34 +0000240 def __init__(self, rc, err_lines, sizes, func_sizes, config,
241 environment):
Simon Glassc05694f2013-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 Glassdb17fb82015-02-05 22:06:15 -0700246 self.config = config
Alex Kiernan4059e302018-05-31 04:48:34 +0000247 self.environment = environment
Simon Glassc05694f2013-04-03 11:07:16 +0000248
249 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
Simon Glasse87bde12014-12-01 17:33:55 -0700250 gnu_make='make', checkout=True, show_unknown=True, step=1,
Stephen Warren97c96902016-04-11 10:48:44 -0600251 no_subdirs=False, full_path=False, verbose_build=False,
Simon Glass6029af12020-04-09 15:08:51 -0600252 mrproper=False, per_board_out_dir=False,
Daniel Schwierzeck20e2ea92018-01-26 16:31:05 +0100253 config_only=False, squash_config_y=False,
Simon Glass9bf9a722021-04-11 16:27:27 +1200254 warnings_as_errors=False, work_in_output=False,
Tom Rini93ebd462022-11-09 19:14:53 -0700255 test_thread_exceptions=False, adjust_cfg=None,
256 allow_missing=False):
Simon Glassc05694f2013-04-03 11:07:16 +0000257 """Create a new Builder object
258
259 Args:
260 toolchains: Toolchains object to use for building
261 base_dir: Base directory to use for builder
262 git_dir: Git directory containing source repository
263 num_threads: Number of builder threads to run
264 num_jobs: Number of jobs to run at once (passed to make as -j)
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900265 gnu_make: the command name of GNU Make.
Simon Glassc05694f2013-04-03 11:07:16 +0000266 checkout: True to check out source, False to skip that step.
267 This is used for testing.
268 show_unknown: Show unknown boards (those not built) in summary
269 step: 1 to process every commit, n to process every nth commit
Simon Glassd48a46c2014-12-01 17:34:00 -0700270 no_subdirs: Don't create subdirectories when building current
271 source for a single board
272 full_path: Return the full path in CROSS_COMPILE and don't set
273 PATH
Simon Glass655b6102014-12-01 17:34:07 -0700274 verbose_build: Run build with V=1 and don't use 'make -s'
Simon Glass6029af12020-04-09 15:08:51 -0600275 mrproper: Always run 'make mrproper' when configuring
Stephen Warren97c96902016-04-11 10:48:44 -0600276 per_board_out_dir: Build in a separate persistent directory per
277 board rather than a thread-specific directory
Simon Glass739e8512016-11-13 14:25:51 -0700278 config_only: Only configure each build, don't build it
Simon Glasscde5c302016-11-13 14:25:53 -0700279 squash_config_y: Convert CONFIG options with the value 'y' to '1'
Daniel Schwierzeck20e2ea92018-01-26 16:31:05 +0100280 warnings_as_errors: Treat all compiler warnings as errors
Simon Glassb6eb8cf2020-03-18 09:42:42 -0600281 work_in_output: Use the output directory as the work directory and
282 don't write to a separate output directory.
Simon Glass9bf9a722021-04-11 16:27:27 +1200283 test_thread_exceptions: Uses for tests only, True to make the
284 threads raise an exception instead of reporting their result.
285 This simulates a failure in the code somewhere
Simon Glasse5650a82022-01-22 05:07:33 -0700286 adjust_cfg_list (list of str): List of changes to make to .config
287 file before building. Each is one of (where C is the config
288 option with or without the CONFIG_ prefix)
289
290 C to enable C
291 ~C to disable C
292 C=val to set the value of C (val must have quotes if C is
293 a string Kconfig
Tom Rini93ebd462022-11-09 19:14:53 -0700294 allow_missing: Run build with BINMAN_ALLOW_MISSING=1
Simon Glasse5650a82022-01-22 05:07:33 -0700295
Simon Glassc05694f2013-04-03 11:07:16 +0000296 """
297 self.toolchains = toolchains
298 self.base_dir = base_dir
Simon Glassb6eb8cf2020-03-18 09:42:42 -0600299 if work_in_output:
300 self._working_dir = base_dir
301 else:
302 self._working_dir = os.path.join(base_dir, '.bm-work')
Simon Glassc05694f2013-04-03 11:07:16 +0000303 self.threads = []
Simon Glassc05694f2013-04-03 11:07:16 +0000304 self.do_make = self.Make
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900305 self.gnu_make = gnu_make
Simon Glassc05694f2013-04-03 11:07:16 +0000306 self.checkout = checkout
307 self.num_threads = num_threads
308 self.num_jobs = num_jobs
309 self.already_done = 0
310 self.force_build = False
311 self.git_dir = git_dir
312 self._show_unknown = show_unknown
313 self._timestamp_count = 10
314 self._build_period_us = None
315 self._complete_delay = None
316 self._next_delay_update = datetime.now()
Simon Glass726ae812020-04-09 15:08:47 -0600317 self._start_time = datetime.now()
Simon Glassc05694f2013-04-03 11:07:16 +0000318 self.force_config_on_failure = True
Simon Glass7041c392014-07-13 12:22:31 -0600319 self.force_build_failures = False
Simon Glassf3018b7a2014-07-14 17:51:02 -0600320 self.force_reconfig = False
Simon Glassc05694f2013-04-03 11:07:16 +0000321 self._step = step
Simon Glass38df2e22014-07-14 17:51:03 -0600322 self.in_tree = False
Simon Glassbb4dffb2014-08-09 15:33:06 -0600323 self._error_lines = 0
Simon Glasse87bde12014-12-01 17:33:55 -0700324 self.no_subdirs = no_subdirs
Simon Glassd48a46c2014-12-01 17:34:00 -0700325 self.full_path = full_path
Simon Glass655b6102014-12-01 17:34:07 -0700326 self.verbose_build = verbose_build
Simon Glass739e8512016-11-13 14:25:51 -0700327 self.config_only = config_only
Simon Glasscde5c302016-11-13 14:25:53 -0700328 self.squash_config_y = squash_config_y
329 self.config_filenames = BASE_CONFIG_FILENAMES
Simon Glassb6eb8cf2020-03-18 09:42:42 -0600330 self.work_in_output = work_in_output
Simon Glasse5650a82022-01-22 05:07:33 -0700331 self.adjust_cfg = adjust_cfg
Tom Rini93ebd462022-11-09 19:14:53 -0700332 self.allow_missing = allow_missing
Simon Glass6c435622022-07-11 19:03:56 -0600333 self._ide = False
Simon Glasse5650a82022-01-22 05:07:33 -0700334
Simon Glasscde5c302016-11-13 14:25:53 -0700335 if not self.squash_config_y:
336 self.config_filenames += EXTRA_CONFIG_FILENAMES
Simon Glass146b6022021-10-19 21:43:24 -0600337 self._terminated = False
338 self._restarting_config = False
Simon Glassc05694f2013-04-03 11:07:16 +0000339
Daniel Schwierzeck20e2ea92018-01-26 16:31:05 +0100340 self.warnings_as_errors = warnings_as_errors
Simon Glassc05694f2013-04-03 11:07:16 +0000341 self.col = terminal.Color()
342
Simon Glass03749d42014-08-28 09:43:44 -0600343 self._re_function = re.compile('(.*): In function.*')
344 self._re_files = re.compile('In file included from.*')
345 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
Simon Glass0db94432018-11-06 16:02:11 -0700346 self._re_dtb_warning = re.compile('(.*): Warning .*')
Simon Glass03749d42014-08-28 09:43:44 -0600347 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
Simon Glassf4ebfba2020-04-09 15:08:53 -0600348 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
349 re.MULTILINE | re.DOTALL)
Simon Glass03749d42014-08-28 09:43:44 -0600350
Simon Glass9bf9a722021-04-11 16:27:27 +1200351 self.thread_exceptions = []
352 self.test_thread_exceptions = test_thread_exceptions
Simon Glassc635d892021-01-30 22:17:46 -0700353 if self.num_threads:
354 self._single_builder = None
355 self.queue = queue.Queue()
356 self.out_queue = queue.Queue()
357 for i in range(self.num_threads):
Simon Glass9bf9a722021-04-11 16:27:27 +1200358 t = builderthread.BuilderThread(
359 self, i, mrproper, per_board_out_dir,
360 test_exception=test_thread_exceptions)
Simon Glassc635d892021-01-30 22:17:46 -0700361 t.setDaemon(True)
362 t.start()
363 self.threads.append(t)
364
365 t = builderthread.ResultThread(self)
Simon Glassc05694f2013-04-03 11:07:16 +0000366 t.setDaemon(True)
367 t.start()
368 self.threads.append(t)
Simon Glassc635d892021-01-30 22:17:46 -0700369 else:
370 self._single_builder = builderthread.BuilderThread(
371 self, -1, mrproper, per_board_out_dir)
Simon Glassc05694f2013-04-03 11:07:16 +0000372
373 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
374 self.re_make_err = re.compile('|'.join(ignore_lines))
375
Simon Glass205ac042016-09-18 16:48:37 -0600376 # Handle existing graceful with SIGINT / Ctrl-C
377 signal.signal(signal.SIGINT, self.signal_handler)
378
Simon Glassc05694f2013-04-03 11:07:16 +0000379 def __del__(self):
380 """Get rid of all threads created by the builder"""
381 for t in self.threads:
382 del t
383
Simon Glass205ac042016-09-18 16:48:37 -0600384 def signal_handler(self, signal, frame):
385 sys.exit(1)
386
Simon Glasseb48bbc2014-08-09 15:33:02 -0600387 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
Simon Glass3394c9f2014-08-28 09:43:43 -0600388 show_detail=False, show_bloat=False,
Alex Kiernan4059e302018-05-31 04:48:34 +0000389 list_error_boards=False, show_config=False,
Simon Glassf4ebfba2020-04-09 15:08:53 -0600390 show_environment=False, filter_dtb_warnings=False,
Simon Glass6c435622022-07-11 19:03:56 -0600391 filter_migration_warnings=False, ide=False):
Simon Glasseb48bbc2014-08-09 15:33:02 -0600392 """Setup display options for the builder.
393
Simon Glass9ea93812020-04-09 15:08:52 -0600394 Args:
395 show_errors: True to show summarised error/warning info
396 show_sizes: Show size deltas
397 show_detail: Show size delta detail for each board if show_sizes
398 show_bloat: Show detail for each function
399 list_error_boards: Show the boards which caused each error/warning
400 show_config: Show config deltas
401 show_environment: Show environment deltas
402 filter_dtb_warnings: Filter out any warnings from the device-tree
403 compiler
Simon Glassf4ebfba2020-04-09 15:08:53 -0600404 filter_migration_warnings: Filter out any warnings about migrating
405 a board to driver model
Simon Glass6c435622022-07-11 19:03:56 -0600406 ide: Create output that can be parsed by an IDE. There is no '+' prefix on
407 error lines and output on stderr stays on stderr.
Simon Glasseb48bbc2014-08-09 15:33:02 -0600408 """
409 self._show_errors = show_errors
410 self._show_sizes = show_sizes
411 self._show_detail = show_detail
412 self._show_bloat = show_bloat
Simon Glass3394c9f2014-08-28 09:43:43 -0600413 self._list_error_boards = list_error_boards
Simon Glassdb17fb82015-02-05 22:06:15 -0700414 self._show_config = show_config
Alex Kiernan4059e302018-05-31 04:48:34 +0000415 self._show_environment = show_environment
Simon Glass9ea93812020-04-09 15:08:52 -0600416 self._filter_dtb_warnings = filter_dtb_warnings
Simon Glassf4ebfba2020-04-09 15:08:53 -0600417 self._filter_migration_warnings = filter_migration_warnings
Simon Glass6c435622022-07-11 19:03:56 -0600418 self._ide = ide
Simon Glasseb48bbc2014-08-09 15:33:02 -0600419
Simon Glassc05694f2013-04-03 11:07:16 +0000420 def _AddTimestamp(self):
421 """Add a new timestamp to the list and record the build period.
422
423 The build period is the length of time taken to perform a single
424 build (one board, one commit).
425 """
426 now = datetime.now()
427 self._timestamps.append(now)
428 count = len(self._timestamps)
429 delta = self._timestamps[-1] - self._timestamps[0]
430 seconds = delta.total_seconds()
431
432 # If we have enough data, estimate build period (time taken for a
433 # single build) and therefore completion time.
434 if count > 1 and self._next_delay_update < now:
435 self._next_delay_update = now + timedelta(seconds=2)
436 if seconds > 0:
437 self._build_period = float(seconds) / count
438 todo = self.count - self.upto
439 self._complete_delay = timedelta(microseconds=
440 self._build_period * todo * 1000000)
441 # Round it
442 self._complete_delay -= timedelta(
443 microseconds=self._complete_delay.microseconds)
444
445 if seconds > 60:
446 self._timestamps.popleft()
447 count -= 1
448
Simon Glassc05694f2013-04-03 11:07:16 +0000449 def SelectCommit(self, commit, checkout=True):
450 """Checkout the selected commit for this build
451 """
452 self.commit = commit
453 if checkout and self.checkout:
Simon Glass761648b2022-01-29 14:14:11 -0700454 gitutil.checkout(commit.hash)
Simon Glassc05694f2013-04-03 11:07:16 +0000455
456 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
457 """Run make
458
459 Args:
460 commit: Commit object that is being built
461 brd: Board object that is being built
Roger Meiere0a0e552014-08-20 22:10:29 +0200462 stage: Stage that we are at (mrproper, config, build)
Simon Glassc05694f2013-04-03 11:07:16 +0000463 cwd: Directory where make should be run
464 args: Arguments to pass to make
Simon Glass840be732022-01-29 14:14:05 -0700465 kwargs: Arguments to pass to command.run_pipe()
Simon Glassc05694f2013-04-03 11:07:16 +0000466 """
Simon Glass146b6022021-10-19 21:43:24 -0600467
468 def check_output(stream, data):
469 if b'Restart config' in data:
470 self._restarting_config = True
471
472 # If we see 'Restart config' following by multiple errors
473 if self._restarting_config:
474 m = RE_NO_DEFAULT.findall(data)
475
476 # Number of occurences of each Kconfig item
477 multiple = [m.count(val) for val in set(m)]
478
479 # If any of them occur more than once, we have a loop
480 if [val for val in multiple if val > 1]:
481 self._terminated = True
482 return True
483 return False
484
485 self._restarting_config = False
486 self._terminated = False
Masahiro Yamada1fe610d2014-07-22 11:19:09 +0900487 cmd = [self.gnu_make] + list(args)
Simon Glass840be732022-01-29 14:14:05 -0700488 result = command.run_pipe([cmd], capture=True, capture_stderr=True,
Simon Glass146b6022021-10-19 21:43:24 -0600489 cwd=cwd, raise_on_error=False, infile='/dev/null',
490 output_func=check_output, **kwargs)
491
492 if self._terminated:
493 # Try to be helpful
494 result.stderr += '(** did you define an int/hex Kconfig with no default? **)'
495
Simon Glass413f91a2015-02-05 22:06:12 -0700496 if self.verbose_build:
497 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
498 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
Simon Glassc05694f2013-04-03 11:07:16 +0000499 return result
500
501 def ProcessResult(self, result):
502 """Process the result of a build, showing progress information
503
504 Args:
Simon Glass78e418e2014-08-09 15:33:03 -0600505 result: A CommandResult object, which indicates the result for
506 a single build
Simon Glassc05694f2013-04-03 11:07:16 +0000507 """
508 col = terminal.Color()
509 if result:
510 target = result.brd.target
511
Simon Glassc05694f2013-04-03 11:07:16 +0000512 self.upto += 1
513 if result.return_code != 0:
514 self.fail += 1
515 elif result.stderr:
516 self.warned += 1
517 if result.already_done:
518 self.already_done += 1
Simon Glass78e418e2014-08-09 15:33:03 -0600519 if self._verbose:
Simon Glass02811582022-01-29 14:14:18 -0700520 terminal.print_clear()
Simon Glass78e418e2014-08-09 15:33:03 -0600521 boards_selected = {target : result.brd}
522 self.ResetResultSummary(boards_selected)
523 self.ProduceResultSummary(result.commit_upto, self.commits,
524 boards_selected)
Simon Glassc05694f2013-04-03 11:07:16 +0000525 else:
526 target = '(starting)'
527
528 # Display separate counts for ok, warned and fail
529 ok = self.upto - self.warned - self.fail
Simon Glassf45d3742022-01-29 14:14:17 -0700530 line = '\r' + self.col.build(self.col.GREEN, '%5d' % ok)
531 line += self.col.build(self.col.YELLOW, '%5d' % self.warned)
532 line += self.col.build(self.col.RED, '%5d' % self.fail)
Simon Glassc05694f2013-04-03 11:07:16 +0000533
Simon Glass69c3a8a2020-04-09 15:08:45 -0600534 line += ' /%-5d ' % self.count
535 remaining = self.count - self.upto
536 if remaining:
Simon Glassf45d3742022-01-29 14:14:17 -0700537 line += self.col.build(self.col.MAGENTA, ' -%-5d ' % remaining)
Simon Glass69c3a8a2020-04-09 15:08:45 -0600538 else:
539 line += ' ' * 8
Simon Glassc05694f2013-04-03 11:07:16 +0000540
541 # Add our current completion time estimate
542 self._AddTimestamp()
543 if self._complete_delay:
Simon Glass69c3a8a2020-04-09 15:08:45 -0600544 line += '%s : ' % self._complete_delay
Simon Glassc05694f2013-04-03 11:07:16 +0000545
Simon Glass69c3a8a2020-04-09 15:08:45 -0600546 line += target
Simon Glass6c435622022-07-11 19:03:56 -0600547 if not self._ide:
548 terminal.print_clear()
549 tprint(line, newline=False, limit_to_line=True)
Simon Glassc05694f2013-04-03 11:07:16 +0000550
551 def _GetOutputDir(self, commit_upto):
552 """Get the name of the output directory for a commit number
553
554 The output directory is typically .../<branch>/<commit>.
555
556 Args:
557 commit_upto: Commit number to use (0..self.count-1)
558 """
Simon Glasse3c85ab2020-04-17 17:51:34 -0600559 if self.work_in_output:
560 return self._working_dir
561
Simon Glasse87bde12014-12-01 17:33:55 -0700562 commit_dir = None
Simon Glassd326ad72014-08-09 15:32:59 -0600563 if self.commits:
564 commit = self.commits[commit_upto]
565 subject = commit.subject.translate(trans_valid_chars)
Simon Glass5dc1ca72020-03-18 09:42:45 -0600566 # See _GetOutputSpaceRemovals() which parses this name
Ovidiu Panaitee8e9cb2020-05-15 09:30:12 +0300567 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
568 commit.hash, subject[:20]))
Simon Glasse87bde12014-12-01 17:33:55 -0700569 elif not self.no_subdirs:
Simon Glassd326ad72014-08-09 15:32:59 -0600570 commit_dir = 'current'
Simon Glasse87bde12014-12-01 17:33:55 -0700571 if not commit_dir:
572 return self.base_dir
573 return os.path.join(self.base_dir, commit_dir)
Simon Glassc05694f2013-04-03 11:07:16 +0000574
575 def GetBuildDir(self, commit_upto, target):
576 """Get the name of the build directory for a commit number
577
578 The build directory is typically .../<branch>/<commit>/<target>.
579
580 Args:
581 commit_upto: Commit number to use (0..self.count-1)
582 target: Target name
583 """
584 output_dir = self._GetOutputDir(commit_upto)
Simon Glasse3c85ab2020-04-17 17:51:34 -0600585 if self.work_in_output:
586 return output_dir
Simon Glassc05694f2013-04-03 11:07:16 +0000587 return os.path.join(output_dir, target)
588
589 def GetDoneFile(self, commit_upto, target):
590 """Get the name of the done file for a commit number
591
592 Args:
593 commit_upto: Commit number to use (0..self.count-1)
594 target: Target name
595 """
596 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
597
598 def GetSizesFile(self, commit_upto, target):
599 """Get the name of the sizes file for a commit number
600
601 Args:
602 commit_upto: Commit number to use (0..self.count-1)
603 target: Target name
604 """
605 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
606
607 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
608 """Get the name of the funcsizes file for a commit number and ELF file
609
610 Args:
611 commit_upto: Commit number to use (0..self.count-1)
612 target: Target name
613 elf_fname: Filename of elf image
614 """
615 return os.path.join(self.GetBuildDir(commit_upto, target),
616 '%s.sizes' % elf_fname.replace('/', '-'))
617
618 def GetObjdumpFile(self, commit_upto, target, elf_fname):
619 """Get the name of the objdump file for a commit number and ELF file
620
621 Args:
622 commit_upto: Commit number to use (0..self.count-1)
623 target: Target name
624 elf_fname: Filename of elf image
625 """
626 return os.path.join(self.GetBuildDir(commit_upto, target),
627 '%s.objdump' % elf_fname.replace('/', '-'))
628
629 def GetErrFile(self, commit_upto, target):
630 """Get the name of the err file for a commit number
631
632 Args:
633 commit_upto: Commit number to use (0..self.count-1)
634 target: Target name
635 """
636 output_dir = self.GetBuildDir(commit_upto, target)
637 return os.path.join(output_dir, 'err')
638
639 def FilterErrors(self, lines):
640 """Filter out errors in which we have no interest
641
642 We should probably use map().
643
644 Args:
645 lines: List of error lines, each a string
646 Returns:
647 New list with only interesting lines included
648 """
649 out_lines = []
Simon Glassf4ebfba2020-04-09 15:08:53 -0600650 if self._filter_migration_warnings:
651 text = '\n'.join(lines)
652 text = self._re_migration_warning.sub('', text)
653 lines = text.splitlines()
Simon Glassc05694f2013-04-03 11:07:16 +0000654 for line in lines:
Simon Glass9ea93812020-04-09 15:08:52 -0600655 if self.re_make_err.search(line):
656 continue
657 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
658 continue
659 out_lines.append(line)
Simon Glassc05694f2013-04-03 11:07:16 +0000660 return out_lines
661
662 def ReadFuncSizes(self, fname, fd):
663 """Read function sizes from the output of 'nm'
664
665 Args:
666 fd: File containing data to read
667 fname: Filename we are reading from (just for errors)
668
669 Returns:
670 Dictionary containing size of each function in bytes, indexed by
671 function name.
672 """
673 sym = {}
674 for line in fd.readlines():
Simon Glass86a2afe2022-07-11 19:04:11 -0600675 line = line.strip()
676 parts = line.split()
677 if line and len(parts) == 3:
678 size, type, name = line.split()
679 if type in 'tTdDbB':
680 # function names begin with '.' on 64-bit powerpc
681 if '.' in name[1:]:
682 name = 'static.' + name.split('.')[0]
683 sym[name] = sym.get(name, 0) + int(size, 16)
Simon Glassc05694f2013-04-03 11:07:16 +0000684 return sym
685
Simon Glassdb17fb82015-02-05 22:06:15 -0700686 def _ProcessConfig(self, fname):
687 """Read in a .config, autoconf.mk or autoconf.h file
688
689 This function handles all config file types. It ignores comments and
690 any #defines which don't start with CONFIG_.
691
692 Args:
693 fname: Filename to read
694
695 Returns:
696 Dictionary:
697 key: Config name (e.g. CONFIG_DM)
698 value: Config value (e.g. 1)
699 """
700 config = {}
701 if os.path.exists(fname):
702 with open(fname) as fd:
703 for line in fd:
704 line = line.strip()
705 if line.startswith('#define'):
706 values = line[8:].split(' ', 1)
707 if len(values) > 1:
708 key, value = values
709 else:
710 key = values[0]
Simon Glasscde5c302016-11-13 14:25:53 -0700711 value = '1' if self.squash_config_y else ''
Simon Glassdb17fb82015-02-05 22:06:15 -0700712 if not key.startswith('CONFIG_'):
713 continue
714 elif not line or line[0] in ['#', '*', '/']:
715 continue
716 else:
717 key, value = line.split('=', 1)
Simon Glasscde5c302016-11-13 14:25:53 -0700718 if self.squash_config_y and value == 'y':
719 value = '1'
Simon Glassdb17fb82015-02-05 22:06:15 -0700720 config[key] = value
721 return config
722
Alex Kiernan4059e302018-05-31 04:48:34 +0000723 def _ProcessEnvironment(self, fname):
724 """Read in a uboot.env file
725
726 This function reads in environment variables from a file.
727
728 Args:
729 fname: Filename to read
730
731 Returns:
732 Dictionary:
733 key: environment variable (e.g. bootlimit)
734 value: value of environment variable (e.g. 1)
735 """
736 environment = {}
737 if os.path.exists(fname):
738 with open(fname) as fd:
739 for line in fd.read().split('\0'):
740 try:
741 key, value = line.split('=', 1)
742 environment[key] = value
743 except ValueError:
744 # ignore lines we can't parse
745 pass
746 return environment
747
Simon Glassdb17fb82015-02-05 22:06:15 -0700748 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
Alex Kiernan4059e302018-05-31 04:48:34 +0000749 read_config, read_environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000750 """Work out the outcome of a build.
751
752 Args:
753 commit_upto: Commit number to check (0..n-1)
754 target: Target board to check
755 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700756 read_config: True to read .config and autoconf.h files
Alex Kiernan4059e302018-05-31 04:48:34 +0000757 read_environment: True to read uboot.env files
Simon Glassc05694f2013-04-03 11:07:16 +0000758
759 Returns:
760 Outcome object
761 """
762 done_file = self.GetDoneFile(commit_upto, target)
763 sizes_file = self.GetSizesFile(commit_upto, target)
764 sizes = {}
765 func_sizes = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700766 config = {}
Alex Kiernan4059e302018-05-31 04:48:34 +0000767 environment = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000768 if os.path.exists(done_file):
769 with open(done_file, 'r') as fd:
Simon Glass91c54a42019-04-26 19:02:23 -0600770 try:
771 return_code = int(fd.readline())
772 except ValueError:
773 # The file may be empty due to running out of disk space.
774 # Try a rebuild
775 return_code = 1
Simon Glassc05694f2013-04-03 11:07:16 +0000776 err_lines = []
777 err_file = self.GetErrFile(commit_upto, target)
778 if os.path.exists(err_file):
779 with open(err_file, 'r') as fd:
780 err_lines = self.FilterErrors(fd.readlines())
781
782 # Decide whether the build was ok, failed or created warnings
783 if return_code:
784 rc = OUTCOME_ERROR
785 elif len(err_lines):
786 rc = OUTCOME_WARNING
787 else:
788 rc = OUTCOME_OK
789
790 # Convert size information to our simple format
791 if os.path.exists(sizes_file):
792 with open(sizes_file, 'r') as fd:
793 for line in fd.readlines():
794 values = line.split()
795 rodata = 0
796 if len(values) > 6:
797 rodata = int(values[6], 16)
798 size_dict = {
799 'all' : int(values[0]) + int(values[1]) +
800 int(values[2]),
801 'text' : int(values[0]) - rodata,
802 'data' : int(values[1]),
803 'bss' : int(values[2]),
804 'rodata' : rodata,
805 }
806 sizes[values[5]] = size_dict
807
808 if read_func_sizes:
809 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
810 for fname in glob.glob(pattern):
811 with open(fname, 'r') as fd:
812 dict_name = os.path.basename(fname).replace('.sizes',
813 '')
814 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
815
Simon Glassdb17fb82015-02-05 22:06:15 -0700816 if read_config:
817 output_dir = self.GetBuildDir(commit_upto, target)
Simon Glasscde5c302016-11-13 14:25:53 -0700818 for name in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700819 fname = os.path.join(output_dir, name)
820 config[name] = self._ProcessConfig(fname)
821
Alex Kiernan4059e302018-05-31 04:48:34 +0000822 if read_environment:
823 output_dir = self.GetBuildDir(commit_upto, target)
824 fname = os.path.join(output_dir, 'uboot.env')
825 environment = self._ProcessEnvironment(fname)
826
827 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
828 environment)
Simon Glassc05694f2013-04-03 11:07:16 +0000829
Alex Kiernan4059e302018-05-31 04:48:34 +0000830 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
Simon Glassc05694f2013-04-03 11:07:16 +0000831
Simon Glassdb17fb82015-02-05 22:06:15 -0700832 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
Alex Kiernan4059e302018-05-31 04:48:34 +0000833 read_config, read_environment):
Simon Glassc05694f2013-04-03 11:07:16 +0000834 """Calculate a summary of the results of building a commit.
835
836 Args:
837 board_selected: Dict containing boards to summarise
838 commit_upto: Commit number to summarize (0..self.count-1)
839 read_func_sizes: True to read function size information
Simon Glassdb17fb82015-02-05 22:06:15 -0700840 read_config: True to read .config and autoconf.h files
Alex Kiernan4059e302018-05-31 04:48:34 +0000841 read_environment: True to read uboot.env files
Simon Glassc05694f2013-04-03 11:07:16 +0000842
843 Returns:
844 Tuple:
Simon Glass6c435622022-07-11 19:03:56 -0600845 Dict containing boards which built this commit:
846 key: board.target
847 value: Builder.Outcome object
Simon Glass03749d42014-08-28 09:43:44 -0600848 List containing a summary of error lines
Simon Glass3394c9f2014-08-28 09:43:43 -0600849 Dict keyed by error line, containing a list of the Board
850 objects with that error
Simon Glass03749d42014-08-28 09:43:44 -0600851 List containing a summary of warning lines
852 Dict keyed by error line, containing a list of the Board
853 objects with that warning
Simon Glasscad8abf2015-08-25 21:52:14 -0600854 Dictionary keyed by board.target. Each value is a dictionary:
855 key: filename - e.g. '.config'
Simon Glassdb17fb82015-02-05 22:06:15 -0700856 value is itself a dictionary:
857 key: config name
858 value: config value
Alex Kiernan4059e302018-05-31 04:48:34 +0000859 Dictionary keyed by board.target. Each value is a dictionary:
860 key: environment variable
861 value: value of environment variable
Simon Glassc05694f2013-04-03 11:07:16 +0000862 """
Simon Glass03749d42014-08-28 09:43:44 -0600863 def AddLine(lines_summary, lines_boards, line, board):
864 line = line.rstrip()
865 if line in lines_boards:
866 lines_boards[line].append(board)
867 else:
868 lines_boards[line] = [board]
869 lines_summary.append(line)
870
Simon Glassc05694f2013-04-03 11:07:16 +0000871 board_dict = {}
872 err_lines_summary = []
Simon Glass3394c9f2014-08-28 09:43:43 -0600873 err_lines_boards = {}
Simon Glass03749d42014-08-28 09:43:44 -0600874 warn_lines_summary = []
875 warn_lines_boards = {}
Simon Glassdb17fb82015-02-05 22:06:15 -0700876 config = {}
Alex Kiernan4059e302018-05-31 04:48:34 +0000877 environment = {}
Simon Glassc05694f2013-04-03 11:07:16 +0000878
Simon Glass8132f982022-07-11 19:03:57 -0600879 for brd in boards_selected.values():
880 outcome = self.GetBuildOutcome(commit_upto, brd.target,
Alex Kiernan4059e302018-05-31 04:48:34 +0000881 read_func_sizes, read_config,
882 read_environment)
Simon Glass8132f982022-07-11 19:03:57 -0600883 board_dict[brd.target] = outcome
Simon Glass03749d42014-08-28 09:43:44 -0600884 last_func = None
885 last_was_warning = False
886 for line in outcome.err_lines:
887 if line:
888 if (self._re_function.match(line) or
889 self._re_files.match(line)):
890 last_func = line
Simon Glass3394c9f2014-08-28 09:43:43 -0600891 else:
Simon Glass0db94432018-11-06 16:02:11 -0700892 is_warning = (self._re_warning.match(line) or
893 self._re_dtb_warning.match(line))
Simon Glass03749d42014-08-28 09:43:44 -0600894 is_note = self._re_note.match(line)
895 if is_warning or (last_was_warning and is_note):
896 if last_func:
897 AddLine(warn_lines_summary, warn_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600898 last_func, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600899 AddLine(warn_lines_summary, warn_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600900 line, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600901 else:
902 if last_func:
903 AddLine(err_lines_summary, err_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600904 last_func, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600905 AddLine(err_lines_summary, err_lines_boards,
Simon Glass8132f982022-07-11 19:03:57 -0600906 line, brd)
Simon Glass03749d42014-08-28 09:43:44 -0600907 last_was_warning = is_warning
908 last_func = None
Simon Glass8132f982022-07-11 19:03:57 -0600909 tconfig = Config(self.config_filenames, brd.target)
Simon Glasscde5c302016-11-13 14:25:53 -0700910 for fname in self.config_filenames:
Simon Glassdb17fb82015-02-05 22:06:15 -0700911 if outcome.config:
Simon Glassc78ed662019-10-31 07:42:53 -0600912 for key, value in outcome.config[fname].items():
Simon Glasscad8abf2015-08-25 21:52:14 -0600913 tconfig.Add(fname, key, value)
Simon Glass8132f982022-07-11 19:03:57 -0600914 config[brd.target] = tconfig
Simon Glassdb17fb82015-02-05 22:06:15 -0700915
Simon Glass8132f982022-07-11 19:03:57 -0600916 tenvironment = Environment(brd.target)
Alex Kiernan4059e302018-05-31 04:48:34 +0000917 if outcome.environment:
Simon Glassc78ed662019-10-31 07:42:53 -0600918 for key, value in outcome.environment.items():
Alex Kiernan4059e302018-05-31 04:48:34 +0000919 tenvironment.Add(key, value)
Simon Glass8132f982022-07-11 19:03:57 -0600920 environment[brd.target] = tenvironment
Alex Kiernan4059e302018-05-31 04:48:34 +0000921
Simon Glass03749d42014-08-28 09:43:44 -0600922 return (board_dict, err_lines_summary, err_lines_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +0000923 warn_lines_summary, warn_lines_boards, config, environment)
Simon Glassc05694f2013-04-03 11:07:16 +0000924
925 def AddOutcome(self, board_dict, arch_list, changes, char, color):
926 """Add an output to our list of outcomes for each architecture
927
928 This simple function adds failing boards (changes) to the
929 relevant architecture string, so we can print the results out
930 sorted by architecture.
931
932 Args:
933 board_dict: Dict containing all boards
934 arch_list: Dict keyed by arch name. Value is a string containing
935 a list of board names which failed for that arch.
936 changes: List of boards to add to arch_list
937 color: terminal.Colour object
938 """
939 done_arch = {}
940 for target in changes:
941 if target in board_dict:
942 arch = board_dict[target].arch
943 else:
944 arch = 'unknown'
Simon Glassf45d3742022-01-29 14:14:17 -0700945 str = self.col.build(color, ' ' + target)
Simon Glassc05694f2013-04-03 11:07:16 +0000946 if not arch in done_arch:
Simon Glassf45d3742022-01-29 14:14:17 -0700947 str = ' %s %s' % (self.col.build(color, char), str)
Simon Glassc05694f2013-04-03 11:07:16 +0000948 done_arch[arch] = True
949 if not arch in arch_list:
950 arch_list[arch] = str
951 else:
952 arch_list[arch] += str
953
954
955 def ColourNum(self, num):
956 color = self.col.RED if num > 0 else self.col.GREEN
957 if num == 0:
958 return '0'
Simon Glassf45d3742022-01-29 14:14:17 -0700959 return self.col.build(color, str(num))
Simon Glassc05694f2013-04-03 11:07:16 +0000960
961 def ResetResultSummary(self, board_selected):
962 """Reset the results summary ready for use.
963
964 Set up the base board list to be all those selected, and set the
965 error lines to empty.
966
967 Following this, calls to PrintResultSummary() will use this
968 information to work out what has changed.
969
970 Args:
971 board_selected: Dict containing boards to summarise, keyed by
972 board.target
973 """
974 self._base_board_dict = {}
Simon Glass8132f982022-07-11 19:03:57 -0600975 for brd in board_selected:
976 self._base_board_dict[brd] = Builder.Outcome(0, [], [], {}, {}, {})
Simon Glassc05694f2013-04-03 11:07:16 +0000977 self._base_err_lines = []
Simon Glass03749d42014-08-28 09:43:44 -0600978 self._base_warn_lines = []
979 self._base_err_line_boards = {}
980 self._base_warn_line_boards = {}
Simon Glasscad8abf2015-08-25 21:52:14 -0600981 self._base_config = None
Alex Kiernan4059e302018-05-31 04:48:34 +0000982 self._base_environment = None
Simon Glassc05694f2013-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 Rini0b48cd62017-05-22 13:48:52 -04001016 if max(args) == 0 and min(args) == 0:
Simon Glassc05694f2013-04-03 11:07:16 +00001017 return
1018 args = [self.ColourNum(x) for x in args]
1019 indent = ' ' * 15
Simon Glass02811582022-01-29 14:14:18 -07001020 tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
Simon Glassf45d3742022-01-29 14:14:17 -07001021 tuple([indent, self.col.build(self.col.YELLOW, fname)] + args))
Simon Glass02811582022-01-29 14:14:18 -07001022 tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
Simon Glass4433aa92014-09-05 19:00:07 -06001023 'delta'))
Simon Glassc05694f2013-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 Glass02811582022-01-29 14:14:18 -07001029 tprint(msg, colour=color)
Simon Glassc05694f2013-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 Glass02811582022-01-29 14:14:18 -07001054 tprint('%10s %-15s:' % ('', result['_target']),
Simon Glass4433aa92014-09-05 19:00:07 -06001055 newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001056 printed_target = True
Simon Glass02811582022-01-29 14:14:18 -07001057 tprint(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001058 if printed_target:
Simon Glass02811582022-01-29 14:14:18 -07001059 tprint()
Simon Glassc05694f2013-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 Suligoie5e3c112020-01-29 09:56:05 +01001076 got bigger, - means smaller). The numbers are the average number
Simon Glassc05694f2013-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 Glassc05694f2013-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 Glassb4002462020-03-18 09:42:43 -06001088 show_detail: Show size delta detail for each board
Simon Glassc05694f2013-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 Glassc78ed662019-10-31 07:42:53 -06001136 for arch, target_list in arch_list.items():
Simon Glassc05694f2013-04-03 11:07:16 +00001137 # Get total difference for each type
1138 totals = {}
1139 for result in target_list:
1140 total = 0
Simon Glassc78ed662019-10-31 07:42:53 -06001141 for name, diff in result.items():
Simon Glassc05694f2013-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 Glass02811582022-01-29 14:14:18 -07001163 tprint('%10s: (for %d/%d boards)' % (arch, count,
Simon Glass4433aa92014-09-05 19:00:07 -06001164 arch_count[arch]), newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001165 printed_arch = True
Simon Glass02811582022-01-29 14:14:18 -07001166 tprint(msg, colour=color, newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001167
1168 if printed_arch:
Simon Glass02811582022-01-29 14:14:18 -07001169 tprint()
Simon Glassc05694f2013-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 Glass03749d42014-08-28 09:43:44 -06001175 err_line_boards, warn_lines, warn_line_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +00001176 config, environment, show_sizes, show_detail,
1177 show_bloat, show_config, show_environment):
Simon Glassc05694f2013-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 Glass3394c9f2014-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 Glass03749d42014-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 Glassdb17fb82015-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 Kiernan4059e302018-05-31 04:48:34 +00001202 environment: Dictionary keyed by environment variable, Each
1203 value is the value of environment variable.
Simon Glassc05694f2013-04-03 11:07:16 +00001204 show_sizes: Show image size deltas
Simon Glassb4002462020-03-18 09:42:43 -06001205 show_detail: Show size delta detail for each board if show_sizes
Simon Glassc05694f2013-04-03 11:07:16 +00001206 show_bloat: Show detail for each function
Simon Glassdb17fb82015-02-05 22:06:15 -07001207 show_config: Show config changes
Alex Kiernan4059e302018-05-31 04:48:34 +00001208 show_environment: Show environment changes
Simon Glassc05694f2013-04-03 11:07:16 +00001209 """
Simon Glass03749d42014-08-28 09:43:44 -06001210 def _BoardList(line, line_boards):
Simon Glass3394c9f2014-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 Glassde0fefc2020-04-09 15:08:36 -06001215 line_boards: boards to search, each a Board
Simon Glass3394c9f2014-08-28 09:43:43 -06001216 Return:
Simon Glassde0fefc2020-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 Glass3394c9f2014-08-28 09:43:43 -06001219 """
Simon Glass5df45222022-07-11 19:04:00 -06001220 brds = []
Simon Glassde0fefc2020-04-09 15:08:36 -06001221 board_set = set()
Simon Glass3394c9f2014-08-28 09:43:43 -06001222 if self._list_error_boards:
Simon Glass8132f982022-07-11 19:03:57 -06001223 for brd in line_boards[line]:
1224 if not brd in board_set:
Simon Glass5df45222022-07-11 19:04:00 -06001225 brds.append(brd)
Simon Glass8132f982022-07-11 19:03:57 -06001226 board_set.add(brd)
Simon Glass5df45222022-07-11 19:04:00 -06001227 return brds
Simon Glass3394c9f2014-08-28 09:43:43 -06001228
Simon Glass03749d42014-08-28 09:43:44 -06001229 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1230 char):
Simon Glassde0fefc2020-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 Glass03749d42014-08-28 09:43:44 -06001249 better_lines = []
1250 worse_lines = []
1251 for line in lines:
1252 if line not in base_lines:
Simon Glassde0fefc2020-04-09 15:08:36 -06001253 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1254 line)
1255 worse_lines.append(errline)
Simon Glass03749d42014-08-28 09:43:44 -06001256 for line in base_lines:
1257 if line not in lines:
Simon Glassde0fefc2020-04-09 15:08:36 -06001258 errline = ErrLine(char + '-',
1259 _BoardList(line, base_line_boards), line)
1260 better_lines.append(errline)
Simon Glass03749d42014-08-28 09:43:44 -06001261 return better_lines, worse_lines
1262
Simon Glassdb17fb82015-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 Glasscad8abf2015-08-25 21:52:14 -06001279 return '%s %s: %s' % (delta, name, out)
Simon Glassdb17fb82015-02-05 22:06:15 -07001280
Simon Glasscad8abf2015-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 Glassdb17fb82015-02-05 22:06:15 -07001283
1284 Args:
Simon Glasscad8abf2015-08-25 21:52:14 -06001285 lines: list to add to
1286 name: config file name
Simon Glassdb17fb82015-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 Glasscad8abf2015-08-25 21:52:14 -06001298 lines.append(_CalcConfig('+', name, config_plus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001299 if config_minus:
Simon Glasscad8abf2015-08-25 21:52:14 -06001300 lines.append(_CalcConfig('-', name, config_minus))
Simon Glassdb17fb82015-02-05 22:06:15 -07001301 if config_change:
Simon Glasscad8abf2015-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 Glass02811582022-01-29 14:14:18 -07001314 tprint(' ' + line, newline=True, colour=col)
Simon Glasscad8abf2015-08-25 21:52:14 -06001315
Simon Glassac500222020-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 Glassde0fefc2020-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 Glassac500222020-04-09 15:08:28 -06001325 colour: Colour to use for output
1326 """
1327 if err_lines:
Simon Glassea49f9b2020-04-09 15:08:37 -06001328 out_list = []
Simon Glassde0fefc2020-04-09 15:08:36 -06001329 for line in err_lines:
Simon Glass5df45222022-07-11 19:04:00 -06001330 names = [brd.target for brd in line.brds]
Simon Glass070589b2020-04-09 15:08:38 -06001331 board_str = ' '.join(names) if names else ''
Simon Glassea49f9b2020-04-09 15:08:37 -06001332 if board_str:
Simon Glassf45d3742022-01-29 14:14:17 -07001333 out = self.col.build(colour, line.char + '(')
1334 out += self.col.build(self.col.MAGENTA, board_str,
Simon Glassea49f9b2020-04-09 15:08:37 -06001335 bright=False)
Simon Glassf45d3742022-01-29 14:14:17 -07001336 out += self.col.build(colour, ') %s' % line.errline)
Simon Glassea49f9b2020-04-09 15:08:37 -06001337 else:
Simon Glassf45d3742022-01-29 14:14:17 -07001338 out = self.col.build(colour, line.char + line.errline)
Simon Glassea49f9b2020-04-09 15:08:37 -06001339 out_list.append(out)
Simon Glass02811582022-01-29 14:14:18 -07001340 tprint('\n'.join(out_list))
Simon Glassac500222020-04-09 15:08:28 -06001341 self._error_lines += 1
1342
Simon Glassdb17fb82015-02-05 22:06:15 -07001343
Simon Glass454507f2018-11-06 16:02:12 -07001344 ok_boards = [] # List of boards fixed since last commit
Simon Glass071a1782018-11-06 16:02:13 -07001345 warn_boards = [] # List of boards with warnings since last commit
Simon Glass454507f2018-11-06 16:02:12 -07001346 err_boards = [] # List of new broken boards since last commit
1347 new_boards = [] # List of boards that didn't exist last time
1348 unknown_boards = [] # List of boards that were not built
Simon Glassc05694f2013-04-03 11:07:16 +00001349
1350 for target in board_dict:
1351 if target not in board_selected:
1352 continue
1353
1354 # If the board was built last time, add its outcome to a list
1355 if target in self._base_board_dict:
1356 base_outcome = self._base_board_dict[target].rc
1357 outcome = board_dict[target]
1358 if outcome.rc == OUTCOME_UNKNOWN:
Simon Glass454507f2018-11-06 16:02:12 -07001359 unknown_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001360 elif outcome.rc < base_outcome:
Simon Glass071a1782018-11-06 16:02:13 -07001361 if outcome.rc == OUTCOME_WARNING:
1362 warn_boards.append(target)
1363 else:
1364 ok_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001365 elif outcome.rc > base_outcome:
Simon Glass071a1782018-11-06 16:02:13 -07001366 if outcome.rc == OUTCOME_WARNING:
1367 warn_boards.append(target)
1368 else:
1369 err_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001370 else:
Simon Glass454507f2018-11-06 16:02:12 -07001371 new_boards.append(target)
Simon Glassc05694f2013-04-03 11:07:16 +00001372
Simon Glassac500222020-04-09 15:08:28 -06001373 # Get a list of errors and warnings that have appeared, and disappeared
Simon Glass03749d42014-08-28 09:43:44 -06001374 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1375 self._base_err_line_boards, err_lines, err_line_boards, '')
1376 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1377 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassc05694f2013-04-03 11:07:16 +00001378
Simon Glass6c435622022-07-11 19:03:56 -06001379 # For the IDE mode, print out all the output
1380 if self._ide:
1381 outcome = board_dict[target]
1382 for line in outcome.err_lines:
1383 sys.stderr.write(line)
1384
Simon Glassc05694f2013-04-03 11:07:16 +00001385 # Display results by arch
Simon Glass6c435622022-07-11 19:03:56 -06001386 elif any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
Simon Glass071a1782018-11-06 16:02:13 -07001387 worse_err, better_err, worse_warn, better_warn)):
Simon Glassc05694f2013-04-03 11:07:16 +00001388 arch_list = {}
Simon Glass454507f2018-11-06 16:02:12 -07001389 self.AddOutcome(board_selected, arch_list, ok_boards, '',
Simon Glassc05694f2013-04-03 11:07:16 +00001390 self.col.GREEN)
Simon Glass071a1782018-11-06 16:02:13 -07001391 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1392 self.col.YELLOW)
Simon Glass454507f2018-11-06 16:02:12 -07001393 self.AddOutcome(board_selected, arch_list, err_boards, '+',
Simon Glassc05694f2013-04-03 11:07:16 +00001394 self.col.RED)
Simon Glass454507f2018-11-06 16:02:12 -07001395 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
Simon Glassc05694f2013-04-03 11:07:16 +00001396 if self._show_unknown:
Simon Glass454507f2018-11-06 16:02:12 -07001397 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
Simon Glassc05694f2013-04-03 11:07:16 +00001398 self.col.MAGENTA)
Simon Glassc78ed662019-10-31 07:42:53 -06001399 for arch, target_list in arch_list.items():
Simon Glass02811582022-01-29 14:14:18 -07001400 tprint('%10s: %s' % (arch, target_list))
Simon Glassbb4dffb2014-08-09 15:33:06 -06001401 self._error_lines += 1
Simon Glassac500222020-04-09 15:08:28 -06001402 _OutputErrLines(better_err, colour=self.col.GREEN)
1403 _OutputErrLines(worse_err, colour=self.col.RED)
1404 _OutputErrLines(better_warn, colour=self.col.CYAN)
Simon Glass564ddac2020-04-09 15:08:35 -06001405 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
Simon Glassc05694f2013-04-03 11:07:16 +00001406
1407 if show_sizes:
1408 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1409 show_bloat)
1410
Alex Kiernan4059e302018-05-31 04:48:34 +00001411 if show_environment and self._base_environment:
1412 lines = []
1413
1414 for target in board_dict:
1415 if target not in board_selected:
1416 continue
1417
1418 tbase = self._base_environment[target]
1419 tenvironment = environment[target]
1420 environment_plus = {}
1421 environment_minus = {}
1422 environment_change = {}
1423 base = tbase.environment
Simon Glassc78ed662019-10-31 07:42:53 -06001424 for key, value in tenvironment.environment.items():
Alex Kiernan4059e302018-05-31 04:48:34 +00001425 if key not in base:
1426 environment_plus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001427 for key, value in base.items():
Alex Kiernan4059e302018-05-31 04:48:34 +00001428 if key not in tenvironment.environment:
1429 environment_minus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001430 for key, value in base.items():
Alex Kiernan4059e302018-05-31 04:48:34 +00001431 new_value = tenvironment.environment.get(key)
1432 if new_value and value != new_value:
1433 desc = '%s -> %s' % (value, new_value)
1434 environment_change[key] = desc
1435
1436 _AddConfig(lines, target, environment_plus, environment_minus,
1437 environment_change)
1438
1439 _OutputConfigInfo(lines)
1440
Simon Glasscad8abf2015-08-25 21:52:14 -06001441 if show_config and self._base_config:
1442 summary = {}
1443 arch_config_plus = {}
1444 arch_config_minus = {}
1445 arch_config_change = {}
1446 arch_list = []
1447
1448 for target in board_dict:
1449 if target not in board_selected:
1450 continue
1451 arch = board_selected[target].arch
1452 if arch not in arch_list:
1453 arch_list.append(arch)
1454
1455 for arch in arch_list:
1456 arch_config_plus[arch] = {}
1457 arch_config_minus[arch] = {}
1458 arch_config_change[arch] = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001459 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001460 arch_config_plus[arch][name] = {}
1461 arch_config_minus[arch][name] = {}
1462 arch_config_change[arch][name] = {}
1463
1464 for target in board_dict:
1465 if target not in board_selected:
Simon Glassdb17fb82015-02-05 22:06:15 -07001466 continue
Simon Glasscad8abf2015-08-25 21:52:14 -06001467
1468 arch = board_selected[target].arch
1469
1470 all_config_plus = {}
1471 all_config_minus = {}
1472 all_config_change = {}
1473 tbase = self._base_config[target]
1474 tconfig = config[target]
1475 lines = []
Simon Glasscde5c302016-11-13 14:25:53 -07001476 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001477 if not tconfig.config[name]:
1478 continue
1479 config_plus = {}
1480 config_minus = {}
1481 config_change = {}
1482 base = tbase.config[name]
Simon Glassc78ed662019-10-31 07:42:53 -06001483 for key, value in tconfig.config[name].items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001484 if key not in base:
1485 config_plus[key] = value
1486 all_config_plus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001487 for key, value in base.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001488 if key not in tconfig.config[name]:
1489 config_minus[key] = value
1490 all_config_minus[key] = value
Simon Glassc78ed662019-10-31 07:42:53 -06001491 for key, value in base.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001492 new_value = tconfig.config.get(key)
1493 if new_value and value != new_value:
1494 desc = '%s -> %s' % (value, new_value)
1495 config_change[key] = desc
1496 all_config_change[key] = desc
1497
1498 arch_config_plus[arch][name].update(config_plus)
1499 arch_config_minus[arch][name].update(config_minus)
1500 arch_config_change[arch][name].update(config_change)
1501
1502 _AddConfig(lines, name, config_plus, config_minus,
1503 config_change)
1504 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1505 all_config_change)
1506 summary[target] = '\n'.join(lines)
1507
1508 lines_by_target = {}
Simon Glassc78ed662019-10-31 07:42:53 -06001509 for target, lines in summary.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001510 if lines in lines_by_target:
1511 lines_by_target[lines].append(target)
1512 else:
1513 lines_by_target[lines] = [target]
1514
1515 for arch in arch_list:
1516 lines = []
1517 all_plus = {}
1518 all_minus = {}
1519 all_change = {}
Simon Glasscde5c302016-11-13 14:25:53 -07001520 for name in self.config_filenames:
Simon Glasscad8abf2015-08-25 21:52:14 -06001521 all_plus.update(arch_config_plus[arch][name])
1522 all_minus.update(arch_config_minus[arch][name])
1523 all_change.update(arch_config_change[arch][name])
1524 _AddConfig(lines, name, arch_config_plus[arch][name],
1525 arch_config_minus[arch][name],
1526 arch_config_change[arch][name])
1527 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1528 #arch_summary[target] = '\n'.join(lines)
1529 if lines:
Simon Glass02811582022-01-29 14:14:18 -07001530 tprint('%s:' % arch)
Simon Glasscad8abf2015-08-25 21:52:14 -06001531 _OutputConfigInfo(lines)
1532
Simon Glassc78ed662019-10-31 07:42:53 -06001533 for lines, targets in lines_by_target.items():
Simon Glasscad8abf2015-08-25 21:52:14 -06001534 if not lines:
1535 continue
Simon Glass02811582022-01-29 14:14:18 -07001536 tprint('%s :' % ' '.join(sorted(targets)))
Simon Glasscad8abf2015-08-25 21:52:14 -06001537 _OutputConfigInfo(lines.split('\n'))
1538
Simon Glassdb17fb82015-02-05 22:06:15 -07001539
Simon Glassc05694f2013-04-03 11:07:16 +00001540 # Save our updated information for the next call to this function
1541 self._base_board_dict = board_dict
1542 self._base_err_lines = err_lines
Simon Glass03749d42014-08-28 09:43:44 -06001543 self._base_warn_lines = warn_lines
1544 self._base_err_line_boards = err_line_boards
1545 self._base_warn_line_boards = warn_line_boards
Simon Glassdb17fb82015-02-05 22:06:15 -07001546 self._base_config = config
Alex Kiernan4059e302018-05-31 04:48:34 +00001547 self._base_environment = environment
Simon Glassc05694f2013-04-03 11:07:16 +00001548
1549 # Get a list of boards that did not get built, if needed
1550 not_built = []
Simon Glass8132f982022-07-11 19:03:57 -06001551 for brd in board_selected:
1552 if not brd in board_dict:
1553 not_built.append(brd)
Simon Glassc05694f2013-04-03 11:07:16 +00001554 if not_built:
Simon Glass02811582022-01-29 14:14:18 -07001555 tprint("Boards not built (%d): %s" % (len(not_built),
Simon Glass4433aa92014-09-05 19:00:07 -06001556 ', '.join(not_built)))
Simon Glassc05694f2013-04-03 11:07:16 +00001557
Simon Glasseb48bbc2014-08-09 15:33:02 -06001558 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glass03749d42014-08-28 09:43:44 -06001559 (board_dict, err_lines, err_line_boards, warn_lines,
Alex Kiernan4059e302018-05-31 04:48:34 +00001560 warn_line_boards, config, environment) = self.GetResultSummary(
Simon Glass3394c9f2014-08-28 09:43:43 -06001561 board_selected, commit_upto,
Simon Glassdb17fb82015-02-05 22:06:15 -07001562 read_func_sizes=self._show_bloat,
Alex Kiernan4059e302018-05-31 04:48:34 +00001563 read_config=self._show_config,
1564 read_environment=self._show_environment)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001565 if commits:
1566 msg = '%02d: %s' % (commit_upto + 1,
1567 commits[commit_upto].subject)
Simon Glass02811582022-01-29 14:14:18 -07001568 tprint(msg, colour=self.col.BLUE)
Simon Glasseb48bbc2014-08-09 15:33:02 -06001569 self.PrintResultSummary(board_selected, board_dict,
Simon Glass3394c9f2014-08-28 09:43:43 -06001570 err_lines if self._show_errors else [], err_line_boards,
Simon Glass03749d42014-08-28 09:43:44 -06001571 warn_lines if self._show_errors else [], warn_line_boards,
Alex Kiernan4059e302018-05-31 04:48:34 +00001572 config, environment, self._show_sizes, self._show_detail,
1573 self._show_bloat, self._show_config, self._show_environment)
Simon Glassc05694f2013-04-03 11:07:16 +00001574
Simon Glasseb48bbc2014-08-09 15:33:02 -06001575 def ShowSummary(self, commits, board_selected):
Simon Glassc05694f2013-04-03 11:07:16 +00001576 """Show a build summary for U-Boot for a given board list.
1577
1578 Reset the result summary, then repeatedly call GetResultSummary on
1579 each commit's results, then display the differences we see.
1580
1581 Args:
1582 commit: Commit objects to summarise
1583 board_selected: Dict containing boards to summarise
Simon Glassc05694f2013-04-03 11:07:16 +00001584 """
Simon Glassd326ad72014-08-09 15:32:59 -06001585 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001586 self.commits = commits
1587 self.ResetResultSummary(board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001588 self._error_lines = 0
Simon Glassc05694f2013-04-03 11:07:16 +00001589
1590 for commit_upto in range(0, self.commit_count, self._step):
Simon Glasseb48bbc2014-08-09 15:33:02 -06001591 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glassbb4dffb2014-08-09 15:33:06 -06001592 if not self._error_lines:
Simon Glass02811582022-01-29 14:14:18 -07001593 tprint('(no errors to report)', colour=self.col.GREEN)
Simon Glassc05694f2013-04-03 11:07:16 +00001594
1595
1596 def SetupBuild(self, board_selected, commits):
1597 """Set up ready to start a build.
1598
1599 Args:
1600 board_selected: Selected boards to build
1601 commits: Selected commits to build
1602 """
1603 # First work out how many commits we will build
Simon Glassc78ed662019-10-31 07:42:53 -06001604 count = (self.commit_count + self._step - 1) // self._step
Simon Glassc05694f2013-04-03 11:07:16 +00001605 self.count = len(board_selected) * count
1606 self.upto = self.warned = self.fail = 0
1607 self._timestamps = collections.deque()
1608
Simon Glassc05694f2013-04-03 11:07:16 +00001609 def GetThreadDir(self, thread_num):
1610 """Get the directory path to the working dir for a thread.
1611
1612 Args:
Simon Glassc635d892021-01-30 22:17:46 -07001613 thread_num: Number of thread to check (-1 for main process, which
1614 is treated as 0)
Simon Glassc05694f2013-04-03 11:07:16 +00001615 """
Simon Glassb6eb8cf2020-03-18 09:42:42 -06001616 if self.work_in_output:
1617 return self._working_dir
Simon Glassc635d892021-01-30 22:17:46 -07001618 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
Simon Glassc05694f2013-04-03 11:07:16 +00001619
Simon Glassd326ad72014-08-09 15:32:59 -06001620 def _PrepareThread(self, thread_num, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001621 """Prepare the working directory for a thread.
1622
1623 This clones or fetches the repo into the thread's work directory.
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001624 Optionally, it can create a linked working tree of the repo in the
1625 thread's work directory instead.
Simon Glassc05694f2013-04-03 11:07:16 +00001626
1627 Args:
1628 thread_num: Thread number (0, 1, ...)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001629 setup_git:
1630 'clone' to set up a git clone
1631 'worktree' to set up a git worktree
Simon Glassc05694f2013-04-03 11:07:16 +00001632 """
1633 thread_dir = self.GetThreadDir(thread_num)
Simon Glass4a1e88b2014-08-09 15:33:00 -06001634 builderthread.Mkdir(thread_dir)
Simon Glassc05694f2013-04-03 11:07:16 +00001635 git_dir = os.path.join(thread_dir, '.git')
1636
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001637 # Create a worktree or a git repo clone for this thread if it
1638 # doesn't already exist
Simon Glassd326ad72014-08-09 15:32:59 -06001639 if setup_git and self.git_dir:
Simon Glassc05694f2013-04-03 11:07:16 +00001640 src_dir = os.path.abspath(self.git_dir)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001641 if os.path.isdir(git_dir):
1642 # This is a clone of the src_dir repo, we can keep using
1643 # it but need to fetch from src_dir.
Simon Glass02811582022-01-29 14:14:18 -07001644 tprint('\rFetching repo for thread %d' % thread_num,
Simon Glass43054932020-04-09 15:08:43 -06001645 newline=False)
Simon Glass761648b2022-01-29 14:14:11 -07001646 gitutil.fetch(git_dir, thread_dir)
Simon Glass02811582022-01-29 14:14:18 -07001647 terminal.print_clear()
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001648 elif os.path.isfile(git_dir):
1649 # This is a worktree of the src_dir repo, we don't need to
1650 # create it again or update it in any way.
1651 pass
1652 elif os.path.exists(git_dir):
1653 # Don't know what could trigger this, but we probably
1654 # can't create a git worktree/clone here.
1655 raise ValueError('Git dir %s exists, but is not a file '
1656 'or a directory.' % git_dir)
1657 elif setup_git == 'worktree':
Simon Glass02811582022-01-29 14:14:18 -07001658 tprint('\rChecking out worktree for thread %d' % thread_num,
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001659 newline=False)
Simon Glass761648b2022-01-29 14:14:11 -07001660 gitutil.add_worktree(src_dir, thread_dir)
Simon Glass02811582022-01-29 14:14:18 -07001661 terminal.print_clear()
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001662 elif setup_git == 'clone' or setup_git == True:
Simon Glass02811582022-01-29 14:14:18 -07001663 tprint('\rCloning repo for thread %d' % thread_num,
Simon Glass2e2a6e62016-09-18 16:48:31 -06001664 newline=False)
Simon Glass761648b2022-01-29 14:14:11 -07001665 gitutil.clone(src_dir, thread_dir)
Simon Glass02811582022-01-29 14:14:18 -07001666 terminal.print_clear()
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001667 else:
1668 raise ValueError("Can't setup git repo with %s." % setup_git)
Simon Glassc05694f2013-04-03 11:07:16 +00001669
Simon Glassd326ad72014-08-09 15:32:59 -06001670 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassc05694f2013-04-03 11:07:16 +00001671 """Prepare the working directory for use.
1672
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001673 Set up the git repo for each thread. Creates a linked working tree
1674 if git-worktree is available, or clones the repo if it isn't.
Simon Glassc05694f2013-04-03 11:07:16 +00001675
1676 Args:
Simon Glassc635d892021-01-30 22:17:46 -07001677 max_threads: Maximum number of threads we expect to need. If 0 then
1678 1 is set up, since the main process still needs somewhere to
1679 work
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001680 setup_git: True to set up a git worktree or a git clone
Simon Glassc05694f2013-04-03 11:07:16 +00001681 """
Simon Glass4a1e88b2014-08-09 15:33:00 -06001682 builderthread.Mkdir(self._working_dir)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001683 if setup_git and self.git_dir:
1684 src_dir = os.path.abspath(self.git_dir)
Simon Glass761648b2022-01-29 14:14:11 -07001685 if gitutil.check_worktree_is_available(src_dir):
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001686 setup_git = 'worktree'
1687 # If we previously added a worktree but the directory for it
1688 # got deleted, we need to prune its files from the repo so
1689 # that we can check out another in its place.
Simon Glass761648b2022-01-29 14:14:11 -07001690 gitutil.prune_worktrees(src_dir)
Alper Nebi Yasakfede44a2020-09-03 15:51:03 +03001691 else:
1692 setup_git = 'clone'
Simon Glassc635d892021-01-30 22:17:46 -07001693
1694 # Always do at least one thread
1695 for thread in range(max(max_threads, 1)):
Simon Glassd326ad72014-08-09 15:32:59 -06001696 self._PrepareThread(thread, setup_git)
Simon Glassc05694f2013-04-03 11:07:16 +00001697
Simon Glass5dc1ca72020-03-18 09:42:45 -06001698 def _GetOutputSpaceRemovals(self):
Simon Glassc05694f2013-04-03 11:07:16 +00001699 """Get the output directories ready to receive files.
1700
Simon Glass5dc1ca72020-03-18 09:42:45 -06001701 Figure out what needs to be deleted in the output directory before it
1702 can be used. We only delete old buildman directories which have the
1703 expected name pattern. See _GetOutputDir().
1704
1705 Returns:
1706 List of full paths of directories to remove
Simon Glassc05694f2013-04-03 11:07:16 +00001707 """
Simon Glassb9a9b7c2014-12-01 17:33:53 -07001708 if not self.commits:
1709 return
Simon Glassc05694f2013-04-03 11:07:16 +00001710 dir_list = []
1711 for commit_upto in range(self.commit_count):
1712 dir_list.append(self._GetOutputDir(commit_upto))
1713
Simon Glass83cb6cc2016-09-18 16:48:32 -06001714 to_remove = []
Simon Glassc05694f2013-04-03 11:07:16 +00001715 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1716 if dirname not in dir_list:
Simon Glass5dc1ca72020-03-18 09:42:45 -06001717 leaf = dirname[len(self.base_dir) + 1:]
Ovidiu Panaitee8e9cb2020-05-15 09:30:12 +03001718 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
Simon Glass5dc1ca72020-03-18 09:42:45 -06001719 if m:
1720 to_remove.append(dirname)
1721 return to_remove
1722
1723 def _PrepareOutputSpace(self):
1724 """Get the output directories ready to receive files.
1725
1726 We delete any output directories which look like ones we need to
1727 create. Having left over directories is confusing when the user wants
1728 to check the output manually.
1729 """
1730 to_remove = self._GetOutputSpaceRemovals()
Simon Glass83cb6cc2016-09-18 16:48:32 -06001731 if to_remove:
Simon Glass02811582022-01-29 14:14:18 -07001732 tprint('Removing %d old build directories...' % len(to_remove),
Simon Glass83cb6cc2016-09-18 16:48:32 -06001733 newline=False)
1734 for dirname in to_remove:
Simon Glassc05694f2013-04-03 11:07:16 +00001735 shutil.rmtree(dirname)
Simon Glass02811582022-01-29 14:14:18 -07001736 terminal.print_clear()
Simon Glassc05694f2013-04-03 11:07:16 +00001737
Simon Glass78e418e2014-08-09 15:33:03 -06001738 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassc05694f2013-04-03 11:07:16 +00001739 """Build all commits for a list of boards
1740
1741 Args:
1742 commits: List of commits to be build, each a Commit object
1743 boards_selected: Dict of selected boards, key is target name,
1744 value is Board object
Simon Glassc05694f2013-04-03 11:07:16 +00001745 keep_outputs: True to save build output files
Simon Glass78e418e2014-08-09 15:33:03 -06001746 verbose: Display build results as they are completed
Simon Glassc2f91072014-08-28 09:43:39 -06001747 Returns:
1748 Tuple containing:
1749 - number of boards that failed to build
1750 - number of boards that issued warnings
Simon Glass9bf9a722021-04-11 16:27:27 +12001751 - list of thread exceptions raised
Simon Glassc05694f2013-04-03 11:07:16 +00001752 """
Simon Glassd326ad72014-08-09 15:32:59 -06001753 self.commit_count = len(commits) if commits else 1
Simon Glassc05694f2013-04-03 11:07:16 +00001754 self.commits = commits
Simon Glass78e418e2014-08-09 15:33:03 -06001755 self._verbose = verbose
Simon Glassc05694f2013-04-03 11:07:16 +00001756
1757 self.ResetResultSummary(board_selected)
Thierry Reding336d5bd2014-08-19 10:22:39 +02001758 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassd326ad72014-08-09 15:32:59 -06001759 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1760 commits is not None)
Simon Glassc05694f2013-04-03 11:07:16 +00001761 self._PrepareOutputSpace()
Simon Glass6c435622022-07-11 19:03:56 -06001762 if not self._ide:
1763 tprint('\rStarting build...', newline=False)
Simon Glassc05694f2013-04-03 11:07:16 +00001764 self.SetupBuild(board_selected, commits)
1765 self.ProcessResult(None)
Simon Glass9bf9a722021-04-11 16:27:27 +12001766 self.thread_exceptions = []
Simon Glassc05694f2013-04-03 11:07:16 +00001767 # Create jobs to build all commits for each board
Simon Glassc78ed662019-10-31 07:42:53 -06001768 for brd in board_selected.values():
Simon Glass4a1e88b2014-08-09 15:33:00 -06001769 job = builderthread.BuilderJob()
Simon Glass8132f982022-07-11 19:03:57 -06001770 job.brd = brd
Simon Glassc05694f2013-04-03 11:07:16 +00001771 job.commits = commits
1772 job.keep_outputs = keep_outputs
Simon Glassb6eb8cf2020-03-18 09:42:42 -06001773 job.work_in_output = self.work_in_output
Simon Glasse5650a82022-01-22 05:07:33 -07001774 job.adjust_cfg = self.adjust_cfg
Simon Glassc05694f2013-04-03 11:07:16 +00001775 job.step = self._step
Simon Glassc635d892021-01-30 22:17:46 -07001776 if self.num_threads:
1777 self.queue.put(job)
1778 else:
Simon Glasse36fe012022-02-11 13:23:19 -07001779 self._single_builder.RunJob(job)
Simon Glassc05694f2013-04-03 11:07:16 +00001780
Simon Glassc635d892021-01-30 22:17:46 -07001781 if self.num_threads:
1782 term = threading.Thread(target=self.queue.join)
1783 term.setDaemon(True)
1784 term.start()
1785 while term.is_alive():
1786 term.join(100)
Simon Glassc05694f2013-04-03 11:07:16 +00001787
Simon Glassc635d892021-01-30 22:17:46 -07001788 # Wait until we have processed all output
1789 self.out_queue.join()
Simon Glass6c435622022-07-11 19:03:56 -06001790 if not self._ide:
1791 tprint()
Simon Glass726ae812020-04-09 15:08:47 -06001792
Simon Glass6c435622022-07-11 19:03:56 -06001793 msg = 'Completed: %d total built' % self.count
1794 if self.already_done:
1795 msg += ' (%d previously' % self.already_done
1796 if self.already_done != self.count:
1797 msg += ', %d newly' % (self.count - self.already_done)
1798 msg += ')'
1799 duration = datetime.now() - self._start_time
1800 if duration > timedelta(microseconds=1000000):
1801 if duration.microseconds >= 500000:
1802 duration = duration + timedelta(seconds=1)
1803 duration = duration - timedelta(microseconds=duration.microseconds)
1804 rate = float(self.count) / duration.total_seconds()
1805 msg += ', duration %s, rate %1.2f' % (duration, rate)
1806 tprint(msg)
1807 if self.thread_exceptions:
1808 tprint('Failed: %d thread exceptions' % len(self.thread_exceptions),
1809 colour=self.col.RED)
Simon Glass726ae812020-04-09 15:08:47 -06001810
Simon Glass9bf9a722021-04-11 16:27:27 +12001811 return (self.fail, self.warned, self.thread_exceptions)