blob: 94f843e2a940a3f11607ca42206985a5168640d0 [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
25from patman.terminal import Print
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:
445 gitutil.Checkout(commit.hash)
446
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 Glass102969b2020-04-09 15:08:42 -0600511 terminal.PrintClear()
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
521 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
522 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
523 line += self.col.Color(self.col.RED, '%5d' % self.fail)
524
Simon Glass6eb76ca2020-04-09 15:08:45 -0600525 line += ' /%-5d ' % self.count
526 remaining = self.count - self.upto
527 if remaining:
528 line += self.col.Color(self.col.MAGENTA, ' -%-5d ' % remaining)
529 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 Glass102969b2020-04-09 15:08:42 -0600538 terminal.PrintClear()
Simon Glass95ed0a22020-04-09 15:08:46 -0600539 Print(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 Glass4653a882014-09-05 19:00:07 -0600669 Print("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'
936 str = self.col.Color(color, ' ' + target)
937 if not arch in done_arch:
Simon Glass63c619e2015-02-05 22:06:11 -0700938 str = ' %s %s' % (self.col.Color(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'
950 return self.col.Color(color, str(num))
951
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 Glass4653a882014-09-05 19:00:07 -06001012 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
1013 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
1014 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
1015 '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 Glass4653a882014-09-05 19:00:07 -06001021 Print(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 Glass4653a882014-09-05 19:00:07 -06001046 Print('%10s %-15s:' % ('', result['_target']),
1047 newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001048 printed_target = True
Simon Glass4653a882014-09-05 19:00:07 -06001049 Print(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001050 if printed_target:
Simon Glass4653a882014-09-05 19:00:07 -06001051 Print()
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
1074 nds32: (3 boards) text -8.0
1075
1076 Args:
1077 board_selected: Dict containing boards to summarise, keyed by
1078 board.target
1079 board_dict: Dict containing boards for which we built this
1080 commit, keyed by board.target. The value is an Outcome object.
Simon Glassf9c094b2020-03-18 09:42:43 -06001081 show_detail: Show size delta detail for each board
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001082 show_bloat: Show detail for each function
1083 """
1084 arch_list = {}
1085 arch_count = {}
1086
1087 # Calculate changes in size for different image parts
1088 # The previous sizes are in Board.sizes, for each board
1089 for target in board_dict:
1090 if target not in board_selected:
1091 continue
1092 base_sizes = self._base_board_dict[target].sizes
1093 outcome = board_dict[target]
1094 sizes = outcome.sizes
1095
1096 # Loop through the list of images, creating a dict of size
1097 # changes for each image/part. We end up with something like
1098 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1099 # which means that U-Boot data increased by 5 bytes and SPL
1100 # text decreased by 4.
1101 err = {'_target' : target}
1102 for image in sizes:
1103 if image in base_sizes:
1104 base_image = base_sizes[image]
1105 # Loop through the text, data, bss parts
1106 for part in sorted(sizes[image]):
1107 diff = sizes[image][part] - base_image[part]
1108 col = None
1109 if diff:
1110 if image == 'u-boot':
1111 name = part
1112 else:
1113 name = image + ':' + part
1114 err[name] = diff
1115 arch = board_selected[target].arch
1116 if not arch in arch_count:
1117 arch_count[arch] = 1
1118 else:
1119 arch_count[arch] += 1
1120 if not sizes:
1121 pass # Only add to our list when we have some stats
1122 elif not arch in arch_list:
1123 arch_list[arch] = [err]
1124 else:
1125 arch_list[arch].append(err)
1126
1127 # We now have a list of image size changes sorted by arch
1128 # Print out a summary of these
Simon Glassc05aa032019-10-31 07:42:53 -06001129 for arch, target_list in arch_list.items():
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001130 # Get total difference for each type
1131 totals = {}
1132 for result in target_list:
1133 total = 0
Simon Glassc05aa032019-10-31 07:42:53 -06001134 for name, diff in result.items():
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001135 if name.startswith('_'):
1136 continue
1137 total += diff
1138 if name in totals:
1139 totals[name] += diff
1140 else:
1141 totals[name] = diff
1142 result['_total_diff'] = total
1143 result['_outcome'] = board_dict[result['_target']]
1144
1145 count = len(target_list)
1146 printed_arch = False
1147 for name in sorted(totals):
1148 diff = totals[name]
1149 if diff:
1150 # Display the average difference in this name for this
1151 # architecture
1152 avg_diff = float(diff) / count
1153 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1154 msg = ' %s %+1.1f' % (name, avg_diff)
1155 if not printed_arch:
Simon Glass4653a882014-09-05 19:00:07 -06001156 Print('%10s: (for %d/%d boards)' % (arch, count,
1157 arch_count[arch]), newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001158 printed_arch = True
Simon Glass4653a882014-09-05 19:00:07 -06001159 Print(msg, colour=color, newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001160
1161 if printed_arch:
Simon Glass4653a882014-09-05 19:00:07 -06001162 Print()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001163 if show_detail:
1164 self.PrintSizeDetail(target_list, show_bloat)
1165
1166
1167 def PrintResultSummary(self, board_selected, board_dict, err_lines,
Simon Glasse30965d2014-08-28 09:43:44 -06001168 err_line_boards, warn_lines, warn_line_boards,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001169 config, environment, show_sizes, show_detail,
1170 show_bloat, show_config, show_environment):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001171 """Compare results with the base results and display delta.
1172
1173 Only boards mentioned in board_selected will be considered. This
1174 function is intended to be called repeatedly with the results of
1175 each commit. It therefore shows a 'diff' between what it saw in
1176 the last call and what it sees now.
1177
1178 Args:
1179 board_selected: Dict containing boards to summarise, keyed by
1180 board.target
1181 board_dict: Dict containing boards for which we built this
1182 commit, keyed by board.target. The value is an Outcome object.
1183 err_lines: A list of errors for this commit, or [] if there is
1184 none, or we don't want to print errors
Simon Glassed966652014-08-28 09:43:43 -06001185 err_line_boards: Dict keyed by error line, containing a list of
1186 the Board objects with that error
Simon Glasse30965d2014-08-28 09:43:44 -06001187 warn_lines: A list of warnings for this commit, or [] if there is
1188 none, or we don't want to print errors
1189 warn_line_boards: Dict keyed by warning line, containing a list of
1190 the Board objects with that warning
Simon Glass843312d2015-02-05 22:06:15 -07001191 config: Dictionary keyed by filename - e.g. '.config'. Each
1192 value is itself a dictionary:
1193 key: config name
1194 value: config value
Alex Kiernan48ae4122018-05-31 04:48:34 +00001195 environment: Dictionary keyed by environment variable, Each
1196 value is the value of environment variable.
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001197 show_sizes: Show image size deltas
Simon Glassf9c094b2020-03-18 09:42:43 -06001198 show_detail: Show size delta detail for each board if show_sizes
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001199 show_bloat: Show detail for each function
Simon Glass843312d2015-02-05 22:06:15 -07001200 show_config: Show config changes
Alex Kiernan48ae4122018-05-31 04:48:34 +00001201 show_environment: Show environment changes
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001202 """
Simon Glasse30965d2014-08-28 09:43:44 -06001203 def _BoardList(line, line_boards):
Simon Glassed966652014-08-28 09:43:43 -06001204 """Helper function to get a line of boards containing a line
1205
1206 Args:
1207 line: Error line to search for
Simon Glass35d696d2020-04-09 15:08:36 -06001208 line_boards: boards to search, each a Board
Simon Glassed966652014-08-28 09:43:43 -06001209 Return:
Simon Glass35d696d2020-04-09 15:08:36 -06001210 List of boards with that error line, or [] if the user has not
1211 requested such a list
Simon Glassed966652014-08-28 09:43:43 -06001212 """
Simon Glass35d696d2020-04-09 15:08:36 -06001213 boards = []
1214 board_set = set()
Simon Glassed966652014-08-28 09:43:43 -06001215 if self._list_error_boards:
Simon Glasse30965d2014-08-28 09:43:44 -06001216 for board in line_boards[line]:
Simon Glass35d696d2020-04-09 15:08:36 -06001217 if not board in board_set:
1218 boards.append(board)
1219 board_set.add(board)
1220 return boards
Simon Glassed966652014-08-28 09:43:43 -06001221
Simon Glasse30965d2014-08-28 09:43:44 -06001222 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1223 char):
Simon Glass35d696d2020-04-09 15:08:36 -06001224 """Calculate the required output based on changes in errors
1225
1226 Args:
1227 base_lines: List of errors/warnings for previous commit
1228 base_line_boards: Dict keyed by error line, containing a list
1229 of the Board objects with that error in the previous commit
1230 lines: List of errors/warning for this commit, each a str
1231 line_boards: Dict keyed by error line, containing a list
1232 of the Board objects with that error in this commit
1233 char: Character representing error ('') or warning ('w'). The
1234 broken ('+') or fixed ('-') characters are added in this
1235 function
1236
1237 Returns:
1238 Tuple
1239 List of ErrLine objects for 'better' lines
1240 List of ErrLine objects for 'worse' lines
1241 """
Simon Glasse30965d2014-08-28 09:43:44 -06001242 better_lines = []
1243 worse_lines = []
1244 for line in lines:
1245 if line not in base_lines:
Simon Glass35d696d2020-04-09 15:08:36 -06001246 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1247 line)
1248 worse_lines.append(errline)
Simon Glasse30965d2014-08-28 09:43:44 -06001249 for line in base_lines:
1250 if line not in lines:
Simon Glass35d696d2020-04-09 15:08:36 -06001251 errline = ErrLine(char + '-',
1252 _BoardList(line, base_line_boards), line)
1253 better_lines.append(errline)
Simon Glasse30965d2014-08-28 09:43:44 -06001254 return better_lines, worse_lines
1255
Simon Glass843312d2015-02-05 22:06:15 -07001256 def _CalcConfig(delta, name, config):
1257 """Calculate configuration changes
1258
1259 Args:
1260 delta: Type of the delta, e.g. '+'
1261 name: name of the file which changed (e.g. .config)
1262 config: configuration change dictionary
1263 key: config name
1264 value: config value
1265 Returns:
1266 String containing the configuration changes which can be
1267 printed
1268 """
1269 out = ''
1270 for key in sorted(config.keys()):
1271 out += '%s=%s ' % (key, config[key])
Simon Glass8270e3c2015-08-25 21:52:14 -06001272 return '%s %s: %s' % (delta, name, out)
Simon Glass843312d2015-02-05 22:06:15 -07001273
Simon Glass8270e3c2015-08-25 21:52:14 -06001274 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1275 """Add changes in configuration to a list
Simon Glass843312d2015-02-05 22:06:15 -07001276
1277 Args:
Simon Glass8270e3c2015-08-25 21:52:14 -06001278 lines: list to add to
1279 name: config file name
Simon Glass843312d2015-02-05 22:06:15 -07001280 config_plus: configurations added, dictionary
1281 key: config name
1282 value: config value
1283 config_minus: configurations removed, dictionary
1284 key: config name
1285 value: config value
1286 config_change: configurations changed, dictionary
1287 key: config name
1288 value: config value
1289 """
1290 if config_plus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001291 lines.append(_CalcConfig('+', name, config_plus))
Simon Glass843312d2015-02-05 22:06:15 -07001292 if config_minus:
Simon Glass8270e3c2015-08-25 21:52:14 -06001293 lines.append(_CalcConfig('-', name, config_minus))
Simon Glass843312d2015-02-05 22:06:15 -07001294 if config_change:
Simon Glass8270e3c2015-08-25 21:52:14 -06001295 lines.append(_CalcConfig('c', name, config_change))
1296
1297 def _OutputConfigInfo(lines):
1298 for line in lines:
1299 if not line:
1300 continue
1301 if line[0] == '+':
1302 col = self.col.GREEN
1303 elif line[0] == '-':
1304 col = self.col.RED
1305 elif line[0] == 'c':
1306 col = self.col.YELLOW
1307 Print(' ' + line, newline=True, colour=col)
1308
Simon Glassb206d872020-04-09 15:08:28 -06001309 def _OutputErrLines(err_lines, colour):
1310 """Output the line of error/warning lines, if not empty
1311
1312 Also increments self._error_lines if err_lines not empty
1313
1314 Args:
Simon Glass35d696d2020-04-09 15:08:36 -06001315 err_lines: List of ErrLine objects, each an error or warning
1316 line, possibly including a list of boards with that
1317 error/warning
Simon Glassb206d872020-04-09 15:08:28 -06001318 colour: Colour to use for output
1319 """
1320 if err_lines:
Simon Glass8c9a2672020-04-09 15:08:37 -06001321 out_list = []
Simon Glass35d696d2020-04-09 15:08:36 -06001322 for line in err_lines:
1323 boards = ''
1324 names = [board.target for board in line.boards]
Simon Glass9ef0ceb2020-04-09 15:08:38 -06001325 board_str = ' '.join(names) if names else ''
Simon Glass8c9a2672020-04-09 15:08:37 -06001326 if board_str:
1327 out = self.col.Color(colour, line.char + '(')
1328 out += self.col.Color(self.col.MAGENTA, board_str,
1329 bright=False)
1330 out += self.col.Color(colour, ') %s' % line.errline)
1331 else:
1332 out = self.col.Color(colour, line.char + line.errline)
1333 out_list.append(out)
1334 Print('\n'.join(out_list))
Simon Glassb206d872020-04-09 15:08:28 -06001335 self._error_lines += 1
1336
Simon Glass843312d2015-02-05 22:06:15 -07001337
Simon Glass4cf2b222018-11-06 16:02:12 -07001338 ok_boards = [] # List of boards fixed since last commit
Simon Glass6af71012018-11-06 16:02:13 -07001339 warn_boards = [] # List of boards with warnings since last commit
Simon Glass4cf2b222018-11-06 16:02:12 -07001340 err_boards = [] # List of new broken boards since last commit
1341 new_boards = [] # List of boards that didn't exist last time
1342 unknown_boards = [] # List of boards that were not built
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001343
1344 for target in board_dict:
1345 if target not in board_selected:
1346 continue
1347
1348 # If the board was built last time, add its outcome to a list
1349 if target in self._base_board_dict:
1350 base_outcome = self._base_board_dict[target].rc
1351 outcome = board_dict[target]
1352 if outcome.rc == OUTCOME_UNKNOWN:
Simon Glass4cf2b222018-11-06 16:02:12 -07001353 unknown_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001354 elif outcome.rc < base_outcome:
Simon Glass6af71012018-11-06 16:02:13 -07001355 if outcome.rc == OUTCOME_WARNING:
1356 warn_boards.append(target)
1357 else:
1358 ok_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001359 elif outcome.rc > base_outcome:
Simon Glass6af71012018-11-06 16:02:13 -07001360 if outcome.rc == OUTCOME_WARNING:
1361 warn_boards.append(target)
1362 else:
1363 err_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001364 else:
Simon Glass4cf2b222018-11-06 16:02:12 -07001365 new_boards.append(target)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001366
Simon Glassb206d872020-04-09 15:08:28 -06001367 # Get a list of errors and warnings that have appeared, and disappeared
Simon Glasse30965d2014-08-28 09:43:44 -06001368 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1369 self._base_err_line_boards, err_lines, err_line_boards, '')
1370 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1371 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001372
1373 # Display results by arch
Simon Glass6af71012018-11-06 16:02:13 -07001374 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1375 worse_err, better_err, worse_warn, better_warn)):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001376 arch_list = {}
Simon Glass4cf2b222018-11-06 16:02:12 -07001377 self.AddOutcome(board_selected, arch_list, ok_boards, '',
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001378 self.col.GREEN)
Simon Glass6af71012018-11-06 16:02:13 -07001379 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1380 self.col.YELLOW)
Simon Glass4cf2b222018-11-06 16:02:12 -07001381 self.AddOutcome(board_selected, arch_list, err_boards, '+',
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001382 self.col.RED)
Simon Glass4cf2b222018-11-06 16:02:12 -07001383 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001384 if self._show_unknown:
Simon Glass4cf2b222018-11-06 16:02:12 -07001385 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001386 self.col.MAGENTA)
Simon Glassc05aa032019-10-31 07:42:53 -06001387 for arch, target_list in arch_list.items():
Simon Glass4653a882014-09-05 19:00:07 -06001388 Print('%10s: %s' % (arch, target_list))
Simon Glass28370c12014-08-09 15:33:06 -06001389 self._error_lines += 1
Simon Glassb206d872020-04-09 15:08:28 -06001390 _OutputErrLines(better_err, colour=self.col.GREEN)
1391 _OutputErrLines(worse_err, colour=self.col.RED)
1392 _OutputErrLines(better_warn, colour=self.col.CYAN)
Simon Glass5627bd92020-04-09 15:08:35 -06001393 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001394
1395 if show_sizes:
1396 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1397 show_bloat)
1398
Alex Kiernan48ae4122018-05-31 04:48:34 +00001399 if show_environment and self._base_environment:
1400 lines = []
1401
1402 for target in board_dict:
1403 if target not in board_selected:
1404 continue
1405
1406 tbase = self._base_environment[target]
1407 tenvironment = environment[target]
1408 environment_plus = {}
1409 environment_minus = {}
1410 environment_change = {}
1411 base = tbase.environment
Simon Glassc05aa032019-10-31 07:42:53 -06001412 for key, value in tenvironment.environment.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +00001413 if key not in base:
1414 environment_plus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001415 for key, value in base.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +00001416 if key not in tenvironment.environment:
1417 environment_minus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001418 for key, value in base.items():
Alex Kiernan48ae4122018-05-31 04:48:34 +00001419 new_value = tenvironment.environment.get(key)
1420 if new_value and value != new_value:
1421 desc = '%s -> %s' % (value, new_value)
1422 environment_change[key] = desc
1423
1424 _AddConfig(lines, target, environment_plus, environment_minus,
1425 environment_change)
1426
1427 _OutputConfigInfo(lines)
1428
Simon Glass8270e3c2015-08-25 21:52:14 -06001429 if show_config and self._base_config:
1430 summary = {}
1431 arch_config_plus = {}
1432 arch_config_minus = {}
1433 arch_config_change = {}
1434 arch_list = []
1435
1436 for target in board_dict:
1437 if target not in board_selected:
Simon Glass843312d2015-02-05 22:06:15 -07001438 continue
Simon Glass8270e3c2015-08-25 21:52:14 -06001439 arch = board_selected[target].arch
1440 if arch not in arch_list:
1441 arch_list.append(arch)
1442
1443 for arch in arch_list:
1444 arch_config_plus[arch] = {}
1445 arch_config_minus[arch] = {}
1446 arch_config_change[arch] = {}
Simon Glassb464f8e2016-11-13 14:25:53 -07001447 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001448 arch_config_plus[arch][name] = {}
1449 arch_config_minus[arch][name] = {}
1450 arch_config_change[arch][name] = {}
1451
1452 for target in board_dict:
1453 if target not in board_selected:
1454 continue
1455
1456 arch = board_selected[target].arch
1457
1458 all_config_plus = {}
1459 all_config_minus = {}
1460 all_config_change = {}
1461 tbase = self._base_config[target]
1462 tconfig = config[target]
1463 lines = []
Simon Glassb464f8e2016-11-13 14:25:53 -07001464 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001465 if not tconfig.config[name]:
1466 continue
1467 config_plus = {}
1468 config_minus = {}
1469 config_change = {}
1470 base = tbase.config[name]
Simon Glassc05aa032019-10-31 07:42:53 -06001471 for key, value in tconfig.config[name].items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001472 if key not in base:
1473 config_plus[key] = value
1474 all_config_plus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001475 for key, value in base.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001476 if key not in tconfig.config[name]:
1477 config_minus[key] = value
1478 all_config_minus[key] = value
Simon Glassc05aa032019-10-31 07:42:53 -06001479 for key, value in base.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001480 new_value = tconfig.config.get(key)
1481 if new_value and value != new_value:
1482 desc = '%s -> %s' % (value, new_value)
1483 config_change[key] = desc
1484 all_config_change[key] = desc
1485
1486 arch_config_plus[arch][name].update(config_plus)
1487 arch_config_minus[arch][name].update(config_minus)
1488 arch_config_change[arch][name].update(config_change)
1489
1490 _AddConfig(lines, name, config_plus, config_minus,
1491 config_change)
1492 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1493 all_config_change)
1494 summary[target] = '\n'.join(lines)
1495
1496 lines_by_target = {}
Simon Glassc05aa032019-10-31 07:42:53 -06001497 for target, lines in summary.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001498 if lines in lines_by_target:
1499 lines_by_target[lines].append(target)
1500 else:
1501 lines_by_target[lines] = [target]
1502
1503 for arch in arch_list:
1504 lines = []
1505 all_plus = {}
1506 all_minus = {}
1507 all_change = {}
Simon Glassb464f8e2016-11-13 14:25:53 -07001508 for name in self.config_filenames:
Simon Glass8270e3c2015-08-25 21:52:14 -06001509 all_plus.update(arch_config_plus[arch][name])
1510 all_minus.update(arch_config_minus[arch][name])
1511 all_change.update(arch_config_change[arch][name])
1512 _AddConfig(lines, name, arch_config_plus[arch][name],
1513 arch_config_minus[arch][name],
1514 arch_config_change[arch][name])
1515 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1516 #arch_summary[target] = '\n'.join(lines)
1517 if lines:
1518 Print('%s:' % arch)
1519 _OutputConfigInfo(lines)
1520
Simon Glassc05aa032019-10-31 07:42:53 -06001521 for lines, targets in lines_by_target.items():
Simon Glass8270e3c2015-08-25 21:52:14 -06001522 if not lines:
1523 continue
1524 Print('%s :' % ' '.join(sorted(targets)))
1525 _OutputConfigInfo(lines.split('\n'))
1526
Simon Glass843312d2015-02-05 22:06:15 -07001527
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001528 # Save our updated information for the next call to this function
1529 self._base_board_dict = board_dict
1530 self._base_err_lines = err_lines
Simon Glasse30965d2014-08-28 09:43:44 -06001531 self._base_warn_lines = warn_lines
1532 self._base_err_line_boards = err_line_boards
1533 self._base_warn_line_boards = warn_line_boards
Simon Glass843312d2015-02-05 22:06:15 -07001534 self._base_config = config
Alex Kiernan48ae4122018-05-31 04:48:34 +00001535 self._base_environment = environment
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001536
1537 # Get a list of boards that did not get built, if needed
1538 not_built = []
1539 for board in board_selected:
1540 if not board in board_dict:
1541 not_built.append(board)
1542 if not_built:
Simon Glass4653a882014-09-05 19:00:07 -06001543 Print("Boards not built (%d): %s" % (len(not_built),
1544 ', '.join(not_built)))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001545
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001546 def ProduceResultSummary(self, commit_upto, commits, board_selected):
Simon Glasse30965d2014-08-28 09:43:44 -06001547 (board_dict, err_lines, err_line_boards, warn_lines,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001548 warn_line_boards, config, environment) = self.GetResultSummary(
Simon Glassed966652014-08-28 09:43:43 -06001549 board_selected, commit_upto,
Simon Glass843312d2015-02-05 22:06:15 -07001550 read_func_sizes=self._show_bloat,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001551 read_config=self._show_config,
1552 read_environment=self._show_environment)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001553 if commits:
1554 msg = '%02d: %s' % (commit_upto + 1,
1555 commits[commit_upto].subject)
Simon Glass4653a882014-09-05 19:00:07 -06001556 Print(msg, colour=self.col.BLUE)
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001557 self.PrintResultSummary(board_selected, board_dict,
Simon Glassed966652014-08-28 09:43:43 -06001558 err_lines if self._show_errors else [], err_line_boards,
Simon Glasse30965d2014-08-28 09:43:44 -06001559 warn_lines if self._show_errors else [], warn_line_boards,
Alex Kiernan48ae4122018-05-31 04:48:34 +00001560 config, environment, self._show_sizes, self._show_detail,
1561 self._show_bloat, self._show_config, self._show_environment)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001562
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001563 def ShowSummary(self, commits, board_selected):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001564 """Show a build summary for U-Boot for a given board list.
1565
1566 Reset the result summary, then repeatedly call GetResultSummary on
1567 each commit's results, then display the differences we see.
1568
1569 Args:
1570 commit: Commit objects to summarise
1571 board_selected: Dict containing boards to summarise
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001572 """
Simon Glassfea58582014-08-09 15:32:59 -06001573 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001574 self.commits = commits
1575 self.ResetResultSummary(board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001576 self._error_lines = 0
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001577
1578 for commit_upto in range(0, self.commit_count, self._step):
Simon Glassb2ea7ab2014-08-09 15:33:02 -06001579 self.ProduceResultSummary(commit_upto, commits, board_selected)
Simon Glass28370c12014-08-09 15:33:06 -06001580 if not self._error_lines:
Simon Glass4653a882014-09-05 19:00:07 -06001581 Print('(no errors to report)', colour=self.col.GREEN)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001582
1583
1584 def SetupBuild(self, board_selected, commits):
1585 """Set up ready to start a build.
1586
1587 Args:
1588 board_selected: Selected boards to build
1589 commits: Selected commits to build
1590 """
1591 # First work out how many commits we will build
Simon Glassc05aa032019-10-31 07:42:53 -06001592 count = (self.commit_count + self._step - 1) // self._step
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001593 self.count = len(board_selected) * count
1594 self.upto = self.warned = self.fail = 0
1595 self._timestamps = collections.deque()
1596
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001597 def GetThreadDir(self, thread_num):
1598 """Get the directory path to the working dir for a thread.
1599
1600 Args:
Simon Glassb82492b2021-01-30 22:17:46 -07001601 thread_num: Number of thread to check (-1 for main process, which
1602 is treated as 0)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001603 """
Simon Glassd829f122020-03-18 09:42:42 -06001604 if self.work_in_output:
1605 return self._working_dir
Simon Glassb82492b2021-01-30 22:17:46 -07001606 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001607
Simon Glassfea58582014-08-09 15:32:59 -06001608 def _PrepareThread(self, thread_num, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001609 """Prepare the working directory for a thread.
1610
1611 This clones or fetches the repo into the thread's work directory.
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001612 Optionally, it can create a linked working tree of the repo in the
1613 thread's work directory instead.
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001614
1615 Args:
1616 thread_num: Thread number (0, 1, ...)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001617 setup_git:
1618 'clone' to set up a git clone
1619 'worktree' to set up a git worktree
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001620 """
1621 thread_dir = self.GetThreadDir(thread_num)
Simon Glass190064b2014-08-09 15:33:00 -06001622 builderthread.Mkdir(thread_dir)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001623 git_dir = os.path.join(thread_dir, '.git')
1624
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001625 # Create a worktree or a git repo clone for this thread if it
1626 # doesn't already exist
Simon Glassfea58582014-08-09 15:32:59 -06001627 if setup_git and self.git_dir:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001628 src_dir = os.path.abspath(self.git_dir)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001629 if os.path.isdir(git_dir):
1630 # This is a clone of the src_dir repo, we can keep using
1631 # it but need to fetch from src_dir.
Simon Glass212c0b82020-04-09 15:08:43 -06001632 Print('\rFetching repo for thread %d' % thread_num,
1633 newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001634 gitutil.Fetch(git_dir, thread_dir)
Simon Glass212c0b82020-04-09 15:08:43 -06001635 terminal.PrintClear()
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001636 elif os.path.isfile(git_dir):
1637 # This is a worktree of the src_dir repo, we don't need to
1638 # create it again or update it in any way.
1639 pass
1640 elif os.path.exists(git_dir):
1641 # Don't know what could trigger this, but we probably
1642 # can't create a git worktree/clone here.
1643 raise ValueError('Git dir %s exists, but is not a file '
1644 'or a directory.' % git_dir)
1645 elif setup_git == 'worktree':
1646 Print('\rChecking out worktree for thread %d' % thread_num,
1647 newline=False)
1648 gitutil.AddWorktree(src_dir, thread_dir)
1649 terminal.PrintClear()
1650 elif setup_git == 'clone' or setup_git == True:
Simon Glass21f0eb32016-09-18 16:48:31 -06001651 Print('\rCloning repo for thread %d' % thread_num,
1652 newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001653 gitutil.Clone(src_dir, thread_dir)
Simon Glass102969b2020-04-09 15:08:42 -06001654 terminal.PrintClear()
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001655 else:
1656 raise ValueError("Can't setup git repo with %s." % setup_git)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001657
Simon Glassfea58582014-08-09 15:32:59 -06001658 def _PrepareWorkingSpace(self, max_threads, setup_git):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001659 """Prepare the working directory for use.
1660
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001661 Set up the git repo for each thread. Creates a linked working tree
1662 if git-worktree is available, or clones the repo if it isn't.
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001663
1664 Args:
Simon Glassb82492b2021-01-30 22:17:46 -07001665 max_threads: Maximum number of threads we expect to need. If 0 then
1666 1 is set up, since the main process still needs somewhere to
1667 work
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001668 setup_git: True to set up a git worktree or a git clone
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001669 """
Simon Glass190064b2014-08-09 15:33:00 -06001670 builderthread.Mkdir(self._working_dir)
Alper Nebi Yasak76de29f2020-09-03 15:51:03 +03001671 if setup_git and self.git_dir:
1672 src_dir = os.path.abspath(self.git_dir)
1673 if gitutil.CheckWorktreeIsAvailable(src_dir):
1674 setup_git = 'worktree'
1675 # If we previously added a worktree but the directory for it
1676 # got deleted, we need to prune its files from the repo so
1677 # that we can check out another in its place.
1678 gitutil.PruneWorktrees(src_dir)
1679 else:
1680 setup_git = 'clone'
Simon Glassb82492b2021-01-30 22:17:46 -07001681
1682 # Always do at least one thread
1683 for thread in range(max(max_threads, 1)):
Simon Glassfea58582014-08-09 15:32:59 -06001684 self._PrepareThread(thread, setup_git)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001685
Simon Glass925f6ad2020-03-18 09:42:45 -06001686 def _GetOutputSpaceRemovals(self):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001687 """Get the output directories ready to receive files.
1688
Simon Glass925f6ad2020-03-18 09:42:45 -06001689 Figure out what needs to be deleted in the output directory before it
1690 can be used. We only delete old buildman directories which have the
1691 expected name pattern. See _GetOutputDir().
1692
1693 Returns:
1694 List of full paths of directories to remove
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001695 """
Simon Glass1a915672014-12-01 17:33:53 -07001696 if not self.commits:
1697 return
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001698 dir_list = []
1699 for commit_upto in range(self.commit_count):
1700 dir_list.append(self._GetOutputDir(commit_upto))
1701
Simon Glassb222abe2016-09-18 16:48:32 -06001702 to_remove = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001703 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1704 if dirname not in dir_list:
Simon Glass925f6ad2020-03-18 09:42:45 -06001705 leaf = dirname[len(self.base_dir) + 1:]
Ovidiu Panait7664b032020-05-15 09:30:12 +03001706 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
Simon Glass925f6ad2020-03-18 09:42:45 -06001707 if m:
1708 to_remove.append(dirname)
1709 return to_remove
1710
1711 def _PrepareOutputSpace(self):
1712 """Get the output directories ready to receive files.
1713
1714 We delete any output directories which look like ones we need to
1715 create. Having left over directories is confusing when the user wants
1716 to check the output manually.
1717 """
1718 to_remove = self._GetOutputSpaceRemovals()
Simon Glassb222abe2016-09-18 16:48:32 -06001719 if to_remove:
Simon Glassb2d89bc2020-03-18 09:42:46 -06001720 Print('Removing %d old build directories...' % len(to_remove),
Simon Glassb222abe2016-09-18 16:48:32 -06001721 newline=False)
1722 for dirname in to_remove:
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001723 shutil.rmtree(dirname)
Simon Glass212c0b82020-04-09 15:08:43 -06001724 terminal.PrintClear()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001725
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001726 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001727 """Build all commits for a list of boards
1728
1729 Args:
1730 commits: List of commits to be build, each a Commit object
1731 boards_selected: Dict of selected boards, key is target name,
1732 value is Board object
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001733 keep_outputs: True to save build output files
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001734 verbose: Display build results as they are completed
Simon Glass2c3deb92014-08-28 09:43:39 -06001735 Returns:
1736 Tuple containing:
1737 - number of boards that failed to build
1738 - number of boards that issued warnings
Simon Glass8116c782021-04-11 16:27:27 +12001739 - list of thread exceptions raised
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001740 """
Simon Glassfea58582014-08-09 15:32:59 -06001741 self.commit_count = len(commits) if commits else 1
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001742 self.commits = commits
Simon Glasse5a0e5d2014-08-09 15:33:03 -06001743 self._verbose = verbose
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001744
1745 self.ResetResultSummary(board_selected)
Thierry Redingf3d015c2014-08-19 10:22:39 +02001746 builderthread.Mkdir(self.base_dir, parents = True)
Simon Glassfea58582014-08-09 15:32:59 -06001747 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1748 commits is not None)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001749 self._PrepareOutputSpace()
Simon Glass745b3952016-09-18 16:48:33 -06001750 Print('\rStarting build...', newline=False)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001751 self.SetupBuild(board_selected, commits)
1752 self.ProcessResult(None)
Simon Glass8116c782021-04-11 16:27:27 +12001753 self.thread_exceptions = []
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001754 # Create jobs to build all commits for each board
Simon Glassc05aa032019-10-31 07:42:53 -06001755 for brd in board_selected.values():
Simon Glass190064b2014-08-09 15:33:00 -06001756 job = builderthread.BuilderJob()
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001757 job.board = brd
1758 job.commits = commits
1759 job.keep_outputs = keep_outputs
Simon Glassd829f122020-03-18 09:42:42 -06001760 job.work_in_output = self.work_in_output
Simon Glass2b4806e2022-01-22 05:07:33 -07001761 job.adjust_cfg = self.adjust_cfg
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001762 job.step = self._step
Simon Glassb82492b2021-01-30 22:17:46 -07001763 if self.num_threads:
1764 self.queue.put(job)
1765 else:
1766 results = self._single_builder.RunJob(job)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001767
Simon Glassb82492b2021-01-30 22:17:46 -07001768 if self.num_threads:
1769 term = threading.Thread(target=self.queue.join)
1770 term.setDaemon(True)
1771 term.start()
1772 while term.is_alive():
1773 term.join(100)
Simon Glassfc3fe1c2013-04-03 11:07:16 +00001774
Simon Glassb82492b2021-01-30 22:17:46 -07001775 # Wait until we have processed all output
1776 self.out_queue.join()
Simon Glass4653a882014-09-05 19:00:07 -06001777 Print()
Simon Glass7b33f212020-04-09 15:08:47 -06001778
1779 msg = 'Completed: %d total built' % self.count
1780 if self.already_done:
1781 msg += ' (%d previously' % self.already_done
1782 if self.already_done != self.count:
1783 msg += ', %d newly' % (self.count - self.already_done)
1784 msg += ')'
1785 duration = datetime.now() - self._start_time
1786 if duration > timedelta(microseconds=1000000):
1787 if duration.microseconds >= 500000:
1788 duration = duration + timedelta(seconds=1)
1789 duration = duration - timedelta(microseconds=duration.microseconds)
Simon Glass38f159c2020-07-19 12:40:26 -06001790 rate = float(self.count) / duration.total_seconds()
1791 msg += ', duration %s, rate %1.2f' % (duration, rate)
Simon Glass7b33f212020-04-09 15:08:47 -06001792 Print(msg)
Simon Glass8116c782021-04-11 16:27:27 +12001793 if self.thread_exceptions:
1794 Print('Failed: %d thread exceptions' % len(self.thread_exceptions),
1795 colour=self.col.RED)
Simon Glass7b33f212020-04-09 15:08:47 -06001796
Simon Glass8116c782021-04-11 16:27:27 +12001797 return (self.fail, self.warned, self.thread_exceptions)