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
214 self.col = terminal.Color()
216 self.queue = Queue.Queue()
217 self.out_queue = Queue.Queue()
218 for i in range(self.num_threads):
219 t = builderthread.BuilderThread(self, i)
222 self.threads.append(t)
224 self.last_line_len = 0
225 t = builderthread.ResultThread(self)
228 self.threads.append(t)
230 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
231 self.re_make_err = re.compile('|'.join(ignore_lines))
234 """Get rid of all threads created by the builder"""
235 for t in self.threads:
238 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
239 show_detail=False, show_bloat=False):
240 """Setup display options for the builder.
242 show_errors: True to show summarised error/warning info
243 show_sizes: Show size deltas
244 show_detail: Show detail for each board
245 show_bloat: Show detail for each function
247 self._show_errors = show_errors
248 self._show_sizes = show_sizes
249 self._show_detail = show_detail
250 self._show_bloat = show_bloat
252 def _AddTimestamp(self):
253 """Add a new timestamp to the list and record the build period.
255 The build period is the length of time taken to perform a single
256 build (one board, one commit).
259 self._timestamps.append(now)
260 count = len(self._timestamps)
261 delta = self._timestamps[-1] - self._timestamps[0]
262 seconds = delta.total_seconds()
264 # If we have enough data, estimate build period (time taken for a
265 # single build) and therefore completion time.
266 if count > 1 and self._next_delay_update < now:
267 self._next_delay_update = now + timedelta(seconds=2)
269 self._build_period = float(seconds) / count
270 todo = self.count - self.upto
271 self._complete_delay = timedelta(microseconds=
272 self._build_period * todo * 1000000)
274 self._complete_delay -= timedelta(
275 microseconds=self._complete_delay.microseconds)
278 self._timestamps.popleft()
281 def ClearLine(self, length):
282 """Clear any characters on the current line
284 Make way for a new line of length 'length', by outputting enough
285 spaces to clear out the old line. Then remember the new length for
289 length: Length of new line, in characters
291 if length < self.last_line_len:
292 print ' ' * (self.last_line_len - length),
294 self.last_line_len = length
297 def SelectCommit(self, commit, checkout=True):
298 """Checkout the selected commit for this build
301 if checkout and self.checkout:
302 gitutil.Checkout(commit.hash)
304 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
308 commit: Commit object that is being built
309 brd: Board object that is being built
310 stage: Stage that we are at (distclean, config, build)
311 cwd: Directory where make should be run
312 args: Arguments to pass to make
313 kwargs: Arguments to pass to command.RunPipe()
315 cmd = [self.gnu_make] + list(args)
316 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
317 cwd=cwd, raise_on_error=False, **kwargs)
320 def ProcessResult(self, result):
321 """Process the result of a build, showing progress information
324 result: A CommandResult object
326 col = terminal.Color()
328 target = result.brd.target
330 if result.return_code < 0:
336 if result.return_code != 0:
340 if result.already_done:
341 self.already_done += 1
343 target = '(starting)'
345 # Display separate counts for ok, warned and fail
346 ok = self.upto - self.warned - self.fail
347 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
348 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
349 line += self.col.Color(self.col.RED, '%5d' % self.fail)
351 name = ' /%-5d ' % self.count
353 # Add our current completion time estimate
355 if self._complete_delay:
356 name += '%s : ' % self._complete_delay
357 # When building all boards for a commit, we can print a commit
359 if result and result.commit_upto is None:
360 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
365 length = 13 + len(name)
366 self.ClearLine(length)
368 def _GetOutputDir(self, commit_upto):
369 """Get the name of the output directory for a commit number
371 The output directory is typically .../<branch>/<commit>.
374 commit_upto: Commit number to use (0..self.count-1)
377 commit = self.commits[commit_upto]
378 subject = commit.subject.translate(trans_valid_chars)
379 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
380 self.commit_count, commit.hash, subject[:20]))
382 commit_dir = 'current'
383 output_dir = os.path.join(self.base_dir, commit_dir)
386 def GetBuildDir(self, commit_upto, target):
387 """Get the name of the build directory for a commit number
389 The build directory is typically .../<branch>/<commit>/<target>.
392 commit_upto: Commit number to use (0..self.count-1)
395 output_dir = self._GetOutputDir(commit_upto)
396 return os.path.join(output_dir, target)
398 def GetDoneFile(self, commit_upto, target):
399 """Get the name of the done file for a commit number
402 commit_upto: Commit number to use (0..self.count-1)
405 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
407 def GetSizesFile(self, commit_upto, target):
408 """Get the name of the sizes 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), 'sizes')
416 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
417 """Get the name of the funcsizes file for a commit number and ELF file
420 commit_upto: Commit number to use (0..self.count-1)
422 elf_fname: Filename of elf image
424 return os.path.join(self.GetBuildDir(commit_upto, target),
425 '%s.sizes' % elf_fname.replace('/', '-'))
427 def GetObjdumpFile(self, commit_upto, target, elf_fname):
428 """Get the name of the objdump file for a commit number and ELF file
431 commit_upto: Commit number to use (0..self.count-1)
433 elf_fname: Filename of elf image
435 return os.path.join(self.GetBuildDir(commit_upto, target),
436 '%s.objdump' % elf_fname.replace('/', '-'))
438 def GetErrFile(self, commit_upto, target):
439 """Get the name of the err file for a commit number
442 commit_upto: Commit number to use (0..self.count-1)
445 output_dir = self.GetBuildDir(commit_upto, target)
446 return os.path.join(output_dir, 'err')
448 def FilterErrors(self, lines):
449 """Filter out errors in which we have no interest
451 We should probably use map().
454 lines: List of error lines, each a string
456 New list with only interesting lines included
460 if not self.re_make_err.search(line):
461 out_lines.append(line)
464 def ReadFuncSizes(self, fname, fd):
465 """Read function sizes from the output of 'nm'
468 fd: File containing data to read
469 fname: Filename we are reading from (just for errors)
472 Dictionary containing size of each function in bytes, indexed by
476 for line in fd.readlines():
478 size, type, name = line[:-1].split()
480 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
483 # function names begin with '.' on 64-bit powerpc
485 name = 'static.' + name.split('.')[0]
486 sym[name] = sym.get(name, 0) + int(size, 16)
489 def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
490 """Work out the outcome of a build.
493 commit_upto: Commit number to check (0..n-1)
494 target: Target board to check
495 read_func_sizes: True to read function size information
500 done_file = self.GetDoneFile(commit_upto, target)
501 sizes_file = self.GetSizesFile(commit_upto, target)
504 if os.path.exists(done_file):
505 with open(done_file, 'r') as fd:
506 return_code = int(fd.readline())
508 err_file = self.GetErrFile(commit_upto, target)
509 if os.path.exists(err_file):
510 with open(err_file, 'r') as fd:
511 err_lines = self.FilterErrors(fd.readlines())
513 # Decide whether the build was ok, failed or created warnings
521 # Convert size information to our simple format
522 if os.path.exists(sizes_file):
523 with open(sizes_file, 'r') as fd:
524 for line in fd.readlines():
525 values = line.split()
528 rodata = int(values[6], 16)
530 'all' : int(values[0]) + int(values[1]) +
532 'text' : int(values[0]) - rodata,
533 'data' : int(values[1]),
534 'bss' : int(values[2]),
537 sizes[values[5]] = size_dict
540 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
541 for fname in glob.glob(pattern):
542 with open(fname, 'r') as fd:
543 dict_name = os.path.basename(fname).replace('.sizes',
545 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
547 return Builder.Outcome(rc, err_lines, sizes, func_sizes)
549 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
551 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
552 """Calculate a summary of the results of building a commit.
555 board_selected: Dict containing boards to summarise
556 commit_upto: Commit number to summarize (0..self.count-1)
557 read_func_sizes: True to read function size information
561 Dict containing boards which passed building this commit.
562 keyed by board.target
563 List containing a summary of error/warning lines
566 err_lines_summary = []
568 for board in boards_selected.itervalues():
569 outcome = self.GetBuildOutcome(commit_upto, board.target,
571 board_dict[board.target] = outcome
572 for err in outcome.err_lines:
573 if err and not err.rstrip() in err_lines_summary:
574 err_lines_summary.append(err.rstrip())
575 return board_dict, err_lines_summary
577 def AddOutcome(self, board_dict, arch_list, changes, char, color):
578 """Add an output to our list of outcomes for each architecture
580 This simple function adds failing boards (changes) to the
581 relevant architecture string, so we can print the results out
582 sorted by architecture.
585 board_dict: Dict containing all boards
586 arch_list: Dict keyed by arch name. Value is a string containing
587 a list of board names which failed for that arch.
588 changes: List of boards to add to arch_list
589 color: terminal.Colour object
592 for target in changes:
593 if target in board_dict:
594 arch = board_dict[target].arch
597 str = self.col.Color(color, ' ' + target)
598 if not arch in done_arch:
599 str = self.col.Color(color, char) + ' ' + str
600 done_arch[arch] = True
601 if not arch in arch_list:
602 arch_list[arch] = str
604 arch_list[arch] += str
607 def ColourNum(self, num):
608 color = self.col.RED if num > 0 else self.col.GREEN
611 return self.col.Color(color, str(num))
613 def ResetResultSummary(self, board_selected):
614 """Reset the results summary ready for use.
616 Set up the base board list to be all those selected, and set the
617 error lines to empty.
619 Following this, calls to PrintResultSummary() will use this
620 information to work out what has changed.
623 board_selected: Dict containing boards to summarise, keyed by
626 self._base_board_dict = {}
627 for board in board_selected:
628 self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
629 self._base_err_lines = []
631 def PrintFuncSizeDetail(self, fname, old, new):
632 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
633 delta, common = [], {}
640 if name not in common:
643 delta.append([-old[name], name])
646 if name not in common:
649 delta.append([new[name], name])
652 diff = new.get(name, 0) - old.get(name, 0)
654 grow, up = grow + 1, up + diff
656 shrink, down = shrink + 1, down - diff
657 delta.append([diff, name])
662 args = [add, -remove, grow, -shrink, up, -down, up - down]
665 args = [self.ColourNum(x) for x in args]
667 print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
668 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
669 print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
671 for diff, name in delta:
673 color = self.col.RED if diff > 0 else self.col.GREEN
674 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
675 old.get(name, '-'), new.get(name,'-'), diff)
676 print self.col.Color(color, msg)
679 def PrintSizeDetail(self, target_list, show_bloat):
680 """Show details size information for each board
683 target_list: List of targets, each a dict containing:
684 'target': Target name
685 'total_diff': Total difference in bytes across all areas
686 <part_name>: Difference for that part
687 show_bloat: Show detail for each function
689 targets_by_diff = sorted(target_list, reverse=True,
690 key=lambda x: x['_total_diff'])
691 for result in targets_by_diff:
692 printed_target = False
693 for name in sorted(result):
695 if name.startswith('_'):
698 color = self.col.RED if diff > 0 else self.col.GREEN
699 msg = ' %s %+d' % (name, diff)
700 if not printed_target:
701 print '%10s %-15s:' % ('', result['_target']),
702 printed_target = True
703 print self.col.Color(color, msg),
707 target = result['_target']
708 outcome = result['_outcome']
709 base_outcome = self._base_board_dict[target]
710 for fname in outcome.func_sizes:
711 self.PrintFuncSizeDetail(fname,
712 base_outcome.func_sizes[fname],
713 outcome.func_sizes[fname])
716 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
718 """Print a summary of image sizes broken down by section.
720 The summary takes the form of one line per architecture. The
721 line contains deltas for each of the sections (+ means the section
722 got bigger, - means smaller). The nunmbers are the average number
723 of bytes that a board in this section increased by.
726 powerpc: (622 boards) text -0.0
727 arm: (285 boards) text -0.0
728 nds32: (3 boards) text -8.0
731 board_selected: Dict containing boards to summarise, keyed by
733 board_dict: Dict containing boards for which we built this
734 commit, keyed by board.target. The value is an Outcome object.
735 show_detail: Show detail for each board
736 show_bloat: Show detail for each function
741 # Calculate changes in size for different image parts
742 # The previous sizes are in Board.sizes, for each board
743 for target in board_dict:
744 if target not in board_selected:
746 base_sizes = self._base_board_dict[target].sizes
747 outcome = board_dict[target]
748 sizes = outcome.sizes
750 # Loop through the list of images, creating a dict of size
751 # changes for each image/part. We end up with something like
752 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
753 # which means that U-Boot data increased by 5 bytes and SPL
754 # text decreased by 4.
755 err = {'_target' : target}
757 if image in base_sizes:
758 base_image = base_sizes[image]
759 # Loop through the text, data, bss parts
760 for part in sorted(sizes[image]):
761 diff = sizes[image][part] - base_image[part]
764 if image == 'u-boot':
767 name = image + ':' + part
769 arch = board_selected[target].arch
770 if not arch in arch_count:
773 arch_count[arch] += 1
775 pass # Only add to our list when we have some stats
776 elif not arch in arch_list:
777 arch_list[arch] = [err]
779 arch_list[arch].append(err)
781 # We now have a list of image size changes sorted by arch
782 # Print out a summary of these
783 for arch, target_list in arch_list.iteritems():
784 # Get total difference for each type
786 for result in target_list:
788 for name, diff in result.iteritems():
789 if name.startswith('_'):
796 result['_total_diff'] = total
797 result['_outcome'] = board_dict[result['_target']]
799 count = len(target_list)
801 for name in sorted(totals):
804 # Display the average difference in this name for this
806 avg_diff = float(diff) / count
807 color = self.col.RED if avg_diff > 0 else self.col.GREEN
808 msg = ' %s %+1.1f' % (name, avg_diff)
810 print '%10s: (for %d/%d boards)' % (arch, count,
813 print self.col.Color(color, msg),
818 self.PrintSizeDetail(target_list, show_bloat)
821 def PrintResultSummary(self, board_selected, board_dict, err_lines,
822 show_sizes, show_detail, show_bloat):
823 """Compare results with the base results and display delta.
825 Only boards mentioned in board_selected will be considered. This
826 function is intended to be called repeatedly with the results of
827 each commit. It therefore shows a 'diff' between what it saw in
828 the last call and what it sees now.
831 board_selected: Dict containing boards to summarise, keyed by
833 board_dict: Dict containing boards for which we built this
834 commit, keyed by board.target. The value is an Outcome object.
835 err_lines: A list of errors for this commit, or [] if there is
836 none, or we don't want to print errors
837 show_sizes: Show image size deltas
838 show_detail: Show detail for each board
839 show_bloat: Show detail for each function
841 better = [] # List of boards fixed since last commit
842 worse = [] # List of new broken boards since last commit
843 new = [] # List of boards that didn't exist last time
844 unknown = [] # List of boards that were not built
846 for target in board_dict:
847 if target not in board_selected:
850 # If the board was built last time, add its outcome to a list
851 if target in self._base_board_dict:
852 base_outcome = self._base_board_dict[target].rc
853 outcome = board_dict[target]
854 if outcome.rc == OUTCOME_UNKNOWN:
855 unknown.append(target)
856 elif outcome.rc < base_outcome:
857 better.append(target)
858 elif outcome.rc > base_outcome:
863 # Get a list of errors that have appeared, and disappeared
866 for line in err_lines:
867 if line not in self._base_err_lines:
868 worse_err.append('+' + line)
869 for line in self._base_err_lines:
870 if line not in err_lines:
871 better_err.append('-' + line)
873 # Display results by arch
874 if better or worse or unknown or new or worse_err or better_err:
876 self.AddOutcome(board_selected, arch_list, better, '',
878 self.AddOutcome(board_selected, arch_list, worse, '+',
880 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
881 if self._show_unknown:
882 self.AddOutcome(board_selected, arch_list, unknown, '?',
884 for arch, target_list in arch_list.iteritems():
885 print '%10s: %s' % (arch, target_list)
887 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
889 print self.col.Color(self.col.RED, '\n'.join(worse_err))
892 self.PrintSizeSummary(board_selected, board_dict, show_detail,
895 # Save our updated information for the next call to this function
896 self._base_board_dict = board_dict
897 self._base_err_lines = err_lines
899 # Get a list of boards that did not get built, if needed
901 for board in board_selected:
902 if not board in board_dict:
903 not_built.append(board)
905 print "Boards not built (%d): %s" % (len(not_built),
906 ', '.join(not_built))
908 def ProduceResultSummary(self, commit_upto, commits, board_selected):
909 board_dict, err_lines = self.GetResultSummary(board_selected,
910 commit_upto, read_func_sizes=self._show_bloat)
912 msg = '%02d: %s' % (commit_upto + 1,
913 commits[commit_upto].subject)
914 print self.col.Color(self.col.BLUE, msg)
915 self.PrintResultSummary(board_selected, board_dict,
916 err_lines if self._show_errors else [],
917 self._show_sizes, self._show_detail, self._show_bloat)
919 def ShowSummary(self, commits, board_selected):
920 """Show a build summary for U-Boot for a given board list.
922 Reset the result summary, then repeatedly call GetResultSummary on
923 each commit's results, then display the differences we see.
926 commit: Commit objects to summarise
927 board_selected: Dict containing boards to summarise
929 self.commit_count = len(commits) if commits else 1
930 self.commits = commits
931 self.ResetResultSummary(board_selected)
933 for commit_upto in range(0, self.commit_count, self._step):
934 self.ProduceResultSummary(commit_upto, commits, board_selected)
937 def SetupBuild(self, board_selected, commits):
938 """Set up ready to start a build.
941 board_selected: Selected boards to build
942 commits: Selected commits to build
944 # First work out how many commits we will build
945 count = (self.commit_count + self._step - 1) / self._step
946 self.count = len(board_selected) * count
947 self.upto = self.warned = self.fail = 0
948 self._timestamps = collections.deque()
950 def BuildBoardsForCommit(self, board_selected, keep_outputs):
951 """Build all boards for a single commit"""
952 self.SetupBuild(board_selected)
953 self.count = len(board_selected)
954 for brd in board_selected.itervalues():
958 job.keep_outputs = keep_outputs
962 self.out_queue.join()
966 def BuildCommits(self, commits, board_selected, show_errors, keep_outputs):
967 """Build all boards for all commits (non-incremental)"""
968 self.commit_count = len(commits)
970 self.ResetResultSummary(board_selected)
971 for self.commit_upto in range(self.commit_count):
972 self.SelectCommit(commits[self.commit_upto])
973 self.SelectOutputDir()
974 builderthread.Mkdir(self.output_dir)
976 self.BuildBoardsForCommit(board_selected, keep_outputs)
977 board_dict, err_lines = self.GetResultSummary()
978 self.PrintResultSummary(board_selected, board_dict,
979 err_lines if show_errors else [])
981 if self.already_done:
982 print '%d builds already done' % self.already_done
984 def GetThreadDir(self, thread_num):
985 """Get the directory path to the working dir for a thread.
988 thread_num: Number of thread to check.
990 return os.path.join(self._working_dir, '%02d' % thread_num)
992 def _PrepareThread(self, thread_num, setup_git):
993 """Prepare the working directory for a thread.
995 This clones or fetches the repo into the thread's work directory.
998 thread_num: Thread number (0, 1, ...)
999 setup_git: True to set up a git repo clone
1001 thread_dir = self.GetThreadDir(thread_num)
1002 builderthread.Mkdir(thread_dir)
1003 git_dir = os.path.join(thread_dir, '.git')
1005 # Clone the repo if it doesn't already exist
1006 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1007 # we have a private index but uses the origin repo's contents?
1008 if setup_git and self.git_dir:
1009 src_dir = os.path.abspath(self.git_dir)
1010 if os.path.exists(git_dir):
1011 gitutil.Fetch(git_dir, thread_dir)
1013 print 'Cloning repo for thread %d' % thread_num
1014 gitutil.Clone(src_dir, thread_dir)
1016 def _PrepareWorkingSpace(self, max_threads, setup_git):
1017 """Prepare the working directory for use.
1019 Set up the git repo for each thread.
1022 max_threads: Maximum number of threads we expect to need.
1023 setup_git: True to set up a git repo clone
1025 builderthread.Mkdir(self._working_dir)
1026 for thread in range(max_threads):
1027 self._PrepareThread(thread, setup_git)
1029 def _PrepareOutputSpace(self):
1030 """Get the output directories ready to receive files.
1032 We delete any output directories which look like ones we need to
1033 create. Having left over directories is confusing when the user wants
1034 to check the output manually.
1037 for commit_upto in range(self.commit_count):
1038 dir_list.append(self._GetOutputDir(commit_upto))
1040 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1041 if dirname not in dir_list:
1042 shutil.rmtree(dirname)
1044 def BuildBoards(self, commits, board_selected, keep_outputs):
1045 """Build all commits for a list of boards
1048 commits: List of commits to be build, each a Commit object
1049 boards_selected: Dict of selected boards, key is target name,
1050 value is Board object
1051 keep_outputs: True to save build output files
1053 self.commit_count = len(commits) if commits else 1
1054 self.commits = commits
1056 self.ResetResultSummary(board_selected)
1057 builderthread.Mkdir(self.base_dir)
1058 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1059 commits is not None)
1060 self._PrepareOutputSpace()
1061 self.SetupBuild(board_selected, commits)
1062 self.ProcessResult(None)
1064 # Create jobs to build all commits for each board
1065 for brd in board_selected.itervalues():
1066 job = builderthread.BuilderJob()
1068 job.commits = commits
1069 job.keep_outputs = keep_outputs
1070 job.step = self._step
1073 # Wait until all jobs are started
1076 # Wait until we have processed all output
1077 self.out_queue.join()