"Das U-Boot" Source Tree
1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2013 The Chromium OS Authors.
3#
4# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
5#
6
7import collections
8from datetime import datetime, timedelta
9import glob
10import os
11import re
12import queue
13import shutil
14import signal
15import string
16import sys
17import threading
18import time
19
20from buildman import builderthread
21from buildman import toolchain
22from patman import gitutil
23from u_boot_pylib import command
24from u_boot_pylib import terminal
25from u_boot_pylib import tools
26from u_boot_pylib.terminal import tprint
27
28# This indicates an new int or hex Kconfig property with no default
29# It hangs the build since the 'conf' tool cannot proceed without valid input.
30#
31# We get a repeat sequence of something like this:
32# >>
33# Break things (BREAK_ME) [] (NEW)
34# Error in reading or end of file.
35# <<
36# which indicates that BREAK_ME has an empty default
37RE_NO_DEFAULT = re.compile(br'\((\w+)\) \[] \(NEW\)')
38
39# Symbol types which appear in the bloat feature (-B). Others are silently
40# dropped when reading in the 'nm' output
41NM_SYMBOL_TYPES = 'tTdDbBr'
42
43"""
44Theory of Operation
45
46Please see README for user documentation, and you should be familiar with
47that before trying to make sense of this.
48
49Buildman works by keeping the machine as busy as possible, building different
50commits for different boards on multiple CPUs at once.
51
52The source repo (self.git_dir) contains all the commits to be built. Each
53thread works on a single board at a time. It checks out the first commit,
54configures it for that board, then builds it. Then it checks out the next
55commit and builds it (typically without re-configuring). When it runs out
56of commits, it gets another job from the builder and starts again with that
57board.
58
59Clearly the builder threads could work either way - they could check out a
60commit and then built it for all boards. Using separate directories for each
61commit/board pair they could leave their build product around afterwards
62also.
63
64The intent behind building a single board for multiple commits, is to make
65use of incremental builds. Since each commit is built incrementally from
66the previous one, builds are faster. Reconfiguring for a different board
67removes all intermediate object files.
68
69Many threads can be working at once, but each has its own working directory.
70When a thread finishes a build, it puts the output files into a result
71directory.
72
73The base directory used by buildman is normally '../<branch>', i.e.
74a directory higher than the source repository and named after the branch
75being built.
76
77Within the base directory, we have one subdirectory for each commit. Within
78that is one subdirectory for each board. Within that is the build output for
79that commit/board combination.
80
81Buildman also create working directories for each thread, in a .bm-work/
82subdirectory in the base dir.
83
84As an example, say we are building branch 'us-net' for boards 'sandbox' and
85'seaboard', and say that us-net has two commits. We will have directories
86like this:
87
88us-net/ base directory
89 01_g4ed4ebc_net--Add-tftp-speed-/
90 sandbox/
91 u-boot.bin
92 seaboard/
93 u-boot.bin
94 02_g4ed4ebc_net--Check-tftp-comp/
95 sandbox/
96 u-boot.bin
97 seaboard/
98 u-boot.bin
99 .bm-work/
100 00/ working directory for thread 0 (contains source checkout)
101 build/ build output
102 01/ working directory for thread 1
103 build/ build output
104 ...
105u-boot/ source directory
106 .git/ repository
107"""
108
109"""Holds information about a particular error line we are outputing
110
111 char: Character representation: '+': error, '-': fixed error, 'w+': warning,
112 'w-' = fixed warning
113 boards: List of Board objects which have line in the error/warning output
114 errline: The text of the error line
115"""
116ErrLine = collections.namedtuple('ErrLine', 'char,brds,errline')
117
118# Possible build outcomes
119OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4))
120
121# Translate a commit subject into a valid filename (and handle unicode)
122trans_valid_chars = str.maketrans('/: ', '---')
123
124BASE_CONFIG_FILENAMES = [
125 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
126]
127
128EXTRA_CONFIG_FILENAMES = [
129 '.config', '.config-spl', '.config-tpl',
130 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
131 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
132]
133
134class Config:
135 """Holds information about configuration settings for a board."""
136 def __init__(self, config_filename, target):
137 self.target = target
138 self.config = {}
139 for fname in config_filename:
140 self.config[fname] = {}
141
142 def add(self, fname, key, value):
143 self.config[fname][key] = value
144
145 def __hash__(self):
146 val = 0
147 for fname in self.config:
148 for key, value in self.config[fname].items():
149 print(key, value)
150 val = val ^ hash(key) & hash(value)
151 return val
152
153class Environment:
154 """Holds information about environment variables for a board."""
155 def __init__(self, target):
156 self.target = target
157 self.environment = {}
158
159 def add(self, key, value):
160 self.environment[key] = value
161
162class Builder:
163 """Class for building U-Boot for a particular commit.
164
165 Public members: (many should ->private)
166 already_done: Number of builds already completed
167 base_dir: Base directory to use for builder
168 checkout: True to check out source, False to skip that step.
169 This is used for testing.
170 col: terminal.Color() object
171 count: Total number of commits to build, which is the number of commits
172 multiplied by the number of boards
173 do_make: Method to call to invoke Make
174 fail: Number of builds that failed due to error
175 force_build: Force building even if a build already exists
176 force_config_on_failure: If a commit fails for a board, disable
177 incremental building for the next commit we build for that
178 board, so that we will see all warnings/errors again.
179 force_build_failures: If a previously-built build (i.e. built on
180 a previous run of buildman) is marked as failed, rebuild it.
181 git_dir: Git directory containing source repository
182 num_jobs: Number of jobs to run at once (passed to make as -j)
183 num_threads: Number of builder threads to run
184 out_queue: Queue of results to process
185 re_make_err: Compiled regular expression for ignore_lines
186 queue: Queue of jobs to run
187 threads: List of active threads
188 toolchains: Toolchains object to use for building
189 upto: Current commit number we are building (0.count-1)
190 warned: Number of builds that produced at least one warning
191 force_reconfig: Reconfigure U-Boot on each comiit. This disables
192 incremental building, where buildman reconfigures on the first
193 commit for a baord, and then just does an incremental build for
194 the following commits. In fact buildman will reconfigure and
195 retry for any failing commits, so generally the only effect of
196 this option is to slow things down.
197 in_tree: Build U-Boot in-tree instead of specifying an output
198 directory separate from the source code. This option is really
199 only useful for testing in-tree builds.
200 work_in_output: Use the output directory as the work directory and
201 don't write to a separate output directory.
202 thread_exceptions: List of exceptions raised by thread jobs
203 no_lto (bool): True to set the NO_LTO flag when building
204 reproducible_builds (bool): True to set SOURCE_DATE_EPOCH=0 for builds
205
206 Private members:
207 _base_board_dict: Last-summarised Dict of boards
208 _base_err_lines: Last-summarised list of errors
209 _base_warn_lines: Last-summarised list of warnings
210 _build_period_us: Time taken for a single build (float object).
211 _complete_delay: Expected delay until completion (timedelta)
212 _next_delay_update: Next time we plan to display a progress update
213 (datatime)
214 _show_unknown: Show unknown boards (those not built) in summary
215 _start_time: Start time for the build
216 _timestamps: List of timestamps for the completion of the last
217 last _timestamp_count builds. Each is a datetime object.
218 _timestamp_count: Number of timestamps to keep in our list.
219 _working_dir: Base working directory containing all threads
220 _single_builder: BuilderThread object for the singer builder, if
221 threading is not being used
222 _terminated: Thread was terminated due to an error
223 _restarting_config: True if 'Restart config' is detected in output
224 _ide: Produce output suitable for an Integrated Development Environment,
225 i.e. dont emit progress information and put errors/warnings on stderr
226 """
227 class Outcome:
228 """Records a build outcome for a single make invocation
229
230 Public Members:
231 rc: Outcome value (OUTCOME_...)
232 err_lines: List of error lines or [] if none
233 sizes: Dictionary of image size information, keyed by filename
234 - Each value is itself a dictionary containing
235 values for 'text', 'data' and 'bss', being the integer
236 size in bytes of each section.
237 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
238 value is itself a dictionary:
239 key: function name
240 value: Size of function in bytes
241 config: Dictionary keyed by filename - e.g. '.config'. Each
242 value is itself a dictionary:
243 key: config name
244 value: config value
245 environment: Dictionary keyed by environment variable, Each
246 value is the value of environment variable.
247 """
248 def __init__(self, rc, err_lines, sizes, func_sizes, config,
249 environment):
250 self.rc = rc
251 self.err_lines = err_lines
252 self.sizes = sizes
253 self.func_sizes = func_sizes
254 self.config = config
255 self.environment = environment
256
257 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
258 gnu_make='make', checkout=True, show_unknown=True, step=1,
259 no_subdirs=False, full_path=False, verbose_build=False,
260 mrproper=False, fallback_mrproper=False,
261 per_board_out_dir=False, config_only=False,
262 squash_config_y=False, warnings_as_errors=False,
263 work_in_output=False, test_thread_exceptions=False,
264 adjust_cfg=None, allow_missing=False, no_lto=False,
265 reproducible_builds=False, force_build=False,
266 force_build_failures=False, force_reconfig=False,
267 in_tree=False, force_config_on_failure=False, make_func=None,
268 dtc_skip=False):
269 """Create a new Builder object
270
271 Args:
272 toolchains: Toolchains object to use for building
273 base_dir: Base directory to use for builder
274 git_dir: Git directory containing source repository
275 num_threads: Number of builder threads to run
276 num_jobs: Number of jobs to run at once (passed to make as -j)
277 gnu_make: the command name of GNU Make.
278 checkout: True to check out source, False to skip that step.
279 This is used for testing.
280 show_unknown: Show unknown boards (those not built) in summary
281 step: 1 to process every commit, n to process every nth commit
282 no_subdirs: Don't create subdirectories when building current
283 source for a single board
284 full_path: Return the full path in CROSS_COMPILE and don't set
285 PATH
286 verbose_build: Run build with V=1 and don't use 'make -s'
287 mrproper: Always run 'make mrproper' when configuring
288 fallback_mrproper: Run 'make mrproper' and retry on build failure
289 per_board_out_dir: Build in a separate persistent directory per
290 board rather than a thread-specific directory
291 config_only: Only configure each build, don't build it
292 squash_config_y: Convert CONFIG options with the value 'y' to '1'
293 warnings_as_errors: Treat all compiler warnings as errors
294 work_in_output: Use the output directory as the work directory and
295 don't write to a separate output directory.
296 test_thread_exceptions: Uses for tests only, True to make the
297 threads raise an exception instead of reporting their result.
298 This simulates a failure in the code somewhere
299 adjust_cfg_list (list of str): List of changes to make to .config
300 file before building. Each is one of (where C is the config
301 option with or without the CONFIG_ prefix)
302
303 C to enable C
304 ~C to disable C
305 C=val to set the value of C (val must have quotes if C is
306 a string Kconfig
307 allow_missing: Run build with BINMAN_ALLOW_MISSING=1
308 no_lto (bool): True to set the NO_LTO flag when building
309 force_build (bool): Rebuild even commits that are already built
310 force_build_failures (bool): Rebuild commits that have not been
311 built, or failed to build
312 force_reconfig (bool): Reconfigure on each commit
313 in_tree (bool): Bulid in tree instead of out-of-tree
314 force_config_on_failure (bool): Reconfigure the build before
315 retrying a failed build
316 make_func (function): Function to call to run 'make'
317 dtc_skip (bool): True to skip building dtc and use the system one
318 """
319 self.toolchains = toolchains
320 self.base_dir = base_dir
321 if work_in_output:
322 self._working_dir = base_dir
323 else:
324 self._working_dir = os.path.join(base_dir, '.bm-work')
325 self.threads = []
326 self.do_make = make_func or self.make
327 self.gnu_make = gnu_make
328 self.checkout = checkout
329 self.num_threads = num_threads
330 self.num_jobs = num_jobs
331 self.already_done = 0
332 self.force_build = False
333 self.git_dir = git_dir
334 self._show_unknown = show_unknown
335 self._timestamp_count = 10
336 self._build_period_us = None
337 self._complete_delay = None
338 self._next_delay_update = datetime.now()
339 self._start_time = None
340 self._step = step
341 self._error_lines = 0
342 self.no_subdirs = no_subdirs
343 self.full_path = full_path
344 self.verbose_build = verbose_build
345 self.config_only = config_only
346 self.squash_config_y = squash_config_y
347 self.config_filenames = BASE_CONFIG_FILENAMES
348 self.work_in_output = work_in_output
349 self.adjust_cfg = adjust_cfg
350 self.allow_missing = allow_missing
351 self._ide = False
352 self.no_lto = no_lto
353 self.reproducible_builds = reproducible_builds
354 self.force_build = force_build
355 self.force_build_failures = force_build_failures
356 self.force_reconfig = force_reconfig
357 self.in_tree = in_tree
358 self.force_config_on_failure = force_config_on_failure
359 self.fallback_mrproper = fallback_mrproper
360 if dtc_skip:
361 self.dtc = shutil.which('dtc')
362 if not self.dtc:
363 raise ValueError('Cannot find dtc')
364 else:
365 self.dtc = None
366
367 if not self.squash_config_y:
368 self.config_filenames += EXTRA_CONFIG_FILENAMES
369 self._terminated = False
370 self._restarting_config = False
371
372 self.warnings_as_errors = warnings_as_errors
373 self.col = terminal.Color()
374
375 self._re_function = re.compile('(.*): In function.*')
376 self._re_files = re.compile('In file included from.*')
377 self._re_warning = re.compile(r'(.*):(\d*):(\d*): warning: .*')
378 self._re_dtb_warning = re.compile('(.*): Warning .*')
379 self._re_note = re.compile(r'(.*):(\d*):(\d*): note: this is the location of the previous.*')
380 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
381 re.MULTILINE | re.DOTALL)
382
383 self.thread_exceptions = []
384 self.test_thread_exceptions = test_thread_exceptions
385 if self.num_threads:
386 self._single_builder = None
387 self.queue = queue.Queue()
388 self.out_queue = queue.Queue()
389 for i in range(self.num_threads):
390 t = builderthread.BuilderThread(
391 self, i, mrproper, per_board_out_dir,
392 test_exception=test_thread_exceptions)
393 t.setDaemon(True)
394 t.start()
395 self.threads.append(t)
396
397 t = builderthread.ResultThread(self)
398 t.setDaemon(True)
399 t.start()
400 self.threads.append(t)
401 else:
402 self._single_builder = builderthread.BuilderThread(
403 self, -1, mrproper, per_board_out_dir)
404
405 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
406 self.re_make_err = re.compile('|'.join(ignore_lines))
407
408 # Handle existing graceful with SIGINT / Ctrl-C
409 signal.signal(signal.SIGINT, self.signal_handler)
410
411 def __del__(self):
412 """Get rid of all threads created by the builder"""
413 for t in self.threads:
414 del t
415
416 def signal_handler(self, signal, frame):
417 sys.exit(1)
418
419 def make_environment(self, toolchain):
420 """Create the environment to use for building
421
422 Args:
423 toolchain (Toolchain): Toolchain to use for building
424
425 Returns:
426 dict:
427 key (str): Variable name
428 value (str): Variable value
429 """
430 env = toolchain.MakeEnvironment(self.full_path)
431 if self.dtc:
432 env[b'DTC'] = tools.to_bytes(self.dtc)
433 return env
434
435 def set_display_options(self, show_errors=False, show_sizes=False,
436 show_detail=False, show_bloat=False,
437 list_error_boards=False, show_config=False,
438 show_environment=False, filter_dtb_warnings=False,
439 filter_migration_warnings=False, ide=False):
440 """Setup display options for the builder.
441
442 Args:
443 show_errors: True to show summarised error/warning info
444 show_sizes: Show size deltas
445 show_detail: Show size delta detail for each board if show_sizes
446 show_bloat: Show detail for each function
447 list_error_boards: Show the boards which caused each error/warning
448 show_config: Show config deltas
449 show_environment: Show environment deltas
450 filter_dtb_warnings: Filter out any warnings from the device-tree
451 compiler
452 filter_migration_warnings: Filter out any warnings about migrating
453 a board to driver model
454 ide: Create output that can be parsed by an IDE. There is no '+' prefix on
455 error lines and output on stderr stays on stderr.
456 """
457 self._show_errors = show_errors
458 self._show_sizes = show_sizes
459 self._show_detail = show_detail
460 self._show_bloat = show_bloat
461 self._list_error_boards = list_error_boards
462 self._show_config = show_config
463 self._show_environment = show_environment
464 self._filter_dtb_warnings = filter_dtb_warnings
465 self._filter_migration_warnings = filter_migration_warnings
466 self._ide = ide
467
468 def _add_timestamp(self):
469 """Add a new timestamp to the list and record the build period.
470
471 The build period is the length of time taken to perform a single
472 build (one board, one commit).
473 """
474 now = datetime.now()
475 self._timestamps.append(now)
476 count = len(self._timestamps)
477 delta = self._timestamps[-1] - self._timestamps[0]
478 seconds = delta.total_seconds()
479
480 # If we have enough data, estimate build period (time taken for a
481 # single build) and therefore completion time.
482 if count > 1 and self._next_delay_update < now:
483 self._next_delay_update = now + timedelta(seconds=2)
484 if seconds > 0:
485 self._build_period = float(seconds) / count
486 todo = self.count - self.upto
487 self._complete_delay = timedelta(microseconds=
488 self._build_period * todo * 1000000)
489 # Round it
490 self._complete_delay -= timedelta(
491 microseconds=self._complete_delay.microseconds)
492
493 if seconds > 60:
494 self._timestamps.popleft()
495 count -= 1
496
497 def select_commit(self, commit, checkout=True):
498 """Checkout the selected commit for this build
499 """
500 self.commit = commit
501 if checkout and self.checkout:
502 gitutil.checkout(commit.hash)
503
504 def make(self, commit, brd, stage, cwd, *args, **kwargs):
505 """Run make
506
507 Args:
508 commit: Commit object that is being built
509 brd: Board object that is being built
510 stage: Stage that we are at (mrproper, config, oldconfig, build)
511 cwd: Directory where make should be run
512 args: Arguments to pass to make
513 kwargs: Arguments to pass to command.run_pipe()
514 """
515
516 def check_output(stream, data):
517 if b'Restart config' in data:
518 self._restarting_config = True
519
520 # If we see 'Restart config' following by multiple errors
521 if self._restarting_config:
522 m = RE_NO_DEFAULT.findall(data)
523
524 # Number of occurences of each Kconfig item
525 multiple = [m.count(val) for val in set(m)]
526
527 # If any of them occur more than once, we have a loop
528 if [val for val in multiple if val > 1]:
529 self._terminated = True
530 return True
531 return False
532
533 self._restarting_config = False
534 self._terminated = False
535 cmd = [self.gnu_make] + list(args)
536 result = command.run_pipe([cmd], capture=True, capture_stderr=True,
537 cwd=cwd, raise_on_error=False, infile='/dev/null',
538 output_func=check_output, **kwargs)
539
540 if self._terminated:
541 # Try to be helpful
542 result.stderr += '(** did you define an int/hex Kconfig with no default? **)'
543
544 if self.verbose_build:
545 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
546 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
547 return result
548
549 def process_result(self, result):
550 """Process the result of a build, showing progress information
551
552 Args:
553 result: A CommandResult object, which indicates the result for
554 a single build
555 """
556 col = terminal.Color()
557 if result:
558 target = result.brd.target
559
560 self.upto += 1
561 if result.return_code != 0:
562 self.fail += 1
563 elif result.stderr:
564 self.warned += 1
565 if result.already_done:
566 self.already_done += 1
567 if self._verbose:
568 terminal.print_clear()
569 boards_selected = {target : result.brd}
570 self.reset_result_summary(boards_selected)
571 self.produce_result_summary(result.commit_upto, self.commits,
572 boards_selected)
573 else:
574 target = '(starting)'
575
576 # Display separate counts for ok, warned and fail
577 ok = self.upto - self.warned - self.fail
578 line = '\r' + self.col.build(self.col.GREEN, '%5d' % ok)
579 line += self.col.build(self.col.YELLOW, '%5d' % self.warned)
580 line += self.col.build(self.col.RED, '%5d' % self.fail)
581
582 line += ' /%-5d ' % self.count
583 remaining = self.count - self.upto
584 if remaining:
585 line += self.col.build(self.col.MAGENTA, ' -%-5d ' % remaining)
586 else:
587 line += ' ' * 8
588
589 # Add our current completion time estimate
590 self._add_timestamp()
591 if self._complete_delay:
592 line += '%s : ' % self._complete_delay
593
594 line += target
595 if not self._ide:
596 terminal.print_clear()
597 tprint(line, newline=False, limit_to_line=True)
598
599 def get_output_dir(self, commit_upto):
600 """Get the name of the output directory for a commit number
601
602 The output directory is typically .../<branch>/<commit>.
603
604 Args:
605 commit_upto: Commit number to use (0..self.count-1)
606 """
607 if self.work_in_output:
608 return self._working_dir
609
610 commit_dir = None
611 if self.commits:
612 commit = self.commits[commit_upto]
613 subject = commit.subject.translate(trans_valid_chars)
614 # See _get_output_space_removals() which parses this name
615 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
616 commit.hash, subject[:20]))
617 elif not self.no_subdirs:
618 commit_dir = 'current'
619 if not commit_dir:
620 return self.base_dir
621 return os.path.join(self.base_dir, commit_dir)
622
623 def get_build_dir(self, commit_upto, target):
624 """Get the name of the build directory for a commit number
625
626 The build directory is typically .../<branch>/<commit>/<target>.
627
628 Args:
629 commit_upto: Commit number to use (0..self.count-1)
630 target: Target name
631 """
632 output_dir = self.get_output_dir(commit_upto)
633 if self.work_in_output:
634 return output_dir
635 return os.path.join(output_dir, target)
636
637 def get_done_file(self, commit_upto, target):
638 """Get the name of the done file for a commit number
639
640 Args:
641 commit_upto: Commit number to use (0..self.count-1)
642 target: Target name
643 """
644 return os.path.join(self.get_build_dir(commit_upto, target), 'done')
645
646 def get_sizes_file(self, commit_upto, target):
647 """Get the name of the sizes file for a commit number
648
649 Args:
650 commit_upto: Commit number to use (0..self.count-1)
651 target: Target name
652 """
653 return os.path.join(self.get_build_dir(commit_upto, target), 'sizes')
654
655 def get_func_sizes_file(self, commit_upto, target, elf_fname):
656 """Get the name of the funcsizes file for a commit number and ELF file
657
658 Args:
659 commit_upto: Commit number to use (0..self.count-1)
660 target: Target name
661 elf_fname: Filename of elf image
662 """
663 return os.path.join(self.get_build_dir(commit_upto, target),
664 '%s.sizes' % elf_fname.replace('/', '-'))
665
666 def get_objdump_file(self, commit_upto, target, elf_fname):
667 """Get the name of the objdump file for a commit number and ELF file
668
669 Args:
670 commit_upto: Commit number to use (0..self.count-1)
671 target: Target name
672 elf_fname: Filename of elf image
673 """
674 return os.path.join(self.get_build_dir(commit_upto, target),
675 '%s.objdump' % elf_fname.replace('/', '-'))
676
677 def get_err_file(self, commit_upto, target):
678 """Get the name of the err file for a commit number
679
680 Args:
681 commit_upto: Commit number to use (0..self.count-1)
682 target: Target name
683 """
684 output_dir = self.get_build_dir(commit_upto, target)
685 return os.path.join(output_dir, 'err')
686
687 def filter_errors(self, lines):
688 """Filter out errors in which we have no interest
689
690 We should probably use map().
691
692 Args:
693 lines: List of error lines, each a string
694 Returns:
695 New list with only interesting lines included
696 """
697 out_lines = []
698 if self._filter_migration_warnings:
699 text = '\n'.join(lines)
700 text = self._re_migration_warning.sub('', text)
701 lines = text.splitlines()
702 for line in lines:
703 if self.re_make_err.search(line):
704 continue
705 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
706 continue
707 out_lines.append(line)
708 return out_lines
709
710 def read_func_sizes(self, fname, fd):
711 """Read function sizes from the output of 'nm'
712
713 Args:
714 fd: File containing data to read
715 fname: Filename we are reading from (just for errors)
716
717 Returns:
718 Dictionary containing size of each function in bytes, indexed by
719 function name.
720 """
721 sym = {}
722 for line in fd.readlines():
723 line = line.strip()
724 parts = line.split()
725 if line and len(parts) == 3:
726 size, type, name = line.split()
727 if type in NM_SYMBOL_TYPES:
728 # function names begin with '.' on 64-bit powerpc
729 if '.' in name[1:]:
730 name = 'static.' + name.split('.')[0]
731 sym[name] = sym.get(name, 0) + int(size, 16)
732 return sym
733
734 def _process_config(self, fname):
735 """Read in a .config, autoconf.mk or autoconf.h file
736
737 This function handles all config file types. It ignores comments and
738 any #defines which don't start with CONFIG_.
739
740 Args:
741 fname: Filename to read
742
743 Returns:
744 Dictionary:
745 key: Config name (e.g. CONFIG_DM)
746 value: Config value (e.g. 1)
747 """
748 config = {}
749 if os.path.exists(fname):
750 with open(fname) as fd:
751 for line in fd:
752 line = line.strip()
753 if line.startswith('#define'):
754 values = line[8:].split(' ', 1)
755 if len(values) > 1:
756 key, value = values
757 else:
758 key = values[0]
759 value = '1' if self.squash_config_y else ''
760 if not key.startswith('CONFIG_'):
761 continue
762 elif not line or line[0] in ['#', '*', '/']:
763 continue
764 else:
765 key, value = line.split('=', 1)
766 if self.squash_config_y and value == 'y':
767 value = '1'
768 config[key] = value
769 return config
770
771 def _process_environment(self, fname):
772 """Read in a uboot.env file
773
774 This function reads in environment variables from a file.
775
776 Args:
777 fname: Filename to read
778
779 Returns:
780 Dictionary:
781 key: environment variable (e.g. bootlimit)
782 value: value of environment variable (e.g. 1)
783 """
784 environment = {}
785 if os.path.exists(fname):
786 with open(fname) as fd:
787 for line in fd.read().split('\0'):
788 try:
789 key, value = line.split('=', 1)
790 environment[key] = value
791 except ValueError:
792 # ignore lines we can't parse
793 pass
794 return environment
795
796 def get_build_outcome(self, commit_upto, target, read_func_sizes,
797 read_config, read_environment):
798 """Work out the outcome of a build.
799
800 Args:
801 commit_upto: Commit number to check (0..n-1)
802 target: Target board to check
803 read_func_sizes: True to read function size information
804 read_config: True to read .config and autoconf.h files
805 read_environment: True to read uboot.env files
806
807 Returns:
808 Outcome object
809 """
810 done_file = self.get_done_file(commit_upto, target)
811 sizes_file = self.get_sizes_file(commit_upto, target)
812 sizes = {}
813 func_sizes = {}
814 config = {}
815 environment = {}
816 if os.path.exists(done_file):
817 with open(done_file, 'r') as fd:
818 try:
819 return_code = int(fd.readline())
820 except ValueError:
821 # The file may be empty due to running out of disk space.
822 # Try a rebuild
823 return_code = 1
824 err_lines = []
825 err_file = self.get_err_file(commit_upto, target)
826 if os.path.exists(err_file):
827 with open(err_file, 'r') as fd:
828 err_lines = self.filter_errors(fd.readlines())
829
830 # Decide whether the build was ok, failed or created warnings
831 if return_code:
832 rc = OUTCOME_ERROR
833 elif len(err_lines):
834 rc = OUTCOME_WARNING
835 else:
836 rc = OUTCOME_OK
837
838 # Convert size information to our simple format
839 if os.path.exists(sizes_file):
840 with open(sizes_file, 'r') as fd:
841 for line in fd.readlines():
842 values = line.split()
843 rodata = 0
844 if len(values) > 6:
845 rodata = int(values[6], 16)
846 size_dict = {
847 'all' : int(values[0]) + int(values[1]) +
848 int(values[2]),
849 'text' : int(values[0]) - rodata,
850 'data' : int(values[1]),
851 'bss' : int(values[2]),
852 'rodata' : rodata,
853 }
854 sizes[values[5]] = size_dict
855
856 if read_func_sizes:
857 pattern = self.get_func_sizes_file(commit_upto, target, '*')
858 for fname in glob.glob(pattern):
859 with open(fname, 'r') as fd:
860 dict_name = os.path.basename(fname).replace('.sizes',
861 '')
862 func_sizes[dict_name] = self.read_func_sizes(fname, fd)
863
864 if read_config:
865 output_dir = self.get_build_dir(commit_upto, target)
866 for name in self.config_filenames:
867 fname = os.path.join(output_dir, name)
868 config[name] = self._process_config(fname)
869
870 if read_environment:
871 output_dir = self.get_build_dir(commit_upto, target)
872 fname = os.path.join(output_dir, 'uboot.env')
873 environment = self._process_environment(fname)
874
875 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
876 environment)
877
878 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
879
880 def get_result_summary(self, boards_selected, commit_upto, read_func_sizes,
881 read_config, read_environment):
882 """Calculate a summary of the results of building a commit.
883
884 Args:
885 board_selected: Dict containing boards to summarise
886 commit_upto: Commit number to summarize (0..self.count-1)
887 read_func_sizes: True to read function size information
888 read_config: True to read .config and autoconf.h files
889 read_environment: True to read uboot.env files
890
891 Returns:
892 Tuple:
893 Dict containing boards which built this commit:
894 key: board.target
895 value: Builder.Outcome object
896 List containing a summary of error lines
897 Dict keyed by error line, containing a list of the Board
898 objects with that error
899 List containing a summary of warning lines
900 Dict keyed by error line, containing a list of the Board
901 objects with that warning
902 Dictionary keyed by board.target. Each value is a dictionary:
903 key: filename - e.g. '.config'
904 value is itself a dictionary:
905 key: config name
906 value: config value
907 Dictionary keyed by board.target. Each value is a dictionary:
908 key: environment variable
909 value: value of environment variable
910 """
911 def add_line(lines_summary, lines_boards, line, board):
912 line = line.rstrip()
913 if line in lines_boards:
914 lines_boards[line].append(board)
915 else:
916 lines_boards[line] = [board]
917 lines_summary.append(line)
918
919 board_dict = {}
920 err_lines_summary = []
921 err_lines_boards = {}
922 warn_lines_summary = []
923 warn_lines_boards = {}
924 config = {}
925 environment = {}
926
927 for brd in boards_selected.values():
928 outcome = self.get_build_outcome(commit_upto, brd.target,
929 read_func_sizes, read_config,
930 read_environment)
931 board_dict[brd.target] = outcome
932 last_func = None
933 last_was_warning = False
934 for line in outcome.err_lines:
935 if line:
936 if (self._re_function.match(line) or
937 self._re_files.match(line)):
938 last_func = line
939 else:
940 is_warning = (self._re_warning.match(line) or
941 self._re_dtb_warning.match(line))
942 is_note = self._re_note.match(line)
943 if is_warning or (last_was_warning and is_note):
944 if last_func:
945 add_line(warn_lines_summary, warn_lines_boards,
946 last_func, brd)
947 add_line(warn_lines_summary, warn_lines_boards,
948 line, brd)
949 else:
950 if last_func:
951 add_line(err_lines_summary, err_lines_boards,
952 last_func, brd)
953 add_line(err_lines_summary, err_lines_boards,
954 line, brd)
955 last_was_warning = is_warning
956 last_func = None
957 tconfig = Config(self.config_filenames, brd.target)
958 for fname in self.config_filenames:
959 if outcome.config:
960 for key, value in outcome.config[fname].items():
961 tconfig.add(fname, key, value)
962 config[brd.target] = tconfig
963
964 tenvironment = Environment(brd.target)
965 if outcome.environment:
966 for key, value in outcome.environment.items():
967 tenvironment.add(key, value)
968 environment[brd.target] = tenvironment
969
970 return (board_dict, err_lines_summary, err_lines_boards,
971 warn_lines_summary, warn_lines_boards, config, environment)
972
973 def add_outcome(self, board_dict, arch_list, changes, char, color):
974 """Add an output to our list of outcomes for each architecture
975
976 This simple function adds failing boards (changes) to the
977 relevant architecture string, so we can print the results out
978 sorted by architecture.
979
980 Args:
981 board_dict: Dict containing all boards
982 arch_list: Dict keyed by arch name. Value is a string containing
983 a list of board names which failed for that arch.
984 changes: List of boards to add to arch_list
985 color: terminal.Colour object
986 """
987 done_arch = {}
988 for target in changes:
989 if target in board_dict:
990 arch = board_dict[target].arch
991 else:
992 arch = 'unknown'
993 str = self.col.build(color, ' ' + target)
994 if not arch in done_arch:
995 str = ' %s %s' % (self.col.build(color, char), str)
996 done_arch[arch] = True
997 if not arch in arch_list:
998 arch_list[arch] = str
999 else:
1000 arch_list[arch] += str
1001
1002
1003 def colour_num(self, num):
1004 color = self.col.RED if num > 0 else self.col.GREEN
1005 if num == 0:
1006 return '0'
1007 return self.col.build(color, str(num))
1008
1009 def reset_result_summary(self, board_selected):
1010 """Reset the results summary ready for use.
1011
1012 Set up the base board list to be all those selected, and set the
1013 error lines to empty.
1014
1015 Following this, calls to print_result_summary() will use this
1016 information to work out what has changed.
1017
1018 Args:
1019 board_selected: Dict containing boards to summarise, keyed by
1020 board.target
1021 """
1022 self._base_board_dict = {}
1023 for brd in board_selected:
1024 self._base_board_dict[brd] = Builder.Outcome(0, [], [], {}, {}, {})
1025 self._base_err_lines = []
1026 self._base_warn_lines = []
1027 self._base_err_line_boards = {}
1028 self._base_warn_line_boards = {}
1029 self._base_config = None
1030 self._base_environment = None
1031
1032 def print_func_size_detail(self, fname, old, new):
1033 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
1034 delta, common = [], {}
1035
1036 for a in old:
1037 if a in new:
1038 common[a] = 1
1039
1040 for name in old:
1041 if name not in common:
1042 remove += 1
1043 down += old[name]
1044 delta.append([-old[name], name])
1045
1046 for name in new:
1047 if name not in common:
1048 add += 1
1049 up += new[name]
1050 delta.append([new[name], name])
1051
1052 for name in common:
1053 diff = new.get(name, 0) - old.get(name, 0)
1054 if diff > 0:
1055 grow, up = grow + 1, up + diff
1056 elif diff < 0:
1057 shrink, down = shrink + 1, down - diff
1058 delta.append([diff, name])
1059
1060 delta.sort()
1061 delta.reverse()
1062
1063 args = [add, -remove, grow, -shrink, up, -down, up - down]
1064 if max(args) == 0 and min(args) == 0:
1065 return
1066 args = [self.colour_num(x) for x in args]
1067 indent = ' ' * 15
1068 tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
1069 tuple([indent, self.col.build(self.col.YELLOW, fname)] + args))
1070 tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
1071 'delta'))
1072 for diff, name in delta:
1073 if diff:
1074 color = self.col.RED if diff > 0 else self.col.GREEN
1075 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1076 old.get(name, '-'), new.get(name,'-'), diff)
1077 tprint(msg, colour=color)
1078
1079
1080 def print_size_detail(self, target_list, show_bloat):
1081 """Show details size information for each board
1082
1083 Args:
1084 target_list: List of targets, each a dict containing:
1085 'target': Target name
1086 'total_diff': Total difference in bytes across all areas
1087 <part_name>: Difference for that part
1088 show_bloat: Show detail for each function
1089 """
1090 targets_by_diff = sorted(target_list, reverse=True,
1091 key=lambda x: x['_total_diff'])
1092 for result in targets_by_diff:
1093 printed_target = False
1094 for name in sorted(result):
1095 diff = result[name]
1096 if name.startswith('_'):
1097 continue
1098 if diff != 0:
1099 color = self.col.RED if diff > 0 else self.col.GREEN
1100 msg = ' %s %+d' % (name, diff)
1101 if not printed_target:
1102 tprint('%10s %-15s:' % ('', result['_target']),
1103 newline=False)
1104 printed_target = True
1105 tprint(msg, colour=color, newline=False)
1106 if printed_target:
1107 tprint()
1108 if show_bloat:
1109 target = result['_target']
1110 outcome = result['_outcome']
1111 base_outcome = self._base_board_dict[target]
1112 for fname in outcome.func_sizes:
1113 self.print_func_size_detail(fname,
1114 base_outcome.func_sizes[fname],
1115 outcome.func_sizes[fname])
1116
1117
1118 def print_size_summary(self, board_selected, board_dict, show_detail,
1119 show_bloat):
1120 """Print a summary of image sizes broken down by section.
1121
1122 The summary takes the form of one line per architecture. The
1123 line contains deltas for each of the sections (+ means the section
1124 got bigger, - means smaller). The numbers are the average number
1125 of bytes that a board in this section increased by.
1126
1127 For example:
1128 powerpc: (622 boards) text -0.0
1129 arm: (285 boards) text -0.0
1130
1131 Args:
1132 board_selected: Dict containing boards to summarise, keyed by
1133 board.target
1134 board_dict: Dict containing boards for which we built this
1135 commit, keyed by board.target. The value is an Outcome object.
1136 show_detail: Show size delta detail for each board
1137 show_bloat: Show detail for each function
1138 """
1139 arch_list = {}
1140 arch_count = {}
1141
1142 # Calculate changes in size for different image parts
1143 # The previous sizes are in Board.sizes, for each board
1144 for target in board_dict:
1145 if target not in board_selected:
1146 continue
1147 base_sizes = self._base_board_dict[target].sizes
1148 outcome = board_dict[target]
1149 sizes = outcome.sizes
1150
1151 # Loop through the list of images, creating a dict of size
1152 # changes for each image/part. We end up with something like
1153 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1154 # which means that U-Boot data increased by 5 bytes and SPL
1155 # text decreased by 4.
1156 err = {'_target' : target}
1157 for image in sizes:
1158 if image in base_sizes:
1159 base_image = base_sizes[image]
1160 # Loop through the text, data, bss parts
1161 for part in sorted(sizes[image]):
1162 diff = sizes[image][part] - base_image[part]
1163 col = None
1164 if diff:
1165 if image == 'u-boot':
1166 name = part
1167 else:
1168 name = image + ':' + part
1169 err[name] = diff
1170 arch = board_selected[target].arch
1171 if not arch in arch_count:
1172 arch_count[arch] = 1
1173 else:
1174 arch_count[arch] += 1
1175 if not sizes:
1176 pass # Only add to our list when we have some stats
1177 elif not arch in arch_list:
1178 arch_list[arch] = [err]
1179 else:
1180 arch_list[arch].append(err)
1181
1182 # We now have a list of image size changes sorted by arch
1183 # Print out a summary of these
1184 for arch, target_list in arch_list.items():
1185 # Get total difference for each type
1186 totals = {}
1187 for result in target_list:
1188 total = 0
1189 for name, diff in result.items():
1190 if name.startswith('_'):
1191 continue
1192 total += diff
1193 if name in totals:
1194 totals[name] += diff
1195 else:
1196 totals[name] = diff
1197 result['_total_diff'] = total
1198 result['_outcome'] = board_dict[result['_target']]
1199
1200 count = len(target_list)
1201 printed_arch = False
1202 for name in sorted(totals):
1203 diff = totals[name]
1204 if diff:
1205 # Display the average difference in this name for this
1206 # architecture
1207 avg_diff = float(diff) / count
1208 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1209 msg = ' %s %+1.1f' % (name, avg_diff)
1210 if not printed_arch:
1211 tprint('%10s: (for %d/%d boards)' % (arch, count,
1212 arch_count[arch]), newline=False)
1213 printed_arch = True
1214 tprint(msg, colour=color, newline=False)
1215
1216 if printed_arch:
1217 tprint()
1218 if show_detail:
1219 self.print_size_detail(target_list, show_bloat)
1220
1221
1222 def print_result_summary(self, board_selected, board_dict, err_lines,
1223 err_line_boards, warn_lines, warn_line_boards,
1224 config, environment, show_sizes, show_detail,
1225 show_bloat, show_config, show_environment):
1226 """Compare results with the base results and display delta.
1227
1228 Only boards mentioned in board_selected will be considered. This
1229 function is intended to be called repeatedly with the results of
1230 each commit. It therefore shows a 'diff' between what it saw in
1231 the last call and what it sees now.
1232
1233 Args:
1234 board_selected: Dict containing boards to summarise, keyed by
1235 board.target
1236 board_dict: Dict containing boards for which we built this
1237 commit, keyed by board.target. The value is an Outcome object.
1238 err_lines: A list of errors for this commit, or [] if there is
1239 none, or we don't want to print errors
1240 err_line_boards: Dict keyed by error line, containing a list of
1241 the Board objects with that error
1242 warn_lines: A list of warnings for this commit, or [] if there is
1243 none, or we don't want to print errors
1244 warn_line_boards: Dict keyed by warning line, containing a list of
1245 the Board objects with that warning
1246 config: Dictionary keyed by filename - e.g. '.config'. Each
1247 value is itself a dictionary:
1248 key: config name
1249 value: config value
1250 environment: Dictionary keyed by environment variable, Each
1251 value is the value of environment variable.
1252 show_sizes: Show image size deltas
1253 show_detail: Show size delta detail for each board if show_sizes
1254 show_bloat: Show detail for each function
1255 show_config: Show config changes
1256 show_environment: Show environment changes
1257 """
1258 def _board_list(line, line_boards):
1259 """Helper function to get a line of boards containing a line
1260
1261 Args:
1262 line: Error line to search for
1263 line_boards: boards to search, each a Board
1264 Return:
1265 List of boards with that error line, or [] if the user has not
1266 requested such a list
1267 """
1268 brds = []
1269 board_set = set()
1270 if self._list_error_boards:
1271 for brd in line_boards[line]:
1272 if not brd in board_set:
1273 brds.append(brd)
1274 board_set.add(brd)
1275 return brds
1276
1277 def _calc_error_delta(base_lines, base_line_boards, lines, line_boards,
1278 char):
1279 """Calculate the required output based on changes in errors
1280
1281 Args:
1282 base_lines: List of errors/warnings for previous commit
1283 base_line_boards: Dict keyed by error line, containing a list
1284 of the Board objects with that error in the previous commit
1285 lines: List of errors/warning for this commit, each a str
1286 line_boards: Dict keyed by error line, containing a list
1287 of the Board objects with that error in this commit
1288 char: Character representing error ('') or warning ('w'). The
1289 broken ('+') or fixed ('-') characters are added in this
1290 function
1291
1292 Returns:
1293 Tuple
1294 List of ErrLine objects for 'better' lines
1295 List of ErrLine objects for 'worse' lines
1296 """
1297 better_lines = []
1298 worse_lines = []
1299 for line in lines:
1300 if line not in base_lines:
1301 errline = ErrLine(char + '+', _board_list(line, line_boards),
1302 line)
1303 worse_lines.append(errline)
1304 for line in base_lines:
1305 if line not in lines:
1306 errline = ErrLine(char + '-',
1307 _board_list(line, base_line_boards), line)
1308 better_lines.append(errline)
1309 return better_lines, worse_lines
1310
1311 def _calc_config(delta, name, config):
1312 """Calculate configuration changes
1313
1314 Args:
1315 delta: Type of the delta, e.g. '+'
1316 name: name of the file which changed (e.g. .config)
1317 config: configuration change dictionary
1318 key: config name
1319 value: config value
1320 Returns:
1321 String containing the configuration changes which can be
1322 printed
1323 """
1324 out = ''
1325 for key in sorted(config.keys()):
1326 out += '%s=%s ' % (key, config[key])
1327 return '%s %s: %s' % (delta, name, out)
1328
1329 def _add_config(lines, name, config_plus, config_minus, config_change):
1330 """Add changes in configuration to a list
1331
1332 Args:
1333 lines: list to add to
1334 name: config file name
1335 config_plus: configurations added, dictionary
1336 key: config name
1337 value: config value
1338 config_minus: configurations removed, dictionary
1339 key: config name
1340 value: config value
1341 config_change: configurations changed, dictionary
1342 key: config name
1343 value: config value
1344 """
1345 if config_plus:
1346 lines.append(_calc_config('+', name, config_plus))
1347 if config_minus:
1348 lines.append(_calc_config('-', name, config_minus))
1349 if config_change:
1350 lines.append(_calc_config('c', name, config_change))
1351
1352 def _output_config_info(lines):
1353 for line in lines:
1354 if not line:
1355 continue
1356 if line[0] == '+':
1357 col = self.col.GREEN
1358 elif line[0] == '-':
1359 col = self.col.RED
1360 elif line[0] == 'c':
1361 col = self.col.YELLOW
1362 tprint(' ' + line, newline=True, colour=col)
1363
1364 def _output_err_lines(err_lines, colour):
1365 """Output the line of error/warning lines, if not empty
1366
1367 Also increments self._error_lines if err_lines not empty
1368
1369 Args:
1370 err_lines: List of ErrLine objects, each an error or warning
1371 line, possibly including a list of boards with that
1372 error/warning
1373 colour: Colour to use for output
1374 """
1375 if err_lines:
1376 out_list = []
1377 for line in err_lines:
1378 names = [brd.target for brd in line.brds]
1379 board_str = ' '.join(names) if names else ''
1380 if board_str:
1381 out = self.col.build(colour, line.char + '(')
1382 out += self.col.build(self.col.MAGENTA, board_str,
1383 bright=False)
1384 out += self.col.build(colour, ') %s' % line.errline)
1385 else:
1386 out = self.col.build(colour, line.char + line.errline)
1387 out_list.append(out)
1388 tprint('\n'.join(out_list))
1389 self._error_lines += 1
1390
1391
1392 ok_boards = [] # List of boards fixed since last commit
1393 warn_boards = [] # List of boards with warnings since last commit
1394 err_boards = [] # List of new broken boards since last commit
1395 new_boards = [] # List of boards that didn't exist last time
1396 unknown_boards = [] # List of boards that were not built
1397
1398 for target in board_dict:
1399 if target not in board_selected:
1400 continue
1401
1402 # If the board was built last time, add its outcome to a list
1403 if target in self._base_board_dict:
1404 base_outcome = self._base_board_dict[target].rc
1405 outcome = board_dict[target]
1406 if outcome.rc == OUTCOME_UNKNOWN:
1407 unknown_boards.append(target)
1408 elif outcome.rc < base_outcome:
1409 if outcome.rc == OUTCOME_WARNING:
1410 warn_boards.append(target)
1411 else:
1412 ok_boards.append(target)
1413 elif outcome.rc > base_outcome:
1414 if outcome.rc == OUTCOME_WARNING:
1415 warn_boards.append(target)
1416 else:
1417 err_boards.append(target)
1418 else:
1419 new_boards.append(target)
1420
1421 # Get a list of errors and warnings that have appeared, and disappeared
1422 better_err, worse_err = _calc_error_delta(self._base_err_lines,
1423 self._base_err_line_boards, err_lines, err_line_boards, '')
1424 better_warn, worse_warn = _calc_error_delta(self._base_warn_lines,
1425 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1426
1427 # For the IDE mode, print out all the output
1428 if self._ide:
1429 outcome = board_dict[target]
1430 for line in outcome.err_lines:
1431 sys.stderr.write(line)
1432
1433 # Display results by arch
1434 elif any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1435 worse_err, better_err, worse_warn, better_warn)):
1436 arch_list = {}
1437 self.add_outcome(board_selected, arch_list, ok_boards, '',
1438 self.col.GREEN)
1439 self.add_outcome(board_selected, arch_list, warn_boards, 'w+',
1440 self.col.YELLOW)
1441 self.add_outcome(board_selected, arch_list, err_boards, '+',
1442 self.col.RED)
1443 self.add_outcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1444 if self._show_unknown:
1445 self.add_outcome(board_selected, arch_list, unknown_boards, '?',
1446 self.col.MAGENTA)
1447 for arch, target_list in arch_list.items():
1448 tprint('%10s: %s' % (arch, target_list))
1449 self._error_lines += 1
1450 _output_err_lines(better_err, colour=self.col.GREEN)
1451 _output_err_lines(worse_err, colour=self.col.RED)
1452 _output_err_lines(better_warn, colour=self.col.CYAN)
1453 _output_err_lines(worse_warn, colour=self.col.YELLOW)
1454
1455 if show_sizes:
1456 self.print_size_summary(board_selected, board_dict, show_detail,
1457 show_bloat)
1458
1459 if show_environment and self._base_environment:
1460 lines = []
1461
1462 for target in board_dict:
1463 if target not in board_selected:
1464 continue
1465
1466 tbase = self._base_environment[target]
1467 tenvironment = environment[target]
1468 environment_plus = {}
1469 environment_minus = {}
1470 environment_change = {}
1471 base = tbase.environment
1472 for key, value in tenvironment.environment.items():
1473 if key not in base:
1474 environment_plus[key] = value
1475 for key, value in base.items():
1476 if key not in tenvironment.environment:
1477 environment_minus[key] = value
1478 for key, value in base.items():
1479 new_value = tenvironment.environment.get(key)
1480 if new_value and value != new_value:
1481 desc = '%s -> %s' % (value, new_value)
1482 environment_change[key] = desc
1483
1484 _add_config(lines, target, environment_plus, environment_minus,
1485 environment_change)
1486
1487 _output_config_info(lines)
1488
1489 if show_config and self._base_config:
1490 summary = {}
1491 arch_config_plus = {}
1492 arch_config_minus = {}
1493 arch_config_change = {}
1494 arch_list = []
1495
1496 for target in board_dict:
1497 if target not in board_selected:
1498 continue
1499 arch = board_selected[target].arch
1500 if arch not in arch_list:
1501 arch_list.append(arch)
1502
1503 for arch in arch_list:
1504 arch_config_plus[arch] = {}
1505 arch_config_minus[arch] = {}
1506 arch_config_change[arch] = {}
1507 for name in self.config_filenames:
1508 arch_config_plus[arch][name] = {}
1509 arch_config_minus[arch][name] = {}
1510 arch_config_change[arch][name] = {}
1511
1512 for target in board_dict:
1513 if target not in board_selected:
1514 continue
1515
1516 arch = board_selected[target].arch
1517
1518 all_config_plus = {}
1519 all_config_minus = {}
1520 all_config_change = {}
1521 tbase = self._base_config[target]
1522 tconfig = config[target]
1523 lines = []
1524 for name in self.config_filenames:
1525 if not tconfig.config[name]:
1526 continue
1527 config_plus = {}
1528 config_minus = {}
1529 config_change = {}
1530 base = tbase.config[name]
1531 for key, value in tconfig.config[name].items():
1532 if key not in base:
1533 config_plus[key] = value
1534 all_config_plus[key] = value
1535 for key, value in base.items():
1536 if key not in tconfig.config[name]:
1537 config_minus[key] = value
1538 all_config_minus[key] = value
1539 for key, value in base.items():
1540 new_value = tconfig.config.get(key)
1541 if new_value and value != new_value:
1542 desc = '%s -> %s' % (value, new_value)
1543 config_change[key] = desc
1544 all_config_change[key] = desc
1545
1546 arch_config_plus[arch][name].update(config_plus)
1547 arch_config_minus[arch][name].update(config_minus)
1548 arch_config_change[arch][name].update(config_change)
1549
1550 _add_config(lines, name, config_plus, config_minus,
1551 config_change)
1552 _add_config(lines, 'all', all_config_plus, all_config_minus,
1553 all_config_change)
1554 summary[target] = '\n'.join(lines)
1555
1556 lines_by_target = {}
1557 for target, lines in summary.items():
1558 if lines in lines_by_target:
1559 lines_by_target[lines].append(target)
1560 else:
1561 lines_by_target[lines] = [target]
1562
1563 for arch in arch_list:
1564 lines = []
1565 all_plus = {}
1566 all_minus = {}
1567 all_change = {}
1568 for name in self.config_filenames:
1569 all_plus.update(arch_config_plus[arch][name])
1570 all_minus.update(arch_config_minus[arch][name])
1571 all_change.update(arch_config_change[arch][name])
1572 _add_config(lines, name, arch_config_plus[arch][name],
1573 arch_config_minus[arch][name],
1574 arch_config_change[arch][name])
1575 _add_config(lines, 'all', all_plus, all_minus, all_change)
1576 #arch_summary[target] = '\n'.join(lines)
1577 if lines:
1578 tprint('%s:' % arch)
1579 _output_config_info(lines)
1580
1581 for lines, targets in lines_by_target.items():
1582 if not lines:
1583 continue
1584 tprint('%s :' % ' '.join(sorted(targets)))
1585 _output_config_info(lines.split('\n'))
1586
1587
1588 # Save our updated information for the next call to this function
1589 self._base_board_dict = board_dict
1590 self._base_err_lines = err_lines
1591 self._base_warn_lines = warn_lines
1592 self._base_err_line_boards = err_line_boards
1593 self._base_warn_line_boards = warn_line_boards
1594 self._base_config = config
1595 self._base_environment = environment
1596
1597 # Get a list of boards that did not get built, if needed
1598 not_built = []
1599 for brd in board_selected:
1600 if not brd in board_dict:
1601 not_built.append(brd)
1602 if not_built:
1603 tprint("Boards not built (%d): %s" % (len(not_built),
1604 ', '.join(not_built)))
1605
1606 def produce_result_summary(self, commit_upto, commits, board_selected):
1607 (board_dict, err_lines, err_line_boards, warn_lines,
1608 warn_line_boards, config, environment) = self.get_result_summary(
1609 board_selected, commit_upto,
1610 read_func_sizes=self._show_bloat,
1611 read_config=self._show_config,
1612 read_environment=self._show_environment)
1613 if commits:
1614 msg = '%02d: %s' % (commit_upto + 1,
1615 commits[commit_upto].subject)
1616 tprint(msg, colour=self.col.BLUE)
1617 self.print_result_summary(board_selected, board_dict,
1618 err_lines if self._show_errors else [], err_line_boards,
1619 warn_lines if self._show_errors else [], warn_line_boards,
1620 config, environment, self._show_sizes, self._show_detail,
1621 self._show_bloat, self._show_config, self._show_environment)
1622
1623 def show_summary(self, commits, board_selected):
1624 """Show a build summary for U-Boot for a given board list.
1625
1626 Reset the result summary, then repeatedly call GetResultSummary on
1627 each commit's results, then display the differences we see.
1628
1629 Args:
1630 commit: Commit objects to summarise
1631 board_selected: Dict containing boards to summarise
1632 """
1633 self.commit_count = len(commits) if commits else 1
1634 self.commits = commits
1635 self.reset_result_summary(board_selected)
1636 self._error_lines = 0
1637
1638 for commit_upto in range(0, self.commit_count, self._step):
1639 self.produce_result_summary(commit_upto, commits, board_selected)
1640 if not self._error_lines:
1641 tprint('(no errors to report)', colour=self.col.GREEN)
1642
1643
1644 def setup_build(self, board_selected, commits):
1645 """Set up ready to start a build.
1646
1647 Args:
1648 board_selected: Selected boards to build
1649 commits: Selected commits to build
1650 """
1651 # First work out how many commits we will build
1652 count = (self.commit_count + self._step - 1) // self._step
1653 self.count = len(board_selected) * count
1654 self.upto = self.warned = self.fail = 0
1655 self._timestamps = collections.deque()
1656
1657 def get_thread_dir(self, thread_num):
1658 """Get the directory path to the working dir for a thread.
1659
1660 Args:
1661 thread_num: Number of thread to check (-1 for main process, which
1662 is treated as 0)
1663 """
1664 if self.work_in_output:
1665 return self._working_dir
1666 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
1667
1668 def _prepare_thread(self, thread_num, setup_git):
1669 """Prepare the working directory for a thread.
1670
1671 This clones or fetches the repo into the thread's work directory.
1672 Optionally, it can create a linked working tree of the repo in the
1673 thread's work directory instead.
1674
1675 Args:
1676 thread_num: Thread number (0, 1, ...)
1677 setup_git:
1678 'clone' to set up a git clone
1679 'worktree' to set up a git worktree
1680 """
1681 thread_dir = self.get_thread_dir(thread_num)
1682 builderthread.mkdir(thread_dir)
1683 git_dir = os.path.join(thread_dir, '.git')
1684
1685 # Create a worktree or a git repo clone for this thread if it
1686 # doesn't already exist
1687 if setup_git and self.git_dir:
1688 src_dir = os.path.abspath(self.git_dir)
1689 if os.path.isdir(git_dir):
1690 # This is a clone of the src_dir repo, we can keep using
1691 # it but need to fetch from src_dir.
1692 tprint('\rFetching repo for thread %d' % thread_num,
1693 newline=False)
1694 gitutil.fetch(git_dir, thread_dir)
1695 terminal.print_clear()
1696 elif os.path.isfile(git_dir):
1697 # This is a worktree of the src_dir repo, we don't need to
1698 # create it again or update it in any way.
1699 pass
1700 elif os.path.exists(git_dir):
1701 # Don't know what could trigger this, but we probably
1702 # can't create a git worktree/clone here.
1703 raise ValueError('Git dir %s exists, but is not a file '
1704 'or a directory.' % git_dir)
1705 elif setup_git == 'worktree':
1706 tprint('\rChecking out worktree for thread %d' % thread_num,
1707 newline=False)
1708 gitutil.add_worktree(src_dir, thread_dir)
1709 terminal.print_clear()
1710 elif setup_git == 'clone' or setup_git == True:
1711 tprint('\rCloning repo for thread %d' % thread_num,
1712 newline=False)
1713 gitutil.clone(src_dir, thread_dir)
1714 terminal.print_clear()
1715 else:
1716 raise ValueError("Can't setup git repo with %s." % setup_git)
1717
1718 def _prepare_working_space(self, max_threads, setup_git):
1719 """Prepare the working directory for use.
1720
1721 Set up the git repo for each thread. Creates a linked working tree
1722 if git-worktree is available, or clones the repo if it isn't.
1723
1724 Args:
1725 max_threads: Maximum number of threads we expect to need. If 0 then
1726 1 is set up, since the main process still needs somewhere to
1727 work
1728 setup_git: True to set up a git worktree or a git clone
1729 """
1730 builderthread.mkdir(self._working_dir)
1731 if setup_git and self.git_dir:
1732 src_dir = os.path.abspath(self.git_dir)
1733 if gitutil.check_worktree_is_available(src_dir):
1734 setup_git = 'worktree'
1735 # If we previously added a worktree but the directory for it
1736 # got deleted, we need to prune its files from the repo so
1737 # that we can check out another in its place.
1738 gitutil.prune_worktrees(src_dir)
1739 else:
1740 setup_git = 'clone'
1741
1742 # Always do at least one thread
1743 for thread in range(max(max_threads, 1)):
1744 self._prepare_thread(thread, setup_git)
1745
1746 def _get_output_space_removals(self):
1747 """Get the output directories ready to receive files.
1748
1749 Figure out what needs to be deleted in the output directory before it
1750 can be used. We only delete old buildman directories which have the
1751 expected name pattern. See get_output_dir().
1752
1753 Returns:
1754 List of full paths of directories to remove
1755 """
1756 if not self.commits:
1757 return
1758 dir_list = []
1759 for commit_upto in range(self.commit_count):
1760 dir_list.append(self.get_output_dir(commit_upto))
1761
1762 to_remove = []
1763 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1764 if dirname not in dir_list:
1765 leaf = dirname[len(self.base_dir) + 1:]
1766 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
1767 if m:
1768 to_remove.append(dirname)
1769 return to_remove
1770
1771 def _prepare_output_space(self):
1772 """Get the output directories ready to receive files.
1773
1774 We delete any output directories which look like ones we need to
1775 create. Having left over directories is confusing when the user wants
1776 to check the output manually.
1777 """
1778 to_remove = self._get_output_space_removals()
1779 if to_remove:
1780 tprint('Removing %d old build directories...' % len(to_remove),
1781 newline=False)
1782 for dirname in to_remove:
1783 shutil.rmtree(dirname)
1784 terminal.print_clear()
1785
1786 def build_boards(self, commits, board_selected, keep_outputs, verbose):
1787 """Build all commits for a list of boards
1788
1789 Args:
1790 commits: List of commits to be build, each a Commit object
1791 boards_selected: Dict of selected boards, key is target name,
1792 value is Board object
1793 keep_outputs: True to save build output files
1794 verbose: Display build results as they are completed
1795 Returns:
1796 Tuple containing:
1797 - number of boards that failed to build
1798 - number of boards that issued warnings
1799 - list of thread exceptions raised
1800 """
1801 self.commit_count = len(commits) if commits else 1
1802 self.commits = commits
1803 self._verbose = verbose
1804
1805 self.reset_result_summary(board_selected)
1806 builderthread.mkdir(self.base_dir, parents = True)
1807 self._prepare_working_space(min(self.num_threads, len(board_selected)),
1808 commits is not None)
1809 self._prepare_output_space()
1810 if not self._ide:
1811 tprint('\rStarting build...', newline=False)
1812 self._start_time = datetime.now()
1813 self.setup_build(board_selected, commits)
1814 self.process_result(None)
1815 self.thread_exceptions = []
1816 # Create jobs to build all commits for each board
1817 for brd in board_selected.values():
1818 job = builderthread.BuilderJob()
1819 job.brd = brd
1820 job.commits = commits
1821 job.keep_outputs = keep_outputs
1822 job.work_in_output = self.work_in_output
1823 job.adjust_cfg = self.adjust_cfg
1824 job.step = self._step
1825 if self.num_threads:
1826 self.queue.put(job)
1827 else:
1828 self._single_builder.run_job(job)
1829
1830 if self.num_threads:
1831 term = threading.Thread(target=self.queue.join)
1832 term.setDaemon(True)
1833 term.start()
1834 while term.is_alive():
1835 term.join(100)
1836
1837 # Wait until we have processed all output
1838 self.out_queue.join()
1839 if not self._ide:
1840 tprint()
1841
1842 msg = 'Completed: %d total built' % self.count
1843 if self.already_done:
1844 msg += ' (%d previously' % self.already_done
1845 if self.already_done != self.count:
1846 msg += ', %d newly' % (self.count - self.already_done)
1847 msg += ')'
1848 duration = datetime.now() - self._start_time
1849 if duration > timedelta(microseconds=1000000):
1850 if duration.microseconds >= 500000:
1851 duration = duration + timedelta(seconds=1)
1852 duration = duration - timedelta(microseconds=duration.microseconds)
1853 rate = float(self.count) / duration.total_seconds()
1854 msg += ', duration %s, rate %1.2f' % (duration, rate)
1855 tprint(msg)
1856 if self.thread_exceptions:
1857 tprint('Failed: %d thread exceptions' % len(self.thread_exceptions),
1858 colour=self.col.RED)
1859
1860 return (self.fail, self.warned, self.thread_exceptions)