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