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