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
23 from terminal import Print
30 Please see README for user documentation, and you should be familiar with
31 that before trying to make sense of this.
33 Buildman works by keeping the machine as busy as possible, building different
34 commits for different boards on multiple CPUs at once.
36 The source repo (self.git_dir) contains all the commits to be built. Each
37 thread works on a single board at a time. It checks out the first commit,
38 configures it for that board, then builds it. Then it checks out the next
39 commit and builds it (typically without re-configuring). When it runs out
40 of commits, it gets another job from the builder and starts again with that
43 Clearly the builder threads could work either way - they could check out a
44 commit and then built it for all boards. Using separate directories for each
45 commit/board pair they could leave their build product around afterwards
48 The intent behind building a single board for multiple commits, is to make
49 use of incremental builds. Since each commit is built incrementally from
50 the previous one, builds are faster. Reconfiguring for a different board
51 removes all intermediate object files.
53 Many threads can be working at once, but each has its own working directory.
54 When a thread finishes a build, it puts the output files into a result
57 The base directory used by buildman is normally '../<branch>', i.e.
58 a directory higher than the source repository and named after the branch
61 Within the base directory, we have one subdirectory for each commit. Within
62 that is one subdirectory for each board. Within that is the build output for
63 that commit/board combination.
65 Buildman also create working directories for each thread, in a .bm-work/
66 subdirectory in the base dir.
68 As an example, say we are building branch 'us-net' for boards 'sandbox' and
69 'seaboard', and say that us-net has two commits. We will have directories
72 us-net/ base directory
73 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
78 02_of_02_g4ed4ebc_net--Check-tftp-comp/
84 00/ working directory for thread 0 (contains source checkout)
86 01/ working directory for thread 1
89 u-boot/ source directory
93 # Possible build outcomes
94 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
96 # Translate a commit subject into a valid filename
97 trans_valid_chars = string.maketrans("/: ", "---")
101 """Class for building U-Boot for a particular commit.
103 Public members: (many should ->private)
104 active: True if the builder is active and has not been stopped
105 already_done: Number of builds already completed
106 base_dir: Base directory to use for builder
107 checkout: True to check out source, False to skip that step.
108 This is used for testing.
109 col: terminal.Color() object
110 count: Number of commits to build
111 do_make: Method to call to invoke Make
112 fail: Number of builds that failed due to error
113 force_build: Force building even if a build already exists
114 force_config_on_failure: If a commit fails for a board, disable
115 incremental building for the next commit we build for that
116 board, so that we will see all warnings/errors again.
117 force_build_failures: If a previously-built build (i.e. built on
118 a previous run of buildman) is marked as failed, rebuild it.
119 git_dir: Git directory containing source repository
120 last_line_len: Length of the last line we printed (used for erasing
121 it with new progress information)
122 num_jobs: Number of jobs to run at once (passed to make as -j)
123 num_threads: Number of builder threads to run
124 out_queue: Queue of results to process
125 re_make_err: Compiled regular expression for ignore_lines
126 queue: Queue of jobs to run
127 threads: List of active threads
128 toolchains: Toolchains object to use for building
129 upto: Current commit number we are building (0.count-1)
130 warned: Number of builds that produced at least one warning
131 force_reconfig: Reconfigure U-Boot on each comiit. This disables
132 incremental building, where buildman reconfigures on the first
133 commit for a baord, and then just does an incremental build for
134 the following commits. In fact buildman will reconfigure and
135 retry for any failing commits, so generally the only effect of
136 this option is to slow things down.
137 in_tree: Build U-Boot in-tree instead of specifying an output
138 directory separate from the source code. This option is really
139 only useful for testing in-tree builds.
142 _base_board_dict: Last-summarised Dict of boards
143 _base_err_lines: Last-summarised list of errors
144 _base_warn_lines: Last-summarised list of warnings
145 _build_period_us: Time taken for a single build (float object).
146 _complete_delay: Expected delay until completion (timedelta)
147 _next_delay_update: Next time we plan to display a progress update
149 _show_unknown: Show unknown boards (those not built) in summary
150 _timestamps: List of timestamps for the completion of the last
151 last _timestamp_count builds. Each is a datetime object.
152 _timestamp_count: Number of timestamps to keep in our list.
153 _working_dir: Base working directory containing all threads
156 """Records a build outcome for a single make invocation
159 rc: Outcome value (OUTCOME_...)
160 err_lines: List of error lines or [] if none
161 sizes: Dictionary of image size information, keyed by filename
162 - Each value is itself a dictionary containing
163 values for 'text', 'data' and 'bss', being the integer
164 size in bytes of each section.
165 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
166 value is itself a dictionary:
168 value: Size of function in bytes
170 def __init__(self, rc, err_lines, sizes, func_sizes):
172 self.err_lines = err_lines
174 self.func_sizes = func_sizes
176 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
177 gnu_make='make', checkout=True, show_unknown=True, step=1):
178 """Create a new Builder object
181 toolchains: Toolchains object to use for building
182 base_dir: Base directory to use for builder
183 git_dir: Git directory containing source repository
184 num_threads: Number of builder threads to run
185 num_jobs: Number of jobs to run at once (passed to make as -j)
186 gnu_make: the command name of GNU Make.
187 checkout: True to check out source, False to skip that step.
188 This is used for testing.
189 show_unknown: Show unknown boards (those not built) in summary
190 step: 1 to process every commit, n to process every nth commit
192 self.toolchains = toolchains
193 self.base_dir = base_dir
194 self._working_dir = os.path.join(base_dir, '.bm-work')
197 self.do_make = self.Make
198 self.gnu_make = gnu_make
199 self.checkout = checkout
200 self.num_threads = num_threads
201 self.num_jobs = num_jobs
202 self.already_done = 0
203 self.force_build = False
204 self.git_dir = git_dir
205 self._show_unknown = show_unknown
206 self._timestamp_count = 10
207 self._build_period_us = None
208 self._complete_delay = None
209 self._next_delay_update = datetime.now()
210 self.force_config_on_failure = True
211 self.force_build_failures = False
212 self.force_reconfig = False
215 self._error_lines = 0
217 self.col = terminal.Color()
219 self._re_function = re.compile('(.*): In function.*')
220 self._re_files = re.compile('In file included from.*')
221 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
222 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
224 self.queue = Queue.Queue()
225 self.out_queue = Queue.Queue()
226 for i in range(self.num_threads):
227 t = builderthread.BuilderThread(self, i)
230 self.threads.append(t)
232 self.last_line_len = 0
233 t = builderthread.ResultThread(self)
236 self.threads.append(t)
238 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
239 self.re_make_err = re.compile('|'.join(ignore_lines))
242 """Get rid of all threads created by the builder"""
243 for t in self.threads:
246 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
247 show_detail=False, show_bloat=False,
248 list_error_boards=False):
249 """Setup display options for the builder.
251 show_errors: True to show summarised error/warning info
252 show_sizes: Show size deltas
253 show_detail: Show detail for each board
254 show_bloat: Show detail for each function
255 list_error_boards: Show the boards which caused each error/warning
257 self._show_errors = show_errors
258 self._show_sizes = show_sizes
259 self._show_detail = show_detail
260 self._show_bloat = show_bloat
261 self._list_error_boards = list_error_boards
263 def _AddTimestamp(self):
264 """Add a new timestamp to the list and record the build period.
266 The build period is the length of time taken to perform a single
267 build (one board, one commit).
270 self._timestamps.append(now)
271 count = len(self._timestamps)
272 delta = self._timestamps[-1] - self._timestamps[0]
273 seconds = delta.total_seconds()
275 # If we have enough data, estimate build period (time taken for a
276 # single build) and therefore completion time.
277 if count > 1 and self._next_delay_update < now:
278 self._next_delay_update = now + timedelta(seconds=2)
280 self._build_period = float(seconds) / count
281 todo = self.count - self.upto
282 self._complete_delay = timedelta(microseconds=
283 self._build_period * todo * 1000000)
285 self._complete_delay -= timedelta(
286 microseconds=self._complete_delay.microseconds)
289 self._timestamps.popleft()
292 def ClearLine(self, length):
293 """Clear any characters on the current line
295 Make way for a new line of length 'length', by outputting enough
296 spaces to clear out the old line. Then remember the new length for
300 length: Length of new line, in characters
302 if length < self.last_line_len:
303 Print(' ' * (self.last_line_len - length), newline=False)
304 Print('\r', newline=False)
305 self.last_line_len = length
308 def SelectCommit(self, commit, checkout=True):
309 """Checkout the selected commit for this build
312 if checkout and self.checkout:
313 gitutil.Checkout(commit.hash)
315 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
319 commit: Commit object that is being built
320 brd: Board object that is being built
321 stage: Stage that we are at (mrproper, config, build)
322 cwd: Directory where make should be run
323 args: Arguments to pass to make
324 kwargs: Arguments to pass to command.RunPipe()
326 cmd = [self.gnu_make] + list(args)
327 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
328 cwd=cwd, raise_on_error=False, **kwargs)
331 def ProcessResult(self, result):
332 """Process the result of a build, showing progress information
335 result: A CommandResult object, which indicates the result for
338 col = terminal.Color()
340 target = result.brd.target
342 if result.return_code < 0:
348 if result.return_code != 0:
352 if result.already_done:
353 self.already_done += 1
355 Print('\r', newline=False)
357 boards_selected = {target : result.brd}
358 self.ResetResultSummary(boards_selected)
359 self.ProduceResultSummary(result.commit_upto, self.commits,
362 target = '(starting)'
364 # Display separate counts for ok, warned and fail
365 ok = self.upto - self.warned - self.fail
366 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
367 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
368 line += self.col.Color(self.col.RED, '%5d' % self.fail)
370 name = ' /%-5d ' % self.count
372 # Add our current completion time estimate
374 if self._complete_delay:
375 name += '%s : ' % self._complete_delay
376 # When building all boards for a commit, we can print a commit
378 if result and result.commit_upto is None:
379 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
383 Print(line + name, newline=False)
384 length = 14 + len(name)
385 self.ClearLine(length)
387 def _GetOutputDir(self, commit_upto):
388 """Get the name of the output directory for a commit number
390 The output directory is typically .../<branch>/<commit>.
393 commit_upto: Commit number to use (0..self.count-1)
396 commit = self.commits[commit_upto]
397 subject = commit.subject.translate(trans_valid_chars)
398 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
399 self.commit_count, commit.hash, subject[:20]))
401 commit_dir = 'current'
402 output_dir = os.path.join(self.base_dir, commit_dir)
405 def GetBuildDir(self, commit_upto, target):
406 """Get the name of the build directory for a commit number
408 The build directory is typically .../<branch>/<commit>/<target>.
411 commit_upto: Commit number to use (0..self.count-1)
414 output_dir = self._GetOutputDir(commit_upto)
415 return os.path.join(output_dir, target)
417 def GetDoneFile(self, commit_upto, target):
418 """Get the name of the done file for a commit number
421 commit_upto: Commit number to use (0..self.count-1)
424 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
426 def GetSizesFile(self, commit_upto, target):
427 """Get the name of the sizes file for a commit number
430 commit_upto: Commit number to use (0..self.count-1)
433 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
435 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
436 """Get the name of the funcsizes file for a commit number and ELF file
439 commit_upto: Commit number to use (0..self.count-1)
441 elf_fname: Filename of elf image
443 return os.path.join(self.GetBuildDir(commit_upto, target),
444 '%s.sizes' % elf_fname.replace('/', '-'))
446 def GetObjdumpFile(self, commit_upto, target, elf_fname):
447 """Get the name of the objdump file for a commit number and ELF file
450 commit_upto: Commit number to use (0..self.count-1)
452 elf_fname: Filename of elf image
454 return os.path.join(self.GetBuildDir(commit_upto, target),
455 '%s.objdump' % elf_fname.replace('/', '-'))
457 def GetErrFile(self, commit_upto, target):
458 """Get the name of the err file for a commit number
461 commit_upto: Commit number to use (0..self.count-1)
464 output_dir = self.GetBuildDir(commit_upto, target)
465 return os.path.join(output_dir, 'err')
467 def FilterErrors(self, lines):
468 """Filter out errors in which we have no interest
470 We should probably use map().
473 lines: List of error lines, each a string
475 New list with only interesting lines included
479 if not self.re_make_err.search(line):
480 out_lines.append(line)
483 def ReadFuncSizes(self, fname, fd):
484 """Read function sizes from the output of 'nm'
487 fd: File containing data to read
488 fname: Filename we are reading from (just for errors)
491 Dictionary containing size of each function in bytes, indexed by
495 for line in fd.readlines():
497 size, type, name = line[:-1].split()
499 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
502 # function names begin with '.' on 64-bit powerpc
504 name = 'static.' + name.split('.')[0]
505 sym[name] = sym.get(name, 0) + int(size, 16)
508 def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
509 """Work out the outcome of a build.
512 commit_upto: Commit number to check (0..n-1)
513 target: Target board to check
514 read_func_sizes: True to read function size information
519 done_file = self.GetDoneFile(commit_upto, target)
520 sizes_file = self.GetSizesFile(commit_upto, target)
523 if os.path.exists(done_file):
524 with open(done_file, 'r') as fd:
525 return_code = int(fd.readline())
527 err_file = self.GetErrFile(commit_upto, target)
528 if os.path.exists(err_file):
529 with open(err_file, 'r') as fd:
530 err_lines = self.FilterErrors(fd.readlines())
532 # Decide whether the build was ok, failed or created warnings
540 # Convert size information to our simple format
541 if os.path.exists(sizes_file):
542 with open(sizes_file, 'r') as fd:
543 for line in fd.readlines():
544 values = line.split()
547 rodata = int(values[6], 16)
549 'all' : int(values[0]) + int(values[1]) +
551 'text' : int(values[0]) - rodata,
552 'data' : int(values[1]),
553 'bss' : int(values[2]),
556 sizes[values[5]] = size_dict
559 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
560 for fname in glob.glob(pattern):
561 with open(fname, 'r') as fd:
562 dict_name = os.path.basename(fname).replace('.sizes',
564 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
566 return Builder.Outcome(rc, err_lines, sizes, func_sizes)
568 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
570 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
571 """Calculate a summary of the results of building a commit.
574 board_selected: Dict containing boards to summarise
575 commit_upto: Commit number to summarize (0..self.count-1)
576 read_func_sizes: True to read function size information
580 Dict containing boards which passed building this commit.
581 keyed by board.target
582 List containing a summary of error lines
583 Dict keyed by error line, containing a list of the Board
584 objects with that error
585 List containing a summary of warning lines
586 Dict keyed by error line, containing a list of the Board
587 objects with that warning
589 def AddLine(lines_summary, lines_boards, line, board):
591 if line in lines_boards:
592 lines_boards[line].append(board)
594 lines_boards[line] = [board]
595 lines_summary.append(line)
598 err_lines_summary = []
599 err_lines_boards = {}
600 warn_lines_summary = []
601 warn_lines_boards = {}
603 for board in boards_selected.itervalues():
604 outcome = self.GetBuildOutcome(commit_upto, board.target,
606 board_dict[board.target] = outcome
608 last_was_warning = False
609 for line in outcome.err_lines:
611 if (self._re_function.match(line) or
612 self._re_files.match(line)):
615 is_warning = self._re_warning.match(line)
616 is_note = self._re_note.match(line)
617 if is_warning or (last_was_warning and is_note):
619 AddLine(warn_lines_summary, warn_lines_boards,
621 AddLine(warn_lines_summary, warn_lines_boards,
625 AddLine(err_lines_summary, err_lines_boards,
627 AddLine(err_lines_summary, err_lines_boards,
629 last_was_warning = is_warning
631 return (board_dict, err_lines_summary, err_lines_boards,
632 warn_lines_summary, warn_lines_boards)
634 def AddOutcome(self, board_dict, arch_list, changes, char, color):
635 """Add an output to our list of outcomes for each architecture
637 This simple function adds failing boards (changes) to the
638 relevant architecture string, so we can print the results out
639 sorted by architecture.
642 board_dict: Dict containing all boards
643 arch_list: Dict keyed by arch name. Value is a string containing
644 a list of board names which failed for that arch.
645 changes: List of boards to add to arch_list
646 color: terminal.Colour object
649 for target in changes:
650 if target in board_dict:
651 arch = board_dict[target].arch
654 str = self.col.Color(color, ' ' + target)
655 if not arch in done_arch:
656 str = self.col.Color(color, char) + ' ' + str
657 done_arch[arch] = True
658 if not arch in arch_list:
659 arch_list[arch] = str
661 arch_list[arch] += str
664 def ColourNum(self, num):
665 color = self.col.RED if num > 0 else self.col.GREEN
668 return self.col.Color(color, str(num))
670 def ResetResultSummary(self, board_selected):
671 """Reset the results summary ready for use.
673 Set up the base board list to be all those selected, and set the
674 error lines to empty.
676 Following this, calls to PrintResultSummary() will use this
677 information to work out what has changed.
680 board_selected: Dict containing boards to summarise, keyed by
683 self._base_board_dict = {}
684 for board in board_selected:
685 self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
686 self._base_err_lines = []
687 self._base_warn_lines = []
688 self._base_err_line_boards = {}
689 self._base_warn_line_boards = {}
691 def PrintFuncSizeDetail(self, fname, old, new):
692 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
693 delta, common = [], {}
700 if name not in common:
703 delta.append([-old[name], name])
706 if name not in common:
709 delta.append([new[name], name])
712 diff = new.get(name, 0) - old.get(name, 0)
714 grow, up = grow + 1, up + diff
716 shrink, down = shrink + 1, down - diff
717 delta.append([diff, name])
722 args = [add, -remove, grow, -shrink, up, -down, up - down]
725 args = [self.ColourNum(x) for x in args]
727 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
728 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
729 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
731 for diff, name in delta:
733 color = self.col.RED if diff > 0 else self.col.GREEN
734 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
735 old.get(name, '-'), new.get(name,'-'), diff)
736 Print(msg, colour=color)
739 def PrintSizeDetail(self, target_list, show_bloat):
740 """Show details size information for each board
743 target_list: List of targets, each a dict containing:
744 'target': Target name
745 'total_diff': Total difference in bytes across all areas
746 <part_name>: Difference for that part
747 show_bloat: Show detail for each function
749 targets_by_diff = sorted(target_list, reverse=True,
750 key=lambda x: x['_total_diff'])
751 for result in targets_by_diff:
752 printed_target = False
753 for name in sorted(result):
755 if name.startswith('_'):
758 color = self.col.RED if diff > 0 else self.col.GREEN
759 msg = ' %s %+d' % (name, diff)
760 if not printed_target:
761 Print('%10s %-15s:' % ('', result['_target']),
763 printed_target = True
764 Print(msg, colour=color, newline=False)
768 target = result['_target']
769 outcome = result['_outcome']
770 base_outcome = self._base_board_dict[target]
771 for fname in outcome.func_sizes:
772 self.PrintFuncSizeDetail(fname,
773 base_outcome.func_sizes[fname],
774 outcome.func_sizes[fname])
777 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
779 """Print a summary of image sizes broken down by section.
781 The summary takes the form of one line per architecture. The
782 line contains deltas for each of the sections (+ means the section
783 got bigger, - means smaller). The nunmbers are the average number
784 of bytes that a board in this section increased by.
787 powerpc: (622 boards) text -0.0
788 arm: (285 boards) text -0.0
789 nds32: (3 boards) text -8.0
792 board_selected: Dict containing boards to summarise, keyed by
794 board_dict: Dict containing boards for which we built this
795 commit, keyed by board.target. The value is an Outcome object.
796 show_detail: Show detail for each board
797 show_bloat: Show detail for each function
802 # Calculate changes in size for different image parts
803 # The previous sizes are in Board.sizes, for each board
804 for target in board_dict:
805 if target not in board_selected:
807 base_sizes = self._base_board_dict[target].sizes
808 outcome = board_dict[target]
809 sizes = outcome.sizes
811 # Loop through the list of images, creating a dict of size
812 # changes for each image/part. We end up with something like
813 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
814 # which means that U-Boot data increased by 5 bytes and SPL
815 # text decreased by 4.
816 err = {'_target' : target}
818 if image in base_sizes:
819 base_image = base_sizes[image]
820 # Loop through the text, data, bss parts
821 for part in sorted(sizes[image]):
822 diff = sizes[image][part] - base_image[part]
825 if image == 'u-boot':
828 name = image + ':' + part
830 arch = board_selected[target].arch
831 if not arch in arch_count:
834 arch_count[arch] += 1
836 pass # Only add to our list when we have some stats
837 elif not arch in arch_list:
838 arch_list[arch] = [err]
840 arch_list[arch].append(err)
842 # We now have a list of image size changes sorted by arch
843 # Print out a summary of these
844 for arch, target_list in arch_list.iteritems():
845 # Get total difference for each type
847 for result in target_list:
849 for name, diff in result.iteritems():
850 if name.startswith('_'):
857 result['_total_diff'] = total
858 result['_outcome'] = board_dict[result['_target']]
860 count = len(target_list)
862 for name in sorted(totals):
865 # Display the average difference in this name for this
867 avg_diff = float(diff) / count
868 color = self.col.RED if avg_diff > 0 else self.col.GREEN
869 msg = ' %s %+1.1f' % (name, avg_diff)
871 Print('%10s: (for %d/%d boards)' % (arch, count,
872 arch_count[arch]), newline=False)
874 Print(msg, colour=color, newline=False)
879 self.PrintSizeDetail(target_list, show_bloat)
882 def PrintResultSummary(self, board_selected, board_dict, err_lines,
883 err_line_boards, warn_lines, warn_line_boards,
884 show_sizes, show_detail, show_bloat):
885 """Compare results with the base results and display delta.
887 Only boards mentioned in board_selected will be considered. This
888 function is intended to be called repeatedly with the results of
889 each commit. It therefore shows a 'diff' between what it saw in
890 the last call and what it sees now.
893 board_selected: Dict containing boards to summarise, keyed by
895 board_dict: Dict containing boards for which we built this
896 commit, keyed by board.target. The value is an Outcome object.
897 err_lines: A list of errors for this commit, or [] if there is
898 none, or we don't want to print errors
899 err_line_boards: Dict keyed by error line, containing a list of
900 the Board objects with that error
901 warn_lines: A list of warnings for this commit, or [] if there is
902 none, or we don't want to print errors
903 warn_line_boards: Dict keyed by warning line, containing a list of
904 the Board objects with that warning
905 show_sizes: Show image size deltas
906 show_detail: Show detail for each board
907 show_bloat: Show detail for each function
909 def _BoardList(line, line_boards):
910 """Helper function to get a line of boards containing a line
913 line: Error line to search for
915 String containing a list of boards with that error line, or
916 '' if the user has not requested such a list
918 if self._list_error_boards:
920 for board in line_boards[line]:
921 names.append(board.target)
922 names_str = '(%s) ' % ','.join(names)
927 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
932 if line not in base_lines:
933 worse_lines.append(char + '+' +
934 _BoardList(line, line_boards) + line)
935 for line in base_lines:
936 if line not in lines:
937 better_lines.append(char + '-' +
938 _BoardList(line, base_line_boards) + line)
939 return better_lines, worse_lines
941 better = [] # List of boards fixed since last commit
942 worse = [] # List of new broken boards since last commit
943 new = [] # List of boards that didn't exist last time
944 unknown = [] # List of boards that were not built
946 for target in board_dict:
947 if target not in board_selected:
950 # If the board was built last time, add its outcome to a list
951 if target in self._base_board_dict:
952 base_outcome = self._base_board_dict[target].rc
953 outcome = board_dict[target]
954 if outcome.rc == OUTCOME_UNKNOWN:
955 unknown.append(target)
956 elif outcome.rc < base_outcome:
957 better.append(target)
958 elif outcome.rc > base_outcome:
963 # Get a list of errors that have appeared, and disappeared
964 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
965 self._base_err_line_boards, err_lines, err_line_boards, '')
966 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
967 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
969 # Display results by arch
970 if (better or worse or unknown or new or worse_err or better_err
971 or worse_warn or better_warn):
973 self.AddOutcome(board_selected, arch_list, better, '',
975 self.AddOutcome(board_selected, arch_list, worse, '+',
977 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
978 if self._show_unknown:
979 self.AddOutcome(board_selected, arch_list, unknown, '?',
981 for arch, target_list in arch_list.iteritems():
982 Print('%10s: %s' % (arch, target_list))
983 self._error_lines += 1
985 Print('\n'.join(better_err), colour=self.col.GREEN)
986 self._error_lines += 1
988 Print('\n'.join(worse_err), colour=self.col.RED)
989 self._error_lines += 1
991 Print('\n'.join(better_warn), colour=self.col.CYAN)
992 self._error_lines += 1
994 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
995 self._error_lines += 1
998 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1001 # Save our updated information for the next call to this function
1002 self._base_board_dict = board_dict
1003 self._base_err_lines = err_lines
1004 self._base_warn_lines = warn_lines
1005 self._base_err_line_boards = err_line_boards
1006 self._base_warn_line_boards = warn_line_boards
1008 # Get a list of boards that did not get built, if needed
1010 for board in board_selected:
1011 if not board in board_dict:
1012 not_built.append(board)
1014 Print("Boards not built (%d): %s" % (len(not_built),
1015 ', '.join(not_built)))
1017 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1018 (board_dict, err_lines, err_line_boards, warn_lines,
1019 warn_line_boards) = self.GetResultSummary(
1020 board_selected, commit_upto,
1021 read_func_sizes=self._show_bloat)
1023 msg = '%02d: %s' % (commit_upto + 1,
1024 commits[commit_upto].subject)
1025 Print(msg, colour=self.col.BLUE)
1026 self.PrintResultSummary(board_selected, board_dict,
1027 err_lines if self._show_errors else [], err_line_boards,
1028 warn_lines if self._show_errors else [], warn_line_boards,
1029 self._show_sizes, self._show_detail, self._show_bloat)
1031 def ShowSummary(self, commits, board_selected):
1032 """Show a build summary for U-Boot for a given board list.
1034 Reset the result summary, then repeatedly call GetResultSummary on
1035 each commit's results, then display the differences we see.
1038 commit: Commit objects to summarise
1039 board_selected: Dict containing boards to summarise
1041 self.commit_count = len(commits) if commits else 1
1042 self.commits = commits
1043 self.ResetResultSummary(board_selected)
1044 self._error_lines = 0
1046 for commit_upto in range(0, self.commit_count, self._step):
1047 self.ProduceResultSummary(commit_upto, commits, board_selected)
1048 if not self._error_lines:
1049 Print('(no errors to report)', colour=self.col.GREEN)
1052 def SetupBuild(self, board_selected, commits):
1053 """Set up ready to start a build.
1056 board_selected: Selected boards to build
1057 commits: Selected commits to build
1059 # First work out how many commits we will build
1060 count = (self.commit_count + self._step - 1) / self._step
1061 self.count = len(board_selected) * count
1062 self.upto = self.warned = self.fail = 0
1063 self._timestamps = collections.deque()
1065 def GetThreadDir(self, thread_num):
1066 """Get the directory path to the working dir for a thread.
1069 thread_num: Number of thread to check.
1071 return os.path.join(self._working_dir, '%02d' % thread_num)
1073 def _PrepareThread(self, thread_num, setup_git):
1074 """Prepare the working directory for a thread.
1076 This clones or fetches the repo into the thread's work directory.
1079 thread_num: Thread number (0, 1, ...)
1080 setup_git: True to set up a git repo clone
1082 thread_dir = self.GetThreadDir(thread_num)
1083 builderthread.Mkdir(thread_dir)
1084 git_dir = os.path.join(thread_dir, '.git')
1086 # Clone the repo if it doesn't already exist
1087 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1088 # we have a private index but uses the origin repo's contents?
1089 if setup_git and self.git_dir:
1090 src_dir = os.path.abspath(self.git_dir)
1091 if os.path.exists(git_dir):
1092 gitutil.Fetch(git_dir, thread_dir)
1094 Print('Cloning repo for thread %d' % thread_num)
1095 gitutil.Clone(src_dir, thread_dir)
1097 def _PrepareWorkingSpace(self, max_threads, setup_git):
1098 """Prepare the working directory for use.
1100 Set up the git repo for each thread.
1103 max_threads: Maximum number of threads we expect to need.
1104 setup_git: True to set up a git repo clone
1106 builderthread.Mkdir(self._working_dir)
1107 for thread in range(max_threads):
1108 self._PrepareThread(thread, setup_git)
1110 def _PrepareOutputSpace(self):
1111 """Get the output directories ready to receive files.
1113 We delete any output directories which look like ones we need to
1114 create. Having left over directories is confusing when the user wants
1115 to check the output manually.
1118 for commit_upto in range(self.commit_count):
1119 dir_list.append(self._GetOutputDir(commit_upto))
1121 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1122 if dirname not in dir_list:
1123 shutil.rmtree(dirname)
1125 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1126 """Build all commits for a list of boards
1129 commits: List of commits to be build, each a Commit object
1130 boards_selected: Dict of selected boards, key is target name,
1131 value is Board object
1132 keep_outputs: True to save build output files
1133 verbose: Display build results as they are completed
1136 - number of boards that failed to build
1137 - number of boards that issued warnings
1139 self.commit_count = len(commits) if commits else 1
1140 self.commits = commits
1141 self._verbose = verbose
1143 self.ResetResultSummary(board_selected)
1144 builderthread.Mkdir(self.base_dir)
1145 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1146 commits is not None)
1147 self._PrepareOutputSpace()
1148 self.SetupBuild(board_selected, commits)
1149 self.ProcessResult(None)
1151 # Create jobs to build all commits for each board
1152 for brd in board_selected.itervalues():
1153 job = builderthread.BuilderJob()
1155 job.commits = commits
1156 job.keep_outputs = keep_outputs
1157 job.step = self._step
1160 # Wait until all jobs are started
1163 # Wait until we have processed all output
1164 self.out_queue.join()
1167 return (self.fail, self.warned)