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