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
24 from terminal import Print
31 Please see README for user documentation, and you should be familiar with
32 that before trying to make sense of this.
34 Buildman works by keeping the machine as busy as possible, building different
35 commits for different boards on multiple CPUs at once.
37 The source repo (self.git_dir) contains all the commits to be built. Each
38 thread works on a single board at a time. It checks out the first commit,
39 configures it for that board, then builds it. Then it checks out the next
40 commit and builds it (typically without re-configuring). When it runs out
41 of commits, it gets another job from the builder and starts again with that
44 Clearly the builder threads could work either way - they could check out a
45 commit and then built it for all boards. Using separate directories for each
46 commit/board pair they could leave their build product around afterwards
49 The intent behind building a single board for multiple commits, is to make
50 use of incremental builds. Since each commit is built incrementally from
51 the previous one, builds are faster. Reconfiguring for a different board
52 removes all intermediate object files.
54 Many threads can be working at once, but each has its own working directory.
55 When a thread finishes a build, it puts the output files into a result
58 The base directory used by buildman is normally '../<branch>', i.e.
59 a directory higher than the source repository and named after the branch
62 Within the base directory, we have one subdirectory for each commit. Within
63 that is one subdirectory for each board. Within that is the build output for
64 that commit/board combination.
66 Buildman also create working directories for each thread, in a .bm-work/
67 subdirectory in the base dir.
69 As an example, say we are building branch 'us-net' for boards 'sandbox' and
70 'seaboard', and say that us-net has two commits. We will have directories
73 us-net/ base directory
74 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
79 02_of_02_g4ed4ebc_net--Check-tftp-comp/
85 00/ working directory for thread 0 (contains source checkout)
87 01/ working directory for thread 1
90 u-boot/ source directory
94 # Possible build outcomes
95 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
97 # Translate a commit subject into a valid filename
98 trans_valid_chars = string.maketrans("/: ", "---")
101 '.config', '.config-spl', '.config-tpl',
102 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
103 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
104 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
108 """Holds information about configuration settings for a board."""
109 def __init__(self, target):
112 for fname in CONFIG_FILENAMES:
113 self.config[fname] = {}
115 def Add(self, fname, key, value):
116 self.config[fname][key] = value
120 for fname in self.config:
121 for key, value in self.config[fname].iteritems():
123 val = val ^ hash(key) & hash(value)
127 """Class for building U-Boot for a particular commit.
129 Public members: (many should ->private)
130 already_done: Number of builds already completed
131 base_dir: Base directory to use for builder
132 checkout: True to check out source, False to skip that step.
133 This is used for testing.
134 col: terminal.Color() object
135 count: Number of commits to build
136 do_make: Method to call to invoke Make
137 fail: Number of builds that failed due to error
138 force_build: Force building even if a build already exists
139 force_config_on_failure: If a commit fails for a board, disable
140 incremental building for the next commit we build for that
141 board, so that we will see all warnings/errors again.
142 force_build_failures: If a previously-built build (i.e. built on
143 a previous run of buildman) is marked as failed, rebuild it.
144 git_dir: Git directory containing source repository
145 last_line_len: Length of the last line we printed (used for erasing
146 it with new progress information)
147 num_jobs: Number of jobs to run at once (passed to make as -j)
148 num_threads: Number of builder threads to run
149 out_queue: Queue of results to process
150 re_make_err: Compiled regular expression for ignore_lines
151 queue: Queue of jobs to run
152 threads: List of active threads
153 toolchains: Toolchains object to use for building
154 upto: Current commit number we are building (0.count-1)
155 warned: Number of builds that produced at least one warning
156 force_reconfig: Reconfigure U-Boot on each comiit. This disables
157 incremental building, where buildman reconfigures on the first
158 commit for a baord, and then just does an incremental build for
159 the following commits. In fact buildman will reconfigure and
160 retry for any failing commits, so generally the only effect of
161 this option is to slow things down.
162 in_tree: Build U-Boot in-tree instead of specifying an output
163 directory separate from the source code. This option is really
164 only useful for testing in-tree builds.
167 _base_board_dict: Last-summarised Dict of boards
168 _base_err_lines: Last-summarised list of errors
169 _base_warn_lines: Last-summarised list of warnings
170 _build_period_us: Time taken for a single build (float object).
171 _complete_delay: Expected delay until completion (timedelta)
172 _next_delay_update: Next time we plan to display a progress update
174 _show_unknown: Show unknown boards (those not built) in summary
175 _timestamps: List of timestamps for the completion of the last
176 last _timestamp_count builds. Each is a datetime object.
177 _timestamp_count: Number of timestamps to keep in our list.
178 _working_dir: Base working directory containing all threads
181 """Records a build outcome for a single make invocation
184 rc: Outcome value (OUTCOME_...)
185 err_lines: List of error lines or [] if none
186 sizes: Dictionary of image size information, keyed by filename
187 - Each value is itself a dictionary containing
188 values for 'text', 'data' and 'bss', being the integer
189 size in bytes of each section.
190 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
191 value is itself a dictionary:
193 value: Size of function in bytes
194 config: Dictionary keyed by filename - e.g. '.config'. Each
195 value is itself a dictionary:
199 def __init__(self, rc, err_lines, sizes, func_sizes, config):
201 self.err_lines = err_lines
203 self.func_sizes = func_sizes
206 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
207 gnu_make='make', checkout=True, show_unknown=True, step=1,
208 no_subdirs=False, full_path=False, verbose_build=False,
209 incremental=False, per_board_out_dir=False):
210 """Create a new Builder object
213 toolchains: Toolchains object to use for building
214 base_dir: Base directory to use for builder
215 git_dir: Git directory containing source repository
216 num_threads: Number of builder threads to run
217 num_jobs: Number of jobs to run at once (passed to make as -j)
218 gnu_make: the command name of GNU Make.
219 checkout: True to check out source, False to skip that step.
220 This is used for testing.
221 show_unknown: Show unknown boards (those not built) in summary
222 step: 1 to process every commit, n to process every nth commit
223 no_subdirs: Don't create subdirectories when building current
224 source for a single board
225 full_path: Return the full path in CROSS_COMPILE and don't set
227 verbose_build: Run build with V=1 and don't use 'make -s'
228 incremental: Always perform incremental builds; don't run make
229 mrproper when configuring
230 per_board_out_dir: Build in a separate persistent directory per
231 board rather than a thread-specific directory
233 self.toolchains = toolchains
234 self.base_dir = base_dir
235 self._working_dir = os.path.join(base_dir, '.bm-work')
237 self.do_make = self.Make
238 self.gnu_make = gnu_make
239 self.checkout = checkout
240 self.num_threads = num_threads
241 self.num_jobs = num_jobs
242 self.already_done = 0
243 self.force_build = False
244 self.git_dir = git_dir
245 self._show_unknown = show_unknown
246 self._timestamp_count = 10
247 self._build_period_us = None
248 self._complete_delay = None
249 self._next_delay_update = datetime.now()
250 self.force_config_on_failure = True
251 self.force_build_failures = False
252 self.force_reconfig = False
255 self._error_lines = 0
256 self.no_subdirs = no_subdirs
257 self.full_path = full_path
258 self.verbose_build = verbose_build
260 self.col = terminal.Color()
262 self._re_function = re.compile('(.*): In function.*')
263 self._re_files = re.compile('In file included from.*')
264 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
265 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
267 self.queue = Queue.Queue()
268 self.out_queue = Queue.Queue()
269 for i in range(self.num_threads):
270 t = builderthread.BuilderThread(self, i, incremental,
274 self.threads.append(t)
276 self.last_line_len = 0
277 t = builderthread.ResultThread(self)
280 self.threads.append(t)
282 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
283 self.re_make_err = re.compile('|'.join(ignore_lines))
286 """Get rid of all threads created by the builder"""
287 for t in self.threads:
290 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
291 show_detail=False, show_bloat=False,
292 list_error_boards=False, show_config=False):
293 """Setup display options for the builder.
295 show_errors: True to show summarised error/warning info
296 show_sizes: Show size deltas
297 show_detail: Show detail for each board
298 show_bloat: Show detail for each function
299 list_error_boards: Show the boards which caused each error/warning
300 show_config: Show config deltas
302 self._show_errors = show_errors
303 self._show_sizes = show_sizes
304 self._show_detail = show_detail
305 self._show_bloat = show_bloat
306 self._list_error_boards = list_error_boards
307 self._show_config = show_config
309 def _AddTimestamp(self):
310 """Add a new timestamp to the list and record the build period.
312 The build period is the length of time taken to perform a single
313 build (one board, one commit).
316 self._timestamps.append(now)
317 count = len(self._timestamps)
318 delta = self._timestamps[-1] - self._timestamps[0]
319 seconds = delta.total_seconds()
321 # If we have enough data, estimate build period (time taken for a
322 # single build) and therefore completion time.
323 if count > 1 and self._next_delay_update < now:
324 self._next_delay_update = now + timedelta(seconds=2)
326 self._build_period = float(seconds) / count
327 todo = self.count - self.upto
328 self._complete_delay = timedelta(microseconds=
329 self._build_period * todo * 1000000)
331 self._complete_delay -= timedelta(
332 microseconds=self._complete_delay.microseconds)
335 self._timestamps.popleft()
338 def ClearLine(self, length):
339 """Clear any characters on the current line
341 Make way for a new line of length 'length', by outputting enough
342 spaces to clear out the old line. Then remember the new length for
346 length: Length of new line, in characters
348 if length < self.last_line_len:
349 Print(' ' * (self.last_line_len - length), newline=False)
350 Print('\r', newline=False)
351 self.last_line_len = length
354 def SelectCommit(self, commit, checkout=True):
355 """Checkout the selected commit for this build
358 if checkout and self.checkout:
359 gitutil.Checkout(commit.hash)
361 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
365 commit: Commit object that is being built
366 brd: Board object that is being built
367 stage: Stage that we are at (mrproper, config, build)
368 cwd: Directory where make should be run
369 args: Arguments to pass to make
370 kwargs: Arguments to pass to command.RunPipe()
372 cmd = [self.gnu_make] + list(args)
373 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
374 cwd=cwd, raise_on_error=False, **kwargs)
375 if self.verbose_build:
376 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
377 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
380 def ProcessResult(self, result):
381 """Process the result of a build, showing progress information
384 result: A CommandResult object, which indicates the result for
387 col = terminal.Color()
389 target = result.brd.target
392 if result.return_code != 0:
396 if result.already_done:
397 self.already_done += 1
399 Print('\r', newline=False)
401 boards_selected = {target : result.brd}
402 self.ResetResultSummary(boards_selected)
403 self.ProduceResultSummary(result.commit_upto, self.commits,
406 target = '(starting)'
408 # Display separate counts for ok, warned and fail
409 ok = self.upto - self.warned - self.fail
410 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
411 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
412 line += self.col.Color(self.col.RED, '%5d' % self.fail)
414 name = ' /%-5d ' % self.count
416 # Add our current completion time estimate
418 if self._complete_delay:
419 name += '%s : ' % self._complete_delay
420 # When building all boards for a commit, we can print a commit
422 if result and result.commit_upto is None:
423 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
427 Print(line + name, newline=False)
428 length = 14 + len(name)
429 self.ClearLine(length)
431 def _GetOutputDir(self, commit_upto):
432 """Get the name of the output directory for a commit number
434 The output directory is typically .../<branch>/<commit>.
437 commit_upto: Commit number to use (0..self.count-1)
441 commit = self.commits[commit_upto]
442 subject = commit.subject.translate(trans_valid_chars)
443 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
444 self.commit_count, commit.hash, subject[:20]))
445 elif not self.no_subdirs:
446 commit_dir = 'current'
449 return os.path.join(self.base_dir, commit_dir)
451 def GetBuildDir(self, commit_upto, target):
452 """Get the name of the build directory for a commit number
454 The build directory is typically .../<branch>/<commit>/<target>.
457 commit_upto: Commit number to use (0..self.count-1)
460 output_dir = self._GetOutputDir(commit_upto)
461 return os.path.join(output_dir, target)
463 def GetDoneFile(self, commit_upto, target):
464 """Get the name of the done file for a commit number
467 commit_upto: Commit number to use (0..self.count-1)
470 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
472 def GetSizesFile(self, commit_upto, target):
473 """Get the name of the sizes file for a commit number
476 commit_upto: Commit number to use (0..self.count-1)
479 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
481 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
482 """Get the name of the funcsizes file for a commit number and ELF file
485 commit_upto: Commit number to use (0..self.count-1)
487 elf_fname: Filename of elf image
489 return os.path.join(self.GetBuildDir(commit_upto, target),
490 '%s.sizes' % elf_fname.replace('/', '-'))
492 def GetObjdumpFile(self, commit_upto, target, elf_fname):
493 """Get the name of the objdump file for a commit number and ELF file
496 commit_upto: Commit number to use (0..self.count-1)
498 elf_fname: Filename of elf image
500 return os.path.join(self.GetBuildDir(commit_upto, target),
501 '%s.objdump' % elf_fname.replace('/', '-'))
503 def GetErrFile(self, commit_upto, target):
504 """Get the name of the err file for a commit number
507 commit_upto: Commit number to use (0..self.count-1)
510 output_dir = self.GetBuildDir(commit_upto, target)
511 return os.path.join(output_dir, 'err')
513 def FilterErrors(self, lines):
514 """Filter out errors in which we have no interest
516 We should probably use map().
519 lines: List of error lines, each a string
521 New list with only interesting lines included
525 if not self.re_make_err.search(line):
526 out_lines.append(line)
529 def ReadFuncSizes(self, fname, fd):
530 """Read function sizes from the output of 'nm'
533 fd: File containing data to read
534 fname: Filename we are reading from (just for errors)
537 Dictionary containing size of each function in bytes, indexed by
541 for line in fd.readlines():
543 size, type, name = line[:-1].split()
545 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
548 # function names begin with '.' on 64-bit powerpc
550 name = 'static.' + name.split('.')[0]
551 sym[name] = sym.get(name, 0) + int(size, 16)
554 def _ProcessConfig(self, fname):
555 """Read in a .config, autoconf.mk or autoconf.h file
557 This function handles all config file types. It ignores comments and
558 any #defines which don't start with CONFIG_.
561 fname: Filename to read
565 key: Config name (e.g. CONFIG_DM)
566 value: Config value (e.g. 1)
569 if os.path.exists(fname):
570 with open(fname) as fd:
573 if line.startswith('#define'):
574 values = line[8:].split(' ', 1)
580 if not key.startswith('CONFIG_'):
582 elif not line or line[0] in ['#', '*', '/']:
585 key, value = line.split('=', 1)
589 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
591 """Work out the outcome of a build.
594 commit_upto: Commit number to check (0..n-1)
595 target: Target board to check
596 read_func_sizes: True to read function size information
597 read_config: True to read .config and autoconf.h files
602 done_file = self.GetDoneFile(commit_upto, target)
603 sizes_file = self.GetSizesFile(commit_upto, target)
607 if os.path.exists(done_file):
608 with open(done_file, 'r') as fd:
609 return_code = int(fd.readline())
611 err_file = self.GetErrFile(commit_upto, target)
612 if os.path.exists(err_file):
613 with open(err_file, 'r') as fd:
614 err_lines = self.FilterErrors(fd.readlines())
616 # Decide whether the build was ok, failed or created warnings
624 # Convert size information to our simple format
625 if os.path.exists(sizes_file):
626 with open(sizes_file, 'r') as fd:
627 for line in fd.readlines():
628 values = line.split()
631 rodata = int(values[6], 16)
633 'all' : int(values[0]) + int(values[1]) +
635 'text' : int(values[0]) - rodata,
636 'data' : int(values[1]),
637 'bss' : int(values[2]),
640 sizes[values[5]] = size_dict
643 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
644 for fname in glob.glob(pattern):
645 with open(fname, 'r') as fd:
646 dict_name = os.path.basename(fname).replace('.sizes',
648 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
651 output_dir = self.GetBuildDir(commit_upto, target)
652 for name in CONFIG_FILENAMES:
653 fname = os.path.join(output_dir, name)
654 config[name] = self._ProcessConfig(fname)
656 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
658 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
660 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
662 """Calculate a summary of the results of building a commit.
665 board_selected: Dict containing boards to summarise
666 commit_upto: Commit number to summarize (0..self.count-1)
667 read_func_sizes: True to read function size information
668 read_config: True to read .config and autoconf.h files
672 Dict containing boards which passed building this commit.
673 keyed by board.target
674 List containing a summary of error lines
675 Dict keyed by error line, containing a list of the Board
676 objects with that error
677 List containing a summary of warning lines
678 Dict keyed by error line, containing a list of the Board
679 objects with that warning
680 Dictionary keyed by board.target. Each value is a dictionary:
681 key: filename - e.g. '.config'
682 value is itself a dictionary:
686 def AddLine(lines_summary, lines_boards, line, board):
688 if line in lines_boards:
689 lines_boards[line].append(board)
691 lines_boards[line] = [board]
692 lines_summary.append(line)
695 err_lines_summary = []
696 err_lines_boards = {}
697 warn_lines_summary = []
698 warn_lines_boards = {}
701 for board in boards_selected.itervalues():
702 outcome = self.GetBuildOutcome(commit_upto, board.target,
703 read_func_sizes, read_config)
704 board_dict[board.target] = outcome
706 last_was_warning = False
707 for line in outcome.err_lines:
709 if (self._re_function.match(line) or
710 self._re_files.match(line)):
713 is_warning = self._re_warning.match(line)
714 is_note = self._re_note.match(line)
715 if is_warning or (last_was_warning and is_note):
717 AddLine(warn_lines_summary, warn_lines_boards,
719 AddLine(warn_lines_summary, warn_lines_boards,
723 AddLine(err_lines_summary, err_lines_boards,
725 AddLine(err_lines_summary, err_lines_boards,
727 last_was_warning = is_warning
729 tconfig = Config(board.target)
730 for fname in CONFIG_FILENAMES:
732 for key, value in outcome.config[fname].iteritems():
733 tconfig.Add(fname, key, value)
734 config[board.target] = tconfig
736 return (board_dict, err_lines_summary, err_lines_boards,
737 warn_lines_summary, warn_lines_boards, config)
739 def AddOutcome(self, board_dict, arch_list, changes, char, color):
740 """Add an output to our list of outcomes for each architecture
742 This simple function adds failing boards (changes) to the
743 relevant architecture string, so we can print the results out
744 sorted by architecture.
747 board_dict: Dict containing all boards
748 arch_list: Dict keyed by arch name. Value is a string containing
749 a list of board names which failed for that arch.
750 changes: List of boards to add to arch_list
751 color: terminal.Colour object
754 for target in changes:
755 if target in board_dict:
756 arch = board_dict[target].arch
759 str = self.col.Color(color, ' ' + target)
760 if not arch in done_arch:
761 str = ' %s %s' % (self.col.Color(color, char), str)
762 done_arch[arch] = True
763 if not arch in arch_list:
764 arch_list[arch] = str
766 arch_list[arch] += str
769 def ColourNum(self, num):
770 color = self.col.RED if num > 0 else self.col.GREEN
773 return self.col.Color(color, str(num))
775 def ResetResultSummary(self, board_selected):
776 """Reset the results summary ready for use.
778 Set up the base board list to be all those selected, and set the
779 error lines to empty.
781 Following this, calls to PrintResultSummary() will use this
782 information to work out what has changed.
785 board_selected: Dict containing boards to summarise, keyed by
788 self._base_board_dict = {}
789 for board in board_selected:
790 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
791 self._base_err_lines = []
792 self._base_warn_lines = []
793 self._base_err_line_boards = {}
794 self._base_warn_line_boards = {}
795 self._base_config = None
797 def PrintFuncSizeDetail(self, fname, old, new):
798 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
799 delta, common = [], {}
806 if name not in common:
809 delta.append([-old[name], name])
812 if name not in common:
815 delta.append([new[name], name])
818 diff = new.get(name, 0) - old.get(name, 0)
820 grow, up = grow + 1, up + diff
822 shrink, down = shrink + 1, down - diff
823 delta.append([diff, name])
828 args = [add, -remove, grow, -shrink, up, -down, up - down]
831 args = [self.ColourNum(x) for x in args]
833 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
834 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
835 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
837 for diff, name in delta:
839 color = self.col.RED if diff > 0 else self.col.GREEN
840 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
841 old.get(name, '-'), new.get(name,'-'), diff)
842 Print(msg, colour=color)
845 def PrintSizeDetail(self, target_list, show_bloat):
846 """Show details size information for each board
849 target_list: List of targets, each a dict containing:
850 'target': Target name
851 'total_diff': Total difference in bytes across all areas
852 <part_name>: Difference for that part
853 show_bloat: Show detail for each function
855 targets_by_diff = sorted(target_list, reverse=True,
856 key=lambda x: x['_total_diff'])
857 for result in targets_by_diff:
858 printed_target = False
859 for name in sorted(result):
861 if name.startswith('_'):
864 color = self.col.RED if diff > 0 else self.col.GREEN
865 msg = ' %s %+d' % (name, diff)
866 if not printed_target:
867 Print('%10s %-15s:' % ('', result['_target']),
869 printed_target = True
870 Print(msg, colour=color, newline=False)
874 target = result['_target']
875 outcome = result['_outcome']
876 base_outcome = self._base_board_dict[target]
877 for fname in outcome.func_sizes:
878 self.PrintFuncSizeDetail(fname,
879 base_outcome.func_sizes[fname],
880 outcome.func_sizes[fname])
883 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
885 """Print a summary of image sizes broken down by section.
887 The summary takes the form of one line per architecture. The
888 line contains deltas for each of the sections (+ means the section
889 got bigger, - means smaller). The nunmbers are the average number
890 of bytes that a board in this section increased by.
893 powerpc: (622 boards) text -0.0
894 arm: (285 boards) text -0.0
895 nds32: (3 boards) text -8.0
898 board_selected: Dict containing boards to summarise, keyed by
900 board_dict: Dict containing boards for which we built this
901 commit, keyed by board.target. The value is an Outcome object.
902 show_detail: Show detail for each board
903 show_bloat: Show detail for each function
908 # Calculate changes in size for different image parts
909 # The previous sizes are in Board.sizes, for each board
910 for target in board_dict:
911 if target not in board_selected:
913 base_sizes = self._base_board_dict[target].sizes
914 outcome = board_dict[target]
915 sizes = outcome.sizes
917 # Loop through the list of images, creating a dict of size
918 # changes for each image/part. We end up with something like
919 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
920 # which means that U-Boot data increased by 5 bytes and SPL
921 # text decreased by 4.
922 err = {'_target' : target}
924 if image in base_sizes:
925 base_image = base_sizes[image]
926 # Loop through the text, data, bss parts
927 for part in sorted(sizes[image]):
928 diff = sizes[image][part] - base_image[part]
931 if image == 'u-boot':
934 name = image + ':' + part
936 arch = board_selected[target].arch
937 if not arch in arch_count:
940 arch_count[arch] += 1
942 pass # Only add to our list when we have some stats
943 elif not arch in arch_list:
944 arch_list[arch] = [err]
946 arch_list[arch].append(err)
948 # We now have a list of image size changes sorted by arch
949 # Print out a summary of these
950 for arch, target_list in arch_list.iteritems():
951 # Get total difference for each type
953 for result in target_list:
955 for name, diff in result.iteritems():
956 if name.startswith('_'):
963 result['_total_diff'] = total
964 result['_outcome'] = board_dict[result['_target']]
966 count = len(target_list)
968 for name in sorted(totals):
971 # Display the average difference in this name for this
973 avg_diff = float(diff) / count
974 color = self.col.RED if avg_diff > 0 else self.col.GREEN
975 msg = ' %s %+1.1f' % (name, avg_diff)
977 Print('%10s: (for %d/%d boards)' % (arch, count,
978 arch_count[arch]), newline=False)
980 Print(msg, colour=color, newline=False)
985 self.PrintSizeDetail(target_list, show_bloat)
988 def PrintResultSummary(self, board_selected, board_dict, err_lines,
989 err_line_boards, warn_lines, warn_line_boards,
990 config, show_sizes, show_detail, show_bloat,
992 """Compare results with the base results and display delta.
994 Only boards mentioned in board_selected will be considered. This
995 function is intended to be called repeatedly with the results of
996 each commit. It therefore shows a 'diff' between what it saw in
997 the last call and what it sees now.
1000 board_selected: Dict containing boards to summarise, keyed by
1002 board_dict: Dict containing boards for which we built this
1003 commit, keyed by board.target. The value is an Outcome object.
1004 err_lines: A list of errors for this commit, or [] if there is
1005 none, or we don't want to print errors
1006 err_line_boards: Dict keyed by error line, containing a list of
1007 the Board objects with that error
1008 warn_lines: A list of warnings for this commit, or [] if there is
1009 none, or we don't want to print errors
1010 warn_line_boards: Dict keyed by warning line, containing a list of
1011 the Board objects with that warning
1012 config: Dictionary keyed by filename - e.g. '.config'. Each
1013 value is itself a dictionary:
1016 show_sizes: Show image size deltas
1017 show_detail: Show detail for each board
1018 show_bloat: Show detail for each function
1019 show_config: Show config changes
1021 def _BoardList(line, line_boards):
1022 """Helper function to get a line of boards containing a line
1025 line: Error line to search for
1027 String containing a list of boards with that error line, or
1028 '' if the user has not requested such a list
1030 if self._list_error_boards:
1032 for board in line_boards[line]:
1033 if not board.target in names:
1034 names.append(board.target)
1035 names_str = '(%s) ' % ','.join(names)
1040 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1045 if line not in base_lines:
1046 worse_lines.append(char + '+' +
1047 _BoardList(line, line_boards) + line)
1048 for line in base_lines:
1049 if line not in lines:
1050 better_lines.append(char + '-' +
1051 _BoardList(line, base_line_boards) + line)
1052 return better_lines, worse_lines
1054 def _CalcConfig(delta, name, config):
1055 """Calculate configuration changes
1058 delta: Type of the delta, e.g. '+'
1059 name: name of the file which changed (e.g. .config)
1060 config: configuration change dictionary
1064 String containing the configuration changes which can be
1068 for key in sorted(config.keys()):
1069 out += '%s=%s ' % (key, config[key])
1070 return '%s %s: %s' % (delta, name, out)
1072 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1073 """Add changes in configuration to a list
1076 lines: list to add to
1077 name: config file name
1078 config_plus: configurations added, dictionary
1081 config_minus: configurations removed, dictionary
1084 config_change: configurations changed, dictionary
1089 lines.append(_CalcConfig('+', name, config_plus))
1091 lines.append(_CalcConfig('-', name, config_minus))
1093 lines.append(_CalcConfig('c', name, config_change))
1095 def _OutputConfigInfo(lines):
1100 col = self.col.GREEN
1101 elif line[0] == '-':
1103 elif line[0] == 'c':
1104 col = self.col.YELLOW
1105 Print(' ' + line, newline=True, colour=col)
1108 better = [] # List of boards fixed since last commit
1109 worse = [] # List of new broken boards since last commit
1110 new = [] # List of boards that didn't exist last time
1111 unknown = [] # List of boards that were not built
1113 for target in board_dict:
1114 if target not in board_selected:
1117 # If the board was built last time, add its outcome to a list
1118 if target in self._base_board_dict:
1119 base_outcome = self._base_board_dict[target].rc
1120 outcome = board_dict[target]
1121 if outcome.rc == OUTCOME_UNKNOWN:
1122 unknown.append(target)
1123 elif outcome.rc < base_outcome:
1124 better.append(target)
1125 elif outcome.rc > base_outcome:
1126 worse.append(target)
1130 # Get a list of errors that have appeared, and disappeared
1131 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1132 self._base_err_line_boards, err_lines, err_line_boards, '')
1133 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1134 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1136 # Display results by arch
1137 if (better or worse or unknown or new or worse_err or better_err
1138 or worse_warn or better_warn):
1140 self.AddOutcome(board_selected, arch_list, better, '',
1142 self.AddOutcome(board_selected, arch_list, worse, '+',
1144 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1145 if self._show_unknown:
1146 self.AddOutcome(board_selected, arch_list, unknown, '?',
1148 for arch, target_list in arch_list.iteritems():
1149 Print('%10s: %s' % (arch, target_list))
1150 self._error_lines += 1
1152 Print('\n'.join(better_err), colour=self.col.GREEN)
1153 self._error_lines += 1
1155 Print('\n'.join(worse_err), colour=self.col.RED)
1156 self._error_lines += 1
1158 Print('\n'.join(better_warn), colour=self.col.CYAN)
1159 self._error_lines += 1
1161 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1162 self._error_lines += 1
1165 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1168 if show_config and self._base_config:
1170 arch_config_plus = {}
1171 arch_config_minus = {}
1172 arch_config_change = {}
1175 for target in board_dict:
1176 if target not in board_selected:
1178 arch = board_selected[target].arch
1179 if arch not in arch_list:
1180 arch_list.append(arch)
1182 for arch in arch_list:
1183 arch_config_plus[arch] = {}
1184 arch_config_minus[arch] = {}
1185 arch_config_change[arch] = {}
1186 for name in CONFIG_FILENAMES:
1187 arch_config_plus[arch][name] = {}
1188 arch_config_minus[arch][name] = {}
1189 arch_config_change[arch][name] = {}
1191 for target in board_dict:
1192 if target not in board_selected:
1195 arch = board_selected[target].arch
1197 all_config_plus = {}
1198 all_config_minus = {}
1199 all_config_change = {}
1200 tbase = self._base_config[target]
1201 tconfig = config[target]
1203 for name in CONFIG_FILENAMES:
1204 if not tconfig.config[name]:
1209 base = tbase.config[name]
1210 for key, value in tconfig.config[name].iteritems():
1212 config_plus[key] = value
1213 all_config_plus[key] = value
1214 for key, value in base.iteritems():
1215 if key not in tconfig.config[name]:
1216 config_minus[key] = value
1217 all_config_minus[key] = value
1218 for key, value in base.iteritems():
1219 new_value = tconfig.config.get(key)
1220 if new_value and value != new_value:
1221 desc = '%s -> %s' % (value, new_value)
1222 config_change[key] = desc
1223 all_config_change[key] = desc
1225 arch_config_plus[arch][name].update(config_plus)
1226 arch_config_minus[arch][name].update(config_minus)
1227 arch_config_change[arch][name].update(config_change)
1229 _AddConfig(lines, name, config_plus, config_minus,
1231 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1233 summary[target] = '\n'.join(lines)
1235 lines_by_target = {}
1236 for target, lines in summary.iteritems():
1237 if lines in lines_by_target:
1238 lines_by_target[lines].append(target)
1240 lines_by_target[lines] = [target]
1242 for arch in arch_list:
1247 for name in CONFIG_FILENAMES:
1248 all_plus.update(arch_config_plus[arch][name])
1249 all_minus.update(arch_config_minus[arch][name])
1250 all_change.update(arch_config_change[arch][name])
1251 _AddConfig(lines, name, arch_config_plus[arch][name],
1252 arch_config_minus[arch][name],
1253 arch_config_change[arch][name])
1254 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1255 #arch_summary[target] = '\n'.join(lines)
1258 _OutputConfigInfo(lines)
1260 for lines, targets in lines_by_target.iteritems():
1263 Print('%s :' % ' '.join(sorted(targets)))
1264 _OutputConfigInfo(lines.split('\n'))
1267 # Save our updated information for the next call to this function
1268 self._base_board_dict = board_dict
1269 self._base_err_lines = err_lines
1270 self._base_warn_lines = warn_lines
1271 self._base_err_line_boards = err_line_boards
1272 self._base_warn_line_boards = warn_line_boards
1273 self._base_config = config
1275 # Get a list of boards that did not get built, if needed
1277 for board in board_selected:
1278 if not board in board_dict:
1279 not_built.append(board)
1281 Print("Boards not built (%d): %s" % (len(not_built),
1282 ', '.join(not_built)))
1284 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1285 (board_dict, err_lines, err_line_boards, warn_lines,
1286 warn_line_boards, config) = self.GetResultSummary(
1287 board_selected, commit_upto,
1288 read_func_sizes=self._show_bloat,
1289 read_config=self._show_config)
1291 msg = '%02d: %s' % (commit_upto + 1,
1292 commits[commit_upto].subject)
1293 Print(msg, colour=self.col.BLUE)
1294 self.PrintResultSummary(board_selected, board_dict,
1295 err_lines if self._show_errors else [], err_line_boards,
1296 warn_lines if self._show_errors else [], warn_line_boards,
1297 config, self._show_sizes, self._show_detail,
1298 self._show_bloat, self._show_config)
1300 def ShowSummary(self, commits, board_selected):
1301 """Show a build summary for U-Boot for a given board list.
1303 Reset the result summary, then repeatedly call GetResultSummary on
1304 each commit's results, then display the differences we see.
1307 commit: Commit objects to summarise
1308 board_selected: Dict containing boards to summarise
1310 self.commit_count = len(commits) if commits else 1
1311 self.commits = commits
1312 self.ResetResultSummary(board_selected)
1313 self._error_lines = 0
1315 for commit_upto in range(0, self.commit_count, self._step):
1316 self.ProduceResultSummary(commit_upto, commits, board_selected)
1317 if not self._error_lines:
1318 Print('(no errors to report)', colour=self.col.GREEN)
1321 def SetupBuild(self, board_selected, commits):
1322 """Set up ready to start a build.
1325 board_selected: Selected boards to build
1326 commits: Selected commits to build
1328 # First work out how many commits we will build
1329 count = (self.commit_count + self._step - 1) / self._step
1330 self.count = len(board_selected) * count
1331 self.upto = self.warned = self.fail = 0
1332 self._timestamps = collections.deque()
1334 def GetThreadDir(self, thread_num):
1335 """Get the directory path to the working dir for a thread.
1338 thread_num: Number of thread to check.
1340 return os.path.join(self._working_dir, '%02d' % thread_num)
1342 def _PrepareThread(self, thread_num, setup_git):
1343 """Prepare the working directory for a thread.
1345 This clones or fetches the repo into the thread's work directory.
1348 thread_num: Thread number (0, 1, ...)
1349 setup_git: True to set up a git repo clone
1351 thread_dir = self.GetThreadDir(thread_num)
1352 builderthread.Mkdir(thread_dir)
1353 git_dir = os.path.join(thread_dir, '.git')
1355 # Clone the repo if it doesn't already exist
1356 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1357 # we have a private index but uses the origin repo's contents?
1358 if setup_git and self.git_dir:
1359 src_dir = os.path.abspath(self.git_dir)
1360 if os.path.exists(git_dir):
1361 gitutil.Fetch(git_dir, thread_dir)
1363 Print('\rCloning repo for thread %d' % thread_num,
1365 gitutil.Clone(src_dir, thread_dir)
1366 Print('\r%s\r' % (' ' * 30), newline=False)
1368 def _PrepareWorkingSpace(self, max_threads, setup_git):
1369 """Prepare the working directory for use.
1371 Set up the git repo for each thread.
1374 max_threads: Maximum number of threads we expect to need.
1375 setup_git: True to set up a git repo clone
1377 builderthread.Mkdir(self._working_dir)
1378 for thread in range(max_threads):
1379 self._PrepareThread(thread, setup_git)
1381 def _PrepareOutputSpace(self):
1382 """Get the output directories ready to receive files.
1384 We delete any output directories which look like ones we need to
1385 create. Having left over directories is confusing when the user wants
1386 to check the output manually.
1388 if not self.commits:
1391 for commit_upto in range(self.commit_count):
1392 dir_list.append(self._GetOutputDir(commit_upto))
1395 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1396 if dirname not in dir_list:
1397 to_remove.append(dirname)
1399 Print('Removing %d old build directories' % len(to_remove),
1401 for dirname in to_remove:
1402 shutil.rmtree(dirname)
1404 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1405 """Build all commits for a list of boards
1408 commits: List of commits to be build, each a Commit object
1409 boards_selected: Dict of selected boards, key is target name,
1410 value is Board object
1411 keep_outputs: True to save build output files
1412 verbose: Display build results as they are completed
1415 - number of boards that failed to build
1416 - number of boards that issued warnings
1418 self.commit_count = len(commits) if commits else 1
1419 self.commits = commits
1420 self._verbose = verbose
1422 self.ResetResultSummary(board_selected)
1423 builderthread.Mkdir(self.base_dir, parents = True)
1424 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1425 commits is not None)
1426 self._PrepareOutputSpace()
1427 Print('\rStarting build...', newline=False)
1428 self.SetupBuild(board_selected, commits)
1429 self.ProcessResult(None)
1431 # Create jobs to build all commits for each board
1432 for brd in board_selected.itervalues():
1433 job = builderthread.BuilderJob()
1435 job.commits = commits
1436 job.keep_outputs = keep_outputs
1437 job.step = self._step
1440 term = threading.Thread(target=self.queue.join)
1441 term.setDaemon(True)
1443 while term.isAlive():
1446 # Wait until we have processed all output
1447 self.out_queue.join()
1450 return (self.fail, self.warned)