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