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