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