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