xref: /rk3399_rockchip-uboot/tools/buildman/builder.py (revision ed9666573eb08b4d8f18855d9f62e22ba0316ee9)
1# Copyright (c) 2013 The Chromium OS Authors.
2#
3# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
4#
5# SPDX-License-Identifier:	GPL-2.0+
6#
7
8import collections
9from datetime import datetime, timedelta
10import glob
11import os
12import re
13import Queue
14import shutil
15import string
16import sys
17import time
18
19import builderthread
20import command
21import gitutil
22import terminal
23import toolchain
24
25
26"""
27Theory of Operation
28
29Please see README for user documentation, and you should be familiar with
30that before trying to make sense of this.
31
32Buildman works by keeping the machine as busy as possible, building different
33commits for different boards on multiple CPUs at once.
34
35The source repo (self.git_dir) contains all the commits to be built. Each
36thread works on a single board at a time. It checks out the first commit,
37configures it for that board, then builds it. Then it checks out the next
38commit and builds it (typically without re-configuring). When it runs out
39of commits, it gets another job from the builder and starts again with that
40board.
41
42Clearly the builder threads could work either way - they could check out a
43commit and then built it for all boards. Using separate directories for each
44commit/board pair they could leave their build product around afterwards
45also.
46
47The intent behind building a single board for multiple commits, is to make
48use of incremental builds. Since each commit is built incrementally from
49the previous one, builds are faster. Reconfiguring for a different board
50removes all intermediate object files.
51
52Many threads can be working at once, but each has its own working directory.
53When a thread finishes a build, it puts the output files into a result
54directory.
55
56The base directory used by buildman is normally '../<branch>', i.e.
57a directory higher than the source repository and named after the branch
58being built.
59
60Within the base directory, we have one subdirectory for each commit. Within
61that is one subdirectory for each board. Within that is the build output for
62that commit/board combination.
63
64Buildman also create working directories for each thread, in a .bm-work/
65subdirectory in the base dir.
66
67As an example, say we are building branch 'us-net' for boards 'sandbox' and
68'seaboard', and say that us-net has two commits. We will have directories
69like this:
70
71us-net/             base directory
72    01_of_02_g4ed4ebc_net--Add-tftp-speed-/
73        sandbox/
74            u-boot.bin
75        seaboard/
76            u-boot.bin
77    02_of_02_g4ed4ebc_net--Check-tftp-comp/
78        sandbox/
79            u-boot.bin
80        seaboard/
81            u-boot.bin
82    .bm-work/
83        00/         working directory for thread 0 (contains source checkout)
84            build/  build output
85        01/         working directory for thread 1
86            build/  build output
87        ...
88u-boot/             source directory
89    .git/           repository
90"""
91
92# Possible build outcomes
93OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
94
95# Translate a commit subject into a valid filename
96trans_valid_chars = string.maketrans("/: ", "---")
97
98
99class Builder:
100    """Class for building U-Boot for a particular commit.
101
102    Public members: (many should ->private)
103        active: True if the builder is active and has not been stopped
104        already_done: Number of builds already completed
105        base_dir: Base directory to use for builder
106        checkout: True to check out source, False to skip that step.
107            This is used for testing.
108        col: terminal.Color() object
109        count: Number of commits to build
110        do_make: Method to call to invoke Make
111        fail: Number of builds that failed due to error
112        force_build: Force building even if a build already exists
113        force_config_on_failure: If a commit fails for a board, disable
114            incremental building for the next commit we build for that
115            board, so that we will see all warnings/errors again.
116        force_build_failures: If a previously-built build (i.e. built on
117            a previous run of buildman) is marked as failed, rebuild it.
118        git_dir: Git directory containing source repository
119        last_line_len: Length of the last line we printed (used for erasing
120            it with new progress information)
121        num_jobs: Number of jobs to run at once (passed to make as -j)
122        num_threads: Number of builder threads to run
123        out_queue: Queue of results to process
124        re_make_err: Compiled regular expression for ignore_lines
125        queue: Queue of jobs to run
126        threads: List of active threads
127        toolchains: Toolchains object to use for building
128        upto: Current commit number we are building (0.count-1)
129        warned: Number of builds that produced at least one warning
130        force_reconfig: Reconfigure U-Boot on each comiit. This disables
131            incremental building, where buildman reconfigures on the first
132            commit for a baord, and then just does an incremental build for
133            the following commits. In fact buildman will reconfigure and
134            retry for any failing commits, so generally the only effect of
135            this option is to slow things down.
136        in_tree: Build U-Boot in-tree instead of specifying an output
137            directory separate from the source code. This option is really
138            only useful for testing in-tree builds.
139
140    Private members:
141        _base_board_dict: Last-summarised Dict of boards
142        _base_err_lines: Last-summarised list of errors
143        _build_period_us: Time taken for a single build (float object).
144        _complete_delay: Expected delay until completion (timedelta)
145        _next_delay_update: Next time we plan to display a progress update
146                (datatime)
147        _show_unknown: Show unknown boards (those not built) in summary
148        _timestamps: List of timestamps for the completion of the last
149            last _timestamp_count builds. Each is a datetime object.
150        _timestamp_count: Number of timestamps to keep in our list.
151        _working_dir: Base working directory containing all threads
152    """
153    class Outcome:
154        """Records a build outcome for a single make invocation
155
156        Public Members:
157            rc: Outcome value (OUTCOME_...)
158            err_lines: List of error lines or [] if none
159            sizes: Dictionary of image size information, keyed by filename
160                - Each value is itself a dictionary containing
161                    values for 'text', 'data' and 'bss', being the integer
162                    size in bytes of each section.
163            func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
164                    value is itself a dictionary:
165                        key: function name
166                        value: Size of function in bytes
167        """
168        def __init__(self, rc, err_lines, sizes, func_sizes):
169            self.rc = rc
170            self.err_lines = err_lines
171            self.sizes = sizes
172            self.func_sizes = func_sizes
173
174    def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
175                 gnu_make='make', checkout=True, show_unknown=True, step=1):
176        """Create a new Builder object
177
178        Args:
179            toolchains: Toolchains object to use for building
180            base_dir: Base directory to use for builder
181            git_dir: Git directory containing source repository
182            num_threads: Number of builder threads to run
183            num_jobs: Number of jobs to run at once (passed to make as -j)
184            gnu_make: the command name of GNU Make.
185            checkout: True to check out source, False to skip that step.
186                This is used for testing.
187            show_unknown: Show unknown boards (those not built) in summary
188            step: 1 to process every commit, n to process every nth commit
189        """
190        self.toolchains = toolchains
191        self.base_dir = base_dir
192        self._working_dir = os.path.join(base_dir, '.bm-work')
193        self.threads = []
194        self.active = True
195        self.do_make = self.Make
196        self.gnu_make = gnu_make
197        self.checkout = checkout
198        self.num_threads = num_threads
199        self.num_jobs = num_jobs
200        self.already_done = 0
201        self.force_build = False
202        self.git_dir = git_dir
203        self._show_unknown = show_unknown
204        self._timestamp_count = 10
205        self._build_period_us = None
206        self._complete_delay = None
207        self._next_delay_update = datetime.now()
208        self.force_config_on_failure = True
209        self.force_build_failures = False
210        self.force_reconfig = False
211        self._step = step
212        self.in_tree = False
213        self._error_lines = 0
214
215        self.col = terminal.Color()
216
217        self.queue = Queue.Queue()
218        self.out_queue = Queue.Queue()
219        for i in range(self.num_threads):
220            t = builderthread.BuilderThread(self, i)
221            t.setDaemon(True)
222            t.start()
223            self.threads.append(t)
224
225        self.last_line_len = 0
226        t = builderthread.ResultThread(self)
227        t.setDaemon(True)
228        t.start()
229        self.threads.append(t)
230
231        ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
232        self.re_make_err = re.compile('|'.join(ignore_lines))
233
234    def __del__(self):
235        """Get rid of all threads created by the builder"""
236        for t in self.threads:
237            del t
238
239    def SetDisplayOptions(self, show_errors=False, show_sizes=False,
240                          show_detail=False, show_bloat=False,
241                          list_error_boards=False):
242        """Setup display options for the builder.
243
244        show_errors: True to show summarised error/warning info
245        show_sizes: Show size deltas
246        show_detail: Show detail for each board
247        show_bloat: Show detail for each function
248        list_error_boards: Show the boards which caused each error/warning
249        """
250        self._show_errors = show_errors
251        self._show_sizes = show_sizes
252        self._show_detail = show_detail
253        self._show_bloat = show_bloat
254        self._list_error_boards = list_error_boards
255
256    def _AddTimestamp(self):
257        """Add a new timestamp to the list and record the build period.
258
259        The build period is the length of time taken to perform a single
260        build (one board, one commit).
261        """
262        now = datetime.now()
263        self._timestamps.append(now)
264        count = len(self._timestamps)
265        delta = self._timestamps[-1] - self._timestamps[0]
266        seconds = delta.total_seconds()
267
268        # If we have enough data, estimate build period (time taken for a
269        # single build) and therefore completion time.
270        if count > 1 and self._next_delay_update < now:
271            self._next_delay_update = now + timedelta(seconds=2)
272            if seconds > 0:
273                self._build_period = float(seconds) / count
274                todo = self.count - self.upto
275                self._complete_delay = timedelta(microseconds=
276                        self._build_period * todo * 1000000)
277                # Round it
278                self._complete_delay -= timedelta(
279                        microseconds=self._complete_delay.microseconds)
280
281        if seconds > 60:
282            self._timestamps.popleft()
283            count -= 1
284
285    def ClearLine(self, length):
286        """Clear any characters on the current line
287
288        Make way for a new line of length 'length', by outputting enough
289        spaces to clear out the old line. Then remember the new length for
290        next time.
291
292        Args:
293            length: Length of new line, in characters
294        """
295        if length < self.last_line_len:
296            print ' ' * (self.last_line_len - length),
297            print '\r',
298        self.last_line_len = length
299        sys.stdout.flush()
300
301    def SelectCommit(self, commit, checkout=True):
302        """Checkout the selected commit for this build
303        """
304        self.commit = commit
305        if checkout and self.checkout:
306            gitutil.Checkout(commit.hash)
307
308    def Make(self, commit, brd, stage, cwd, *args, **kwargs):
309        """Run make
310
311        Args:
312            commit: Commit object that is being built
313            brd: Board object that is being built
314            stage: Stage that we are at (mrproper, config, build)
315            cwd: Directory where make should be run
316            args: Arguments to pass to make
317            kwargs: Arguments to pass to command.RunPipe()
318        """
319        cmd = [self.gnu_make] + list(args)
320        result = command.RunPipe([cmd], capture=True, capture_stderr=True,
321                cwd=cwd, raise_on_error=False, **kwargs)
322        return result
323
324    def ProcessResult(self, result):
325        """Process the result of a build, showing progress information
326
327        Args:
328            result: A CommandResult object, which indicates the result for
329                    a single build
330        """
331        col = terminal.Color()
332        if result:
333            target = result.brd.target
334
335            if result.return_code < 0:
336                self.active = False
337                command.StopAll()
338                return
339
340            self.upto += 1
341            if result.return_code != 0:
342                self.fail += 1
343            elif result.stderr:
344                self.warned += 1
345            if result.already_done:
346                self.already_done += 1
347            if self._verbose:
348                print '\r',
349                self.ClearLine(0)
350                boards_selected = {target : result.brd}
351                self.ResetResultSummary(boards_selected)
352                self.ProduceResultSummary(result.commit_upto, self.commits,
353                                          boards_selected)
354        else:
355            target = '(starting)'
356
357        # Display separate counts for ok, warned and fail
358        ok = self.upto - self.warned - self.fail
359        line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
360        line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
361        line += self.col.Color(self.col.RED, '%5d' % self.fail)
362
363        name = ' /%-5d  ' % self.count
364
365        # Add our current completion time estimate
366        self._AddTimestamp()
367        if self._complete_delay:
368            name += '%s  : ' % self._complete_delay
369        # When building all boards for a commit, we can print a commit
370        # progress message.
371        if result and result.commit_upto is None:
372            name += 'commit %2d/%-3d' % (self.commit_upto + 1,
373                    self.commit_count)
374
375        name += target
376        print line + name,
377        length = 14 + len(name)
378        self.ClearLine(length)
379
380    def _GetOutputDir(self, commit_upto):
381        """Get the name of the output directory for a commit number
382
383        The output directory is typically .../<branch>/<commit>.
384
385        Args:
386            commit_upto: Commit number to use (0..self.count-1)
387        """
388        if self.commits:
389            commit = self.commits[commit_upto]
390            subject = commit.subject.translate(trans_valid_chars)
391            commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
392                    self.commit_count, commit.hash, subject[:20]))
393        else:
394            commit_dir = 'current'
395        output_dir = os.path.join(self.base_dir, commit_dir)
396        return output_dir
397
398    def GetBuildDir(self, commit_upto, target):
399        """Get the name of the build directory for a commit number
400
401        The build directory is typically .../<branch>/<commit>/<target>.
402
403        Args:
404            commit_upto: Commit number to use (0..self.count-1)
405            target: Target name
406        """
407        output_dir = self._GetOutputDir(commit_upto)
408        return os.path.join(output_dir, target)
409
410    def GetDoneFile(self, commit_upto, target):
411        """Get the name of the done file for a commit number
412
413        Args:
414            commit_upto: Commit number to use (0..self.count-1)
415            target: Target name
416        """
417        return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
418
419    def GetSizesFile(self, commit_upto, target):
420        """Get the name of the sizes file for a commit number
421
422        Args:
423            commit_upto: Commit number to use (0..self.count-1)
424            target: Target name
425        """
426        return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
427
428    def GetFuncSizesFile(self, commit_upto, target, elf_fname):
429        """Get the name of the funcsizes file for a commit number and ELF file
430
431        Args:
432            commit_upto: Commit number to use (0..self.count-1)
433            target: Target name
434            elf_fname: Filename of elf image
435        """
436        return os.path.join(self.GetBuildDir(commit_upto, target),
437                            '%s.sizes' % elf_fname.replace('/', '-'))
438
439    def GetObjdumpFile(self, commit_upto, target, elf_fname):
440        """Get the name of the objdump file for a commit number and ELF file
441
442        Args:
443            commit_upto: Commit number to use (0..self.count-1)
444            target: Target name
445            elf_fname: Filename of elf image
446        """
447        return os.path.join(self.GetBuildDir(commit_upto, target),
448                            '%s.objdump' % elf_fname.replace('/', '-'))
449
450    def GetErrFile(self, commit_upto, target):
451        """Get the name of the err file for a commit number
452
453        Args:
454            commit_upto: Commit number to use (0..self.count-1)
455            target: Target name
456        """
457        output_dir = self.GetBuildDir(commit_upto, target)
458        return os.path.join(output_dir, 'err')
459
460    def FilterErrors(self, lines):
461        """Filter out errors in which we have no interest
462
463        We should probably use map().
464
465        Args:
466            lines: List of error lines, each a string
467        Returns:
468            New list with only interesting lines included
469        """
470        out_lines = []
471        for line in lines:
472            if not self.re_make_err.search(line):
473                out_lines.append(line)
474        return out_lines
475
476    def ReadFuncSizes(self, fname, fd):
477        """Read function sizes from the output of 'nm'
478
479        Args:
480            fd: File containing data to read
481            fname: Filename we are reading from (just for errors)
482
483        Returns:
484            Dictionary containing size of each function in bytes, indexed by
485            function name.
486        """
487        sym = {}
488        for line in fd.readlines():
489            try:
490                size, type, name = line[:-1].split()
491            except:
492                print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
493                continue
494            if type in 'tTdDbB':
495                # function names begin with '.' on 64-bit powerpc
496                if '.' in name[1:]:
497                    name = 'static.' + name.split('.')[0]
498                sym[name] = sym.get(name, 0) + int(size, 16)
499        return sym
500
501    def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
502        """Work out the outcome of a build.
503
504        Args:
505            commit_upto: Commit number to check (0..n-1)
506            target: Target board to check
507            read_func_sizes: True to read function size information
508
509        Returns:
510            Outcome object
511        """
512        done_file = self.GetDoneFile(commit_upto, target)
513        sizes_file = self.GetSizesFile(commit_upto, target)
514        sizes = {}
515        func_sizes = {}
516        if os.path.exists(done_file):
517            with open(done_file, 'r') as fd:
518                return_code = int(fd.readline())
519                err_lines = []
520                err_file = self.GetErrFile(commit_upto, target)
521                if os.path.exists(err_file):
522                    with open(err_file, 'r') as fd:
523                        err_lines = self.FilterErrors(fd.readlines())
524
525                # Decide whether the build was ok, failed or created warnings
526                if return_code:
527                    rc = OUTCOME_ERROR
528                elif len(err_lines):
529                    rc = OUTCOME_WARNING
530                else:
531                    rc = OUTCOME_OK
532
533                # Convert size information to our simple format
534                if os.path.exists(sizes_file):
535                    with open(sizes_file, 'r') as fd:
536                        for line in fd.readlines():
537                            values = line.split()
538                            rodata = 0
539                            if len(values) > 6:
540                                rodata = int(values[6], 16)
541                            size_dict = {
542                                'all' : int(values[0]) + int(values[1]) +
543                                        int(values[2]),
544                                'text' : int(values[0]) - rodata,
545                                'data' : int(values[1]),
546                                'bss' : int(values[2]),
547                                'rodata' : rodata,
548                            }
549                            sizes[values[5]] = size_dict
550
551            if read_func_sizes:
552                pattern = self.GetFuncSizesFile(commit_upto, target, '*')
553                for fname in glob.glob(pattern):
554                    with open(fname, 'r') as fd:
555                        dict_name = os.path.basename(fname).replace('.sizes',
556                                                                    '')
557                        func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
558
559            return Builder.Outcome(rc, err_lines, sizes, func_sizes)
560
561        return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
562
563    def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
564        """Calculate a summary of the results of building a commit.
565
566        Args:
567            board_selected: Dict containing boards to summarise
568            commit_upto: Commit number to summarize (0..self.count-1)
569            read_func_sizes: True to read function size information
570
571        Returns:
572            Tuple:
573                Dict containing boards which passed building this commit.
574                    keyed by board.target
575                List containing a summary of error/warning lines
576                Dict keyed by error line, containing a list of the Board
577                    objects with that error
578        """
579        board_dict = {}
580        err_lines_summary = []
581        err_lines_boards = {}
582
583        for board in boards_selected.itervalues():
584            outcome = self.GetBuildOutcome(commit_upto, board.target,
585                                           read_func_sizes)
586            board_dict[board.target] = outcome
587            for err in outcome.err_lines:
588                if err:
589                    err = err.rstrip()
590                    if err in err_lines_boards:
591                        err_lines_boards[err].append(board)
592                    else:
593                        err_lines_boards[err] = [board]
594                        err_lines_summary.append(err.rstrip())
595        return board_dict, err_lines_summary, err_lines_boards
596
597    def AddOutcome(self, board_dict, arch_list, changes, char, color):
598        """Add an output to our list of outcomes for each architecture
599
600        This simple function adds failing boards (changes) to the
601        relevant architecture string, so we can print the results out
602        sorted by architecture.
603
604        Args:
605             board_dict: Dict containing all boards
606             arch_list: Dict keyed by arch name. Value is a string containing
607                    a list of board names which failed for that arch.
608             changes: List of boards to add to arch_list
609             color: terminal.Colour object
610        """
611        done_arch = {}
612        for target in changes:
613            if target in board_dict:
614                arch = board_dict[target].arch
615            else:
616                arch = 'unknown'
617            str = self.col.Color(color, ' ' + target)
618            if not arch in done_arch:
619                str = self.col.Color(color, char) + '  ' + str
620                done_arch[arch] = True
621            if not arch in arch_list:
622                arch_list[arch] = str
623            else:
624                arch_list[arch] += str
625
626
627    def ColourNum(self, num):
628        color = self.col.RED if num > 0 else self.col.GREEN
629        if num == 0:
630            return '0'
631        return self.col.Color(color, str(num))
632
633    def ResetResultSummary(self, board_selected):
634        """Reset the results summary ready for use.
635
636        Set up the base board list to be all those selected, and set the
637        error lines to empty.
638
639        Following this, calls to PrintResultSummary() will use this
640        information to work out what has changed.
641
642        Args:
643            board_selected: Dict containing boards to summarise, keyed by
644                board.target
645        """
646        self._base_board_dict = {}
647        for board in board_selected:
648            self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
649        self._base_err_lines = []
650
651    def PrintFuncSizeDetail(self, fname, old, new):
652        grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
653        delta, common = [], {}
654
655        for a in old:
656            if a in new:
657                common[a] = 1
658
659        for name in old:
660            if name not in common:
661                remove += 1
662                down += old[name]
663                delta.append([-old[name], name])
664
665        for name in new:
666            if name not in common:
667                add += 1
668                up += new[name]
669                delta.append([new[name], name])
670
671        for name in common:
672                diff = new.get(name, 0) - old.get(name, 0)
673                if diff > 0:
674                    grow, up = grow + 1, up + diff
675                elif diff < 0:
676                    shrink, down = shrink + 1, down - diff
677                delta.append([diff, name])
678
679        delta.sort()
680        delta.reverse()
681
682        args = [add, -remove, grow, -shrink, up, -down, up - down]
683        if max(args) == 0:
684            return
685        args = [self.ColourNum(x) for x in args]
686        indent = ' ' * 15
687        print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
688               tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
689        print '%s  %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
690                                        'delta')
691        for diff, name in delta:
692            if diff:
693                color = self.col.RED if diff > 0 else self.col.GREEN
694                msg = '%s  %-38s %7s %7s %+7d' % (indent, name,
695                        old.get(name, '-'), new.get(name,'-'), diff)
696                print self.col.Color(color, msg)
697
698
699    def PrintSizeDetail(self, target_list, show_bloat):
700        """Show details size information for each board
701
702        Args:
703            target_list: List of targets, each a dict containing:
704                    'target': Target name
705                    'total_diff': Total difference in bytes across all areas
706                    <part_name>: Difference for that part
707            show_bloat: Show detail for each function
708        """
709        targets_by_diff = sorted(target_list, reverse=True,
710        key=lambda x: x['_total_diff'])
711        for result in targets_by_diff:
712            printed_target = False
713            for name in sorted(result):
714                diff = result[name]
715                if name.startswith('_'):
716                    continue
717                if diff != 0:
718                    color = self.col.RED if diff > 0 else self.col.GREEN
719                msg = ' %s %+d' % (name, diff)
720                if not printed_target:
721                    print '%10s  %-15s:' % ('', result['_target']),
722                    printed_target = True
723                print self.col.Color(color, msg),
724            if printed_target:
725                print
726                if show_bloat:
727                    target = result['_target']
728                    outcome = result['_outcome']
729                    base_outcome = self._base_board_dict[target]
730                    for fname in outcome.func_sizes:
731                        self.PrintFuncSizeDetail(fname,
732                                                 base_outcome.func_sizes[fname],
733                                                 outcome.func_sizes[fname])
734
735
736    def PrintSizeSummary(self, board_selected, board_dict, show_detail,
737                         show_bloat):
738        """Print a summary of image sizes broken down by section.
739
740        The summary takes the form of one line per architecture. The
741        line contains deltas for each of the sections (+ means the section
742        got bigger, - means smaller). The nunmbers are the average number
743        of bytes that a board in this section increased by.
744
745        For example:
746           powerpc: (622 boards)   text -0.0
747          arm: (285 boards)   text -0.0
748          nds32: (3 boards)   text -8.0
749
750        Args:
751            board_selected: Dict containing boards to summarise, keyed by
752                board.target
753            board_dict: Dict containing boards for which we built this
754                commit, keyed by board.target. The value is an Outcome object.
755            show_detail: Show detail for each board
756            show_bloat: Show detail for each function
757        """
758        arch_list = {}
759        arch_count = {}
760
761        # Calculate changes in size for different image parts
762        # The previous sizes are in Board.sizes, for each board
763        for target in board_dict:
764            if target not in board_selected:
765                continue
766            base_sizes = self._base_board_dict[target].sizes
767            outcome = board_dict[target]
768            sizes = outcome.sizes
769
770            # Loop through the list of images, creating a dict of size
771            # changes for each image/part. We end up with something like
772            # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
773            # which means that U-Boot data increased by 5 bytes and SPL
774            # text decreased by 4.
775            err = {'_target' : target}
776            for image in sizes:
777                if image in base_sizes:
778                    base_image = base_sizes[image]
779                    # Loop through the text, data, bss parts
780                    for part in sorted(sizes[image]):
781                        diff = sizes[image][part] - base_image[part]
782                        col = None
783                        if diff:
784                            if image == 'u-boot':
785                                name = part
786                            else:
787                                name = image + ':' + part
788                            err[name] = diff
789            arch = board_selected[target].arch
790            if not arch in arch_count:
791                arch_count[arch] = 1
792            else:
793                arch_count[arch] += 1
794            if not sizes:
795                pass    # Only add to our list when we have some stats
796            elif not arch in arch_list:
797                arch_list[arch] = [err]
798            else:
799                arch_list[arch].append(err)
800
801        # We now have a list of image size changes sorted by arch
802        # Print out a summary of these
803        for arch, target_list in arch_list.iteritems():
804            # Get total difference for each type
805            totals = {}
806            for result in target_list:
807                total = 0
808                for name, diff in result.iteritems():
809                    if name.startswith('_'):
810                        continue
811                    total += diff
812                    if name in totals:
813                        totals[name] += diff
814                    else:
815                        totals[name] = diff
816                result['_total_diff'] = total
817                result['_outcome'] = board_dict[result['_target']]
818
819            count = len(target_list)
820            printed_arch = False
821            for name in sorted(totals):
822                diff = totals[name]
823                if diff:
824                    # Display the average difference in this name for this
825                    # architecture
826                    avg_diff = float(diff) / count
827                    color = self.col.RED if avg_diff > 0 else self.col.GREEN
828                    msg = ' %s %+1.1f' % (name, avg_diff)
829                    if not printed_arch:
830                        print '%10s: (for %d/%d boards)' % (arch, count,
831                                arch_count[arch]),
832                        printed_arch = True
833                    print self.col.Color(color, msg),
834
835            if printed_arch:
836                print
837                if show_detail:
838                    self.PrintSizeDetail(target_list, show_bloat)
839
840
841    def PrintResultSummary(self, board_selected, board_dict, err_lines,
842                           err_line_boards, show_sizes, show_detail,
843                           show_bloat):
844        """Compare results with the base results and display delta.
845
846        Only boards mentioned in board_selected will be considered. This
847        function is intended to be called repeatedly with the results of
848        each commit. It therefore shows a 'diff' between what it saw in
849        the last call and what it sees now.
850
851        Args:
852            board_selected: Dict containing boards to summarise, keyed by
853                board.target
854            board_dict: Dict containing boards for which we built this
855                commit, keyed by board.target. The value is an Outcome object.
856            err_lines: A list of errors for this commit, or [] if there is
857                none, or we don't want to print errors
858            err_line_boards: Dict keyed by error line, containing a list of
859                the Board objects with that error
860            show_sizes: Show image size deltas
861            show_detail: Show detail for each board
862            show_bloat: Show detail for each function
863        """
864        def _BoardList(line):
865            """Helper function to get a line of boards containing a line
866
867            Args:
868                line: Error line to search for
869            Return:
870                String containing a list of boards with that error line, or
871                '' if the user has not requested such a list
872            """
873            if self._list_error_boards:
874                names = []
875                for board in err_line_boards[line]:
876                    names.append(board.target)
877                names_str = '(%s) ' % ','.join(names)
878            else:
879                names_str = ''
880            return names_str
881
882        better = []     # List of boards fixed since last commit
883        worse = []      # List of new broken boards since last commit
884        new = []        # List of boards that didn't exist last time
885        unknown = []    # List of boards that were not built
886
887        for target in board_dict:
888            if target not in board_selected:
889                continue
890
891            # If the board was built last time, add its outcome to a list
892            if target in self._base_board_dict:
893                base_outcome = self._base_board_dict[target].rc
894                outcome = board_dict[target]
895                if outcome.rc == OUTCOME_UNKNOWN:
896                    unknown.append(target)
897                elif outcome.rc < base_outcome:
898                    better.append(target)
899                elif outcome.rc > base_outcome:
900                    worse.append(target)
901            else:
902                new.append(target)
903
904        # Get a list of errors that have appeared, and disappeared
905        better_err = []
906        worse_err = []
907        for line in err_lines:
908            if line not in self._base_err_lines:
909                worse_err.append('+' + _BoardList(line) + line)
910        for line in self._base_err_lines:
911            if line not in err_lines:
912                better_err.append('-' + line)
913
914        # Display results by arch
915        if better or worse or unknown or new or worse_err or better_err:
916            arch_list = {}
917            self.AddOutcome(board_selected, arch_list, better, '',
918                    self.col.GREEN)
919            self.AddOutcome(board_selected, arch_list, worse, '+',
920                    self.col.RED)
921            self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
922            if self._show_unknown:
923                self.AddOutcome(board_selected, arch_list, unknown, '?',
924                        self.col.MAGENTA)
925            for arch, target_list in arch_list.iteritems():
926                print '%10s: %s' % (arch, target_list)
927                self._error_lines += 1
928            if better_err:
929                print self.col.Color(self.col.GREEN, '\n'.join(better_err))
930                self._error_lines += 1
931            if worse_err:
932                print self.col.Color(self.col.RED, '\n'.join(worse_err))
933                self._error_lines += 1
934
935        if show_sizes:
936            self.PrintSizeSummary(board_selected, board_dict, show_detail,
937                                  show_bloat)
938
939        # Save our updated information for the next call to this function
940        self._base_board_dict = board_dict
941        self._base_err_lines = err_lines
942
943        # Get a list of boards that did not get built, if needed
944        not_built = []
945        for board in board_selected:
946            if not board in board_dict:
947                not_built.append(board)
948        if not_built:
949            print "Boards not built (%d): %s" % (len(not_built),
950                    ', '.join(not_built))
951
952    def ProduceResultSummary(self, commit_upto, commits, board_selected):
953            board_dict, err_lines, err_line_boards = self.GetResultSummary(
954                    board_selected, commit_upto,
955                    read_func_sizes=self._show_bloat)
956            if commits:
957                msg = '%02d: %s' % (commit_upto + 1,
958                        commits[commit_upto].subject)
959                print self.col.Color(self.col.BLUE, msg)
960            self.PrintResultSummary(board_selected, board_dict,
961                    err_lines if self._show_errors else [], err_line_boards,
962                    self._show_sizes, self._show_detail, self._show_bloat)
963
964    def ShowSummary(self, commits, board_selected):
965        """Show a build summary for U-Boot for a given board list.
966
967        Reset the result summary, then repeatedly call GetResultSummary on
968        each commit's results, then display the differences we see.
969
970        Args:
971            commit: Commit objects to summarise
972            board_selected: Dict containing boards to summarise
973        """
974        self.commit_count = len(commits) if commits else 1
975        self.commits = commits
976        self.ResetResultSummary(board_selected)
977        self._error_lines = 0
978
979        for commit_upto in range(0, self.commit_count, self._step):
980            self.ProduceResultSummary(commit_upto, commits, board_selected)
981        if not self._error_lines:
982            print self.col.Color(self.col.GREEN, '(no errors to report)')
983
984
985    def SetupBuild(self, board_selected, commits):
986        """Set up ready to start a build.
987
988        Args:
989            board_selected: Selected boards to build
990            commits: Selected commits to build
991        """
992        # First work out how many commits we will build
993        count = (self.commit_count + self._step - 1) / self._step
994        self.count = len(board_selected) * count
995        self.upto = self.warned = self.fail = 0
996        self._timestamps = collections.deque()
997
998    def GetThreadDir(self, thread_num):
999        """Get the directory path to the working dir for a thread.
1000
1001        Args:
1002            thread_num: Number of thread to check.
1003        """
1004        return os.path.join(self._working_dir, '%02d' % thread_num)
1005
1006    def _PrepareThread(self, thread_num, setup_git):
1007        """Prepare the working directory for a thread.
1008
1009        This clones or fetches the repo into the thread's work directory.
1010
1011        Args:
1012            thread_num: Thread number (0, 1, ...)
1013            setup_git: True to set up a git repo clone
1014        """
1015        thread_dir = self.GetThreadDir(thread_num)
1016        builderthread.Mkdir(thread_dir)
1017        git_dir = os.path.join(thread_dir, '.git')
1018
1019        # Clone the repo if it doesn't already exist
1020        # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1021        # we have a private index but uses the origin repo's contents?
1022        if setup_git and self.git_dir:
1023            src_dir = os.path.abspath(self.git_dir)
1024            if os.path.exists(git_dir):
1025                gitutil.Fetch(git_dir, thread_dir)
1026            else:
1027                print 'Cloning repo for thread %d' % thread_num
1028                gitutil.Clone(src_dir, thread_dir)
1029
1030    def _PrepareWorkingSpace(self, max_threads, setup_git):
1031        """Prepare the working directory for use.
1032
1033        Set up the git repo for each thread.
1034
1035        Args:
1036            max_threads: Maximum number of threads we expect to need.
1037            setup_git: True to set up a git repo clone
1038        """
1039        builderthread.Mkdir(self._working_dir)
1040        for thread in range(max_threads):
1041            self._PrepareThread(thread, setup_git)
1042
1043    def _PrepareOutputSpace(self):
1044        """Get the output directories ready to receive files.
1045
1046        We delete any output directories which look like ones we need to
1047        create. Having left over directories is confusing when the user wants
1048        to check the output manually.
1049        """
1050        dir_list = []
1051        for commit_upto in range(self.commit_count):
1052            dir_list.append(self._GetOutputDir(commit_upto))
1053
1054        for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1055            if dirname not in dir_list:
1056                shutil.rmtree(dirname)
1057
1058    def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1059        """Build all commits for a list of boards
1060
1061        Args:
1062            commits: List of commits to be build, each a Commit object
1063            boards_selected: Dict of selected boards, key is target name,
1064                    value is Board object
1065            keep_outputs: True to save build output files
1066            verbose: Display build results as they are completed
1067        Returns:
1068            Tuple containing:
1069                - number of boards that failed to build
1070                - number of boards that issued warnings
1071        """
1072        self.commit_count = len(commits) if commits else 1
1073        self.commits = commits
1074        self._verbose = verbose
1075
1076        self.ResetResultSummary(board_selected)
1077        builderthread.Mkdir(self.base_dir)
1078        self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1079                commits is not None)
1080        self._PrepareOutputSpace()
1081        self.SetupBuild(board_selected, commits)
1082        self.ProcessResult(None)
1083
1084        # Create jobs to build all commits for each board
1085        for brd in board_selected.itervalues():
1086            job = builderthread.BuilderJob()
1087            job.board = brd
1088            job.commits = commits
1089            job.keep_outputs = keep_outputs
1090            job.step = self._step
1091            self.queue.put(job)
1092
1093        # Wait until all jobs are started
1094        self.queue.join()
1095
1096        # Wait until we have processed all output
1097        self.out_queue.join()
1098        print
1099        self.ClearLine(0)
1100        return (self.fail, self.warned)
1101