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