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