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 _AddTimestamp(self):
239 """Add a new timestamp to the list and record the build period.
241 The build period is the length of time taken to perform a single
242 build (one board, one commit).
245 self._timestamps.append(now)
246 count = len(self._timestamps)
247 delta = self._timestamps[-1] - self._timestamps[0]
248 seconds = delta.total_seconds()
250 # If we have enough data, estimate build period (time taken for a
251 # single build) and therefore completion time.
252 if count > 1 and self._next_delay_update < now:
253 self._next_delay_update = now + timedelta(seconds=2)
255 self._build_period = float(seconds) / count
256 todo = self.count - self.upto
257 self._complete_delay = timedelta(microseconds=
258 self._build_period * todo * 1000000)
260 self._complete_delay -= timedelta(
261 microseconds=self._complete_delay.microseconds)
264 self._timestamps.popleft()
267 def ClearLine(self, length):
268 """Clear any characters on the current line
270 Make way for a new line of length 'length', by outputting enough
271 spaces to clear out the old line. Then remember the new length for
275 length: Length of new line, in characters
277 if length < self.last_line_len:
278 print ' ' * (self.last_line_len - length),
280 self.last_line_len = length
283 def SelectCommit(self, commit, checkout=True):
284 """Checkout the selected commit for this build
287 if checkout and self.checkout:
288 gitutil.Checkout(commit.hash)
290 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
294 commit: Commit object that is being built
295 brd: Board object that is being built
296 stage: Stage that we are at (distclean, config, build)
297 cwd: Directory where make should be run
298 args: Arguments to pass to make
299 kwargs: Arguments to pass to command.RunPipe()
301 cmd = [self.gnu_make] + list(args)
302 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
303 cwd=cwd, raise_on_error=False, **kwargs)
306 def ProcessResult(self, result):
307 """Process the result of a build, showing progress information
310 result: A CommandResult object
312 col = terminal.Color()
314 target = result.brd.target
316 if result.return_code < 0:
322 if result.return_code != 0:
326 if result.already_done:
327 self.already_done += 1
329 target = '(starting)'
331 # Display separate counts for ok, warned and fail
332 ok = self.upto - self.warned - self.fail
333 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
334 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
335 line += self.col.Color(self.col.RED, '%5d' % self.fail)
337 name = ' /%-5d ' % self.count
339 # Add our current completion time estimate
341 if self._complete_delay:
342 name += '%s : ' % self._complete_delay
343 # When building all boards for a commit, we can print a commit
345 if result and result.commit_upto is None:
346 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
351 length = 13 + len(name)
352 self.ClearLine(length)
354 def _GetOutputDir(self, commit_upto):
355 """Get the name of the output directory for a commit number
357 The output directory is typically .../<branch>/<commit>.
360 commit_upto: Commit number to use (0..self.count-1)
363 commit = self.commits[commit_upto]
364 subject = commit.subject.translate(trans_valid_chars)
365 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
366 self.commit_count, commit.hash, subject[:20]))
368 commit_dir = 'current'
369 output_dir = os.path.join(self.base_dir, commit_dir)
372 def GetBuildDir(self, commit_upto, target):
373 """Get the name of the build directory for a commit number
375 The build directory is typically .../<branch>/<commit>/<target>.
378 commit_upto: Commit number to use (0..self.count-1)
381 output_dir = self._GetOutputDir(commit_upto)
382 return os.path.join(output_dir, target)
384 def GetDoneFile(self, commit_upto, target):
385 """Get the name of the done file for a commit number
388 commit_upto: Commit number to use (0..self.count-1)
391 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
393 def GetSizesFile(self, commit_upto, target):
394 """Get the name of the sizes file for a commit number
397 commit_upto: Commit number to use (0..self.count-1)
400 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
402 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
403 """Get the name of the funcsizes file for a commit number and ELF file
406 commit_upto: Commit number to use (0..self.count-1)
408 elf_fname: Filename of elf image
410 return os.path.join(self.GetBuildDir(commit_upto, target),
411 '%s.sizes' % elf_fname.replace('/', '-'))
413 def GetObjdumpFile(self, commit_upto, target, elf_fname):
414 """Get the name of the objdump file for a commit number and ELF file
417 commit_upto: Commit number to use (0..self.count-1)
419 elf_fname: Filename of elf image
421 return os.path.join(self.GetBuildDir(commit_upto, target),
422 '%s.objdump' % elf_fname.replace('/', '-'))
424 def GetErrFile(self, commit_upto, target):
425 """Get the name of the err file for a commit number
428 commit_upto: Commit number to use (0..self.count-1)
431 output_dir = self.GetBuildDir(commit_upto, target)
432 return os.path.join(output_dir, 'err')
434 def FilterErrors(self, lines):
435 """Filter out errors in which we have no interest
437 We should probably use map().
440 lines: List of error lines, each a string
442 New list with only interesting lines included
446 if not self.re_make_err.search(line):
447 out_lines.append(line)
450 def ReadFuncSizes(self, fname, fd):
451 """Read function sizes from the output of 'nm'
454 fd: File containing data to read
455 fname: Filename we are reading from (just for errors)
458 Dictionary containing size of each function in bytes, indexed by
462 for line in fd.readlines():
464 size, type, name = line[:-1].split()
466 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
469 # function names begin with '.' on 64-bit powerpc
471 name = 'static.' + name.split('.')[0]
472 sym[name] = sym.get(name, 0) + int(size, 16)
475 def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
476 """Work out the outcome of a build.
479 commit_upto: Commit number to check (0..n-1)
480 target: Target board to check
481 read_func_sizes: True to read function size information
486 done_file = self.GetDoneFile(commit_upto, target)
487 sizes_file = self.GetSizesFile(commit_upto, target)
490 if os.path.exists(done_file):
491 with open(done_file, 'r') as fd:
492 return_code = int(fd.readline())
494 err_file = self.GetErrFile(commit_upto, target)
495 if os.path.exists(err_file):
496 with open(err_file, 'r') as fd:
497 err_lines = self.FilterErrors(fd.readlines())
499 # Decide whether the build was ok, failed or created warnings
507 # Convert size information to our simple format
508 if os.path.exists(sizes_file):
509 with open(sizes_file, 'r') as fd:
510 for line in fd.readlines():
511 values = line.split()
514 rodata = int(values[6], 16)
516 'all' : int(values[0]) + int(values[1]) +
518 'text' : int(values[0]) - rodata,
519 'data' : int(values[1]),
520 'bss' : int(values[2]),
523 sizes[values[5]] = size_dict
526 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
527 for fname in glob.glob(pattern):
528 with open(fname, 'r') as fd:
529 dict_name = os.path.basename(fname).replace('.sizes',
531 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
533 return Builder.Outcome(rc, err_lines, sizes, func_sizes)
535 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
537 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
538 """Calculate a summary of the results of building a commit.
541 board_selected: Dict containing boards to summarise
542 commit_upto: Commit number to summarize (0..self.count-1)
543 read_func_sizes: True to read function size information
547 Dict containing boards which passed building this commit.
548 keyed by board.target
549 List containing a summary of error/warning lines
552 err_lines_summary = []
554 for board in boards_selected.itervalues():
555 outcome = self.GetBuildOutcome(commit_upto, board.target,
557 board_dict[board.target] = outcome
558 for err in outcome.err_lines:
559 if err and not err.rstrip() in err_lines_summary:
560 err_lines_summary.append(err.rstrip())
561 return board_dict, err_lines_summary
563 def AddOutcome(self, board_dict, arch_list, changes, char, color):
564 """Add an output to our list of outcomes for each architecture
566 This simple function adds failing boards (changes) to the
567 relevant architecture string, so we can print the results out
568 sorted by architecture.
571 board_dict: Dict containing all boards
572 arch_list: Dict keyed by arch name. Value is a string containing
573 a list of board names which failed for that arch.
574 changes: List of boards to add to arch_list
575 color: terminal.Colour object
578 for target in changes:
579 if target in board_dict:
580 arch = board_dict[target].arch
583 str = self.col.Color(color, ' ' + target)
584 if not arch in done_arch:
585 str = self.col.Color(color, char) + ' ' + str
586 done_arch[arch] = True
587 if not arch in arch_list:
588 arch_list[arch] = str
590 arch_list[arch] += str
593 def ColourNum(self, num):
594 color = self.col.RED if num > 0 else self.col.GREEN
597 return self.col.Color(color, str(num))
599 def ResetResultSummary(self, board_selected):
600 """Reset the results summary ready for use.
602 Set up the base board list to be all those selected, and set the
603 error lines to empty.
605 Following this, calls to PrintResultSummary() will use this
606 information to work out what has changed.
609 board_selected: Dict containing boards to summarise, keyed by
612 self._base_board_dict = {}
613 for board in board_selected:
614 self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
615 self._base_err_lines = []
617 def PrintFuncSizeDetail(self, fname, old, new):
618 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
619 delta, common = [], {}
626 if name not in common:
629 delta.append([-old[name], name])
632 if name not in common:
635 delta.append([new[name], name])
638 diff = new.get(name, 0) - old.get(name, 0)
640 grow, up = grow + 1, up + diff
642 shrink, down = shrink + 1, down - diff
643 delta.append([diff, name])
648 args = [add, -remove, grow, -shrink, up, -down, up - down]
651 args = [self.ColourNum(x) for x in args]
653 print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
654 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
655 print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
657 for diff, name in delta:
659 color = self.col.RED if diff > 0 else self.col.GREEN
660 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
661 old.get(name, '-'), new.get(name,'-'), diff)
662 print self.col.Color(color, msg)
665 def PrintSizeDetail(self, target_list, show_bloat):
666 """Show details size information for each board
669 target_list: List of targets, each a dict containing:
670 'target': Target name
671 'total_diff': Total difference in bytes across all areas
672 <part_name>: Difference for that part
673 show_bloat: Show detail for each function
675 targets_by_diff = sorted(target_list, reverse=True,
676 key=lambda x: x['_total_diff'])
677 for result in targets_by_diff:
678 printed_target = False
679 for name in sorted(result):
681 if name.startswith('_'):
684 color = self.col.RED if diff > 0 else self.col.GREEN
685 msg = ' %s %+d' % (name, diff)
686 if not printed_target:
687 print '%10s %-15s:' % ('', result['_target']),
688 printed_target = True
689 print self.col.Color(color, msg),
693 target = result['_target']
694 outcome = result['_outcome']
695 base_outcome = self._base_board_dict[target]
696 for fname in outcome.func_sizes:
697 self.PrintFuncSizeDetail(fname,
698 base_outcome.func_sizes[fname],
699 outcome.func_sizes[fname])
702 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
704 """Print a summary of image sizes broken down by section.
706 The summary takes the form of one line per architecture. The
707 line contains deltas for each of the sections (+ means the section
708 got bigger, - means smaller). The nunmbers are the average number
709 of bytes that a board in this section increased by.
712 powerpc: (622 boards) text -0.0
713 arm: (285 boards) text -0.0
714 nds32: (3 boards) text -8.0
717 board_selected: Dict containing boards to summarise, keyed by
719 board_dict: Dict containing boards for which we built this
720 commit, keyed by board.target. The value is an Outcome object.
721 show_detail: Show detail for each board
722 show_bloat: Show detail for each function
727 # Calculate changes in size for different image parts
728 # The previous sizes are in Board.sizes, for each board
729 for target in board_dict:
730 if target not in board_selected:
732 base_sizes = self._base_board_dict[target].sizes
733 outcome = board_dict[target]
734 sizes = outcome.sizes
736 # Loop through the list of images, creating a dict of size
737 # changes for each image/part. We end up with something like
738 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
739 # which means that U-Boot data increased by 5 bytes and SPL
740 # text decreased by 4.
741 err = {'_target' : target}
743 if image in base_sizes:
744 base_image = base_sizes[image]
745 # Loop through the text, data, bss parts
746 for part in sorted(sizes[image]):
747 diff = sizes[image][part] - base_image[part]
750 if image == 'u-boot':
753 name = image + ':' + part
755 arch = board_selected[target].arch
756 if not arch in arch_count:
759 arch_count[arch] += 1
761 pass # Only add to our list when we have some stats
762 elif not arch in arch_list:
763 arch_list[arch] = [err]
765 arch_list[arch].append(err)
767 # We now have a list of image size changes sorted by arch
768 # Print out a summary of these
769 for arch, target_list in arch_list.iteritems():
770 # Get total difference for each type
772 for result in target_list:
774 for name, diff in result.iteritems():
775 if name.startswith('_'):
782 result['_total_diff'] = total
783 result['_outcome'] = board_dict[result['_target']]
785 count = len(target_list)
787 for name in sorted(totals):
790 # Display the average difference in this name for this
792 avg_diff = float(diff) / count
793 color = self.col.RED if avg_diff > 0 else self.col.GREEN
794 msg = ' %s %+1.1f' % (name, avg_diff)
796 print '%10s: (for %d/%d boards)' % (arch, count,
799 print self.col.Color(color, msg),
804 self.PrintSizeDetail(target_list, show_bloat)
807 def PrintResultSummary(self, board_selected, board_dict, err_lines,
808 show_sizes, show_detail, show_bloat):
809 """Compare results with the base results and display delta.
811 Only boards mentioned in board_selected will be considered. This
812 function is intended to be called repeatedly with the results of
813 each commit. It therefore shows a 'diff' between what it saw in
814 the last call and what it sees now.
817 board_selected: Dict containing boards to summarise, keyed by
819 board_dict: Dict containing boards for which we built this
820 commit, keyed by board.target. The value is an Outcome object.
821 err_lines: A list of errors for this commit, or [] if there is
822 none, or we don't want to print errors
823 show_sizes: Show image size deltas
824 show_detail: Show detail for each board
825 show_bloat: Show detail for each function
827 better = [] # List of boards fixed since last commit
828 worse = [] # List of new broken boards since last commit
829 new = [] # List of boards that didn't exist last time
830 unknown = [] # List of boards that were not built
832 for target in board_dict:
833 if target not in board_selected:
836 # If the board was built last time, add its outcome to a list
837 if target in self._base_board_dict:
838 base_outcome = self._base_board_dict[target].rc
839 outcome = board_dict[target]
840 if outcome.rc == OUTCOME_UNKNOWN:
841 unknown.append(target)
842 elif outcome.rc < base_outcome:
843 better.append(target)
844 elif outcome.rc > base_outcome:
849 # Get a list of errors that have appeared, and disappeared
852 for line in err_lines:
853 if line not in self._base_err_lines:
854 worse_err.append('+' + line)
855 for line in self._base_err_lines:
856 if line not in err_lines:
857 better_err.append('-' + line)
859 # Display results by arch
860 if better or worse or unknown or new or worse_err or better_err:
862 self.AddOutcome(board_selected, arch_list, better, '',
864 self.AddOutcome(board_selected, arch_list, worse, '+',
866 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
867 if self._show_unknown:
868 self.AddOutcome(board_selected, arch_list, unknown, '?',
870 for arch, target_list in arch_list.iteritems():
871 print '%10s: %s' % (arch, target_list)
873 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
875 print self.col.Color(self.col.RED, '\n'.join(worse_err))
878 self.PrintSizeSummary(board_selected, board_dict, show_detail,
881 # Save our updated information for the next call to this function
882 self._base_board_dict = board_dict
883 self._base_err_lines = err_lines
885 # Get a list of boards that did not get built, if needed
887 for board in board_selected:
888 if not board in board_dict:
889 not_built.append(board)
891 print "Boards not built (%d): %s" % (len(not_built),
892 ', '.join(not_built))
895 def ShowSummary(self, commits, board_selected, show_errors, show_sizes,
896 show_detail, show_bloat):
897 """Show a build summary for U-Boot for a given board list.
899 Reset the result summary, then repeatedly call GetResultSummary on
900 each commit's results, then display the differences we see.
903 commit: Commit objects to summarise
904 board_selected: Dict containing boards to summarise
905 show_errors: Show errors that occured
906 show_sizes: Show size deltas
907 show_detail: Show detail for each board
908 show_bloat: Show detail for each function
910 self.commit_count = len(commits) if commits else 1
911 self.commits = commits
912 self.ResetResultSummary(board_selected)
914 for commit_upto in range(0, self.commit_count, self._step):
915 board_dict, err_lines = self.GetResultSummary(board_selected,
916 commit_upto, read_func_sizes=show_bloat)
918 msg = '%02d: %s' % (commit_upto + 1,
919 commits[commit_upto].subject)
922 print self.col.Color(self.col.BLUE, msg)
923 self.PrintResultSummary(board_selected, board_dict,
924 err_lines if show_errors else [], show_sizes, show_detail,
928 def SetupBuild(self, board_selected, commits):
929 """Set up ready to start a build.
932 board_selected: Selected boards to build
933 commits: Selected commits to build
935 # First work out how many commits we will build
936 count = (self.commit_count + self._step - 1) / self._step
937 self.count = len(board_selected) * count
938 self.upto = self.warned = self.fail = 0
939 self._timestamps = collections.deque()
941 def BuildBoardsForCommit(self, board_selected, keep_outputs):
942 """Build all boards for a single commit"""
943 self.SetupBuild(board_selected)
944 self.count = len(board_selected)
945 for brd in board_selected.itervalues():
949 job.keep_outputs = keep_outputs
953 self.out_queue.join()
957 def BuildCommits(self, commits, board_selected, show_errors, keep_outputs):
958 """Build all boards for all commits (non-incremental)"""
959 self.commit_count = len(commits)
961 self.ResetResultSummary(board_selected)
962 for self.commit_upto in range(self.commit_count):
963 self.SelectCommit(commits[self.commit_upto])
964 self.SelectOutputDir()
965 builderthread.Mkdir(self.output_dir)
967 self.BuildBoardsForCommit(board_selected, keep_outputs)
968 board_dict, err_lines = self.GetResultSummary()
969 self.PrintResultSummary(board_selected, board_dict,
970 err_lines if show_errors else [])
972 if self.already_done:
973 print '%d builds already done' % self.already_done
975 def GetThreadDir(self, thread_num):
976 """Get the directory path to the working dir for a thread.
979 thread_num: Number of thread to check.
981 return os.path.join(self._working_dir, '%02d' % thread_num)
983 def _PrepareThread(self, thread_num, setup_git):
984 """Prepare the working directory for a thread.
986 This clones or fetches the repo into the thread's work directory.
989 thread_num: Thread number (0, 1, ...)
990 setup_git: True to set up a git repo clone
992 thread_dir = self.GetThreadDir(thread_num)
993 builderthread.Mkdir(thread_dir)
994 git_dir = os.path.join(thread_dir, '.git')
996 # Clone the repo if it doesn't already exist
997 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
998 # we have a private index but uses the origin repo's contents?
999 if setup_git and self.git_dir:
1000 src_dir = os.path.abspath(self.git_dir)
1001 if os.path.exists(git_dir):
1002 gitutil.Fetch(git_dir, thread_dir)
1004 print 'Cloning repo for thread %d' % thread_num
1005 gitutil.Clone(src_dir, thread_dir)
1007 def _PrepareWorkingSpace(self, max_threads, setup_git):
1008 """Prepare the working directory for use.
1010 Set up the git repo for each thread.
1013 max_threads: Maximum number of threads we expect to need.
1014 setup_git: True to set up a git repo clone
1016 builderthread.Mkdir(self._working_dir)
1017 for thread in range(max_threads):
1018 self._PrepareThread(thread, setup_git)
1020 def _PrepareOutputSpace(self):
1021 """Get the output directories ready to receive files.
1023 We delete any output directories which look like ones we need to
1024 create. Having left over directories is confusing when the user wants
1025 to check the output manually.
1028 for commit_upto in range(self.commit_count):
1029 dir_list.append(self._GetOutputDir(commit_upto))
1031 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1032 if dirname not in dir_list:
1033 shutil.rmtree(dirname)
1035 def BuildBoards(self, commits, board_selected, show_errors, keep_outputs):
1036 """Build all commits for a list of boards
1039 commits: List of commits to be build, each a Commit object
1040 boards_selected: Dict of selected boards, key is target name,
1041 value is Board object
1042 show_errors: True to show summarised error/warning info
1043 keep_outputs: True to save build output files
1045 self.commit_count = len(commits) if commits else 1
1046 self.commits = commits
1048 self.ResetResultSummary(board_selected)
1049 builderthread.Mkdir(self.base_dir)
1050 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1051 commits is not None)
1052 self._PrepareOutputSpace()
1053 self.SetupBuild(board_selected, commits)
1054 self.ProcessResult(None)
1056 # Create jobs to build all commits for each board
1057 for brd in board_selected.itervalues():
1058 job = builderthread.BuilderJob()
1060 job.commits = commits
1061 job.keep_outputs = keep_outputs
1062 job.step = self._step
1065 # Wait until all jobs are started
1068 # Wait until we have processed all output
1069 self.out_queue.join()