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
25 from terminal import Print
32 Please see README for user documentation, and you should be familiar with
33 that before trying to make sense of this.
35 Buildman works by keeping the machine as busy as possible, building different
36 commits for different boards on multiple CPUs at once.
38 The source repo (self.git_dir) contains all the commits to be built. Each
39 thread works on a single board at a time. It checks out the first commit,
40 configures it for that board, then builds it. Then it checks out the next
41 commit and builds it (typically without re-configuring). When it runs out
42 of commits, it gets another job from the builder and starts again with that
45 Clearly the builder threads could work either way - they could check out a
46 commit and then built it for all boards. Using separate directories for each
47 commit/board pair they could leave their build product around afterwards
50 The intent behind building a single board for multiple commits, is to make
51 use of incremental builds. Since each commit is built incrementally from
52 the previous one, builds are faster. Reconfiguring for a different board
53 removes all intermediate object files.
55 Many threads can be working at once, but each has its own working directory.
56 When a thread finishes a build, it puts the output files into a result
59 The base directory used by buildman is normally '../<branch>', i.e.
60 a directory higher than the source repository and named after the branch
63 Within the base directory, we have one subdirectory for each commit. Within
64 that is one subdirectory for each board. Within that is the build output for
65 that commit/board combination.
67 Buildman also create working directories for each thread, in a .bm-work/
68 subdirectory in the base dir.
70 As an example, say we are building branch 'us-net' for boards 'sandbox' and
71 'seaboard', and say that us-net has two commits. We will have directories
74 us-net/ base directory
75 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
80 02_of_02_g4ed4ebc_net--Check-tftp-comp/
86 00/ working directory for thread 0 (contains source checkout)
88 01/ working directory for thread 1
91 u-boot/ source directory
95 # Possible build outcomes
96 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
98 # Translate a commit subject into a valid filename
99 trans_valid_chars = string.maketrans("/: ", "---")
101 BASE_CONFIG_FILENAMES = [
102 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
105 EXTRA_CONFIG_FILENAMES = [
106 '.config', '.config-spl', '.config-tpl',
107 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
108 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
112 """Holds information about configuration settings for a board."""
113 def __init__(self, config_filename, target):
116 for fname in config_filename:
117 self.config[fname] = {}
119 def Add(self, fname, key, value):
120 self.config[fname][key] = value
124 for fname in self.config:
125 for key, value in self.config[fname].iteritems():
127 val = val ^ hash(key) & hash(value)
131 """Class for building U-Boot for a particular commit.
133 Public members: (many should ->private)
134 already_done: Number of builds already completed
135 base_dir: Base directory to use for builder
136 checkout: True to check out source, False to skip that step.
137 This is used for testing.
138 col: terminal.Color() object
139 count: Number of commits to build
140 do_make: Method to call to invoke Make
141 fail: Number of builds that failed due to error
142 force_build: Force building even if a build already exists
143 force_config_on_failure: If a commit fails for a board, disable
144 incremental building for the next commit we build for that
145 board, so that we will see all warnings/errors again.
146 force_build_failures: If a previously-built build (i.e. built on
147 a previous run of buildman) is marked as failed, rebuild it.
148 git_dir: Git directory containing source repository
149 last_line_len: Length of the last line we printed (used for erasing
150 it with new progress information)
151 num_jobs: Number of jobs to run at once (passed to make as -j)
152 num_threads: Number of builder threads to run
153 out_queue: Queue of results to process
154 re_make_err: Compiled regular expression for ignore_lines
155 queue: Queue of jobs to run
156 threads: List of active threads
157 toolchains: Toolchains object to use for building
158 upto: Current commit number we are building (0.count-1)
159 warned: Number of builds that produced at least one warning
160 force_reconfig: Reconfigure U-Boot on each comiit. This disables
161 incremental building, where buildman reconfigures on the first
162 commit for a baord, and then just does an incremental build for
163 the following commits. In fact buildman will reconfigure and
164 retry for any failing commits, so generally the only effect of
165 this option is to slow things down.
166 in_tree: Build U-Boot in-tree instead of specifying an output
167 directory separate from the source code. This option is really
168 only useful for testing in-tree builds.
171 _base_board_dict: Last-summarised Dict of boards
172 _base_err_lines: Last-summarised list of errors
173 _base_warn_lines: Last-summarised list of warnings
174 _build_period_us: Time taken for a single build (float object).
175 _complete_delay: Expected delay until completion (timedelta)
176 _next_delay_update: Next time we plan to display a progress update
178 _show_unknown: Show unknown boards (those not built) in summary
179 _timestamps: List of timestamps for the completion of the last
180 last _timestamp_count builds. Each is a datetime object.
181 _timestamp_count: Number of timestamps to keep in our list.
182 _working_dir: Base working directory containing all threads
185 """Records a build outcome for a single make invocation
188 rc: Outcome value (OUTCOME_...)
189 err_lines: List of error lines or [] if none
190 sizes: Dictionary of image size information, keyed by filename
191 - Each value is itself a dictionary containing
192 values for 'text', 'data' and 'bss', being the integer
193 size in bytes of each section.
194 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
195 value is itself a dictionary:
197 value: Size of function in bytes
198 config: Dictionary keyed by filename - e.g. '.config'. Each
199 value is itself a dictionary:
203 def __init__(self, rc, err_lines, sizes, func_sizes, config):
205 self.err_lines = err_lines
207 self.func_sizes = func_sizes
210 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
211 gnu_make='make', checkout=True, show_unknown=True, step=1,
212 no_subdirs=False, full_path=False, verbose_build=False,
213 incremental=False, per_board_out_dir=False,
214 config_only=False, squash_config_y=False):
215 """Create a new Builder object
218 toolchains: Toolchains object to use for building
219 base_dir: Base directory to use for builder
220 git_dir: Git directory containing source repository
221 num_threads: Number of builder threads to run
222 num_jobs: Number of jobs to run at once (passed to make as -j)
223 gnu_make: the command name of GNU Make.
224 checkout: True to check out source, False to skip that step.
225 This is used for testing.
226 show_unknown: Show unknown boards (those not built) in summary
227 step: 1 to process every commit, n to process every nth commit
228 no_subdirs: Don't create subdirectories when building current
229 source for a single board
230 full_path: Return the full path in CROSS_COMPILE and don't set
232 verbose_build: Run build with V=1 and don't use 'make -s'
233 incremental: Always perform incremental builds; don't run make
234 mrproper when configuring
235 per_board_out_dir: Build in a separate persistent directory per
236 board rather than a thread-specific directory
237 config_only: Only configure each build, don't build it
238 squash_config_y: Convert CONFIG options with the value 'y' to '1'
240 self.toolchains = toolchains
241 self.base_dir = base_dir
242 self._working_dir = os.path.join(base_dir, '.bm-work')
244 self.do_make = self.Make
245 self.gnu_make = gnu_make
246 self.checkout = checkout
247 self.num_threads = num_threads
248 self.num_jobs = num_jobs
249 self.already_done = 0
250 self.force_build = False
251 self.git_dir = git_dir
252 self._show_unknown = show_unknown
253 self._timestamp_count = 10
254 self._build_period_us = None
255 self._complete_delay = None
256 self._next_delay_update = datetime.now()
257 self.force_config_on_failure = True
258 self.force_build_failures = False
259 self.force_reconfig = False
262 self._error_lines = 0
263 self.no_subdirs = no_subdirs
264 self.full_path = full_path
265 self.verbose_build = verbose_build
266 self.config_only = config_only
267 self.squash_config_y = squash_config_y
268 self.config_filenames = BASE_CONFIG_FILENAMES
269 if not self.squash_config_y:
270 self.config_filenames += EXTRA_CONFIG_FILENAMES
272 self.col = terminal.Color()
274 self._re_function = re.compile('(.*): In function.*')
275 self._re_files = re.compile('In file included from.*')
276 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
277 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
279 self.queue = Queue.Queue()
280 self.out_queue = Queue.Queue()
281 for i in range(self.num_threads):
282 t = builderthread.BuilderThread(self, i, incremental,
286 self.threads.append(t)
288 self.last_line_len = 0
289 t = builderthread.ResultThread(self)
292 self.threads.append(t)
294 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
295 self.re_make_err = re.compile('|'.join(ignore_lines))
297 # Handle existing graceful with SIGINT / Ctrl-C
298 signal.signal(signal.SIGINT, self.signal_handler)
301 """Get rid of all threads created by the builder"""
302 for t in self.threads:
305 def signal_handler(self, signal, frame):
308 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
309 show_detail=False, show_bloat=False,
310 list_error_boards=False, show_config=False):
311 """Setup display options for the builder.
313 show_errors: True to show summarised error/warning info
314 show_sizes: Show size deltas
315 show_detail: Show detail for each board
316 show_bloat: Show detail for each function
317 list_error_boards: Show the boards which caused each error/warning
318 show_config: Show config deltas
320 self._show_errors = show_errors
321 self._show_sizes = show_sizes
322 self._show_detail = show_detail
323 self._show_bloat = show_bloat
324 self._list_error_boards = list_error_boards
325 self._show_config = show_config
327 def _AddTimestamp(self):
328 """Add a new timestamp to the list and record the build period.
330 The build period is the length of time taken to perform a single
331 build (one board, one commit).
334 self._timestamps.append(now)
335 count = len(self._timestamps)
336 delta = self._timestamps[-1] - self._timestamps[0]
337 seconds = delta.total_seconds()
339 # If we have enough data, estimate build period (time taken for a
340 # single build) and therefore completion time.
341 if count > 1 and self._next_delay_update < now:
342 self._next_delay_update = now + timedelta(seconds=2)
344 self._build_period = float(seconds) / count
345 todo = self.count - self.upto
346 self._complete_delay = timedelta(microseconds=
347 self._build_period * todo * 1000000)
349 self._complete_delay -= timedelta(
350 microseconds=self._complete_delay.microseconds)
353 self._timestamps.popleft()
356 def ClearLine(self, length):
357 """Clear any characters on the current line
359 Make way for a new line of length 'length', by outputting enough
360 spaces to clear out the old line. Then remember the new length for
364 length: Length of new line, in characters
366 if length < self.last_line_len:
367 Print(' ' * (self.last_line_len - length), newline=False)
368 Print('\r', newline=False)
369 self.last_line_len = length
372 def SelectCommit(self, commit, checkout=True):
373 """Checkout the selected commit for this build
376 if checkout and self.checkout:
377 gitutil.Checkout(commit.hash)
379 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
383 commit: Commit object that is being built
384 brd: Board object that is being built
385 stage: Stage that we are at (mrproper, config, build)
386 cwd: Directory where make should be run
387 args: Arguments to pass to make
388 kwargs: Arguments to pass to command.RunPipe()
390 cmd = [self.gnu_make] + list(args)
391 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
392 cwd=cwd, raise_on_error=False, **kwargs)
393 if self.verbose_build:
394 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
395 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
398 def ProcessResult(self, result):
399 """Process the result of a build, showing progress information
402 result: A CommandResult object, which indicates the result for
405 col = terminal.Color()
407 target = result.brd.target
410 if result.return_code != 0:
414 if result.already_done:
415 self.already_done += 1
417 Print('\r', newline=False)
419 boards_selected = {target : result.brd}
420 self.ResetResultSummary(boards_selected)
421 self.ProduceResultSummary(result.commit_upto, self.commits,
424 target = '(starting)'
426 # Display separate counts for ok, warned and fail
427 ok = self.upto - self.warned - self.fail
428 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
429 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
430 line += self.col.Color(self.col.RED, '%5d' % self.fail)
432 name = ' /%-5d ' % self.count
434 # Add our current completion time estimate
436 if self._complete_delay:
437 name += '%s : ' % self._complete_delay
438 # When building all boards for a commit, we can print a commit
440 if result and result.commit_upto is None:
441 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
445 Print(line + name, newline=False)
446 length = 16 + len(name)
447 self.ClearLine(length)
449 def _GetOutputDir(self, commit_upto):
450 """Get the name of the output directory for a commit number
452 The output directory is typically .../<branch>/<commit>.
455 commit_upto: Commit number to use (0..self.count-1)
459 commit = self.commits[commit_upto]
460 subject = commit.subject.translate(trans_valid_chars)
461 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
462 self.commit_count, commit.hash, subject[:20]))
463 elif not self.no_subdirs:
464 commit_dir = 'current'
467 return os.path.join(self.base_dir, commit_dir)
469 def GetBuildDir(self, commit_upto, target):
470 """Get the name of the build directory for a commit number
472 The build directory is typically .../<branch>/<commit>/<target>.
475 commit_upto: Commit number to use (0..self.count-1)
478 output_dir = self._GetOutputDir(commit_upto)
479 return os.path.join(output_dir, target)
481 def GetDoneFile(self, commit_upto, target):
482 """Get the name of the done file for a commit number
485 commit_upto: Commit number to use (0..self.count-1)
488 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
490 def GetSizesFile(self, commit_upto, target):
491 """Get the name of the sizes file for a commit number
494 commit_upto: Commit number to use (0..self.count-1)
497 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
499 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
500 """Get the name of the funcsizes file for a commit number and ELF file
503 commit_upto: Commit number to use (0..self.count-1)
505 elf_fname: Filename of elf image
507 return os.path.join(self.GetBuildDir(commit_upto, target),
508 '%s.sizes' % elf_fname.replace('/', '-'))
510 def GetObjdumpFile(self, commit_upto, target, elf_fname):
511 """Get the name of the objdump file for a commit number and ELF file
514 commit_upto: Commit number to use (0..self.count-1)
516 elf_fname: Filename of elf image
518 return os.path.join(self.GetBuildDir(commit_upto, target),
519 '%s.objdump' % elf_fname.replace('/', '-'))
521 def GetErrFile(self, commit_upto, target):
522 """Get the name of the err file for a commit number
525 commit_upto: Commit number to use (0..self.count-1)
528 output_dir = self.GetBuildDir(commit_upto, target)
529 return os.path.join(output_dir, 'err')
531 def FilterErrors(self, lines):
532 """Filter out errors in which we have no interest
534 We should probably use map().
537 lines: List of error lines, each a string
539 New list with only interesting lines included
543 if not self.re_make_err.search(line):
544 out_lines.append(line)
547 def ReadFuncSizes(self, fname, fd):
548 """Read function sizes from the output of 'nm'
551 fd: File containing data to read
552 fname: Filename we are reading from (just for errors)
555 Dictionary containing size of each function in bytes, indexed by
559 for line in fd.readlines():
561 size, type, name = line[:-1].split()
563 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
566 # function names begin with '.' on 64-bit powerpc
568 name = 'static.' + name.split('.')[0]
569 sym[name] = sym.get(name, 0) + int(size, 16)
572 def _ProcessConfig(self, fname):
573 """Read in a .config, autoconf.mk or autoconf.h file
575 This function handles all config file types. It ignores comments and
576 any #defines which don't start with CONFIG_.
579 fname: Filename to read
583 key: Config name (e.g. CONFIG_DM)
584 value: Config value (e.g. 1)
587 if os.path.exists(fname):
588 with open(fname) as fd:
591 if line.startswith('#define'):
592 values = line[8:].split(' ', 1)
597 value = '1' if self.squash_config_y else ''
598 if not key.startswith('CONFIG_'):
600 elif not line or line[0] in ['#', '*', '/']:
603 key, value = line.split('=', 1)
604 if self.squash_config_y and value == 'y':
609 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
611 """Work out the outcome of a build.
614 commit_upto: Commit number to check (0..n-1)
615 target: Target board to check
616 read_func_sizes: True to read function size information
617 read_config: True to read .config and autoconf.h files
622 done_file = self.GetDoneFile(commit_upto, target)
623 sizes_file = self.GetSizesFile(commit_upto, target)
627 if os.path.exists(done_file):
628 with open(done_file, 'r') as fd:
629 return_code = int(fd.readline())
631 err_file = self.GetErrFile(commit_upto, target)
632 if os.path.exists(err_file):
633 with open(err_file, 'r') as fd:
634 err_lines = self.FilterErrors(fd.readlines())
636 # Decide whether the build was ok, failed or created warnings
644 # Convert size information to our simple format
645 if os.path.exists(sizes_file):
646 with open(sizes_file, 'r') as fd:
647 for line in fd.readlines():
648 values = line.split()
651 rodata = int(values[6], 16)
653 'all' : int(values[0]) + int(values[1]) +
655 'text' : int(values[0]) - rodata,
656 'data' : int(values[1]),
657 'bss' : int(values[2]),
660 sizes[values[5]] = size_dict
663 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
664 for fname in glob.glob(pattern):
665 with open(fname, 'r') as fd:
666 dict_name = os.path.basename(fname).replace('.sizes',
668 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
671 output_dir = self.GetBuildDir(commit_upto, target)
672 for name in self.config_filenames:
673 fname = os.path.join(output_dir, name)
674 config[name] = self._ProcessConfig(fname)
676 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
678 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
680 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
682 """Calculate a summary of the results of building a commit.
685 board_selected: Dict containing boards to summarise
686 commit_upto: Commit number to summarize (0..self.count-1)
687 read_func_sizes: True to read function size information
688 read_config: True to read .config and autoconf.h files
692 Dict containing boards which passed building this commit.
693 keyed by board.target
694 List containing a summary of error lines
695 Dict keyed by error line, containing a list of the Board
696 objects with that error
697 List containing a summary of warning lines
698 Dict keyed by error line, containing a list of the Board
699 objects with that warning
700 Dictionary keyed by board.target. Each value is a dictionary:
701 key: filename - e.g. '.config'
702 value is itself a dictionary:
706 def AddLine(lines_summary, lines_boards, line, board):
708 if line in lines_boards:
709 lines_boards[line].append(board)
711 lines_boards[line] = [board]
712 lines_summary.append(line)
715 err_lines_summary = []
716 err_lines_boards = {}
717 warn_lines_summary = []
718 warn_lines_boards = {}
721 for board in boards_selected.itervalues():
722 outcome = self.GetBuildOutcome(commit_upto, board.target,
723 read_func_sizes, read_config)
724 board_dict[board.target] = outcome
726 last_was_warning = False
727 for line in outcome.err_lines:
729 if (self._re_function.match(line) or
730 self._re_files.match(line)):
733 is_warning = self._re_warning.match(line)
734 is_note = self._re_note.match(line)
735 if is_warning or (last_was_warning and is_note):
737 AddLine(warn_lines_summary, warn_lines_boards,
739 AddLine(warn_lines_summary, warn_lines_boards,
743 AddLine(err_lines_summary, err_lines_boards,
745 AddLine(err_lines_summary, err_lines_boards,
747 last_was_warning = is_warning
749 tconfig = Config(self.config_filenames, board.target)
750 for fname in self.config_filenames:
752 for key, value in outcome.config[fname].iteritems():
753 tconfig.Add(fname, key, value)
754 config[board.target] = tconfig
756 return (board_dict, err_lines_summary, err_lines_boards,
757 warn_lines_summary, warn_lines_boards, config)
759 def AddOutcome(self, board_dict, arch_list, changes, char, color):
760 """Add an output to our list of outcomes for each architecture
762 This simple function adds failing boards (changes) to the
763 relevant architecture string, so we can print the results out
764 sorted by architecture.
767 board_dict: Dict containing all boards
768 arch_list: Dict keyed by arch name. Value is a string containing
769 a list of board names which failed for that arch.
770 changes: List of boards to add to arch_list
771 color: terminal.Colour object
774 for target in changes:
775 if target in board_dict:
776 arch = board_dict[target].arch
779 str = self.col.Color(color, ' ' + target)
780 if not arch in done_arch:
781 str = ' %s %s' % (self.col.Color(color, char), str)
782 done_arch[arch] = True
783 if not arch in arch_list:
784 arch_list[arch] = str
786 arch_list[arch] += str
789 def ColourNum(self, num):
790 color = self.col.RED if num > 0 else self.col.GREEN
793 return self.col.Color(color, str(num))
795 def ResetResultSummary(self, board_selected):
796 """Reset the results summary ready for use.
798 Set up the base board list to be all those selected, and set the
799 error lines to empty.
801 Following this, calls to PrintResultSummary() will use this
802 information to work out what has changed.
805 board_selected: Dict containing boards to summarise, keyed by
808 self._base_board_dict = {}
809 for board in board_selected:
810 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
811 self._base_err_lines = []
812 self._base_warn_lines = []
813 self._base_err_line_boards = {}
814 self._base_warn_line_boards = {}
815 self._base_config = None
817 def PrintFuncSizeDetail(self, fname, old, new):
818 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
819 delta, common = [], {}
826 if name not in common:
829 delta.append([-old[name], name])
832 if name not in common:
835 delta.append([new[name], name])
838 diff = new.get(name, 0) - old.get(name, 0)
840 grow, up = grow + 1, up + diff
842 shrink, down = shrink + 1, down - diff
843 delta.append([diff, name])
848 args = [add, -remove, grow, -shrink, up, -down, up - down]
851 args = [self.ColourNum(x) for x in args]
853 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
854 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
855 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
857 for diff, name in delta:
859 color = self.col.RED if diff > 0 else self.col.GREEN
860 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
861 old.get(name, '-'), new.get(name,'-'), diff)
862 Print(msg, colour=color)
865 def PrintSizeDetail(self, target_list, show_bloat):
866 """Show details size information for each board
869 target_list: List of targets, each a dict containing:
870 'target': Target name
871 'total_diff': Total difference in bytes across all areas
872 <part_name>: Difference for that part
873 show_bloat: Show detail for each function
875 targets_by_diff = sorted(target_list, reverse=True,
876 key=lambda x: x['_total_diff'])
877 for result in targets_by_diff:
878 printed_target = False
879 for name in sorted(result):
881 if name.startswith('_'):
884 color = self.col.RED if diff > 0 else self.col.GREEN
885 msg = ' %s %+d' % (name, diff)
886 if not printed_target:
887 Print('%10s %-15s:' % ('', result['_target']),
889 printed_target = True
890 Print(msg, colour=color, newline=False)
894 target = result['_target']
895 outcome = result['_outcome']
896 base_outcome = self._base_board_dict[target]
897 for fname in outcome.func_sizes:
898 self.PrintFuncSizeDetail(fname,
899 base_outcome.func_sizes[fname],
900 outcome.func_sizes[fname])
903 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
905 """Print a summary of image sizes broken down by section.
907 The summary takes the form of one line per architecture. The
908 line contains deltas for each of the sections (+ means the section
909 got bigger, - means smaller). The nunmbers are the average number
910 of bytes that a board in this section increased by.
913 powerpc: (622 boards) text -0.0
914 arm: (285 boards) text -0.0
915 nds32: (3 boards) text -8.0
918 board_selected: Dict containing boards to summarise, keyed by
920 board_dict: Dict containing boards for which we built this
921 commit, keyed by board.target. The value is an Outcome object.
922 show_detail: Show detail for each board
923 show_bloat: Show detail for each function
928 # Calculate changes in size for different image parts
929 # The previous sizes are in Board.sizes, for each board
930 for target in board_dict:
931 if target not in board_selected:
933 base_sizes = self._base_board_dict[target].sizes
934 outcome = board_dict[target]
935 sizes = outcome.sizes
937 # Loop through the list of images, creating a dict of size
938 # changes for each image/part. We end up with something like
939 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
940 # which means that U-Boot data increased by 5 bytes and SPL
941 # text decreased by 4.
942 err = {'_target' : target}
944 if image in base_sizes:
945 base_image = base_sizes[image]
946 # Loop through the text, data, bss parts
947 for part in sorted(sizes[image]):
948 diff = sizes[image][part] - base_image[part]
951 if image == 'u-boot':
954 name = image + ':' + part
956 arch = board_selected[target].arch
957 if not arch in arch_count:
960 arch_count[arch] += 1
962 pass # Only add to our list when we have some stats
963 elif not arch in arch_list:
964 arch_list[arch] = [err]
966 arch_list[arch].append(err)
968 # We now have a list of image size changes sorted by arch
969 # Print out a summary of these
970 for arch, target_list in arch_list.iteritems():
971 # Get total difference for each type
973 for result in target_list:
975 for name, diff in result.iteritems():
976 if name.startswith('_'):
983 result['_total_diff'] = total
984 result['_outcome'] = board_dict[result['_target']]
986 count = len(target_list)
988 for name in sorted(totals):
991 # Display the average difference in this name for this
993 avg_diff = float(diff) / count
994 color = self.col.RED if avg_diff > 0 else self.col.GREEN
995 msg = ' %s %+1.1f' % (name, avg_diff)
997 Print('%10s: (for %d/%d boards)' % (arch, count,
998 arch_count[arch]), newline=False)
1000 Print(msg, colour=color, newline=False)
1005 self.PrintSizeDetail(target_list, show_bloat)
1008 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1009 err_line_boards, warn_lines, warn_line_boards,
1010 config, show_sizes, show_detail, show_bloat,
1012 """Compare results with the base results and display delta.
1014 Only boards mentioned in board_selected will be considered. This
1015 function is intended to be called repeatedly with the results of
1016 each commit. It therefore shows a 'diff' between what it saw in
1017 the last call and what it sees now.
1020 board_selected: Dict containing boards to summarise, keyed by
1022 board_dict: Dict containing boards for which we built this
1023 commit, keyed by board.target. The value is an Outcome object.
1024 err_lines: A list of errors for this commit, or [] if there is
1025 none, or we don't want to print errors
1026 err_line_boards: Dict keyed by error line, containing a list of
1027 the Board objects with that error
1028 warn_lines: A list of warnings for this commit, or [] if there is
1029 none, or we don't want to print errors
1030 warn_line_boards: Dict keyed by warning line, containing a list of
1031 the Board objects with that warning
1032 config: Dictionary keyed by filename - e.g. '.config'. Each
1033 value is itself a dictionary:
1036 show_sizes: Show image size deltas
1037 show_detail: Show detail for each board
1038 show_bloat: Show detail for each function
1039 show_config: Show config changes
1041 def _BoardList(line, line_boards):
1042 """Helper function to get a line of boards containing a line
1045 line: Error line to search for
1047 String containing a list of boards with that error line, or
1048 '' if the user has not requested such a list
1050 if self._list_error_boards:
1052 for board in line_boards[line]:
1053 if not board.target in names:
1054 names.append(board.target)
1055 names_str = '(%s) ' % ','.join(names)
1060 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1065 if line not in base_lines:
1066 worse_lines.append(char + '+' +
1067 _BoardList(line, line_boards) + line)
1068 for line in base_lines:
1069 if line not in lines:
1070 better_lines.append(char + '-' +
1071 _BoardList(line, base_line_boards) + line)
1072 return better_lines, worse_lines
1074 def _CalcConfig(delta, name, config):
1075 """Calculate configuration changes
1078 delta: Type of the delta, e.g. '+'
1079 name: name of the file which changed (e.g. .config)
1080 config: configuration change dictionary
1084 String containing the configuration changes which can be
1088 for key in sorted(config.keys()):
1089 out += '%s=%s ' % (key, config[key])
1090 return '%s %s: %s' % (delta, name, out)
1092 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1093 """Add changes in configuration to a list
1096 lines: list to add to
1097 name: config file name
1098 config_plus: configurations added, dictionary
1101 config_minus: configurations removed, dictionary
1104 config_change: configurations changed, dictionary
1109 lines.append(_CalcConfig('+', name, config_plus))
1111 lines.append(_CalcConfig('-', name, config_minus))
1113 lines.append(_CalcConfig('c', name, config_change))
1115 def _OutputConfigInfo(lines):
1120 col = self.col.GREEN
1121 elif line[0] == '-':
1123 elif line[0] == 'c':
1124 col = self.col.YELLOW
1125 Print(' ' + line, newline=True, colour=col)
1128 better = [] # List of boards fixed since last commit
1129 worse = [] # List of new broken boards since last commit
1130 new = [] # List of boards that didn't exist last time
1131 unknown = [] # List of boards that were not built
1133 for target in board_dict:
1134 if target not in board_selected:
1137 # If the board was built last time, add its outcome to a list
1138 if target in self._base_board_dict:
1139 base_outcome = self._base_board_dict[target].rc
1140 outcome = board_dict[target]
1141 if outcome.rc == OUTCOME_UNKNOWN:
1142 unknown.append(target)
1143 elif outcome.rc < base_outcome:
1144 better.append(target)
1145 elif outcome.rc > base_outcome:
1146 worse.append(target)
1150 # Get a list of errors that have appeared, and disappeared
1151 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1152 self._base_err_line_boards, err_lines, err_line_boards, '')
1153 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1154 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1156 # Display results by arch
1157 if (better or worse or unknown or new or worse_err or better_err
1158 or worse_warn or better_warn):
1160 self.AddOutcome(board_selected, arch_list, better, '',
1162 self.AddOutcome(board_selected, arch_list, worse, '+',
1164 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1165 if self._show_unknown:
1166 self.AddOutcome(board_selected, arch_list, unknown, '?',
1168 for arch, target_list in arch_list.iteritems():
1169 Print('%10s: %s' % (arch, target_list))
1170 self._error_lines += 1
1172 Print('\n'.join(better_err), colour=self.col.GREEN)
1173 self._error_lines += 1
1175 Print('\n'.join(worse_err), colour=self.col.RED)
1176 self._error_lines += 1
1178 Print('\n'.join(better_warn), colour=self.col.CYAN)
1179 self._error_lines += 1
1181 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1182 self._error_lines += 1
1185 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1188 if show_config and self._base_config:
1190 arch_config_plus = {}
1191 arch_config_minus = {}
1192 arch_config_change = {}
1195 for target in board_dict:
1196 if target not in board_selected:
1198 arch = board_selected[target].arch
1199 if arch not in arch_list:
1200 arch_list.append(arch)
1202 for arch in arch_list:
1203 arch_config_plus[arch] = {}
1204 arch_config_minus[arch] = {}
1205 arch_config_change[arch] = {}
1206 for name in self.config_filenames:
1207 arch_config_plus[arch][name] = {}
1208 arch_config_minus[arch][name] = {}
1209 arch_config_change[arch][name] = {}
1211 for target in board_dict:
1212 if target not in board_selected:
1215 arch = board_selected[target].arch
1217 all_config_plus = {}
1218 all_config_minus = {}
1219 all_config_change = {}
1220 tbase = self._base_config[target]
1221 tconfig = config[target]
1223 for name in self.config_filenames:
1224 if not tconfig.config[name]:
1229 base = tbase.config[name]
1230 for key, value in tconfig.config[name].iteritems():
1232 config_plus[key] = value
1233 all_config_plus[key] = value
1234 for key, value in base.iteritems():
1235 if key not in tconfig.config[name]:
1236 config_minus[key] = value
1237 all_config_minus[key] = value
1238 for key, value in base.iteritems():
1239 new_value = tconfig.config.get(key)
1240 if new_value and value != new_value:
1241 desc = '%s -> %s' % (value, new_value)
1242 config_change[key] = desc
1243 all_config_change[key] = desc
1245 arch_config_plus[arch][name].update(config_plus)
1246 arch_config_minus[arch][name].update(config_minus)
1247 arch_config_change[arch][name].update(config_change)
1249 _AddConfig(lines, name, config_plus, config_minus,
1251 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1253 summary[target] = '\n'.join(lines)
1255 lines_by_target = {}
1256 for target, lines in summary.iteritems():
1257 if lines in lines_by_target:
1258 lines_by_target[lines].append(target)
1260 lines_by_target[lines] = [target]
1262 for arch in arch_list:
1267 for name in self.config_filenames:
1268 all_plus.update(arch_config_plus[arch][name])
1269 all_minus.update(arch_config_minus[arch][name])
1270 all_change.update(arch_config_change[arch][name])
1271 _AddConfig(lines, name, arch_config_plus[arch][name],
1272 arch_config_minus[arch][name],
1273 arch_config_change[arch][name])
1274 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1275 #arch_summary[target] = '\n'.join(lines)
1278 _OutputConfigInfo(lines)
1280 for lines, targets in lines_by_target.iteritems():
1283 Print('%s :' % ' '.join(sorted(targets)))
1284 _OutputConfigInfo(lines.split('\n'))
1287 # Save our updated information for the next call to this function
1288 self._base_board_dict = board_dict
1289 self._base_err_lines = err_lines
1290 self._base_warn_lines = warn_lines
1291 self._base_err_line_boards = err_line_boards
1292 self._base_warn_line_boards = warn_line_boards
1293 self._base_config = config
1295 # Get a list of boards that did not get built, if needed
1297 for board in board_selected:
1298 if not board in board_dict:
1299 not_built.append(board)
1301 Print("Boards not built (%d): %s" % (len(not_built),
1302 ', '.join(not_built)))
1304 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1305 (board_dict, err_lines, err_line_boards, warn_lines,
1306 warn_line_boards, config) = self.GetResultSummary(
1307 board_selected, commit_upto,
1308 read_func_sizes=self._show_bloat,
1309 read_config=self._show_config)
1311 msg = '%02d: %s' % (commit_upto + 1,
1312 commits[commit_upto].subject)
1313 Print(msg, colour=self.col.BLUE)
1314 self.PrintResultSummary(board_selected, board_dict,
1315 err_lines if self._show_errors else [], err_line_boards,
1316 warn_lines if self._show_errors else [], warn_line_boards,
1317 config, self._show_sizes, self._show_detail,
1318 self._show_bloat, self._show_config)
1320 def ShowSummary(self, commits, board_selected):
1321 """Show a build summary for U-Boot for a given board list.
1323 Reset the result summary, then repeatedly call GetResultSummary on
1324 each commit's results, then display the differences we see.
1327 commit: Commit objects to summarise
1328 board_selected: Dict containing boards to summarise
1330 self.commit_count = len(commits) if commits else 1
1331 self.commits = commits
1332 self.ResetResultSummary(board_selected)
1333 self._error_lines = 0
1335 for commit_upto in range(0, self.commit_count, self._step):
1336 self.ProduceResultSummary(commit_upto, commits, board_selected)
1337 if not self._error_lines:
1338 Print('(no errors to report)', colour=self.col.GREEN)
1341 def SetupBuild(self, board_selected, commits):
1342 """Set up ready to start a build.
1345 board_selected: Selected boards to build
1346 commits: Selected commits to build
1348 # First work out how many commits we will build
1349 count = (self.commit_count + self._step - 1) / self._step
1350 self.count = len(board_selected) * count
1351 self.upto = self.warned = self.fail = 0
1352 self._timestamps = collections.deque()
1354 def GetThreadDir(self, thread_num):
1355 """Get the directory path to the working dir for a thread.
1358 thread_num: Number of thread to check.
1360 return os.path.join(self._working_dir, '%02d' % thread_num)
1362 def _PrepareThread(self, thread_num, setup_git):
1363 """Prepare the working directory for a thread.
1365 This clones or fetches the repo into the thread's work directory.
1368 thread_num: Thread number (0, 1, ...)
1369 setup_git: True to set up a git repo clone
1371 thread_dir = self.GetThreadDir(thread_num)
1372 builderthread.Mkdir(thread_dir)
1373 git_dir = os.path.join(thread_dir, '.git')
1375 # Clone the repo if it doesn't already exist
1376 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1377 # we have a private index but uses the origin repo's contents?
1378 if setup_git and self.git_dir:
1379 src_dir = os.path.abspath(self.git_dir)
1380 if os.path.exists(git_dir):
1381 gitutil.Fetch(git_dir, thread_dir)
1383 Print('\rCloning repo for thread %d' % thread_num,
1385 gitutil.Clone(src_dir, thread_dir)
1386 Print('\r%s\r' % (' ' * 30), newline=False)
1388 def _PrepareWorkingSpace(self, max_threads, setup_git):
1389 """Prepare the working directory for use.
1391 Set up the git repo for each thread.
1394 max_threads: Maximum number of threads we expect to need.
1395 setup_git: True to set up a git repo clone
1397 builderthread.Mkdir(self._working_dir)
1398 for thread in range(max_threads):
1399 self._PrepareThread(thread, setup_git)
1401 def _PrepareOutputSpace(self):
1402 """Get the output directories ready to receive files.
1404 We delete any output directories which look like ones we need to
1405 create. Having left over directories is confusing when the user wants
1406 to check the output manually.
1408 if not self.commits:
1411 for commit_upto in range(self.commit_count):
1412 dir_list.append(self._GetOutputDir(commit_upto))
1415 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1416 if dirname not in dir_list:
1417 to_remove.append(dirname)
1419 Print('Removing %d old build directories' % len(to_remove),
1421 for dirname in to_remove:
1422 shutil.rmtree(dirname)
1424 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1425 """Build all commits for a list of boards
1428 commits: List of commits to be build, each a Commit object
1429 boards_selected: Dict of selected boards, key is target name,
1430 value is Board object
1431 keep_outputs: True to save build output files
1432 verbose: Display build results as they are completed
1435 - number of boards that failed to build
1436 - number of boards that issued warnings
1438 self.commit_count = len(commits) if commits else 1
1439 self.commits = commits
1440 self._verbose = verbose
1442 self.ResetResultSummary(board_selected)
1443 builderthread.Mkdir(self.base_dir, parents = True)
1444 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1445 commits is not None)
1446 self._PrepareOutputSpace()
1447 Print('\rStarting build...', newline=False)
1448 self.SetupBuild(board_selected, commits)
1449 self.ProcessResult(None)
1451 # Create jobs to build all commits for each board
1452 for brd in board_selected.itervalues():
1453 job = builderthread.BuilderJob()
1455 job.commits = commits
1456 job.keep_outputs = keep_outputs
1457 job.step = self._step
1460 term = threading.Thread(target=self.queue.join)
1461 term.setDaemon(True)
1463 while term.isAlive():
1466 # Wait until we have processed all output
1467 self.out_queue.join()
1470 return (self.fail, self.warned)