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 _base_warn_lines: Last-summarised list of warnings
144 _build_period_us: Time taken for a single build (float object).
145 _complete_delay: Expected delay until completion (timedelta)
146 _next_delay_update: Next time we plan to display a progress update
148 _show_unknown: Show unknown boards (those not built) in summary
149 _timestamps: List of timestamps for the completion of the last
150 last _timestamp_count builds. Each is a datetime object.
151 _timestamp_count: Number of timestamps to keep in our list.
152 _working_dir: Base working directory containing all threads
155 """Records a build outcome for a single make invocation
158 rc: Outcome value (OUTCOME_...)
159 err_lines: List of error lines or [] if none
160 sizes: Dictionary of image size information, keyed by filename
161 - Each value is itself a dictionary containing
162 values for 'text', 'data' and 'bss', being the integer
163 size in bytes of each section.
164 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
165 value is itself a dictionary:
167 value: Size of function in bytes
169 def __init__(self, rc, err_lines, sizes, func_sizes):
171 self.err_lines = err_lines
173 self.func_sizes = func_sizes
175 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
176 gnu_make='make', checkout=True, show_unknown=True, step=1):
177 """Create a new Builder object
180 toolchains: Toolchains object to use for building
181 base_dir: Base directory to use for builder
182 git_dir: Git directory containing source repository
183 num_threads: Number of builder threads to run
184 num_jobs: Number of jobs to run at once (passed to make as -j)
185 gnu_make: the command name of GNU Make.
186 checkout: True to check out source, False to skip that step.
187 This is used for testing.
188 show_unknown: Show unknown boards (those not built) in summary
189 step: 1 to process every commit, n to process every nth commit
191 self.toolchains = toolchains
192 self.base_dir = base_dir
193 self._working_dir = os.path.join(base_dir, '.bm-work')
196 self.do_make = self.Make
197 self.gnu_make = gnu_make
198 self.checkout = checkout
199 self.num_threads = num_threads
200 self.num_jobs = num_jobs
201 self.already_done = 0
202 self.force_build = False
203 self.git_dir = git_dir
204 self._show_unknown = show_unknown
205 self._timestamp_count = 10
206 self._build_period_us = None
207 self._complete_delay = None
208 self._next_delay_update = datetime.now()
209 self.force_config_on_failure = True
210 self.force_build_failures = False
211 self.force_reconfig = False
214 self._error_lines = 0
216 self.col = terminal.Color()
218 self._re_function = re.compile('(.*): In function.*')
219 self._re_files = re.compile('In file included from.*')
220 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
221 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
223 self.queue = Queue.Queue()
224 self.out_queue = Queue.Queue()
225 for i in range(self.num_threads):
226 t = builderthread.BuilderThread(self, i)
229 self.threads.append(t)
231 self.last_line_len = 0
232 t = builderthread.ResultThread(self)
235 self.threads.append(t)
237 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
238 self.re_make_err = re.compile('|'.join(ignore_lines))
241 """Get rid of all threads created by the builder"""
242 for t in self.threads:
245 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
246 show_detail=False, show_bloat=False,
247 list_error_boards=False):
248 """Setup display options for the builder.
250 show_errors: True to show summarised error/warning info
251 show_sizes: Show size deltas
252 show_detail: Show detail for each board
253 show_bloat: Show detail for each function
254 list_error_boards: Show the boards which caused each error/warning
256 self._show_errors = show_errors
257 self._show_sizes = show_sizes
258 self._show_detail = show_detail
259 self._show_bloat = show_bloat
260 self._list_error_boards = list_error_boards
262 def _AddTimestamp(self):
263 """Add a new timestamp to the list and record the build period.
265 The build period is the length of time taken to perform a single
266 build (one board, one commit).
269 self._timestamps.append(now)
270 count = len(self._timestamps)
271 delta = self._timestamps[-1] - self._timestamps[0]
272 seconds = delta.total_seconds()
274 # If we have enough data, estimate build period (time taken for a
275 # single build) and therefore completion time.
276 if count > 1 and self._next_delay_update < now:
277 self._next_delay_update = now + timedelta(seconds=2)
279 self._build_period = float(seconds) / count
280 todo = self.count - self.upto
281 self._complete_delay = timedelta(microseconds=
282 self._build_period * todo * 1000000)
284 self._complete_delay -= timedelta(
285 microseconds=self._complete_delay.microseconds)
288 self._timestamps.popleft()
291 def ClearLine(self, length):
292 """Clear any characters on the current line
294 Make way for a new line of length 'length', by outputting enough
295 spaces to clear out the old line. Then remember the new length for
299 length: Length of new line, in characters
301 if length < self.last_line_len:
302 print ' ' * (self.last_line_len - length),
304 self.last_line_len = length
307 def SelectCommit(self, commit, checkout=True):
308 """Checkout the selected commit for this build
311 if checkout and self.checkout:
312 gitutil.Checkout(commit.hash)
314 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
318 commit: Commit object that is being built
319 brd: Board object that is being built
320 stage: Stage that we are at (mrproper, config, build)
321 cwd: Directory where make should be run
322 args: Arguments to pass to make
323 kwargs: Arguments to pass to command.RunPipe()
325 cmd = [self.gnu_make] + list(args)
326 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
327 cwd=cwd, raise_on_error=False, **kwargs)
330 def ProcessResult(self, result):
331 """Process the result of a build, showing progress information
334 result: A CommandResult object, which indicates the result for
337 col = terminal.Color()
339 target = result.brd.target
341 if result.return_code < 0:
347 if result.return_code != 0:
351 if result.already_done:
352 self.already_done += 1
356 boards_selected = {target : result.brd}
357 self.ResetResultSummary(boards_selected)
358 self.ProduceResultSummary(result.commit_upto, self.commits,
361 target = '(starting)'
363 # Display separate counts for ok, warned and fail
364 ok = self.upto - self.warned - self.fail
365 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
366 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
367 line += self.col.Color(self.col.RED, '%5d' % self.fail)
369 name = ' /%-5d ' % self.count
371 # Add our current completion time estimate
373 if self._complete_delay:
374 name += '%s : ' % self._complete_delay
375 # When building all boards for a commit, we can print a commit
377 if result and result.commit_upto is None:
378 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
383 length = 14 + len(name)
384 self.ClearLine(length)
386 def _GetOutputDir(self, commit_upto):
387 """Get the name of the output directory for a commit number
389 The output directory is typically .../<branch>/<commit>.
392 commit_upto: Commit number to use (0..self.count-1)
395 commit = self.commits[commit_upto]
396 subject = commit.subject.translate(trans_valid_chars)
397 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
398 self.commit_count, commit.hash, subject[:20]))
400 commit_dir = 'current'
401 output_dir = os.path.join(self.base_dir, commit_dir)
404 def GetBuildDir(self, commit_upto, target):
405 """Get the name of the build directory for a commit number
407 The build directory is typically .../<branch>/<commit>/<target>.
410 commit_upto: Commit number to use (0..self.count-1)
413 output_dir = self._GetOutputDir(commit_upto)
414 return os.path.join(output_dir, target)
416 def GetDoneFile(self, commit_upto, target):
417 """Get the name of the done file for a commit number
420 commit_upto: Commit number to use (0..self.count-1)
423 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
425 def GetSizesFile(self, commit_upto, target):
426 """Get the name of the sizes file for a commit number
429 commit_upto: Commit number to use (0..self.count-1)
432 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
434 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
435 """Get the name of the funcsizes file for a commit number and ELF file
438 commit_upto: Commit number to use (0..self.count-1)
440 elf_fname: Filename of elf image
442 return os.path.join(self.GetBuildDir(commit_upto, target),
443 '%s.sizes' % elf_fname.replace('/', '-'))
445 def GetObjdumpFile(self, commit_upto, target, elf_fname):
446 """Get the name of the objdump file for a commit number and ELF file
449 commit_upto: Commit number to use (0..self.count-1)
451 elf_fname: Filename of elf image
453 return os.path.join(self.GetBuildDir(commit_upto, target),
454 '%s.objdump' % elf_fname.replace('/', '-'))
456 def GetErrFile(self, commit_upto, target):
457 """Get the name of the err file for a commit number
460 commit_upto: Commit number to use (0..self.count-1)
463 output_dir = self.GetBuildDir(commit_upto, target)
464 return os.path.join(output_dir, 'err')
466 def FilterErrors(self, lines):
467 """Filter out errors in which we have no interest
469 We should probably use map().
472 lines: List of error lines, each a string
474 New list with only interesting lines included
478 if not self.re_make_err.search(line):
479 out_lines.append(line)
482 def ReadFuncSizes(self, fname, fd):
483 """Read function sizes from the output of 'nm'
486 fd: File containing data to read
487 fname: Filename we are reading from (just for errors)
490 Dictionary containing size of each function in bytes, indexed by
494 for line in fd.readlines():
496 size, type, name = line[:-1].split()
498 print "Invalid line in file '%s': '%s'" % (fname, line[:-1])
501 # function names begin with '.' on 64-bit powerpc
503 name = 'static.' + name.split('.')[0]
504 sym[name] = sym.get(name, 0) + int(size, 16)
507 def GetBuildOutcome(self, commit_upto, target, read_func_sizes):
508 """Work out the outcome of a build.
511 commit_upto: Commit number to check (0..n-1)
512 target: Target board to check
513 read_func_sizes: True to read function size information
518 done_file = self.GetDoneFile(commit_upto, target)
519 sizes_file = self.GetSizesFile(commit_upto, target)
522 if os.path.exists(done_file):
523 with open(done_file, 'r') as fd:
524 return_code = int(fd.readline())
526 err_file = self.GetErrFile(commit_upto, target)
527 if os.path.exists(err_file):
528 with open(err_file, 'r') as fd:
529 err_lines = self.FilterErrors(fd.readlines())
531 # Decide whether the build was ok, failed or created warnings
539 # Convert size information to our simple format
540 if os.path.exists(sizes_file):
541 with open(sizes_file, 'r') as fd:
542 for line in fd.readlines():
543 values = line.split()
546 rodata = int(values[6], 16)
548 'all' : int(values[0]) + int(values[1]) +
550 'text' : int(values[0]) - rodata,
551 'data' : int(values[1]),
552 'bss' : int(values[2]),
555 sizes[values[5]] = size_dict
558 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
559 for fname in glob.glob(pattern):
560 with open(fname, 'r') as fd:
561 dict_name = os.path.basename(fname).replace('.sizes',
563 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
565 return Builder.Outcome(rc, err_lines, sizes, func_sizes)
567 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {})
569 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes):
570 """Calculate a summary of the results of building a commit.
573 board_selected: Dict containing boards to summarise
574 commit_upto: Commit number to summarize (0..self.count-1)
575 read_func_sizes: True to read function size information
579 Dict containing boards which passed building this commit.
580 keyed by board.target
581 List containing a summary of error lines
582 Dict keyed by error line, containing a list of the Board
583 objects with that error
584 List containing a summary of warning lines
585 Dict keyed by error line, containing a list of the Board
586 objects with that warning
588 def AddLine(lines_summary, lines_boards, line, board):
590 if line in lines_boards:
591 lines_boards[line].append(board)
593 lines_boards[line] = [board]
594 lines_summary.append(line)
597 err_lines_summary = []
598 err_lines_boards = {}
599 warn_lines_summary = []
600 warn_lines_boards = {}
602 for board in boards_selected.itervalues():
603 outcome = self.GetBuildOutcome(commit_upto, board.target,
605 board_dict[board.target] = outcome
607 last_was_warning = False
608 for line in outcome.err_lines:
610 if (self._re_function.match(line) or
611 self._re_files.match(line)):
614 is_warning = self._re_warning.match(line)
615 is_note = self._re_note.match(line)
616 if is_warning or (last_was_warning and is_note):
618 AddLine(warn_lines_summary, warn_lines_boards,
620 AddLine(warn_lines_summary, warn_lines_boards,
624 AddLine(err_lines_summary, err_lines_boards,
626 AddLine(err_lines_summary, err_lines_boards,
628 last_was_warning = is_warning
630 return (board_dict, err_lines_summary, err_lines_boards,
631 warn_lines_summary, warn_lines_boards)
633 def AddOutcome(self, board_dict, arch_list, changes, char, color):
634 """Add an output to our list of outcomes for each architecture
636 This simple function adds failing boards (changes) to the
637 relevant architecture string, so we can print the results out
638 sorted by architecture.
641 board_dict: Dict containing all boards
642 arch_list: Dict keyed by arch name. Value is a string containing
643 a list of board names which failed for that arch.
644 changes: List of boards to add to arch_list
645 color: terminal.Colour object
648 for target in changes:
649 if target in board_dict:
650 arch = board_dict[target].arch
653 str = self.col.Color(color, ' ' + target)
654 if not arch in done_arch:
655 str = self.col.Color(color, char) + ' ' + str
656 done_arch[arch] = True
657 if not arch in arch_list:
658 arch_list[arch] = str
660 arch_list[arch] += str
663 def ColourNum(self, num):
664 color = self.col.RED if num > 0 else self.col.GREEN
667 return self.col.Color(color, str(num))
669 def ResetResultSummary(self, board_selected):
670 """Reset the results summary ready for use.
672 Set up the base board list to be all those selected, and set the
673 error lines to empty.
675 Following this, calls to PrintResultSummary() will use this
676 information to work out what has changed.
679 board_selected: Dict containing boards to summarise, keyed by
682 self._base_board_dict = {}
683 for board in board_selected:
684 self._base_board_dict[board] = Builder.Outcome(0, [], [], {})
685 self._base_err_lines = []
686 self._base_warn_lines = []
687 self._base_err_line_boards = {}
688 self._base_warn_line_boards = {}
690 def PrintFuncSizeDetail(self, fname, old, new):
691 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
692 delta, common = [], {}
699 if name not in common:
702 delta.append([-old[name], name])
705 if name not in common:
708 delta.append([new[name], name])
711 diff = new.get(name, 0) - old.get(name, 0)
713 grow, up = grow + 1, up + diff
715 shrink, down = shrink + 1, down - diff
716 delta.append([diff, name])
721 args = [add, -remove, grow, -shrink, up, -down, up - down]
724 args = [self.ColourNum(x) for x in args]
726 print ('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
727 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
728 print '%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
730 for diff, name in delta:
732 color = self.col.RED if diff > 0 else self.col.GREEN
733 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
734 old.get(name, '-'), new.get(name,'-'), diff)
735 print self.col.Color(color, msg)
738 def PrintSizeDetail(self, target_list, show_bloat):
739 """Show details size information for each board
742 target_list: List of targets, each a dict containing:
743 'target': Target name
744 'total_diff': Total difference in bytes across all areas
745 <part_name>: Difference for that part
746 show_bloat: Show detail for each function
748 targets_by_diff = sorted(target_list, reverse=True,
749 key=lambda x: x['_total_diff'])
750 for result in targets_by_diff:
751 printed_target = False
752 for name in sorted(result):
754 if name.startswith('_'):
757 color = self.col.RED if diff > 0 else self.col.GREEN
758 msg = ' %s %+d' % (name, diff)
759 if not printed_target:
760 print '%10s %-15s:' % ('', result['_target']),
761 printed_target = True
762 print self.col.Color(color, msg),
766 target = result['_target']
767 outcome = result['_outcome']
768 base_outcome = self._base_board_dict[target]
769 for fname in outcome.func_sizes:
770 self.PrintFuncSizeDetail(fname,
771 base_outcome.func_sizes[fname],
772 outcome.func_sizes[fname])
775 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
777 """Print a summary of image sizes broken down by section.
779 The summary takes the form of one line per architecture. The
780 line contains deltas for each of the sections (+ means the section
781 got bigger, - means smaller). The nunmbers are the average number
782 of bytes that a board in this section increased by.
785 powerpc: (622 boards) text -0.0
786 arm: (285 boards) text -0.0
787 nds32: (3 boards) text -8.0
790 board_selected: Dict containing boards to summarise, keyed by
792 board_dict: Dict containing boards for which we built this
793 commit, keyed by board.target. The value is an Outcome object.
794 show_detail: Show detail for each board
795 show_bloat: Show detail for each function
800 # Calculate changes in size for different image parts
801 # The previous sizes are in Board.sizes, for each board
802 for target in board_dict:
803 if target not in board_selected:
805 base_sizes = self._base_board_dict[target].sizes
806 outcome = board_dict[target]
807 sizes = outcome.sizes
809 # Loop through the list of images, creating a dict of size
810 # changes for each image/part. We end up with something like
811 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
812 # which means that U-Boot data increased by 5 bytes and SPL
813 # text decreased by 4.
814 err = {'_target' : target}
816 if image in base_sizes:
817 base_image = base_sizes[image]
818 # Loop through the text, data, bss parts
819 for part in sorted(sizes[image]):
820 diff = sizes[image][part] - base_image[part]
823 if image == 'u-boot':
826 name = image + ':' + part
828 arch = board_selected[target].arch
829 if not arch in arch_count:
832 arch_count[arch] += 1
834 pass # Only add to our list when we have some stats
835 elif not arch in arch_list:
836 arch_list[arch] = [err]
838 arch_list[arch].append(err)
840 # We now have a list of image size changes sorted by arch
841 # Print out a summary of these
842 for arch, target_list in arch_list.iteritems():
843 # Get total difference for each type
845 for result in target_list:
847 for name, diff in result.iteritems():
848 if name.startswith('_'):
855 result['_total_diff'] = total
856 result['_outcome'] = board_dict[result['_target']]
858 count = len(target_list)
860 for name in sorted(totals):
863 # Display the average difference in this name for this
865 avg_diff = float(diff) / count
866 color = self.col.RED if avg_diff > 0 else self.col.GREEN
867 msg = ' %s %+1.1f' % (name, avg_diff)
869 print '%10s: (for %d/%d boards)' % (arch, count,
872 print self.col.Color(color, msg),
877 self.PrintSizeDetail(target_list, show_bloat)
880 def PrintResultSummary(self, board_selected, board_dict, err_lines,
881 err_line_boards, warn_lines, warn_line_boards,
882 show_sizes, show_detail, show_bloat):
883 """Compare results with the base results and display delta.
885 Only boards mentioned in board_selected will be considered. This
886 function is intended to be called repeatedly with the results of
887 each commit. It therefore shows a 'diff' between what it saw in
888 the last call and what it sees now.
891 board_selected: Dict containing boards to summarise, keyed by
893 board_dict: Dict containing boards for which we built this
894 commit, keyed by board.target. The value is an Outcome object.
895 err_lines: A list of errors for this commit, or [] if there is
896 none, or we don't want to print errors
897 err_line_boards: Dict keyed by error line, containing a list of
898 the Board objects with that error
899 warn_lines: A list of warnings for this commit, or [] if there is
900 none, or we don't want to print errors
901 warn_line_boards: Dict keyed by warning line, containing a list of
902 the Board objects with that warning
903 show_sizes: Show image size deltas
904 show_detail: Show detail for each board
905 show_bloat: Show detail for each function
907 def _BoardList(line, line_boards):
908 """Helper function to get a line of boards containing a line
911 line: Error line to search for
913 String containing a list of boards with that error line, or
914 '' if the user has not requested such a list
916 if self._list_error_boards:
918 for board in line_boards[line]:
919 names.append(board.target)
920 names_str = '(%s) ' % ','.join(names)
925 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
930 if line not in base_lines:
931 worse_lines.append(char + '+' +
932 _BoardList(line, line_boards) + line)
933 for line in base_lines:
934 if line not in lines:
935 better_lines.append(char + '-' +
936 _BoardList(line, base_line_boards) + line)
937 return better_lines, worse_lines
939 better = [] # List of boards fixed since last commit
940 worse = [] # List of new broken boards since last commit
941 new = [] # List of boards that didn't exist last time
942 unknown = [] # List of boards that were not built
944 for target in board_dict:
945 if target not in board_selected:
948 # If the board was built last time, add its outcome to a list
949 if target in self._base_board_dict:
950 base_outcome = self._base_board_dict[target].rc
951 outcome = board_dict[target]
952 if outcome.rc == OUTCOME_UNKNOWN:
953 unknown.append(target)
954 elif outcome.rc < base_outcome:
955 better.append(target)
956 elif outcome.rc > base_outcome:
961 # Get a list of errors that have appeared, and disappeared
962 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
963 self._base_err_line_boards, err_lines, err_line_boards, '')
964 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
965 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
967 # Display results by arch
968 if (better or worse or unknown or new or worse_err or better_err
969 or worse_warn or better_warn):
971 self.AddOutcome(board_selected, arch_list, better, '',
973 self.AddOutcome(board_selected, arch_list, worse, '+',
975 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
976 if self._show_unknown:
977 self.AddOutcome(board_selected, arch_list, unknown, '?',
979 for arch, target_list in arch_list.iteritems():
980 print '%10s: %s' % (arch, target_list)
981 self._error_lines += 1
983 print self.col.Color(self.col.GREEN, '\n'.join(better_err))
984 self._error_lines += 1
986 print self.col.Color(self.col.RED, '\n'.join(worse_err))
987 self._error_lines += 1
989 print self.col.Color(self.col.YELLOW, '\n'.join(better_warn))
990 self._error_lines += 1
992 print self.col.Color(self.col.MAGENTA, '\n'.join(worse_warn))
993 self._error_lines += 1
996 self.PrintSizeSummary(board_selected, board_dict, show_detail,
999 # Save our updated information for the next call to this function
1000 self._base_board_dict = board_dict
1001 self._base_err_lines = err_lines
1002 self._base_warn_lines = warn_lines
1003 self._base_err_line_boards = err_line_boards
1004 self._base_warn_line_boards = warn_line_boards
1006 # Get a list of boards that did not get built, if needed
1008 for board in board_selected:
1009 if not board in board_dict:
1010 not_built.append(board)
1012 print "Boards not built (%d): %s" % (len(not_built),
1013 ', '.join(not_built))
1015 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1016 (board_dict, err_lines, err_line_boards, warn_lines,
1017 warn_line_boards) = self.GetResultSummary(
1018 board_selected, commit_upto,
1019 read_func_sizes=self._show_bloat)
1021 msg = '%02d: %s' % (commit_upto + 1,
1022 commits[commit_upto].subject)
1023 print self.col.Color(self.col.BLUE, msg)
1024 self.PrintResultSummary(board_selected, board_dict,
1025 err_lines if self._show_errors else [], err_line_boards,
1026 warn_lines if self._show_errors else [], warn_line_boards,
1027 self._show_sizes, self._show_detail, self._show_bloat)
1029 def ShowSummary(self, commits, board_selected):
1030 """Show a build summary for U-Boot for a given board list.
1032 Reset the result summary, then repeatedly call GetResultSummary on
1033 each commit's results, then display the differences we see.
1036 commit: Commit objects to summarise
1037 board_selected: Dict containing boards to summarise
1039 self.commit_count = len(commits) if commits else 1
1040 self.commits = commits
1041 self.ResetResultSummary(board_selected)
1042 self._error_lines = 0
1044 for commit_upto in range(0, self.commit_count, self._step):
1045 self.ProduceResultSummary(commit_upto, commits, board_selected)
1046 if not self._error_lines:
1047 print self.col.Color(self.col.GREEN, '(no errors to report)')
1050 def SetupBuild(self, board_selected, commits):
1051 """Set up ready to start a build.
1054 board_selected: Selected boards to build
1055 commits: Selected commits to build
1057 # First work out how many commits we will build
1058 count = (self.commit_count + self._step - 1) / self._step
1059 self.count = len(board_selected) * count
1060 self.upto = self.warned = self.fail = 0
1061 self._timestamps = collections.deque()
1063 def GetThreadDir(self, thread_num):
1064 """Get the directory path to the working dir for a thread.
1067 thread_num: Number of thread to check.
1069 return os.path.join(self._working_dir, '%02d' % thread_num)
1071 def _PrepareThread(self, thread_num, setup_git):
1072 """Prepare the working directory for a thread.
1074 This clones or fetches the repo into the thread's work directory.
1077 thread_num: Thread number (0, 1, ...)
1078 setup_git: True to set up a git repo clone
1080 thread_dir = self.GetThreadDir(thread_num)
1081 builderthread.Mkdir(thread_dir)
1082 git_dir = os.path.join(thread_dir, '.git')
1084 # Clone the repo if it doesn't already exist
1085 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1086 # we have a private index but uses the origin repo's contents?
1087 if setup_git and self.git_dir:
1088 src_dir = os.path.abspath(self.git_dir)
1089 if os.path.exists(git_dir):
1090 gitutil.Fetch(git_dir, thread_dir)
1092 print 'Cloning repo for thread %d' % thread_num
1093 gitutil.Clone(src_dir, thread_dir)
1095 def _PrepareWorkingSpace(self, max_threads, setup_git):
1096 """Prepare the working directory for use.
1098 Set up the git repo for each thread.
1101 max_threads: Maximum number of threads we expect to need.
1102 setup_git: True to set up a git repo clone
1104 builderthread.Mkdir(self._working_dir)
1105 for thread in range(max_threads):
1106 self._PrepareThread(thread, setup_git)
1108 def _PrepareOutputSpace(self):
1109 """Get the output directories ready to receive files.
1111 We delete any output directories which look like ones we need to
1112 create. Having left over directories is confusing when the user wants
1113 to check the output manually.
1116 for commit_upto in range(self.commit_count):
1117 dir_list.append(self._GetOutputDir(commit_upto))
1119 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1120 if dirname not in dir_list:
1121 shutil.rmtree(dirname)
1123 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1124 """Build all commits for a list of boards
1127 commits: List of commits to be build, each a Commit object
1128 boards_selected: Dict of selected boards, key is target name,
1129 value is Board object
1130 keep_outputs: True to save build output files
1131 verbose: Display build results as they are completed
1134 - number of boards that failed to build
1135 - number of boards that issued warnings
1137 self.commit_count = len(commits) if commits else 1
1138 self.commits = commits
1139 self._verbose = verbose
1141 self.ResetResultSummary(board_selected)
1142 builderthread.Mkdir(self.base_dir)
1143 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1144 commits is not None)
1145 self._PrepareOutputSpace()
1146 self.SetupBuild(board_selected, commits)
1147 self.ProcessResult(None)
1149 # Create jobs to build all commits for each board
1150 for brd in board_selected.itervalues():
1151 job = builderthread.BuilderJob()
1153 job.commits = commits
1154 job.keep_outputs = keep_outputs
1155 job.step = self._step
1158 # Wait until all jobs are started
1161 # Wait until we have processed all output
1162 self.out_queue.join()
1165 return (self.fail, self.warned)