blob: aa2ffe16f6c3d67e5803be0b79b720e26dc59b27 [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 Glassfc3fe1c2013-04-03 11:07:16 +0000216 """
217 class Outcome:
218 """Records a build outcome for a single make invocation
219
220 Public Members:
221 rc: Outcome value (OUTCOME_...)
222 err_lines: List of error lines or [] if none
223 sizes: Dictionary of image size information, keyed by filename
224 - Each value is itself a dictionary containing
225 values for 'text', 'data' and 'bss', being the integer
226 size in bytes of each section.
227 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
228 value is itself a dictionary:
229 key: function name
230 value: Size of function in bytes
Simon Glass843312d2015-02-05 22:06:15 -0700231 config: Dictionary keyed by filename - e.g. '.config'. Each
232 value is itself a dictionary:
233 key: config name
234 value: config value
Alex Kiernan48ae4122018-05-31 04:48:34 +0000235 environment: Dictionary keyed by environment variable, Each
236 value is the value of environment variable.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000237 """
Alex Kiernan48ae4122018-05-31 04:48:34 +0000238 def __init__(self, rc, err_lines, sizes, func_sizes, config,
239 environment):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000240 self.rc = rc
241 self.err_lines = err_lines
242 self.sizes = sizes
243 self.func_sizes = func_sizes
Simon Glass843312d2015-02-05 22:06:15 -0700244 self.config = config
Alex Kiernan48ae4122018-05-31 04:48:34 +0000245 self.environment = environment
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000246
247 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
Simon Glass5971ab52014-12-01 17:33:55 -0700248 gnu_make='make', checkout=True, show_unknown=True, step=1,
Stephen Warrenf79f1e02016-04-11 10:48:44 -0600249 no_subdirs=False, full_path=False, verbose_build=False,
Simon Glasseb70a2c2020-04-09 15:08:51 -0600250 mrproper=False, per_board_out_dir=False,
Daniel Schwierzeck2371d1b2018-01-26 16:31:05 +0100251 config_only=False, squash_config_y=False,
Simon Glass8116c782021-04-11 16:27:27 +1200252 warnings_as_errors=False, work_in_output=False,
Simon Glass2b4806e2022-01-22 05:07:33 -0700253 test_thread_exceptions=False, adjust_cfg=None):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000254 """Create a new Builder object
255
256 Args:
257 toolchains: Toolchains object to use for building
258 base_dir: Base directory to use for builder
259 git_dir: Git directory containing source repository
260 num_threads: Number of builder threads to run
261 num_jobs: Number of jobs to run at once (passed to make as -j)
Masahiro Yamada99796922014-07-22 11:19:09 +0900262 gnu_make: the command name of GNU Make.
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000263 checkout: True to check out source, False to skip that step.
264 This is used for testing.
265 show_unknown: Show unknown boards (those not built) in summary
266 step: 1 to process every commit, n to process every nth commit
Simon Glassbb1501f2014-12-01 17:34:00 -0700267 no_subdirs: Don't create subdirectories when building current
268 source for a single board
269 full_path: Return the full path in CROSS_COMPILE and don't set
270 PATH
Simon Glassd2ce6582014-12-01 17:34:07 -0700271 verbose_build: Run build with V=1 and don't use 'make -s'
Simon Glasseb70a2c2020-04-09 15:08:51 -0600272 mrproper: Always run 'make mrproper' when configuring
Stephen Warrenf79f1e02016-04-11 10:48:44 -0600273 per_board_out_dir: Build in a separate persistent directory per
274 board rather than a thread-specific directory
Simon Glassb50113f2016-11-13 14:25:51 -0700275 config_only: Only configure each build, don't build it
Simon Glassb464f8e2016-11-13 14:25:53 -0700276 squash_config_y: Convert CONFIG options with the value 'y' to '1'
Daniel Schwierzeck2371d1b2018-01-26 16:31:05 +0100277 warnings_as_errors: Treat all compiler warnings as errors
Simon Glassd829f122020-03-18 09:42:42 -0600278 work_in_output: Use the output directory as the work directory and
279 don't write to a separate output directory.
Simon Glass8116c782021-04-11 16:27:27 +1200280 test_thread_exceptions: Uses for tests only, True to make the
281 threads raise an exception instead of reporting their result.
282 This simulates a failure in the code somewhere
Simon Glass2b4806e2022-01-22 05:07:33 -0700283 adjust_cfg_list (list of str): List of changes to make to .config
284 file before building. Each is one of (where C is the config
285 option with or without the CONFIG_ prefix)
286
287 C to enable C
288 ~C to disable C
289 C=val to set the value of C (val must have quotes if C is
290 a string Kconfig
291
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000292 """
293 self.toolchains = toolchains
294 self.base_dir = base_dir
Simon Glassd829f122020-03-18 09:42:42 -0600295 if work_in_output:
296 self._working_dir = base_dir
297 else:
298 self._working_dir = os.path.join(base_dir, '.bm-work')
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000299 self.threads = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000300 self.do_make = self.Make
Masahiro Yamada99796922014-07-22 11:19:09 +0900301 self.gnu_make = gnu_make
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000302 self.checkout = checkout
303 self.num_threads = num_threads
304 self.num_jobs = num_jobs
305 self.already_done = 0
306 self.force_build = False
307 self.git_dir = git_dir
308 self._show_unknown = show_unknown
309 self._timestamp_count = 10
310 self._build_period_us = None
311 self._complete_delay = None
312 self._next_delay_update = datetime.now()
Simon Glass7b33f212020-04-09 15:08:47 -0600313 self._start_time = datetime.now()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000314 self.force_config_on_failure = True
Simon Glass4266dc22014-07-13 12:22:31 -0600315 self.force_build_failures = False
Simon Glass97e91522014-07-14 17:51:02 -0600316 self.force_reconfig = False
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000317 self._step = step
Simon Glass189a4962014-07-14 17:51:03 -0600318 self.in_tree = False
Simon Glass28370c12014-08-09 15:33:06 -0600319 self._error_lines = 0
Simon Glass5971ab52014-12-01 17:33:55 -0700320 self.no_subdirs = no_subdirs
Simon Glassbb1501f2014-12-01 17:34:00 -0700321 self.full_path = full_path
Simon Glassd2ce6582014-12-01 17:34:07 -0700322 self.verbose_build = verbose_build
Simon Glassb50113f2016-11-13 14:25:51 -0700323 self.config_only = config_only
Simon Glassb464f8e2016-11-13 14:25:53 -0700324 self.squash_config_y = squash_config_y
325 self.config_filenames = BASE_CONFIG_FILENAMES
Simon Glassd829f122020-03-18 09:42:42 -0600326 self.work_in_output = work_in_output
Simon Glass2b4806e2022-01-22 05:07:33 -0700327 self.adjust_cfg = adjust_cfg
328
Simon Glassb464f8e2016-11-13 14:25:53 -0700329 if not self.squash_config_y:
330 self.config_filenames += EXTRA_CONFIG_FILENAMES
Simon Glass7bf83a52021-10-19 21:43:24 -0600331 self._terminated = False
332 self._restarting_config = False
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000333
Daniel Schwierzeck2371d1b2018-01-26 16:31:05 +0100334 self.warnings_as_errors = warnings_as_errors
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000335 self.col = terminal.Color()
336
Simon Glasse30965d2014-08-28 09:43:44 -0600337 self._re_function = re.compile('(.*): In function.*')
338 self._re_files = re.compile('In file included from.*')
339 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
Simon Glass2d483332018-11-06 16:02:11 -0700340 self._re_dtb_warning = re.compile('(.*): Warning .*')
Simon Glasse30965d2014-08-28 09:43:44 -0600341 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
Simon Glass113a8a52020-04-09 15:08:53 -0600342 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
343 re.MULTILINE | re.DOTALL)
Simon Glasse30965d2014-08-28 09:43:44 -0600344
Simon Glass8116c782021-04-11 16:27:27 +1200345 self.thread_exceptions = []
346 self.test_thread_exceptions = test_thread_exceptions
Simon Glassb82492b2021-01-30 22:17:46 -0700347 if self.num_threads:
348 self._single_builder = None
349 self.queue = queue.Queue()
350 self.out_queue = queue.Queue()
351 for i in range(self.num_threads):
Simon Glass8116c782021-04-11 16:27:27 +1200352 t = builderthread.BuilderThread(
353 self, i, mrproper, per_board_out_dir,
354 test_exception=test_thread_exceptions)
Simon Glassb82492b2021-01-30 22:17:46 -0700355 t.setDaemon(True)
356 t.start()
357 self.threads.append(t)
358
359 t = builderthread.ResultThread(self)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000360 t.setDaemon(True)
361 t.start()
362 self.threads.append(t)
Simon Glassb82492b2021-01-30 22:17:46 -0700363 else:
364 self._single_builder = builderthread.BuilderThread(
365 self, -1, mrproper, per_board_out_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000366
367 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
368 self.re_make_err = re.compile('|'.join(ignore_lines))
369
Simon Glass2f256642016-09-18 16:48:37 -0600370 # Handle existing graceful with SIGINT / Ctrl-C
371 signal.signal(signal.SIGINT, self.signal_handler)
372
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000373 def __del__(self):
374 """Get rid of all threads created by the builder"""
375 for t in self.threads:
376 del t
377
Simon Glass2f256642016-09-18 16:48:37 -0600378 def signal_handler(self, signal, frame):
379 sys.exit(1)
380
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600381 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
Simon Glassed966652014-08-28 09:43:43 -0600382 show_detail=False, show_bloat=False,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000383 list_error_boards=False, show_config=False,
Simon Glass113a8a52020-04-09 15:08:53 -0600384 show_environment=False, filter_dtb_warnings=False,
385 filter_migration_warnings=False):
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600386 """Setup display options for the builder.
387
Simon Glass174592b2020-04-09 15:08:52 -0600388 Args:
389 show_errors: True to show summarised error/warning info
390 show_sizes: Show size deltas
391 show_detail: Show size delta detail for each board if show_sizes
392 show_bloat: Show detail for each function
393 list_error_boards: Show the boards which caused each error/warning
394 show_config: Show config deltas
395 show_environment: Show environment deltas
396 filter_dtb_warnings: Filter out any warnings from the device-tree
397 compiler
Simon Glass113a8a52020-04-09 15:08:53 -0600398 filter_migration_warnings: Filter out any warnings about migrating
399 a board to driver model
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600400 """
401 self._show_errors = show_errors
402 self._show_sizes = show_sizes
403 self._show_detail = show_detail
404 self._show_bloat = show_bloat
Simon Glassed966652014-08-28 09:43:43 -0600405 self._list_error_boards = list_error_boards
Simon Glass843312d2015-02-05 22:06:15 -0700406 self._show_config = show_config
Alex Kiernan48ae4122018-05-31 04:48:34 +0000407 self._show_environment = show_environment
Simon Glass174592b2020-04-09 15:08:52 -0600408 self._filter_dtb_warnings = filter_dtb_warnings
Simon Glass113a8a52020-04-09 15:08:53 -0600409 self._filter_migration_warnings = filter_migration_warnings
Simon Glassb2ea7ab2014-08-09 15:33:02 -0600410
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000411 def _AddTimestamp(self):
412 """Add a new timestamp to the list and record the build period.
413
414 The build period is the length of time taken to perform a single
415 build (one board, one commit).
416 """
417 now = datetime.now()
418 self._timestamps.append(now)
419 count = len(self._timestamps)
420 delta = self._timestamps[-1] - self._timestamps[0]
421 seconds = delta.total_seconds()
422
423 # If we have enough data, estimate build period (time taken for a
424 # single build) and therefore completion time.
425 if count > 1 and self._next_delay_update < now:
426 self._next_delay_update = now + timedelta(seconds=2)
427 if seconds > 0:
428 self._build_period = float(seconds) / count
429 todo = self.count - self.upto
430 self._complete_delay = timedelta(microseconds=
431 self._build_period * todo * 1000000)
432 # Round it
433 self._complete_delay -= timedelta(
434 microseconds=self._complete_delay.microseconds)
435
436 if seconds > 60:
437 self._timestamps.popleft()
438 count -= 1
439
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000440 def SelectCommit(self, commit, checkout=True):
441 """Checkout the selected commit for this build
442 """
443 self.commit = commit
444 if checkout and self.checkout:
Simon Glass0157b182022-01-29 14:14:11 -0700445 gitutil.checkout(commit.hash)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000446
447 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
448 """Run make
449
450 Args:
451 commit: Commit object that is being built
452 brd: Board object that is being built
Roger Meierfd18a892014-08-20 22:10:29 +0200453 stage: Stage that we are at (mrproper, config, build)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000454 cwd: Directory where make should be run
455 args: Arguments to pass to make
Simon Glassd9800692022-01-29 14:14:05 -0700456 kwargs: Arguments to pass to command.run_pipe()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000457 """
Simon Glass7bf83a52021-10-19 21:43:24 -0600458
459 def check_output(stream, data):
460 if b'Restart config' in data:
461 self._restarting_config = True
462
463 # If we see 'Restart config' following by multiple errors
464 if self._restarting_config:
465 m = RE_NO_DEFAULT.findall(data)
466
467 # Number of occurences of each Kconfig item
468 multiple = [m.count(val) for val in set(m)]
469
470 # If any of them occur more than once, we have a loop
471 if [val for val in multiple if val > 1]:
472 self._terminated = True
473 return True
474 return False
475
476 self._restarting_config = False
477 self._terminated = False
Masahiro Yamada99796922014-07-22 11:19:09 +0900478 cmd = [self.gnu_make] + list(args)
Simon Glassd9800692022-01-29 14:14:05 -0700479 result = command.run_pipe([cmd], capture=True, capture_stderr=True,
Simon Glass7bf83a52021-10-19 21:43:24 -0600480 cwd=cwd, raise_on_error=False, infile='/dev/null',
481 output_func=check_output, **kwargs)
482
483 if self._terminated:
484 # Try to be helpful
485 result.stderr += '(** did you define an int/hex Kconfig with no default? **)'
486
Simon Glass40f11fc2015-02-05 22:06:12 -0700487 if self.verbose_build:
488 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
489 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000490 return result
491
492 def ProcessResult(self, result):
493 """Process the result of a build, showing progress information
494
495 Args:
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600496 result: A CommandResult object, which indicates the result for
497 a single build
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000498 """
499 col = terminal.Color()
500 if result:
501 target = result.brd.target
502
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000503 self.upto += 1
504 if result.return_code != 0:
505 self.fail += 1
506 elif result.stderr:
507 self.warned += 1
508 if result.already_done:
509 self.already_done += 1
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600510 if self._verbose:
Simon Glass098b10f2022-01-29 14:14:18 -0700511 terminal.print_clear()
Simon Glasse5a0e5d2014-08-09 15:33:03 -0600512 boards_selected = {target : result.brd}
513 self.ResetResultSummary(boards_selected)
514 self.ProduceResultSummary(result.commit_upto, self.commits,
515 boards_selected)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000516 else:
517 target = '(starting)'
518
519 # Display separate counts for ok, warned and fail
520 ok = self.upto - self.warned - self.fail
Simon Glass252ac582022-01-29 14:14:17 -0700521 line = '\r' + self.col.build(self.col.GREEN, '%5d' % ok)
522 line += self.col.build(self.col.YELLOW, '%5d' % self.warned)
523 line += self.col.build(self.col.RED, '%5d' % self.fail)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000524
Simon Glass6eb76ca2020-04-09 15:08:45 -0600525 line += ' /%-5d ' % self.count
526 remaining = self.count - self.upto
527 if remaining:
Simon Glass252ac582022-01-29 14:14:17 -0700528 line += self.col.build(self.col.MAGENTA, ' -%-5d ' % remaining)
Simon Glass6eb76ca2020-04-09 15:08:45 -0600529 else:
530 line += ' ' * 8
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000531
532 # Add our current completion time estimate
533 self._AddTimestamp()
534 if self._complete_delay:
Simon Glass6eb76ca2020-04-09 15:08:45 -0600535 line += '%s : ' % self._complete_delay
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000536
Simon Glass6eb76ca2020-04-09 15:08:45 -0600537 line += target
Simon Glass098b10f2022-01-29 14:14:18 -0700538 terminal.print_clear()
539 tprint(line, newline=False, limit_to_line=True)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000540
541 def _GetOutputDir(self, commit_upto):
542 """Get the name of the output directory for a commit number
543
544 The output directory is typically .../<branch>/<commit>.
545
546 Args:
547 commit_upto: Commit number to use (0..self.count-1)
548 """
Simon Glass60b285f2020-04-17 17:51:34 -0600549 if self.work_in_output:
550 return self._working_dir
551
Simon Glass5971ab52014-12-01 17:33:55 -0700552 commit_dir = None
Simon Glassfea58582014-08-09 15:32:59 -0600553 if self.commits:
554 commit = self.commits[commit_upto]
555 subject = commit.subject.translate(trans_valid_chars)
Simon Glass925f6ad2020-03-18 09:42:45 -0600556 # See _GetOutputSpaceRemovals() which parses this name
Ovidiu Panait7664b032020-05-15 09:30:12 +0300557 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
558 commit.hash, subject[:20]))
Simon Glass5971ab52014-12-01 17:33:55 -0700559 elif not self.no_subdirs:
Simon Glassfea58582014-08-09 15:32:59 -0600560 commit_dir = 'current'
Simon Glass5971ab52014-12-01 17:33:55 -0700561 if not commit_dir:
562 return self.base_dir
563 return os.path.join(self.base_dir, commit_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000564
565 def GetBuildDir(self, commit_upto, target):
566 """Get the name of the build directory for a commit number
567
568 The build directory is typically .../<branch>/<commit>/<target>.
569
570 Args:
571 commit_upto: Commit number to use (0..self.count-1)
572 target: Target name
573 """
574 output_dir = self._GetOutputDir(commit_upto)
Simon Glass60b285f2020-04-17 17:51:34 -0600575 if self.work_in_output:
576 return output_dir
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000577 return os.path.join(output_dir, target)
578
579 def GetDoneFile(self, commit_upto, target):
580 """Get the name of the done file for a commit number
581
582 Args:
583 commit_upto: Commit number to use (0..self.count-1)
584 target: Target name
585 """
586 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
587
588 def GetSizesFile(self, commit_upto, target):
589 """Get the name of the sizes file for a commit number
590
591 Args:
592 commit_upto: Commit number to use (0..self.count-1)
593 target: Target name
594 """
595 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
596
597 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
598 """Get the name of the funcsizes file for a commit number and ELF file
599
600 Args:
601 commit_upto: Commit number to use (0..self.count-1)
602 target: Target name
603 elf_fname: Filename of elf image
604 """
605 return os.path.join(self.GetBuildDir(commit_upto, target),
606 '%s.sizes' % elf_fname.replace('/', '-'))
607
608 def GetObjdumpFile(self, commit_upto, target, elf_fname):
609 """Get the name of the objdump file for a commit number and ELF file
610
611 Args:
612 commit_upto: Commit number to use (0..self.count-1)
613 target: Target name
614 elf_fname: Filename of elf image
615 """
616 return os.path.join(self.GetBuildDir(commit_upto, target),
617 '%s.objdump' % elf_fname.replace('/', '-'))
618
619 def GetErrFile(self, commit_upto, target):
620 """Get the name of the err file for a commit number
621
622 Args:
623 commit_upto: Commit number to use (0..self.count-1)
624 target: Target name
625 """
626 output_dir = self.GetBuildDir(commit_upto, target)
627 return os.path.join(output_dir, 'err')
628
629 def FilterErrors(self, lines):
630 """Filter out errors in which we have no interest
631
632 We should probably use map().
633
634 Args:
635 lines: List of error lines, each a string
636 Returns:
637 New list with only interesting lines included
638 """
639 out_lines = []
Simon Glass113a8a52020-04-09 15:08:53 -0600640 if self._filter_migration_warnings:
641 text = '\n'.join(lines)
642 text = self._re_migration_warning.sub('', text)
643 lines = text.splitlines()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000644 for line in lines:
Simon Glass174592b2020-04-09 15:08:52 -0600645 if self.re_make_err.search(line):
646 continue
647 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
648 continue
649 out_lines.append(line)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000650 return out_lines
651
652 def ReadFuncSizes(self, fname, fd):
653 """Read function sizes from the output of 'nm'
654
655 Args:
656 fd: File containing data to read
657 fname: Filename we are reading from (just for errors)
658
659 Returns:
660 Dictionary containing size of each function in bytes, indexed by
661 function name.
662 """
663 sym = {}
664 for line in fd.readlines():
665 try:
Tom Rinid08c38c2019-12-06 15:31:31 -0500666 if line.strip():
667 size, type, name = line[:-1].split()
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000668 except:
Simon Glass098b10f2022-01-29 14:14:18 -0700669 tprint("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000670 continue
671 if type in 'tTdDbB':
672 # function names begin with '.' on 64-bit powerpc
673 if '.' in name[1:]:
674 name = 'static.' + name.split('.')[0]
675 sym[name] = sym.get(name, 0) + int(size, 16)
676 return sym
677
Simon Glass843312d2015-02-05 22:06:15 -0700678 def _ProcessConfig(self, fname):
679 """Read in a .config, autoconf.mk or autoconf.h file
680
681 This function handles all config file types. It ignores comments and
682 any #defines which don't start with CONFIG_.
683
684 Args:
685 fname: Filename to read
686
687 Returns:
688 Dictionary:
689 key: Config name (e.g. CONFIG_DM)
690 value: Config value (e.g. 1)
691 """
692 config = {}
693 if os.path.exists(fname):
694 with open(fname) as fd:
695 for line in fd:
696 line = line.strip()
697 if line.startswith('#define'):
698 values = line[8:].split(' ', 1)
699 if len(values) > 1:
700 key, value = values
701 else:
702 key = values[0]
Simon Glassb464f8e2016-11-13 14:25:53 -0700703 value = '1' if self.squash_config_y else ''
Simon Glass843312d2015-02-05 22:06:15 -0700704 if not key.startswith('CONFIG_'):
705 continue
706 elif not line or line[0] in ['#', '*', '/']:
707 continue
708 else:
709 key, value = line.split('=', 1)
Simon Glassb464f8e2016-11-13 14:25:53 -0700710 if self.squash_config_y and value == 'y':
711 value = '1'
Simon Glass843312d2015-02-05 22:06:15 -0700712 config[key] = value
713 return config
714
Alex Kiernan48ae4122018-05-31 04:48:34 +0000715 def _ProcessEnvironment(self, fname):
716 """Read in a uboot.env file
717
718 This function reads in environment variables from a file.
719
720 Args:
721 fname: Filename to read
722
723 Returns:
724 Dictionary:
725 key: environment variable (e.g. bootlimit)
726 value: value of environment variable (e.g. 1)
727 """
728 environment = {}
729 if os.path.exists(fname):
730 with open(fname) as fd:
731 for line in fd.read().split('\0'):
732 try:
733 key, value = line.split('=', 1)
734 environment[key] = value
735 except ValueError:
736 # ignore lines we can't parse
737 pass
738 return environment
739
Simon Glass843312d2015-02-05 22:06:15 -0700740 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000741 read_config, read_environment):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000742 """Work out the outcome of a build.
743
744 Args:
745 commit_upto: Commit number to check (0..n-1)
746 target: Target board to check
747 read_func_sizes: True to read function size information
Simon Glass843312d2015-02-05 22:06:15 -0700748 read_config: True to read .config and autoconf.h files
Alex Kiernan48ae4122018-05-31 04:48:34 +0000749 read_environment: True to read uboot.env files
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000750
751 Returns:
752 Outcome object
753 """
754 done_file = self.GetDoneFile(commit_upto, target)
755 sizes_file = self.GetSizesFile(commit_upto, target)
756 sizes = {}
757 func_sizes = {}
Simon Glass843312d2015-02-05 22:06:15 -0700758 config = {}
Alex Kiernan48ae4122018-05-31 04:48:34 +0000759 environment = {}
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000760 if os.path.exists(done_file):
761 with open(done_file, 'r') as fd:
Simon Glass347ea0b2019-04-26 19:02:23 -0600762 try:
763 return_code = int(fd.readline())
764 except ValueError:
765 # The file may be empty due to running out of disk space.
766 # Try a rebuild
767 return_code = 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000768 err_lines = []
769 err_file = self.GetErrFile(commit_upto, target)
770 if os.path.exists(err_file):
771 with open(err_file, 'r') as fd:
772 err_lines = self.FilterErrors(fd.readlines())
773
774 # Decide whether the build was ok, failed or created warnings
775 if return_code:
776 rc = OUTCOME_ERROR
777 elif len(err_lines):
778 rc = OUTCOME_WARNING
779 else:
780 rc = OUTCOME_OK
781
782 # Convert size information to our simple format
783 if os.path.exists(sizes_file):
784 with open(sizes_file, 'r') as fd:
785 for line in fd.readlines():
786 values = line.split()
787 rodata = 0
788 if len(values) > 6:
789 rodata = int(values[6], 16)
790 size_dict = {
791 'all' : int(values[0]) + int(values[1]) +
792 int(values[2]),
793 'text' : int(values[0]) - rodata,
794 'data' : int(values[1]),
795 'bss' : int(values[2]),
796 'rodata' : rodata,
797 }
798 sizes[values[5]] = size_dict
799
800 if read_func_sizes:
801 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
802 for fname in glob.glob(pattern):
803 with open(fname, 'r') as fd:
804 dict_name = os.path.basename(fname).replace('.sizes',
805 '')
806 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
807
Simon Glass843312d2015-02-05 22:06:15 -0700808 if read_config:
809 output_dir = self.GetBuildDir(commit_upto, target)
Simon Glassb464f8e2016-11-13 14:25:53 -0700810 for name in self.config_filenames:
Simon Glass843312d2015-02-05 22:06:15 -0700811 fname = os.path.join(output_dir, name)
812 config[name] = self._ProcessConfig(fname)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000813
Alex Kiernan48ae4122018-05-31 04:48:34 +0000814 if read_environment:
815 output_dir = self.GetBuildDir(commit_upto, target)
816 fname = os.path.join(output_dir, 'uboot.env')
817 environment = self._ProcessEnvironment(fname)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000818
Alex Kiernan48ae4122018-05-31 04:48:34 +0000819 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
820 environment)
821
822 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
Simon Glass843312d2015-02-05 22:06:15 -0700823
824 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000825 read_config, read_environment):
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000826 """Calculate a summary of the results of building a commit.
827
828 Args:
829 board_selected: Dict containing boards to summarise
830 commit_upto: Commit number to summarize (0..self.count-1)
831 read_func_sizes: True to read function size information
Simon Glass843312d2015-02-05 22:06:15 -0700832 read_config: True to read .config and autoconf.h files
Alex Kiernan48ae4122018-05-31 04:48:34 +0000833 read_environment: True to read uboot.env files
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000834
835 Returns:
836 Tuple:
837 Dict containing boards which passed building this commit.
838 keyed by board.target
Simon Glasse30965d2014-08-28 09:43:44 -0600839 List containing a summary of error lines
Simon Glassed966652014-08-28 09:43:43 -0600840 Dict keyed by error line, containing a list of the Board
841 objects with that error
Simon Glasse30965d2014-08-28 09:43:44 -0600842 List containing a summary of warning lines
843 Dict keyed by error line, containing a list of the Board
844 objects with that warning
Simon Glass8270e3c2015-08-25 21:52:14 -0600845 Dictionary keyed by board.target. Each value is a dictionary:
846 key: filename - e.g. '.config'
Simon Glass843312d2015-02-05 22:06:15 -0700847 value is itself a dictionary:
848 key: config name
849 value: config value
Alex Kiernan48ae4122018-05-31 04:48:34 +0000850 Dictionary keyed by board.target. Each value is a dictionary:
851 key: environment variable
852 value: value of environment variable
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000853 """
Simon Glasse30965d2014-08-28 09:43:44 -0600854 def AddLine(lines_summary, lines_boards, line, board):
855 line = line.rstrip()
856 if line in lines_boards:
857 lines_boards[line].append(board)
858 else:
859 lines_boards[line] = [board]
860 lines_summary.append(line)
861
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000862 board_dict = {}
863 err_lines_summary = []
Simon Glassed966652014-08-28 09:43:43 -0600864 err_lines_boards = {}
Simon Glasse30965d2014-08-28 09:43:44 -0600865 warn_lines_summary = []
866 warn_lines_boards = {}
Simon Glass843312d2015-02-05 22:06:15 -0700867 config = {}
Alex Kiernan48ae4122018-05-31 04:48:34 +0000868 environment = {}
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000869
Simon Glassc05aa032019-10-31 07:42:53 -0600870 for board in boards_selected.values():
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000871 outcome = self.GetBuildOutcome(commit_upto, board.target,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000872 read_func_sizes, read_config,
873 read_environment)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000874 board_dict[board.target] = outcome
Simon Glasse30965d2014-08-28 09:43:44 -0600875 last_func = None
876 last_was_warning = False
877 for line in outcome.err_lines:
878 if line:
879 if (self._re_function.match(line) or
880 self._re_files.match(line)):
881 last_func = line
Simon Glassed966652014-08-28 09:43:43 -0600882 else:
Simon Glass2d483332018-11-06 16:02:11 -0700883 is_warning = (self._re_warning.match(line) or
884 self._re_dtb_warning.match(line))
Simon Glasse30965d2014-08-28 09:43:44 -0600885 is_note = self._re_note.match(line)
886 if is_warning or (last_was_warning and is_note):
887 if last_func:
888 AddLine(warn_lines_summary, warn_lines_boards,
889 last_func, board)
890 AddLine(warn_lines_summary, warn_lines_boards,
891 line, board)
892 else:
893 if last_func:
894 AddLine(err_lines_summary, err_lines_boards,
895 last_func, board)
896 AddLine(err_lines_summary, err_lines_boards,
897 line, board)
898 last_was_warning = is_warning
899 last_func = None
Simon Glassb464f8e2016-11-13 14:25:53 -0700900 tconfig = Config(self.config_filenames, board.target)
901 for fname in self.config_filenames:
Simon Glass843312d2015-02-05 22:06:15 -0700902 if outcome.config:
Simon Glassc05aa032019-10-31 07:42:53 -0600903 for key, value in outcome.config[fname].items():
Simon Glass8270e3c2015-08-25 21:52:14 -0600904 tconfig.Add(fname, key, value)
905 config[board.target] = tconfig
Simon Glass843312d2015-02-05 22:06:15 -0700906
Alex Kiernan48ae4122018-05-31 04:48:34 +0000907 tenvironment = Environment(board.target)
908 if outcome.environment:
Simon Glassc05aa032019-10-31 07:42:53 -0600909 for key, value in outcome.environment.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +0000910 tenvironment.Add(key, value)
911 environment[board.target] = tenvironment
912
Simon Glasse30965d2014-08-28 09:43:44 -0600913 return (board_dict, err_lines_summary, err_lines_boards,
Alex Kiernan48ae4122018-05-31 04:48:34 +0000914 warn_lines_summary, warn_lines_boards, config, environment)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000915
916 def AddOutcome(self, board_dict, arch_list, changes, char, color):
917 """Add an output to our list of outcomes for each architecture
918
919 This simple function adds failing boards (changes) to the
920 relevant architecture string, so we can print the results out
921 sorted by architecture.
922
923 Args:
924 board_dict: Dict containing all boards
925 arch_list: Dict keyed by arch name. Value is a string containing
926 a list of board names which failed for that arch.
927 changes: List of boards to add to arch_list
928 color: terminal.Colour object
929 """
930 done_arch = {}
931 for target in changes:
932 if target in board_dict:
933 arch = board_dict[target].arch
934 else:
935 arch = 'unknown'
Simon Glass252ac582022-01-29 14:14:17 -0700936 str = self.col.build(color, ' ' + target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000937 if not arch in done_arch:
Simon Glass252ac582022-01-29 14:14:17 -0700938 str = ' %s %s' % (self.col.build(color, char), str)
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000939 done_arch[arch] = True
940 if not arch in arch_list:
941 arch_list[arch] = str
942 else:
943 arch_list[arch] += str
944
945
946 def ColourNum(self, num):
947 color = self.col.RED if num > 0 else self.col.GREEN
948 if num == 0:
949 return '0'
Simon Glass252ac582022-01-29 14:14:17 -0700950 return self.col.build(color, str(num))
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000951
952 def ResetResultSummary(self, board_selected):
953 """Reset the results summary ready for use.
954
955 Set up the base board list to be all those selected, and set the
956 error lines to empty.
957
958 Following this, calls to PrintResultSummary() will use this
959 information to work out what has changed.
960
961 Args:
962 board_selected: Dict containing boards to summarise, keyed by
963 board.target
964 """
965 self._base_board_dict = {}
966 for board in board_selected:
Alex Kiernan48ae4122018-05-31 04:48:34 +0000967 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
968 {})
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000969 self._base_err_lines = []
Simon Glasse30965d2014-08-28 09:43:44 -0600970 self._base_warn_lines = []
971 self._base_err_line_boards = {}
972 self._base_warn_line_boards = {}
Simon Glass8270e3c2015-08-25 21:52:14 -0600973 self._base_config = None
Alex Kiernan48ae4122018-05-31 04:48:34 +0000974 self._base_environment = None
Simon Glassfc3fe1c2013-04-03 11:07:16 +0000975
976 def PrintFuncSizeDetail(self, fname, old, new):
977 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
978 delta, common = [], {}
979
980 for a in old:
981 if a in new:
982 common[a] = 1
983
984 for name in old:
985 if name not in common:
986 remove += 1
987 down += old[name]
988 delta.append([-old[name], name])
989
990 for name in new:
991 if name not in common:
992 add += 1
993 up += new[name]
994 delta.append([new[name], name])
995
996 for name in common:
997 diff = new.get(name, 0) - old.get(name, 0)
998 if diff > 0:
999 grow, up = grow + 1, up + diff
1000 elif diff < 0:
1001 shrink, down = shrink + 1, down - diff
1002 delta.append([diff, name])
1003
1004 delta.sort()
1005 delta.reverse()
1006
1007 args = [add, -remove, grow, -shrink, up, -down, up - down]
Tom Rinid5686a62017-05-22 13:48:52 -04001008 if max(args) == 0 and min(args) == 0:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001009 return
1010 args = [self.ColourNum(x) for x in args]
1011 indent = ' ' * 15
Simon Glass098b10f2022-01-29 14:14:18 -07001012 tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
Simon Glass252ac582022-01-29 14:14:17 -07001013 tuple([indent, self.col.build(self.col.YELLOW, fname)] + args))
Simon Glass098b10f2022-01-29 14:14:18 -07001014 tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
Simon Glass4653a882014-09-05 19:00:07 -06001015 'delta'))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001016 for diff, name in delta:
1017 if diff:
1018 color = self.col.RED if diff > 0 else self.col.GREEN
1019 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1020 old.get(name, '-'), new.get(name,'-'), diff)
Simon Glass098b10f2022-01-29 14:14:18 -07001021 tprint(msg, colour=color)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001022
1023
1024 def PrintSizeDetail(self, target_list, show_bloat):
1025 """Show details size information for each board
1026
1027 Args:
1028 target_list: List of targets, each a dict containing:
1029 'target': Target name
1030 'total_diff': Total difference in bytes across all areas
1031 <part_name>: Difference for that part
1032 show_bloat: Show detail for each function
1033 """
1034 targets_by_diff = sorted(target_list, reverse=True,
1035 key=lambda x: x['_total_diff'])
1036 for result in targets_by_diff:
1037 printed_target = False
1038 for name in sorted(result):
1039 diff = result[name]
1040 if name.startswith('_'):
1041 continue
1042 if diff != 0:
1043 color = self.col.RED if diff > 0 else self.col.GREEN
1044 msg = ' %s %+d' % (name, diff)
1045 if not printed_target:
Simon Glass098b10f2022-01-29 14:14:18 -07001046 tprint('%10s %-15s:' % ('', result['_target']),
Simon Glass4653a882014-09-05 19:00:07 -06001047 newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001048 printed_target = True
Simon Glass098b10f2022-01-29 14:14:18 -07001049 tprint(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001050 if printed_target:
Simon Glass098b10f2022-01-29 14:14:18 -07001051 tprint()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001052 if show_bloat:
1053 target = result['_target']
1054 outcome = result['_outcome']
1055 base_outcome = self._base_board_dict[target]
1056 for fname in outcome.func_sizes:
1057 self.PrintFuncSizeDetail(fname,
1058 base_outcome.func_sizes[fname],
1059 outcome.func_sizes[fname])
1060
1061
1062 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
1063 show_bloat):
1064 """Print a summary of image sizes broken down by section.
1065
1066 The summary takes the form of one line per architecture. The
1067 line contains deltas for each of the sections (+ means the section
Flavio Suligoi9de5c392020-01-29 09:56:05 +01001068 got bigger, - means smaller). The numbers are the average number
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001069 of bytes that a board in this section increased by.
1070
1071 For example:
1072 powerpc: (622 boards) text -0.0
1073 arm: (285 boards) text -0.0
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001074
1075 Args:
1076 board_selected: Dict containing boards to summarise, keyed by
1077 board.target
1078 board_dict: Dict containing boards for which we built this
1079 commit, keyed by board.target. The value is an Outcome object.
Simon Glassf9c094b2020-03-18 09:42:43 -06001080 show_detail: Show size delta detail for each board
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001081 show_bloat: Show detail for each function
1082 """
1083 arch_list = {}
1084 arch_count = {}
1085
1086 # Calculate changes in size for different image parts
1087 # The previous sizes are in Board.sizes, for each board
1088 for target in board_dict:
1089 if target not in board_selected:
1090 continue
1091 base_sizes = self._base_board_dict[target].sizes
1092 outcome = board_dict[target]
1093 sizes = outcome.sizes
1094
1095 # Loop through the list of images, creating a dict of size
1096 # changes for each image/part. We end up with something like
1097 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1098 # which means that U-Boot data increased by 5 bytes and SPL
1099 # text decreased by 4.
1100 err = {'_target' : target}
1101 for image in sizes:
1102 if image in base_sizes:
1103 base_image = base_sizes[image]
1104 # Loop through the text, data, bss parts
1105 for part in sorted(sizes[image]):
1106 diff = sizes[image][part] - base_image[part]
1107 col = None
1108 if diff:
1109 if image == 'u-boot':
1110 name = part
1111 else:
1112 name = image + ':' + part
1113 err[name] = diff
1114 arch = board_selected[target].arch
1115 if not arch in arch_count:
1116 arch_count[arch] = 1
1117 else:
1118 arch_count[arch] += 1
1119 if not sizes:
1120 pass # Only add to our list when we have some stats
1121 elif not arch in arch_list:
1122 arch_list[arch] = [err]
1123 else:
1124 arch_list[arch].append(err)
1125
1126 # We now have a list of image size changes sorted by arch
1127 # Print out a summary of these
Simon Glassc05aa032019-10-31 07:42:53 -06001128 for arch, target_list in arch_list.items():
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001129 # Get total difference for each type
1130 totals = {}
1131 for result in target_list:
1132 total = 0
Simon Glassc05aa032019-10-31 07:42:53 -06001133 for name, diff in result.items():
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001134 if name.startswith('_'):
1135 continue
1136 total += diff
1137 if name in totals:
1138 totals[name] += diff
1139 else:
1140 totals[name] = diff
1141 result['_total_diff'] = total
1142 result['_outcome'] = board_dict[result['_target']]
1143
1144 count = len(target_list)
1145 printed_arch = False
1146 for name in sorted(totals):
1147 diff = totals[name]
1148 if diff:
1149 # Display the average difference in this name for this
1150 # architecture
1151 avg_diff = float(diff) / count
1152 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1153 msg = ' %s %+1.1f' % (name, avg_diff)
1154 if not printed_arch:
Simon Glass098b10f2022-01-29 14:14:18 -07001155 tprint('%10s: (for %d/%d boards)' % (arch, count,
Simon Glass4653a882014-09-05 19:00:07 -06001156 arch_count[arch]), newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001157 printed_arch = True
Simon Glass098b10f2022-01-29 14:14:18 -07001158 tprint(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001159
1160 if printed_arch:
Simon Glass098b10f2022-01-29 14:14:18 -07001161 tprint()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001162 if show_detail:
1163 self.PrintSizeDetail(target_list, show_bloat)
1164
1165
1166 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glasse30965d2014-08-28 09:43:44 -06001167 err_line_boards, warn_lines, warn_line_boards,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001168 config, environment, show_sizes, show_detail,
1169 show_bloat, show_config, show_environment):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001170 """Compare results with the base results and display delta.
1171
1172 Only boards mentioned in board_selected will be considered. This
1173 function is intended to be called repeatedly with the results of
1174 each commit. It therefore shows a 'diff' between what it saw in
1175 the last call and what it sees now.
1176
1177 Args:
1178 board_selected: Dict containing boards to summarise, keyed by
1179 board.target
1180 board_dict: Dict containing boards for which we built this
1181 commit, keyed by board.target. The value is an Outcome object.
1182 err_lines: A list of errors for this commit, or [] if there is
1183 none, or we don't want to print errors
Simon Glassed966652014-08-28 09:43:43 -06001184 err_line_boards: Dict keyed by error line, containing a list of
1185 the Board objects with that error
Simon Glasse30965d2014-08-28 09:43:44 -06001186 warn_lines: A list of warnings for this commit, or [] if there is
1187 none, or we don't want to print errors
1188 warn_line_boards: Dict keyed by warning line, containing a list of
1189 the Board objects with that warning
Simon Glass843312d2015-02-05 22:06:15 -07001190 config: Dictionary keyed by filename - e.g. '.config'. Each
1191 value is itself a dictionary:
1192 key: config name
1193 value: config value
Alex Kiernan48ae4122018-05-31 04:48:34 +00001194 environment: Dictionary keyed by environment variable, Each
1195 value is the value of environment variable.
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001196 show_sizes: Show image size deltas
Simon Glassf9c094b2020-03-18 09:42:43 -06001197 show_detail: Show size delta detail for each board if show_sizes
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001198 show_bloat: Show detail for each function
Simon Glass843312d2015-02-05 22:06:15 -07001199 show_config: Show config changes
Alex Kiernan48ae4122018-05-31 04:48:34 +00001200 show_environment: Show environment changes
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001201 """
Simon Glasse30965d2014-08-28 09:43:44 -06001202 def _BoardList(line, line_boards):
Simon Glassed966652014-08-28 09:43:43 -06001203 """Helper function to get a line of boards containing a line
1204
1205 Args:
1206 line: Error line to search for
Simon Glass35d696d2020-04-09 15:08:36 -06001207 line_boards: boards to search, each a Board
Simon Glassed966652014-08-28 09:43:43 -06001208 Return:
Simon Glass35d696d2020-04-09 15:08:36 -06001209 List of boards with that error line, or [] if the user has not
1210 requested such a list
Simon Glassed966652014-08-28 09:43:43 -06001211 """
Simon Glass35d696d2020-04-09 15:08:36 -06001212 boards = []
1213 board_set = set()
Simon Glassed966652014-08-28 09:43:43 -06001214 if self._list_error_boards:
Simon Glasse30965d2014-08-28 09:43:44 -06001215 for board in line_boards[line]:
Simon Glass35d696d2020-04-09 15:08:36 -06001216 if not board in board_set:
1217 boards.append(board)
1218 board_set.add(board)
1219 return boards
Simon Glassed966652014-08-28 09:43:43 -06001220
Simon Glasse30965d2014-08-28 09:43:44 -06001221 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1222 char):
Simon Glass35d696d2020-04-09 15:08:36 -06001223 """Calculate the required output based on changes in errors
1224
1225 Args:
1226 base_lines: List of errors/warnings for previous commit
1227 base_line_boards: Dict keyed by error line, containing a list
1228 of the Board objects with that error in the previous commit
1229 lines: List of errors/warning for this commit, each a str
1230 line_boards: Dict keyed by error line, containing a list
1231 of the Board objects with that error in this commit
1232 char: Character representing error ('') or warning ('w'). The
1233 broken ('+') or fixed ('-') characters are added in this
1234 function
1235
1236 Returns:
1237 Tuple
1238 List of ErrLine objects for 'better' lines
1239 List of ErrLine objects for 'worse' lines
1240 """
Simon Glasse30965d2014-08-28 09:43:44 -06001241 better_lines = []
1242 worse_lines = []
1243 for line in lines:
1244 if line not in base_lines:
Simon Glass35d696d2020-04-09 15:08:36 -06001245 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1246 line)
1247 worse_lines.append(errline)
Simon Glasse30965d2014-08-28 09:43:44 -06001248 for line in base_lines:
1249 if line not in lines:
Simon Glass35d696d2020-04-09 15:08:36 -06001250 errline = ErrLine(char + '-',
1251 _BoardList(line, base_line_boards), line)
1252 better_lines.append(errline)
Simon Glasse30965d2014-08-28 09:43:44 -06001253 return better_lines, worse_lines
1254
Simon Glass843312d2015-02-05 22:06:15 -07001255 def _CalcConfig(delta, name, config):
1256 """Calculate configuration changes
1257
1258 Args:
1259 delta: Type of the delta, e.g. '+'
1260 name: name of the file which changed (e.g. .config)
1261 config: configuration change dictionary
1262 key: config name
1263 value: config value
1264 Returns:
1265 String containing the configuration changes which can be
1266 printed
1267 """
1268 out = ''
1269 for key in sorted(config.keys()):
1270 out += '%s=%s ' % (key, config[key])
Simon Glass8270e3c2015-08-25 21:52:14 -06001271 return '%s %s: %s' % (delta, name, out)
Simon Glass843312d2015-02-05 22:06:15 -07001272
Simon Glass8270e3c2015-08-25 21:52:14 -06001273 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1274 """Add changes in configuration to a list
Simon Glass843312d2015-02-05 22:06:15 -07001275
1276 Args:
Simon Glass8270e3c2015-08-25 21:52:14 -06001277 lines: list to add to
1278 name: config file name
Simon Glass843312d2015-02-05 22:06:15 -07001279 config_plus: configurations added, dictionary
1280 key: config name
1281 value: config value
1282 config_minus: configurations removed, dictionary
1283 key: config name
1284 value: config value
1285 config_change: configurations changed, dictionary
1286 key: config name
1287 value: config value
1288 """
1289 if config_plus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001290 lines.append(_CalcConfig('+', name, config_plus))
Simon Glass843312d2015-02-05 22:06:15 -07001291 if config_minus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001292 lines.append(_CalcConfig('-', name, config_minus))
Simon Glass843312d2015-02-05 22:06:15 -07001293 if config_change:
Simon Glass8270e3c2015-08-25 21:52:14 -06001294 lines.append(_CalcConfig('c', name, config_change))
1295
1296 def _OutputConfigInfo(lines):
1297 for line in lines:
1298 if not line:
1299 continue
1300 if line[0] == '+':
1301 col = self.col.GREEN
1302 elif line[0] == '-':
1303 col = self.col.RED
1304 elif line[0] == 'c':
1305 col = self.col.YELLOW
Simon Glass098b10f2022-01-29 14:14:18 -07001306 tprint(' ' + line, newline=True, colour=col)
Simon Glass8270e3c2015-08-25 21:52:14 -06001307
Simon Glassb206d872020-04-09 15:08:28 -06001308 def _OutputErrLines(err_lines, colour):
1309 """Output the line of error/warning lines, if not empty
1310
1311 Also increments self._error_lines if err_lines not empty
1312
1313 Args:
Simon Glass35d696d2020-04-09 15:08:36 -06001314 err_lines: List of ErrLine objects, each an error or warning
1315 line, possibly including a list of boards with that
1316 error/warning
Simon Glassb206d872020-04-09 15:08:28 -06001317 colour: Colour to use for output
1318 """
1319 if err_lines:
Simon Glass8c9a2672020-04-09 15:08:37 -06001320 out_list = []
Simon Glass35d696d2020-04-09 15:08:36 -06001321 for line in err_lines:
1322 boards = ''
1323 names = [board.target for board in line.boards]
Simon Glass9ef0ceb2020-04-09 15:08:38 -06001324 board_str = ' '.join(names) if names else ''
Simon Glass8c9a2672020-04-09 15:08:37 -06001325 if board_str:
Simon Glass252ac582022-01-29 14:14:17 -07001326 out = self.col.build(colour, line.char + '(')
1327 out += self.col.build(self.col.MAGENTA, board_str,
Simon Glass8c9a2672020-04-09 15:08:37 -06001328 bright=False)
Simon Glass252ac582022-01-29 14:14:17 -07001329 out += self.col.build(colour, ') %s' % line.errline)
Simon Glass8c9a2672020-04-09 15:08:37 -06001330 else:
Simon Glass252ac582022-01-29 14:14:17 -07001331 out = self.col.build(colour, line.char + line.errline)
Simon Glass8c9a2672020-04-09 15:08:37 -06001332 out_list.append(out)
Simon Glass098b10f2022-01-29 14:14:18 -07001333 tprint('\n'.join(out_list))
Simon Glassb206d872020-04-09 15:08:28 -06001334 self._error_lines += 1
1335
Simon Glass843312d2015-02-05 22:06:15 -07001336
Simon Glass4cf2b222018-11-06 16:02:12 -07001337 ok_boards = [] # List of boards fixed since last commit
Simon Glass6af71012018-11-06 16:02:13 -07001338 warn_boards = [] # List of boards with warnings since last commit
Simon Glass4cf2b222018-11-06 16:02:12 -07001339 err_boards = [] # List of new broken boards since last commit
1340 new_boards = [] # List of boards that didn't exist last time
1341 unknown_boards = [] # List of boards that were not built
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001342
1343 for target in board_dict:
1344 if target not in board_selected:
1345 continue
1346
1347 # If the board was built last time, add its outcome to a list
1348 if target in self._base_board_dict:
1349 base_outcome = self._base_board_dict[target].rc
1350 outcome = board_dict[target]
1351 if outcome.rc == OUTCOME_UNKNOWN:
Simon Glass4cf2b222018-11-06 16:02:12 -07001352 unknown_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001353 elif outcome.rc < base_outcome:
Simon Glass6af71012018-11-06 16:02:13 -07001354 if outcome.rc == OUTCOME_WARNING:
1355 warn_boards.append(target)
1356 else:
1357 ok_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001358 elif outcome.rc > base_outcome:
Simon Glass6af71012018-11-06 16:02:13 -07001359 if outcome.rc == OUTCOME_WARNING:
1360 warn_boards.append(target)
1361 else:
1362 err_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001363 else:
Simon Glass4cf2b222018-11-06 16:02:12 -07001364 new_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001365
Simon Glassb206d872020-04-09 15:08:28 -06001366 # Get a list of errors and warnings that have appeared, and disappeared
Simon Glasse30965d2014-08-28 09:43:44 -06001367 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1368 self._base_err_line_boards, err_lines, err_line_boards, '')
1369 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1370 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001371
1372 # Display results by arch
Simon Glass6af71012018-11-06 16:02:13 -07001373 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1374 worse_err, better_err, worse_warn, better_warn)):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001375 arch_list = {}
Simon Glass4cf2b222018-11-06 16:02:12 -07001376 self.AddOutcome(board_selected, arch_list, ok_boards, '',
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001377 self.col.GREEN)
Simon Glass6af71012018-11-06 16:02:13 -07001378 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1379 self.col.YELLOW)
Simon Glass4cf2b222018-11-06 16:02:12 -07001380 self.AddOutcome(board_selected, arch_list, err_boards, '+',
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001381 self.col.RED)
Simon Glass4cf2b222018-11-06 16:02:12 -07001382 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001383 if self._show_unknown:
Simon Glass4cf2b222018-11-06 16:02:12 -07001384 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001385 self.col.MAGENTA)
Simon Glassc05aa032019-10-31 07:42:53 -06001386 for arch, target_list in arch_list.items():
Simon Glass098b10f2022-01-29 14:14:18 -07001387 tprint('%10s: %s' % (arch, target_list))
Simon Glass28370c12014-08-09 15:33:06 -06001388 self._error_lines += 1
Simon Glassb206d872020-04-09 15:08:28 -06001389 _OutputErrLines(better_err, colour=self.col.GREEN)
1390 _OutputErrLines(worse_err, colour=self.col.RED)
1391 _OutputErrLines(better_warn, colour=self.col.CYAN)
Simon Glass5627bd92020-04-09 15:08:35 -06001392 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001393
1394 if show_sizes:
1395 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1396 show_bloat)
1397
Alex Kiernan48ae4122018-05-31 04:48:34 +00001398 if show_environment and self._base_environment:
1399 lines = []
1400
1401 for target in board_dict:
1402 if target not in board_selected:
1403 continue
1404
1405 tbase = self._base_environment[target]
1406 tenvironment = environment[target]
1407 environment_plus = {}
1408 environment_minus = {}
1409 environment_change = {}
1410 base = tbase.environment
Simon Glassc05aa032019-10-31 07:42:53 -06001411 for key, value in tenvironment.environment.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +00001412 if key not in base:
1413 environment_plus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001414 for key, value in base.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +00001415 if key not in tenvironment.environment:
1416 environment_minus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001417 for key, value in base.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +00001418 new_value = tenvironment.environment.get(key)
1419 if new_value and value != new_value:
1420 desc = '%s -> %s' % (value, new_value)
1421 environment_change[key] = desc
1422
1423 _AddConfig(lines, target, environment_plus, environment_minus,
1424 environment_change)
1425
1426 _OutputConfigInfo(lines)
1427
Simon Glass8270e3c2015-08-25 21:52:14 -06001428 if show_config and self._base_config:
1429 summary = {}
1430 arch_config_plus = {}
1431 arch_config_minus = {}
1432 arch_config_change = {}
1433 arch_list = []
1434
1435 for target in board_dict:
1436 if target not in board_selected:
Simon Glass843312d2015-02-05 22:06:15 -07001437 continue
Simon Glass8270e3c2015-08-25 21:52:14 -06001438 arch = board_selected[target].arch
1439 if arch not in arch_list:
1440 arch_list.append(arch)
1441
1442 for arch in arch_list:
1443 arch_config_plus[arch] = {}
1444 arch_config_minus[arch] = {}
1445 arch_config_change[arch] = {}
Simon Glassb464f8e2016-11-13 14:25:53 -07001446 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001447 arch_config_plus[arch][name] = {}
1448 arch_config_minus[arch][name] = {}
1449 arch_config_change[arch][name] = {}
1450
1451 for target in board_dict:
1452 if target not in board_selected:
1453 continue
1454
1455 arch = board_selected[target].arch
1456
1457 all_config_plus = {}
1458 all_config_minus = {}
1459 all_config_change = {}
1460 tbase = self._base_config[target]
1461 tconfig = config[target]
1462 lines = []
Simon Glassb464f8e2016-11-13 14:25:53 -07001463 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001464 if not tconfig.config[name]:
1465 continue
1466 config_plus = {}
1467 config_minus = {}
1468 config_change = {}
1469 base = tbase.config[name]
Simon Glassc05aa032019-10-31 07:42:53 -06001470 for key, value in tconfig.config[name].items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001471 if key not in base:
1472 config_plus[key] = value
1473 all_config_plus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001474 for key, value in base.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001475 if key not in tconfig.config[name]:
1476 config_minus[key] = value
1477 all_config_minus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001478 for key, value in base.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001479 new_value = tconfig.config.get(key)
1480 if new_value and value != new_value:
1481 desc = '%s -> %s' % (value, new_value)
1482 config_change[key] = desc
1483 all_config_change[key] = desc
1484
1485 arch_config_plus[arch][name].update(config_plus)
1486 arch_config_minus[arch][name].update(config_minus)
1487 arch_config_change[arch][name].update(config_change)
1488
1489 _AddConfig(lines, name, config_plus, config_minus,
1490 config_change)
1491 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1492 all_config_change)
1493 summary[target] = '\n'.join(lines)
1494
1495 lines_by_target = {}
Simon Glassc05aa032019-10-31 07:42:53 -06001496 for target, lines in summary.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001497 if lines in lines_by_target:
1498 lines_by_target[lines].append(target)
1499 else:
1500 lines_by_target[lines] = [target]
1501
1502 for arch in arch_list:
1503 lines = []
1504 all_plus = {}
1505 all_minus = {}
1506 all_change = {}
Simon Glassb464f8e2016-11-13 14:25:53 -07001507 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001508 all_plus.update(arch_config_plus[arch][name])
1509 all_minus.update(arch_config_minus[arch][name])
1510 all_change.update(arch_config_change[arch][name])
1511 _AddConfig(lines, name, arch_config_plus[arch][name],
1512 arch_config_minus[arch][name],
1513 arch_config_change[arch][name])
1514 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1515 #arch_summary[target] = '\n'.join(lines)
1516 if lines:
Simon Glass098b10f2022-01-29 14:14:18 -07001517 tprint('%s:' % arch)
Simon Glass8270e3c2015-08-25 21:52:14 -06001518 _OutputConfigInfo(lines)
1519
Simon Glassc05aa032019-10-31 07:42:53 -06001520 for lines, targets in lines_by_target.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001521 if not lines:
1522 continue
Simon Glass098b10f2022-01-29 14:14:18 -07001523 tprint('%s :' % ' '.join(sorted(targets)))
Simon Glass8270e3c2015-08-25 21:52:14 -06001524 _OutputConfigInfo(lines.split('\n'))
1525
Simon Glass843312d2015-02-05 22:06:15 -07001526
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001527 # Save our updated information for the next call to this function
1528 self._base_board_dict = board_dict
1529 self._base_err_lines = err_lines
Simon Glasse30965d2014-08-28 09:43:44 -06001530 self._base_warn_lines = warn_lines
1531 self._base_err_line_boards = err_line_boards
1532 self._base_warn_line_boards = warn_line_boards
Simon Glass843312d2015-02-05 22:06:15 -07001533 self._base_config = config
Alex Kiernan48ae4122018-05-31 04:48:34 +00001534 self._base_environment = environment
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001535
1536 # Get a list of boards that did not get built, if needed
1537 not_built = []
1538 for board in board_selected:
1539 if not board in board_dict:
1540 not_built.append(board)
1541 if not_built:
Simon Glass098b10f2022-01-29 14:14:18 -07001542 tprint("Boards not built (%d): %s" % (len(not_built),
Simon Glass4653a882014-09-05 19:00:07 -06001543 ', '.join(not_built)))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001544
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001545 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glasse30965d2014-08-28 09:43:44 -06001546 (board_dict, err_lines, err_line_boards, warn_lines,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001547 warn_line_boards, config, environment) = self.GetResultSummary(
Simon Glassed966652014-08-28 09:43:43 -06001548 board_selected, commit_upto,
Simon Glass843312d2015-02-05 22:06:15 -07001549 read_func_sizes=self._show_bloat,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001550 read_config=self._show_config,
1551 read_environment=self._show_environment)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001552 if commits:
1553 msg = '%02d: %s' % (commit_upto + 1,
1554 commits[commit_upto].subject)
Simon Glass098b10f2022-01-29 14:14:18 -07001555 tprint(msg, colour=self.col.BLUE)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001556 self.PrintResultSummary(board_selected, board_dict,
Simon Glassed966652014-08-28 09:43:43 -06001557 err_lines if self._show_errors else [], err_line_boards,
Simon Glasse30965d2014-08-28 09:43:44 -06001558 warn_lines if self._show_errors else [], warn_line_boards,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001559 config, environment, self._show_sizes, self._show_detail,
1560 self._show_bloat, self._show_config, self._show_environment)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001561
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001562 def ShowSummary(self, commits, board_selected):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001563 """Show a build summary for U-Boot for a given board list.
1564
1565 Reset the result summary, then repeatedly call GetResultSummary on
1566 each commit's results, then display the differences we see.
1567
1568 Args:
1569 commit: Commit objects to summarise
1570 board_selected: Dict containing boards to summarise
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001571 """
Simon Glassfea58582014-08-09 15:32:59 -06001572 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001573 self.commits = commits
1574 self.ResetResultSummary(board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001575 self._error_lines = 0
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001576
1577 for commit_upto in range(0, self.commit_count, self._step):
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001578 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001579 if not self._error_lines:
Simon Glass098b10f2022-01-29 14:14:18 -07001580 tprint('(no errors to report)', colour=self.col.GREEN)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001581
1582
1583 def SetupBuild(self, board_selected, commits):
1584 """Set up ready to start a build.
1585
1586 Args:
1587 board_selected: Selected boards to build
1588 commits: Selected commits to build
1589 """
1590 # First work out how many commits we will build
Simon Glassc05aa032019-10-31 07:42:53 -06001591 count = (self.commit_count + self._step - 1) // self._step
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001592 self.count = len(board_selected) * count
1593 self.upto = self.warned = self.fail = 0
1594 self._timestamps = collections.deque()
1595
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001596 def GetThreadDir(self, thread_num):
1597 """Get the directory path to the working dir for a thread.
1598
1599 Args:
Simon Glassb82492b2021-01-30 22:17:46 -07001600 thread_num: Number of thread to check (-1 for main process, which
1601 is treated as 0)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001602 """
Simon Glassd829f122020-03-18 09:42:42 -06001603 if self.work_in_output:
1604 return self._working_dir
Simon Glassb82492b2021-01-30 22:17:46 -07001605 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001606
Simon Glassfea58582014-08-09 15:32:59 -06001607 def _PrepareThread(self, thread_num, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001608 """Prepare the working directory for a thread.
1609
1610 This clones or fetches the repo into the thread's work directory.
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001611 Optionally, it can create a linked working tree of the repo in the
1612 thread's work directory instead.
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001613
1614 Args:
1615 thread_num: Thread number (0, 1, ...)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001616 setup_git:
1617 'clone' to set up a git clone
1618 'worktree' to set up a git worktree
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001619 """
1620 thread_dir = self.GetThreadDir(thread_num)
Simon Glass190064b2014-08-09 15:33:00 -06001621 builderthread.Mkdir(thread_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001622 git_dir = os.path.join(thread_dir, '.git')
1623
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001624 # Create a worktree or a git repo clone for this thread if it
1625 # doesn't already exist
Simon Glassfea58582014-08-09 15:32:59 -06001626 if setup_git and self.git_dir:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001627 src_dir = os.path.abspath(self.git_dir)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001628 if os.path.isdir(git_dir):
1629 # This is a clone of the src_dir repo, we can keep using
1630 # it but need to fetch from src_dir.
Simon Glass098b10f2022-01-29 14:14:18 -07001631 tprint('\rFetching repo for thread %d' % thread_num,
Simon Glass212c0b82020-04-09 15:08:43 -06001632 newline=False)
Simon Glass0157b182022-01-29 14:14:11 -07001633 gitutil.fetch(git_dir, thread_dir)
Simon Glass098b10f2022-01-29 14:14:18 -07001634 terminal.print_clear()
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001635 elif os.path.isfile(git_dir):
1636 # This is a worktree of the src_dir repo, we don't need to
1637 # create it again or update it in any way.
1638 pass
1639 elif os.path.exists(git_dir):
1640 # Don't know what could trigger this, but we probably
1641 # can't create a git worktree/clone here.
1642 raise ValueError('Git dir %s exists, but is not a file '
1643 'or a directory.' % git_dir)
1644 elif setup_git == 'worktree':
Simon Glass098b10f2022-01-29 14:14:18 -07001645 tprint('\rChecking out worktree for thread %d' % thread_num,
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001646 newline=False)
Simon Glass0157b182022-01-29 14:14:11 -07001647 gitutil.add_worktree(src_dir, thread_dir)
Simon Glass098b10f2022-01-29 14:14:18 -07001648 terminal.print_clear()
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001649 elif setup_git == 'clone' or setup_git == True:
Simon Glass098b10f2022-01-29 14:14:18 -07001650 tprint('\rCloning repo for thread %d' % thread_num,
Simon Glass21f0eb32016-09-18 16:48:31 -06001651 newline=False)
Simon Glass0157b182022-01-29 14:14:11 -07001652 gitutil.clone(src_dir, thread_dir)
Simon Glass098b10f2022-01-29 14:14:18 -07001653 terminal.print_clear()
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001654 else:
1655 raise ValueError("Can't setup git repo with %s." % setup_git)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001656
Simon Glassfea58582014-08-09 15:32:59 -06001657 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001658 """Prepare the working directory for use.
1659
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001660 Set up the git repo for each thread. Creates a linked working tree
1661 if git-worktree is available, or clones the repo if it isn't.
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001662
1663 Args:
Simon Glassb82492b2021-01-30 22:17:46 -07001664 max_threads: Maximum number of threads we expect to need. If 0 then
1665 1 is set up, since the main process still needs somewhere to
1666 work
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001667 setup_git: True to set up a git worktree or a git clone
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001668 """
Simon Glass190064b2014-08-09 15:33:00 -06001669 builderthread.Mkdir(self._working_dir)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001670 if setup_git and self.git_dir:
1671 src_dir = os.path.abspath(self.git_dir)
Simon Glass0157b182022-01-29 14:14:11 -07001672 if gitutil.check_worktree_is_available(src_dir):
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001673 setup_git = 'worktree'
1674 # If we previously added a worktree but the directory for it
1675 # got deleted, we need to prune its files from the repo so
1676 # that we can check out another in its place.
Simon Glass0157b182022-01-29 14:14:11 -07001677 gitutil.prune_worktrees(src_dir)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001678 else:
1679 setup_git = 'clone'
Simon Glassb82492b2021-01-30 22:17:46 -07001680
1681 # Always do at least one thread
1682 for thread in range(max(max_threads, 1)):
Simon Glassfea58582014-08-09 15:32:59 -06001683 self._PrepareThread(thread, setup_git)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001684
Simon Glass925f6ad2020-03-18 09:42:45 -06001685 def _GetOutputSpaceRemovals(self):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001686 """Get the output directories ready to receive files.
1687
Simon Glass925f6ad2020-03-18 09:42:45 -06001688 Figure out what needs to be deleted in the output directory before it
1689 can be used. We only delete old buildman directories which have the
1690 expected name pattern. See _GetOutputDir().
1691
1692 Returns:
1693 List of full paths of directories to remove
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001694 """
Simon Glass1a915672014-12-01 17:33:53 -07001695 if not self.commits:
1696 return
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001697 dir_list = []
1698 for commit_upto in range(self.commit_count):
1699 dir_list.append(self._GetOutputDir(commit_upto))
1700
Simon Glassb222abe2016-09-18 16:48:32 -06001701 to_remove = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001702 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1703 if dirname not in dir_list:
Simon Glass925f6ad2020-03-18 09:42:45 -06001704 leaf = dirname[len(self.base_dir) + 1:]
Ovidiu Panait7664b032020-05-15 09:30:12 +03001705 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
Simon Glass925f6ad2020-03-18 09:42:45 -06001706 if m:
1707 to_remove.append(dirname)
1708 return to_remove
1709
1710 def _PrepareOutputSpace(self):
1711 """Get the output directories ready to receive files.
1712
1713 We delete any output directories which look like ones we need to
1714 create. Having left over directories is confusing when the user wants
1715 to check the output manually.
1716 """
1717 to_remove = self._GetOutputSpaceRemovals()
Simon Glassb222abe2016-09-18 16:48:32 -06001718 if to_remove:
Simon Glass098b10f2022-01-29 14:14:18 -07001719 tprint('Removing %d old build directories...' % len(to_remove),
Simon Glassb222abe2016-09-18 16:48:32 -06001720 newline=False)
1721 for dirname in to_remove:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001722 shutil.rmtree(dirname)
Simon Glass098b10f2022-01-29 14:14:18 -07001723 terminal.print_clear()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001724
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001725 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001726 """Build all commits for a list of boards
1727
1728 Args:
1729 commits: List of commits to be build, each a Commit object
1730 boards_selected: Dict of selected boards, key is target name,
1731 value is Board object
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001732 keep_outputs: True to save build output files
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001733 verbose: Display build results as they are completed
Simon Glass2c3deb92014-08-28 09:43:39 -06001734 Returns:
1735 Tuple containing:
1736 - number of boards that failed to build
1737 - number of boards that issued warnings
Simon Glass8116c782021-04-11 16:27:27 +12001738 - list of thread exceptions raised
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001739 """
Simon Glassfea58582014-08-09 15:32:59 -06001740 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001741 self.commits = commits
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001742 self._verbose = verbose
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001743
1744 self.ResetResultSummary(board_selected)
Thierry Redingf3d015c2014-08-19 10:22:39 +02001745 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassfea58582014-08-09 15:32:59 -06001746 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1747 commits is not None)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001748 self._PrepareOutputSpace()
Simon Glass098b10f2022-01-29 14:14:18 -07001749 tprint('\rStarting build...', newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001750 self.SetupBuild(board_selected, commits)
1751 self.ProcessResult(None)
Simon Glass8116c782021-04-11 16:27:27 +12001752 self.thread_exceptions = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001753 # Create jobs to build all commits for each board
Simon Glassc05aa032019-10-31 07:42:53 -06001754 for brd in board_selected.values():
Simon Glass190064b2014-08-09 15:33:00 -06001755 job = builderthread.BuilderJob()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001756 job.board = brd
1757 job.commits = commits
1758 job.keep_outputs = keep_outputs
Simon Glassd829f122020-03-18 09:42:42 -06001759 job.work_in_output = self.work_in_output
Simon Glass2b4806e2022-01-22 05:07:33 -07001760 job.adjust_cfg = self.adjust_cfg
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001761 job.step = self._step
Simon Glassb82492b2021-01-30 22:17:46 -07001762 if self.num_threads:
1763 self.queue.put(job)
1764 else:
Simon Glassac053352022-02-11 13:23:19 -07001765 self._single_builder.RunJob(job)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001766
Simon Glassb82492b2021-01-30 22:17:46 -07001767 if self.num_threads:
1768 term = threading.Thread(target=self.queue.join)
1769 term.setDaemon(True)
1770 term.start()
1771 while term.is_alive():
1772 term.join(100)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001773
Simon Glassb82492b2021-01-30 22:17:46 -07001774 # Wait until we have processed all output
1775 self.out_queue.join()
Simon Glass098b10f2022-01-29 14:14:18 -07001776 tprint()
Simon Glass7b33f212020-04-09 15:08:47 -06001777
1778 msg = 'Completed: %d total built' % self.count
1779 if self.already_done:
1780 msg += ' (%d previously' % self.already_done
1781 if self.already_done != self.count:
1782 msg += ', %d newly' % (self.count - self.already_done)
1783 msg += ')'
1784 duration = datetime.now() - self._start_time
1785 if duration > timedelta(microseconds=1000000):
1786 if duration.microseconds >= 500000:
1787 duration = duration + timedelta(seconds=1)
1788 duration = duration - timedelta(microseconds=duration.microseconds)
Simon Glass38f159c2020-07-19 12:40:26 -06001789 rate = float(self.count) / duration.total_seconds()
1790 msg += ', duration %s, rate %1.2f' % (duration, rate)
Simon Glass098b10f2022-01-29 14:14:18 -07001791 tprint(msg)
Simon Glass8116c782021-04-11 16:27:27 +12001792 if self.thread_exceptions:
Simon Glass098b10f2022-01-29 14:14:18 -07001793 tprint('Failed: %d thread exceptions' % len(self.thread_exceptions),
Simon Glass8116c782021-04-11 16:27:27 +12001794 colour=self.col.RED)
Simon Glass7b33f212020-04-09 15:08:47 -06001795
Simon Glass8116c782021-04-11 16:27:27 +12001796 return (self.fail, self.warned, self.thread_exceptions)