1 # Copyright (c) 2013 The Chromium OS Authors.
3 # Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
5 # SPDX-License-Identifier: GPL-2.0+
9 from datetime import datetime, timedelta
29 Please see README for user documentation, and you should be familiar with
30 that before trying to make sense of this.
32 Buildman works by keeping the machine as busy as possible, building different
33 commits for different boards on multiple CPUs at once.
35 The source repo (self.git_dir) contains all the commits to be built. Each
36 thread works on a single board at a time. It checks out the first commit,
37 configures it for that board, then builds it. Then it checks out the next
38 commit and builds it (typically without re-configuring). When it runs out
39 of commits, it gets another job from the builder and starts again with that
42 Clearly the builder threads could work either way - they could check out a
43 commit and then built it for all boards. Using separate directories for each
44 commit/board pair they could leave their build product around afterwards
47 The intent behind building a single board for multiple commits, is to make
48 use of incremental builds. Since each commit is built incrementally from
49 the previous one, builds are faster. Reconfiguring for a different board
50 removes all intermediate object files.
52 Many threads can be working at once, but each has its own working directory.
53 When a thread finishes a build, it puts the output files into a result
56 The base directory used by buildman is normally '../<branch>', i.e.
57 a directory higher than the source repository and named after the branch
60 Within the base directory, we have one subdirectory for each commit. Within
61 that is one subdirectory for each board. Within that is the build output for
62 that commit/board combination.
64 Buildman also create working directories for each thread, in a .bm-work/
65 subdirectory in the base dir.
67 As an example, say we are building branch 'us-net' for boards 'sandbox' and
68 'seaboard', and say that us-net has two commits. We will have directories
71 us-net/ base directory
72 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
77 02_of_02_g4ed4ebc_net--Check-tftp-comp/
83 00/ working directory for thread 0 (contains source checkout)
85 01/ working directory for thread 1
88 u-boot/ source directory
92 # Possible build outcomes
93 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
95 # Translate a commit subject into a valid filename
96 trans_valid_chars = string.maketrans("/: ", "---")
100 """Class for building U-Boot for a particular commit.
102 Public members: (many should ->private)
103 active: True if the builder is active and has not been stopped
104 already_done: Number of builds already completed
105 base_dir: Base directory to use for builder
106 checkout: True to check out source, False to skip that step.
107 This is used for testing.
108 col: terminal.Color() object
109 count: Number of commits to build
110 do_make: Method to call to invoke Make
111 fail: Number of builds that failed due to error
112 force_build: Force building even if a build already exists
113 force_config_on_failure: If a commit fails for a board, disable
114 incremental building for the next commit we build for that
115 board, so that we will see all warnings/errors again.
116 force_build_failures: If a previously-built build (i.e. built on
117 a previous run of buildman) is marked as failed, rebuild it.
118 git_dir: Git directory containing source repository
119 last_line_len: Length of the last line we printed (used for erasing
120 it with new progress information)
121 num_jobs: Number of jobs to run at once (passed to make as -j)
122 num_threads: Number of builder threads to run
123 out_queue: Queue of results to process
124 re_make_err: Compiled regular expression for ignore_lines
125 queue: Queue of jobs to run
126 threads: List of active threads
127 toolchains: Toolchains object to use for building
128 upto: Current commit number we are building (0.count-1)
129 warned: Number of builds that produced at least one warning
130 force_reconfig: Reconfigure U-Boot on each comiit. This disables
131 incremental building, where buildman reconfigures on the first
132 commit for a baord, and then just does an incremental build for
133 the following commits. In fact buildman will reconfigure and
134 retry for any failing commits, so generally the only effect of
135 this option is to slow things down.
136 in_tree: Build U-Boot in-tree instead of specifying an output
137 directory separate from the source code. This option is really
138 only useful for testing in-tree builds.
141 _base_board_dict: Last-summarised Dict of boards
142 _base_err_lines: Last-summarised list of errors
143 _build_period_us: Time taken for a single build (float object).
144 _complete_delay: Expected delay until completion (timedelta)
145 _next_delay_update: Next time we plan to display a progress update
147 _show_unknown: Show unknown boards (those not built) in summary
148 _timestamps: List of timestamps for the completion of the last
149 last _timestamp_count builds. Each is a datetime object.
150 _timestamp_count: Number of timestamps to keep in our list.
151 _working_dir: Base working directory containing all threads
154 """Records a build outcome for a single make invocation
157 rc: Outcome value (OUTCOME_...)
158 err_lines: List of error lines or [] if none
159 sizes: Dictionary of image size information, keyed by filename
160 - Each value is itself a dictionary containing
161 values for 'text', 'data' and 'bss', being the integer
162 size in bytes of each section.
163 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
164 value is itself a dictionary:
166 value: Size of function in bytes
168 def __init__(self, rc, err_lines, sizes, func_sizes):
170 self.err_lines = err_lines
172 self.func_sizes = func_sizes
174 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
175 gnu_make='make', checkout=True, show_unknown=True, step=1):
176 """Create a new Builder object
179 toolchains: Toolchains object to use for building
180 base_dir: Base directory to use for builder
181 git_dir: Git directory containing source repository
182 num_threads: Number of builder threads to run
183 num_jobs: Number of jobs to run at once (passed to make as -j)
184 gnu_make: the command name of GNU Make.
185 checkout: True to check out source, False to skip that step.
186 This is used for testing.
187 show_unknown: Show unknown boards (those not built) in summary
188 step: 1 to process every commit, n to process every nth commit
190 self.toolchains = toolchains
191 self.base_dir = base_dir
192 self._working_dir = os.path.join(base_dir, '.bm-work')
195 self.do_make = self.Make
196 self.gnu_make = gnu_make
197 self.checkout = checkout
198 self.num_threads = num_threads
199 self.num_jobs = num_jobs
200 self.already_done = 0
201 self.force_build = False
202 self.git_dir = git_dir
203 self._show_unknown = show_unknown
204 self._timestamp_count = 10
205 self._build_period_us = None
206 self._complete_delay = None
207 self._next_delay_update = datetime.now()
208 self.force_config_on_failure = True
209 self.force_build_failures = False
210 self.force_reconfig = False
213 self._error_lines = 0
215 self.col = terminal.Color()
217 self.queue = Queue.Queue()
218 self.out_queue = Queue.Queue()
219 for i in range(self.num_threads):
220 t = builderthread.BuilderThread(self, i)
223 self.threads.append(t)
225 self.last_line_len = 0
226 t = builderthread.ResultThread(self)
229 self.threads.append(t)
231 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
232 self.re_make_err = re.compile('|'.join(ignore_lines))
235 """Get rid of all threads created by the builder"""
236 for t in self.threads:
239 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
240 show_detail=False, show_bloat=False):
241 """Setup display options for the builder.
243 show_errors: True to show summarised error/warning info
244 show_sizes: Show size deltas
245 show_detail: Show detail for each board
246 show_bloat: Show detail for each function
248 self._show_errors = show_errors
249 self._show_sizes = show_sizes
250 self._show_detail = show_detail
251 self._show_bloat = show_bloat
253 def _AddTimestamp(self):
254 """Add a new timestamp to the list and record the build period.
256 The build period is the length of time taken to perform a single
257 build (one board, one commit).
260 self._timestamps.append(now)
261 count = len(self._timestamps)
262 delta = self._timestamps[-1] - self._timestamps[0]
263 seconds = delta.total_seconds()
265 # If we have enough data, estimate build period (time taken for a
266 # single build) and therefore completion time.
267 if count > 1 and self._next_delay_update < now:
268 self._next_delay_update = now + timedelta(seconds=2)
270 self._build_period = float(seconds) / count
271 todo = self.count - self.upto
272 self._complete_delay = timedelta(microseconds=
273 self._build_period * todo * 1000000)
275 self._complete_delay -= timedelta(
276 microseconds=self._complete_delay.microseconds)
279 self._timestamps.popleft()
282 def ClearLine(self, length):
283 """Clear any characters on the current line
285 Make way for a new line of length 'length', by outputting enough
286 spaces to clear out the old line. Then remember the new length for
290 length: Length of new line, in characters
292 if length < self.last_line_len:
293 print ' ' * (self.last_line_len - length),
295 self.last_line_len = length
298 def SelectCommit(self, commit, checkout=True):
299 """Checkout the selected commit for this build
302 if checkout and self.checkout:
303 gitutil.Checkout(commit.hash)
305 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
309 commit: Commit object that is being built
310 brd: Board object that is being built
311 stage: Stage that we are at (mrproper, config, build)
312 cwd: Directory where make should be run
313 args: Arguments to pass to make
314 kwargs: Arguments to pass to command.RunPipe()
316 cmd = [self.gnu_make] + list(args)
317 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
318 cwd=cwd, raise_on_error=False, **kwargs)
321 def ProcessResult(self, result):
322 """Process the result of a build, showing progress information
325 result: A CommandResult object, which indicates the result for
328 col = terminal.Color()
330 target = result.brd.target
332 if result.return_code < 0:
338 if result.return_code != 0:
342 if result.already_done:
343 self.already_done += 1
347 boards_selected = {target : result.brd}
348 self.ResetResultSummary(boards_selected)
349 self.ProduceResultSummary(result.commit_upto, self.commits,
352 target = '(starting)'
354 # Display separate counts for ok, warned and fail
355 ok = self.upto - self.warned - self.fail
356 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
357 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
358 line += self.col.Color(self.col.RED, '%5d' % self.fail)
360 name = ' /%-5d ' % self.count
362 # Add our current completion time estimate
364 if self._complete_delay:
365 name += '%s : ' % self._complete_delay
366 # When building all boards for a commit, we can print a commit
368 if result and result.commit_upto is None:
369 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
374 length = 14 + len(name)
375 self.ClearLine(length)
377 def _GetOutputDir(self, commit_upto):
378 """Get the name of the output directory for a commit number
380 The output directory is typically .../<branch>/<commit>.
383 commit_upto: Commit number to use (0..self.count-1)
386 commit = self.commits[commit_upto]
387 subject = commit.subject.translate(trans_valid_chars)
388 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
389 self.commit_count, commit.hash, subject[:20]))
391 commit_dir = 'current'
392 output_dir = os.path.join(self.base_dir, commit_dir)
395 def GetBuildDir(self, commit_upto, target):
396 """Get the name of the build directory for a commit number
398 The build directory is typically .../<branch>/<commit>/<target>.
401 commit_upto: Commit number to use (0..self.count-1)
404 output_dir = self._GetOutputDir(commit_upto)
405 return os.path.join(output_dir, target)
407 def GetDoneFile(self, commit_upto, target):
408 """Get the name of the done file for a commit number
411 commit_upto: Commit number to use (0..self.count-1)
414 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
416 def GetSizesFile(self, commit_upto, target):
417 """Get the name of the sizes file for a commit number
420 commit_upto: Commit number to use (0..self.count-1)
423 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
425 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
426 """Get the name of the funcsizes file for a commit number and ELF file
429 commit_upto: Commit number to use (0..self.count-1)
431 elf_fname: Filename of elf image
433 return os.path.join(self.GetBuildDir(commit_upto, target),
434 '%s.sizes' % elf_fname.replace('/', '-'))
436 def GetObjdumpFile(self, commit_upto, target, elf_fname):
437 """Get the name of the objdump file for a commit number and ELF file
440 commit_upto: Commit number to use (0..self.count-1)
442 elf_fname: Filename of elf image
444 return os.path.join(self.GetBuildDir(commit_upto, target),
445 '%s.objdump' % elf_fname.replace('/', '-'))
447 def GetErrFile(self, commit_upto, target):
448 """Get the name of the err file for a commit number
451 commit_upto: Commit number to use (0..self.count-1)
454 output_dir = self.GetBuildDir(commit_upto, target)
455 return os.path.join(output_dir, 'err')
457 def FilterErrors(self, lines):
458 """Filter out errors in which we have no interest
460 We should probably use map().
463 lines: List of error lines, each a string
465 New list with only interesting lines included
469 if not self.re_make_err.search(line):
470 out_lines.append(line)
473 def ReadFuncSizes(self, fname, fd):
474 """Read function sizes from the output of 'nm'
477 fd: File containing data to read
478 fname: Filename we are reading from (just for errors)
481 Dictionary containing size of each function in bytes, indexed by
485 for line in fd.readlines():
487 size, type, name = line[:-1].split()
489 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
492 # function names begin with '.' on 64-bit powerpc
494 name = 'static.' + name.split('.')[0]
495 sym[name] = sym.get(name, 0) + int(size, 16)
498 def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
499 """Work out the outcome of a build.
502 commit_upto: Commit number to check (0..n-1)
503 target: Target board to check
504 read_func_sizes: True to read function size information
509 done_file = self.GetDoneFile(commit_upto, target)
510 sizes_file = self.GetSizesFile(commit_upto, target)
513 if os.path.exists(done_file):
514 with open(done_file, 'r') as fd:
515 return_code = int(fd.readline())
517 err_file = self.GetErrFile(commit_upto, target)
518 if os.path.exists(err_file):
519 with open(err_file, 'r') as fd:
520 err_lines = self.FilterErrors(fd.readlines())
522 # Decide whether the build was ok, failed or created warnings
530 # Convert size information to our simple format
531 if os.path.exists(sizes_file):
532 with open(sizes_file, 'r') as fd:
533 for line in fd.readlines():
534 values = line.split()
537 rodata = int(values[6], 16)
539 'all' : int(values[0]) + int(values[1]) +
541 'text' : int(values[0]) - rodata,
542 'data' : int(values[1]),
543 'bss' : int(values[2]),
546 sizes[values[5]] = size_dict
549 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
550 for fname in glob.glob(pattern):
551 with open(fname, 'r') as fd:
552 dict_name = os.path.basename(fname).replace('.sizes',
554 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
556 return Builder.Outcome(rc, err_lines, sizes, func_sizes)
558 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
560 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
561 """Calculate a summary of the results of building a commit.
564 board_selected: Dict containing boards to summarise
565 commit_upto: Commit number to summarize (0..self.count-1)
566 read_func_sizes: True to read function size information
570 Dict containing boards which passed building this commit.
571 keyed by board.target
572 List containing a summary of error/warning lines
575 err_lines_summary = []
577 for board in boards_selected.itervalues():
578 outcome = self.GetBuildOutcome(commit_upto, board.target,
580 board_dict[board.target] = outcome
581 for err in outcome.err_lines:
582 if err and not err.rstrip() in err_lines_summary:
583 err_lines_summary.append(err.rstrip())
584 return board_dict, err_lines_summary
586 def AddOutcome(self, board_dict, arch_list, changes, char, color):
587 """Add an output to our list of outcomes for each architecture
589 This simple function adds failing boards (changes) to the
590 relevant architecture string, so we can print the results out
591 sorted by architecture.
594 board_dict: Dict containing all boards
595 arch_list: Dict keyed by arch name. Value is a string containing
596 a list of board names which failed for that arch.
597 changes: List of boards to add to arch_list
598 color: terminal.Colour object
601 for target in changes:
602 if target in board_dict:
603 arch = board_dict[target].arch
606 str = self.col.Color(color, ' ' + target)
607 if not arch in done_arch:
608 str = self.col.Color(color, char) + ' ' + str
609 done_arch[arch] = True
610 if not arch in arch_list:
611 arch_list[arch] = str
613 arch_list[arch] += str
616 def ColourNum(self, num):
617 color = self.col.RED if num > 0 else self.col.GREEN
620 return self.col.Color(color, str(num))
622 def ResetResultSummary(self, board_selected):
623 """Reset the results summary ready for use.
625 Set up the base board list to be all those selected, and set the
626 error lines to empty.
628 Following this, calls to PrintResultSummary() will use this
629 information to work out what has changed.
632 board_selected: Dict containing boards to summarise, keyed by
635 self._base_board_dict = {}
636 for board in board_selected:
637 self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
638 self._base_err_lines = []
640 def PrintFuncSizeDetail(self, fname, old, new):
641 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
642 delta, common = [], {}
649 if name not in common:
652 delta.append([-old[name], name])
655 if name not in common:
658 delta.append([new[name], name])
661 diff = new.get(name, 0) - old.get(name, 0)
663 grow, up = grow + 1, up + diff
665 shrink, down = shrink + 1, down - diff
666 delta.append([diff, name])
671 args = [add, -remove, grow, -shrink, up, -down, up - down]
674 args = [self.ColourNum(x) for x in args]
676 print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
677 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
678 print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
680 for diff, name in delta:
682 color = self.col.RED if diff > 0 else self.col.GREEN
683 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
684 old.get(name, '-'), new.get(name,'-'), diff)
685 print self.col.Color(color, msg)
688 def PrintSizeDetail(self, target_list, show_bloat):
689 """Show details size information for each board
692 target_list: List of targets, each a dict containing:
693 'target': Target name
694 'total_diff': Total difference in bytes across all areas
695 <part_name>: Difference for that part
696 show_bloat: Show detail for each function
698 targets_by_diff = sorted(target_list, reverse=True,
699 key=lambda x: x['_total_diff'])
700 for result in targets_by_diff:
701 printed_target = False
702 for name in sorted(result):
704 if name.startswith('_'):
707 color = self.col.RED if diff > 0 else self.col.GREEN
708 msg = ' %s %+d' % (name, diff)
709 if not printed_target:
710 print '%10s %-15s:' % ('', result['_target']),
711 printed_target = True
712 print self.col.Color(color, msg),
716 target = result['_target']
717 outcome = result['_outcome']
718 base_outcome = self._base_board_dict[target]
719 for fname in outcome.func_sizes:
720 self.PrintFuncSizeDetail(fname,
721 base_outcome.func_sizes[fname],
722 outcome.func_sizes[fname])
725 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
727 """Print a summary of image sizes broken down by section.
729 The summary takes the form of one line per architecture. The
730 line contains deltas for each of the sections (+ means the section
731 got bigger, - means smaller). The nunmbers are the average number
732 of bytes that a board in this section increased by.
735 powerpc: (622 boards) text -0.0
736 arm: (285 boards) text -0.0
737 nds32: (3 boards) text -8.0
740 board_selected: Dict containing boards to summarise, keyed by
742 board_dict: Dict containing boards for which we built this
743 commit, keyed by board.target. The value is an Outcome object.
744 show_detail: Show detail for each board
745 show_bloat: Show detail for each function
750 # Calculate changes in size for different image parts
751 # The previous sizes are in Board.sizes, for each board
752 for target in board_dict:
753 if target not in board_selected:
755 base_sizes = self._base_board_dict[target].sizes
756 outcome = board_dict[target]
757 sizes = outcome.sizes
759 # Loop through the list of images, creating a dict of size
760 # changes for each image/part. We end up with something like
761 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
762 # which means that U-Boot data increased by 5 bytes and SPL
763 # text decreased by 4.
764 err = {'_target' : target}
766 if image in base_sizes:
767 base_image = base_sizes[image]
768 # Loop through the text, data, bss parts
769 for part in sorted(sizes[image]):
770 diff = sizes[image][part] - base_image[part]
773 if image == 'u-boot':
776 name = image + ':' + part
778 arch = board_selected[target].arch
779 if not arch in arch_count:
782 arch_count[arch] += 1
784 pass # Only add to our list when we have some stats
785 elif not arch in arch_list:
786 arch_list[arch] = [err]
788 arch_list[arch].append(err)
790 # We now have a list of image size changes sorted by arch
791 # Print out a summary of these
792 for arch, target_list in arch_list.iteritems():
793 # Get total difference for each type
795 for result in target_list:
797 for name, diff in result.iteritems():
798 if name.startswith('_'):
805 result['_total_diff'] = total
806 result['_outcome'] = board_dict[result['_target']]
808 count = len(target_list)
810 for name in sorted(totals):
813 # Display the average difference in this name for this
815 avg_diff = float(diff) / count
816 color = self.col.RED if avg_diff > 0 else self.col.GREEN
817 msg = ' %s %+1.1f' % (name, avg_diff)
819 print '%10s: (for %d/%d boards)' % (arch, count,
822 print self.col.Color(color, msg),
827 self.PrintSizeDetail(target_list, show_bloat)
830 def PrintResultSummary(self, board_selected, board_dict, err_lines,
831 show_sizes, show_detail, show_bloat):
832 """Compare results with the base results and display delta.
834 Only boards mentioned in board_selected will be considered. This
835 function is intended to be called repeatedly with the results of
836 each commit. It therefore shows a 'diff' between what it saw in
837 the last call and what it sees now.
840 board_selected: Dict containing boards to summarise, keyed by
842 board_dict: Dict containing boards for which we built this
843 commit, keyed by board.target. The value is an Outcome object.
844 err_lines: A list of errors for this commit, or [] if there is
845 none, or we don't want to print errors
846 show_sizes: Show image size deltas
847 show_detail: Show detail for each board
848 show_bloat: Show detail for each function
850 better = [] # List of boards fixed since last commit
851 worse = [] # List of new broken boards since last commit
852 new = [] # List of boards that didn't exist last time
853 unknown = [] # List of boards that were not built
855 for target in board_dict:
856 if target not in board_selected:
859 # If the board was built last time, add its outcome to a list
860 if target in self._base_board_dict:
861 base_outcome = self._base_board_dict[target].rc
862 outcome = board_dict[target]
863 if outcome.rc == OUTCOME_UNKNOWN:
864 unknown.append(target)
865 elif outcome.rc < base_outcome:
866 better.append(target)
867 elif outcome.rc > base_outcome:
872 # Get a list of errors that have appeared, and disappeared
875 for line in err_lines:
876 if line not in self._base_err_lines:
877 worse_err.append('+' + line)
878 for line in self._base_err_lines:
879 if line not in err_lines:
880 better_err.append('-' + line)
882 # Display results by arch
883 if better or worse or unknown or new or worse_err or better_err:
885 self.AddOutcome(board_selected, arch_list, better, '',
887 self.AddOutcome(board_selected, arch_list, worse, '+',
889 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
890 if self._show_unknown:
891 self.AddOutcome(board_selected, arch_list, unknown, '?',
893 for arch, target_list in arch_list.iteritems():
894 print '%10s: %s' % (arch, target_list)
895 self._error_lines += 1
897 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
898 self._error_lines += 1
900 print self.col.Color(self.col.RED, '\n'.join(worse_err))
901 self._error_lines += 1
904 self.PrintSizeSummary(board_selected, board_dict, show_detail,
907 # Save our updated information for the next call to this function
908 self._base_board_dict = board_dict
909 self._base_err_lines = err_lines
911 # Get a list of boards that did not get built, if needed
913 for board in board_selected:
914 if not board in board_dict:
915 not_built.append(board)
917 print "Boards not built (%d): %s" % (len(not_built),
918 ', '.join(not_built))
920 def ProduceResultSummary(self, commit_upto, commits, board_selected):
921 board_dict, err_lines = self.GetResultSummary(board_selected,
922 commit_upto, read_func_sizes=self._show_bloat)
924 msg = '%02d: %s' % (commit_upto + 1,
925 commits[commit_upto].subject)
926 print self.col.Color(self.col.BLUE, msg)
927 self.PrintResultSummary(board_selected, board_dict,
928 err_lines if self._show_errors else [],
929 self._show_sizes, self._show_detail, self._show_bloat)
931 def ShowSummary(self, commits, board_selected):
932 """Show a build summary for U-Boot for a given board list.
934 Reset the result summary, then repeatedly call GetResultSummary on
935 each commit's results, then display the differences we see.
938 commit: Commit objects to summarise
939 board_selected: Dict containing boards to summarise
941 self.commit_count = len(commits) if commits else 1
942 self.commits = commits
943 self.ResetResultSummary(board_selected)
944 self._error_lines = 0
946 for commit_upto in range(0, self.commit_count, self._step):
947 self.ProduceResultSummary(commit_upto, commits, board_selected)
948 if not self._error_lines:
949 print self.col.Color(self.col.GREEN, '(no errors to report)')
952 def SetupBuild(self, board_selected, commits):
953 """Set up ready to start a build.
956 board_selected: Selected boards to build
957 commits: Selected commits to build
959 # First work out how many commits we will build
960 count = (self.commit_count + self._step - 1) / self._step
961 self.count = len(board_selected) * count
962 self.upto = self.warned = self.fail = 0
963 self._timestamps = collections.deque()
965 def GetThreadDir(self, thread_num):
966 """Get the directory path to the working dir for a thread.
969 thread_num: Number of thread to check.
971 return os.path.join(self._working_dir, '%02d' % thread_num)
973 def _PrepareThread(self, thread_num, setup_git):
974 """Prepare the working directory for a thread.
976 This clones or fetches the repo into the thread's work directory.
979 thread_num: Thread number (0, 1, ...)
980 setup_git: True to set up a git repo clone
982 thread_dir = self.GetThreadDir(thread_num)
983 builderthread.Mkdir(thread_dir)
984 git_dir = os.path.join(thread_dir, '.git')
986 # Clone the repo if it doesn't already exist
987 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
988 # we have a private index but uses the origin repo's contents?
989 if setup_git and self.git_dir:
990 src_dir = os.path.abspath(self.git_dir)
991 if os.path.exists(git_dir):
992 gitutil.Fetch(git_dir, thread_dir)
994 print 'Cloning repo for thread %d' % thread_num
995 gitutil.Clone(src_dir, thread_dir)
997 def _PrepareWorkingSpace(self, max_threads, setup_git):
998 """Prepare the working directory for use.
1000 Set up the git repo for each thread.
1003 max_threads: Maximum number of threads we expect to need.
1004 setup_git: True to set up a git repo clone
1006 builderthread.Mkdir(self._working_dir)
1007 for thread in range(max_threads):
1008 self._PrepareThread(thread, setup_git)
1010 def _PrepareOutputSpace(self):
1011 """Get the output directories ready to receive files.
1013 We delete any output directories which look like ones we need to
1014 create. Having left over directories is confusing when the user wants
1015 to check the output manually.
1018 for commit_upto in range(self.commit_count):
1019 dir_list.append(self._GetOutputDir(commit_upto))
1021 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1022 if dirname not in dir_list:
1023 shutil.rmtree(dirname)
1025 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1026 """Build all commits for a list of boards
1029 commits: List of commits to be build, each a Commit object
1030 boards_selected: Dict of selected boards, key is target name,
1031 value is Board object
1032 keep_outputs: True to save build output files
1033 verbose: Display build results as they are completed
1035 self.commit_count = len(commits) if commits else 1
1036 self.commits = commits
1037 self._verbose = verbose
1039 self.ResetResultSummary(board_selected)
1040 builderthread.Mkdir(self.base_dir)
1041 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1042 commits is not None)
1043 self._PrepareOutputSpace()
1044 self.SetupBuild(board_selected, commits)
1045 self.ProcessResult(None)
1047 # Create jobs to build all commits for each board
1048 for brd in board_selected.itervalues():
1049 job = builderthread.BuilderJob()
1051 job.commits = commits
1052 job.keep_outputs = keep_outputs
1053 job.step = self._step
1056 # Wait until all jobs are started
1059 # Wait until we have processed all output
1060 self.out_queue.join()