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