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