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