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