1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2013 The Chromium OS Authors.
4 # Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
8 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 (and handle unicode)
98 trans_valid_chars = string.maketrans('/: ', '---')
99 trans_valid_chars = trans_valid_chars.decode('latin-1')
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 warnings_as_errors=False):
216 """Create a new Builder object
219 toolchains: Toolchains object to use for building
220 base_dir: Base directory to use for builder
221 git_dir: Git directory containing source repository
222 num_threads: Number of builder threads to run
223 num_jobs: Number of jobs to run at once (passed to make as -j)
224 gnu_make: the command name of GNU Make.
225 checkout: True to check out source, False to skip that step.
226 This is used for testing.
227 show_unknown: Show unknown boards (those not built) in summary
228 step: 1 to process every commit, n to process every nth commit
229 no_subdirs: Don't create subdirectories when building current
230 source for a single board
231 full_path: Return the full path in CROSS_COMPILE and don't set
233 verbose_build: Run build with V=1 and don't use 'make -s'
234 incremental: Always perform incremental builds; don't run make
235 mrproper when configuring
236 per_board_out_dir: Build in a separate persistent directory per
237 board rather than a thread-specific directory
238 config_only: Only configure each build, don't build it
239 squash_config_y: Convert CONFIG options with the value 'y' to '1'
240 warnings_as_errors: Treat all compiler warnings as errors
242 self.toolchains = toolchains
243 self.base_dir = base_dir
244 self._working_dir = os.path.join(base_dir, '.bm-work')
246 self.do_make = self.Make
247 self.gnu_make = gnu_make
248 self.checkout = checkout
249 self.num_threads = num_threads
250 self.num_jobs = num_jobs
251 self.already_done = 0
252 self.force_build = False
253 self.git_dir = git_dir
254 self._show_unknown = show_unknown
255 self._timestamp_count = 10
256 self._build_period_us = None
257 self._complete_delay = None
258 self._next_delay_update = datetime.now()
259 self.force_config_on_failure = True
260 self.force_build_failures = False
261 self.force_reconfig = False
264 self._error_lines = 0
265 self.no_subdirs = no_subdirs
266 self.full_path = full_path
267 self.verbose_build = verbose_build
268 self.config_only = config_only
269 self.squash_config_y = squash_config_y
270 self.config_filenames = BASE_CONFIG_FILENAMES
271 if not self.squash_config_y:
272 self.config_filenames += EXTRA_CONFIG_FILENAMES
274 self.warnings_as_errors = warnings_as_errors
275 self.col = terminal.Color()
277 self._re_function = re.compile('(.*): In function.*')
278 self._re_files = re.compile('In file included from.*')
279 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
280 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
282 self.queue = Queue.Queue()
283 self.out_queue = Queue.Queue()
284 for i in range(self.num_threads):
285 t = builderthread.BuilderThread(self, i, incremental,
289 self.threads.append(t)
291 self.last_line_len = 0
292 t = builderthread.ResultThread(self)
295 self.threads.append(t)
297 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
298 self.re_make_err = re.compile('|'.join(ignore_lines))
300 # Handle existing graceful with SIGINT / Ctrl-C
301 signal.signal(signal.SIGINT, self.signal_handler)
304 """Get rid of all threads created by the builder"""
305 for t in self.threads:
308 def signal_handler(self, signal, frame):
311 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
312 show_detail=False, show_bloat=False,
313 list_error_boards=False, show_config=False):
314 """Setup display options for the builder.
316 show_errors: True to show summarised error/warning info
317 show_sizes: Show size deltas
318 show_detail: Show detail for each board
319 show_bloat: Show detail for each function
320 list_error_boards: Show the boards which caused each error/warning
321 show_config: Show config deltas
323 self._show_errors = show_errors
324 self._show_sizes = show_sizes
325 self._show_detail = show_detail
326 self._show_bloat = show_bloat
327 self._list_error_boards = list_error_boards
328 self._show_config = show_config
330 def _AddTimestamp(self):
331 """Add a new timestamp to the list and record the build period.
333 The build period is the length of time taken to perform a single
334 build (one board, one commit).
337 self._timestamps.append(now)
338 count = len(self._timestamps)
339 delta = self._timestamps[-1] - self._timestamps[0]
340 seconds = delta.total_seconds()
342 # If we have enough data, estimate build period (time taken for a
343 # single build) and therefore completion time.
344 if count > 1 and self._next_delay_update < now:
345 self._next_delay_update = now + timedelta(seconds=2)
347 self._build_period = float(seconds) / count
348 todo = self.count - self.upto
349 self._complete_delay = timedelta(microseconds=
350 self._build_period * todo * 1000000)
352 self._complete_delay -= timedelta(
353 microseconds=self._complete_delay.microseconds)
356 self._timestamps.popleft()
359 def ClearLine(self, length):
360 """Clear any characters on the current line
362 Make way for a new line of length 'length', by outputting enough
363 spaces to clear out the old line. Then remember the new length for
367 length: Length of new line, in characters
369 if length < self.last_line_len:
370 Print(' ' * (self.last_line_len - length), newline=False)
371 Print('\r', newline=False)
372 self.last_line_len = length
375 def SelectCommit(self, commit, checkout=True):
376 """Checkout the selected commit for this build
379 if checkout and self.checkout:
380 gitutil.Checkout(commit.hash)
382 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
386 commit: Commit object that is being built
387 brd: Board object that is being built
388 stage: Stage that we are at (mrproper, config, build)
389 cwd: Directory where make should be run
390 args: Arguments to pass to make
391 kwargs: Arguments to pass to command.RunPipe()
393 cmd = [self.gnu_make] + list(args)
394 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
395 cwd=cwd, raise_on_error=False, **kwargs)
396 if self.verbose_build:
397 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
398 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
401 def ProcessResult(self, result):
402 """Process the result of a build, showing progress information
405 result: A CommandResult object, which indicates the result for
408 col = terminal.Color()
410 target = result.brd.target
413 if result.return_code != 0:
417 if result.already_done:
418 self.already_done += 1
420 Print('\r', newline=False)
422 boards_selected = {target : result.brd}
423 self.ResetResultSummary(boards_selected)
424 self.ProduceResultSummary(result.commit_upto, self.commits,
427 target = '(starting)'
429 # Display separate counts for ok, warned and fail
430 ok = self.upto - self.warned - self.fail
431 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
432 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
433 line += self.col.Color(self.col.RED, '%5d' % self.fail)
435 name = ' /%-5d ' % self.count
437 # Add our current completion time estimate
439 if self._complete_delay:
440 name += '%s : ' % self._complete_delay
441 # When building all boards for a commit, we can print a commit
443 if result and result.commit_upto is None:
444 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
448 Print(line + name, newline=False)
449 length = 16 + len(name)
450 self.ClearLine(length)
452 def _GetOutputDir(self, commit_upto):
453 """Get the name of the output directory for a commit number
455 The output directory is typically .../<branch>/<commit>.
458 commit_upto: Commit number to use (0..self.count-1)
462 commit = self.commits[commit_upto]
463 subject = commit.subject.translate(trans_valid_chars)
464 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
465 self.commit_count, commit.hash, subject[:20]))
466 elif not self.no_subdirs:
467 commit_dir = 'current'
470 return os.path.join(self.base_dir, commit_dir)
472 def GetBuildDir(self, commit_upto, target):
473 """Get the name of the build directory for a commit number
475 The build directory is typically .../<branch>/<commit>/<target>.
478 commit_upto: Commit number to use (0..self.count-1)
481 output_dir = self._GetOutputDir(commit_upto)
482 return os.path.join(output_dir, target)
484 def GetDoneFile(self, commit_upto, target):
485 """Get the name of the done file for a commit number
488 commit_upto: Commit number to use (0..self.count-1)
491 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
493 def GetSizesFile(self, commit_upto, target):
494 """Get the name of the sizes file for a commit number
497 commit_upto: Commit number to use (0..self.count-1)
500 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
502 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
503 """Get the name of the funcsizes file for a commit number and ELF file
506 commit_upto: Commit number to use (0..self.count-1)
508 elf_fname: Filename of elf image
510 return os.path.join(self.GetBuildDir(commit_upto, target),
511 '%s.sizes' % elf_fname.replace('/', '-'))
513 def GetObjdumpFile(self, commit_upto, target, elf_fname):
514 """Get the name of the objdump file for a commit number and ELF file
517 commit_upto: Commit number to use (0..self.count-1)
519 elf_fname: Filename of elf image
521 return os.path.join(self.GetBuildDir(commit_upto, target),
522 '%s.objdump' % elf_fname.replace('/', '-'))
524 def GetErrFile(self, commit_upto, target):
525 """Get the name of the err file for a commit number
528 commit_upto: Commit number to use (0..self.count-1)
531 output_dir = self.GetBuildDir(commit_upto, target)
532 return os.path.join(output_dir, 'err')
534 def FilterErrors(self, lines):
535 """Filter out errors in which we have no interest
537 We should probably use map().
540 lines: List of error lines, each a string
542 New list with only interesting lines included
546 if not self.re_make_err.search(line):
547 out_lines.append(line)
550 def ReadFuncSizes(self, fname, fd):
551 """Read function sizes from the output of 'nm'
554 fd: File containing data to read
555 fname: Filename we are reading from (just for errors)
558 Dictionary containing size of each function in bytes, indexed by
562 for line in fd.readlines():
564 size, type, name = line[:-1].split()
566 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
569 # function names begin with '.' on 64-bit powerpc
571 name = 'static.' + name.split('.')[0]
572 sym[name] = sym.get(name, 0) + int(size, 16)
575 def _ProcessConfig(self, fname):
576 """Read in a .config, autoconf.mk or autoconf.h file
578 This function handles all config file types. It ignores comments and
579 any #defines which don't start with CONFIG_.
582 fname: Filename to read
586 key: Config name (e.g. CONFIG_DM)
587 value: Config value (e.g. 1)
590 if os.path.exists(fname):
591 with open(fname) as fd:
594 if line.startswith('#define'):
595 values = line[8:].split(' ', 1)
600 value = '1' if self.squash_config_y else ''
601 if not key.startswith('CONFIG_'):
603 elif not line or line[0] in ['#', '*', '/']:
606 key, value = line.split('=', 1)
607 if self.squash_config_y and value == 'y':
612 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
614 """Work out the outcome of a build.
617 commit_upto: Commit number to check (0..n-1)
618 target: Target board to check
619 read_func_sizes: True to read function size information
620 read_config: True to read .config and autoconf.h files
625 done_file = self.GetDoneFile(commit_upto, target)
626 sizes_file = self.GetSizesFile(commit_upto, target)
630 if os.path.exists(done_file):
631 with open(done_file, 'r') as fd:
632 return_code = int(fd.readline())
634 err_file = self.GetErrFile(commit_upto, target)
635 if os.path.exists(err_file):
636 with open(err_file, 'r') as fd:
637 err_lines = self.FilterErrors(fd.readlines())
639 # Decide whether the build was ok, failed or created warnings
647 # Convert size information to our simple format
648 if os.path.exists(sizes_file):
649 with open(sizes_file, 'r') as fd:
650 for line in fd.readlines():
651 values = line.split()
654 rodata = int(values[6], 16)
656 'all' : int(values[0]) + int(values[1]) +
658 'text' : int(values[0]) - rodata,
659 'data' : int(values[1]),
660 'bss' : int(values[2]),
663 sizes[values[5]] = size_dict
666 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
667 for fname in glob.glob(pattern):
668 with open(fname, 'r') as fd:
669 dict_name = os.path.basename(fname).replace('.sizes',
671 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
674 output_dir = self.GetBuildDir(commit_upto, target)
675 for name in self.config_filenames:
676 fname = os.path.join(output_dir, name)
677 config[name] = self._ProcessConfig(fname)
679 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
681 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
683 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
685 """Calculate a summary of the results of building a commit.
688 board_selected: Dict containing boards to summarise
689 commit_upto: Commit number to summarize (0..self.count-1)
690 read_func_sizes: True to read function size information
691 read_config: True to read .config and autoconf.h files
695 Dict containing boards which passed building this commit.
696 keyed by board.target
697 List containing a summary of error lines
698 Dict keyed by error line, containing a list of the Board
699 objects with that error
700 List containing a summary of warning lines
701 Dict keyed by error line, containing a list of the Board
702 objects with that warning
703 Dictionary keyed by board.target. Each value is a dictionary:
704 key: filename - e.g. '.config'
705 value is itself a dictionary:
709 def AddLine(lines_summary, lines_boards, line, board):
711 if line in lines_boards:
712 lines_boards[line].append(board)
714 lines_boards[line] = [board]
715 lines_summary.append(line)
718 err_lines_summary = []
719 err_lines_boards = {}
720 warn_lines_summary = []
721 warn_lines_boards = {}
724 for board in boards_selected.itervalues():
725 outcome = self.GetBuildOutcome(commit_upto, board.target,
726 read_func_sizes, read_config)
727 board_dict[board.target] = outcome
729 last_was_warning = False
730 for line in outcome.err_lines:
732 if (self._re_function.match(line) or
733 self._re_files.match(line)):
736 is_warning = self._re_warning.match(line)
737 is_note = self._re_note.match(line)
738 if is_warning or (last_was_warning and is_note):
740 AddLine(warn_lines_summary, warn_lines_boards,
742 AddLine(warn_lines_summary, warn_lines_boards,
746 AddLine(err_lines_summary, err_lines_boards,
748 AddLine(err_lines_summary, err_lines_boards,
750 last_was_warning = is_warning
752 tconfig = Config(self.config_filenames, board.target)
753 for fname in self.config_filenames:
755 for key, value in outcome.config[fname].iteritems():
756 tconfig.Add(fname, key, value)
757 config[board.target] = tconfig
759 return (board_dict, err_lines_summary, err_lines_boards,
760 warn_lines_summary, warn_lines_boards, config)
762 def AddOutcome(self, board_dict, arch_list, changes, char, color):
763 """Add an output to our list of outcomes for each architecture
765 This simple function adds failing boards (changes) to the
766 relevant architecture string, so we can print the results out
767 sorted by architecture.
770 board_dict: Dict containing all boards
771 arch_list: Dict keyed by arch name. Value is a string containing
772 a list of board names which failed for that arch.
773 changes: List of boards to add to arch_list
774 color: terminal.Colour object
777 for target in changes:
778 if target in board_dict:
779 arch = board_dict[target].arch
782 str = self.col.Color(color, ' ' + target)
783 if not arch in done_arch:
784 str = ' %s %s' % (self.col.Color(color, char), str)
785 done_arch[arch] = True
786 if not arch in arch_list:
787 arch_list[arch] = str
789 arch_list[arch] += str
792 def ColourNum(self, num):
793 color = self.col.RED if num > 0 else self.col.GREEN
796 return self.col.Color(color, str(num))
798 def ResetResultSummary(self, board_selected):
799 """Reset the results summary ready for use.
801 Set up the base board list to be all those selected, and set the
802 error lines to empty.
804 Following this, calls to PrintResultSummary() will use this
805 information to work out what has changed.
808 board_selected: Dict containing boards to summarise, keyed by
811 self._base_board_dict = {}
812 for board in board_selected:
813 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
814 self._base_err_lines = []
815 self._base_warn_lines = []
816 self._base_err_line_boards = {}
817 self._base_warn_line_boards = {}
818 self._base_config = None
820 def PrintFuncSizeDetail(self, fname, old, new):
821 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
822 delta, common = [], {}
829 if name not in common:
832 delta.append([-old[name], name])
835 if name not in common:
838 delta.append([new[name], name])
841 diff = new.get(name, 0) - old.get(name, 0)
843 grow, up = grow + 1, up + diff
845 shrink, down = shrink + 1, down - diff
846 delta.append([diff, name])
851 args = [add, -remove, grow, -shrink, up, -down, up - down]
852 if max(args) == 0 and min(args) == 0:
854 args = [self.ColourNum(x) for x in args]
856 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
857 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
858 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
860 for diff, name in delta:
862 color = self.col.RED if diff > 0 else self.col.GREEN
863 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
864 old.get(name, '-'), new.get(name,'-'), diff)
865 Print(msg, colour=color)
868 def PrintSizeDetail(self, target_list, show_bloat):
869 """Show details size information for each board
872 target_list: List of targets, each a dict containing:
873 'target': Target name
874 'total_diff': Total difference in bytes across all areas
875 <part_name>: Difference for that part
876 show_bloat: Show detail for each function
878 targets_by_diff = sorted(target_list, reverse=True,
879 key=lambda x: x['_total_diff'])
880 for result in targets_by_diff:
881 printed_target = False
882 for name in sorted(result):
884 if name.startswith('_'):
887 color = self.col.RED if diff > 0 else self.col.GREEN
888 msg = ' %s %+d' % (name, diff)
889 if not printed_target:
890 Print('%10s %-15s:' % ('', result['_target']),
892 printed_target = True
893 Print(msg, colour=color, newline=False)
897 target = result['_target']
898 outcome = result['_outcome']
899 base_outcome = self._base_board_dict[target]
900 for fname in outcome.func_sizes:
901 self.PrintFuncSizeDetail(fname,
902 base_outcome.func_sizes[fname],
903 outcome.func_sizes[fname])
906 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
908 """Print a summary of image sizes broken down by section.
910 The summary takes the form of one line per architecture. The
911 line contains deltas for each of the sections (+ means the section
912 got bigger, - means smaller). The nunmbers are the average number
913 of bytes that a board in this section increased by.
916 powerpc: (622 boards) text -0.0
917 arm: (285 boards) text -0.0
918 nds32: (3 boards) text -8.0
921 board_selected: Dict containing boards to summarise, keyed by
923 board_dict: Dict containing boards for which we built this
924 commit, keyed by board.target. The value is an Outcome object.
925 show_detail: Show detail for each board
926 show_bloat: Show detail for each function
931 # Calculate changes in size for different image parts
932 # The previous sizes are in Board.sizes, for each board
933 for target in board_dict:
934 if target not in board_selected:
936 base_sizes = self._base_board_dict[target].sizes
937 outcome = board_dict[target]
938 sizes = outcome.sizes
940 # Loop through the list of images, creating a dict of size
941 # changes for each image/part. We end up with something like
942 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
943 # which means that U-Boot data increased by 5 bytes and SPL
944 # text decreased by 4.
945 err = {'_target' : target}
947 if image in base_sizes:
948 base_image = base_sizes[image]
949 # Loop through the text, data, bss parts
950 for part in sorted(sizes[image]):
951 diff = sizes[image][part] - base_image[part]
954 if image == 'u-boot':
957 name = image + ':' + part
959 arch = board_selected[target].arch
960 if not arch in arch_count:
963 arch_count[arch] += 1
965 pass # Only add to our list when we have some stats
966 elif not arch in arch_list:
967 arch_list[arch] = [err]
969 arch_list[arch].append(err)
971 # We now have a list of image size changes sorted by arch
972 # Print out a summary of these
973 for arch, target_list in arch_list.iteritems():
974 # Get total difference for each type
976 for result in target_list:
978 for name, diff in result.iteritems():
979 if name.startswith('_'):
986 result['_total_diff'] = total
987 result['_outcome'] = board_dict[result['_target']]
989 count = len(target_list)
991 for name in sorted(totals):
994 # Display the average difference in this name for this
996 avg_diff = float(diff) / count
997 color = self.col.RED if avg_diff > 0 else self.col.GREEN
998 msg = ' %s %+1.1f' % (name, avg_diff)
1000 Print('%10s: (for %d/%d boards)' % (arch, count,
1001 arch_count[arch]), newline=False)
1003 Print(msg, colour=color, newline=False)
1008 self.PrintSizeDetail(target_list, show_bloat)
1011 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1012 err_line_boards, warn_lines, warn_line_boards,
1013 config, show_sizes, show_detail, show_bloat,
1015 """Compare results with the base results and display delta.
1017 Only boards mentioned in board_selected will be considered. This
1018 function is intended to be called repeatedly with the results of
1019 each commit. It therefore shows a 'diff' between what it saw in
1020 the last call and what it sees now.
1023 board_selected: Dict containing boards to summarise, keyed by
1025 board_dict: Dict containing boards for which we built this
1026 commit, keyed by board.target. The value is an Outcome object.
1027 err_lines: A list of errors for this commit, or [] if there is
1028 none, or we don't want to print errors
1029 err_line_boards: Dict keyed by error line, containing a list of
1030 the Board objects with that error
1031 warn_lines: A list of warnings for this commit, or [] if there is
1032 none, or we don't want to print errors
1033 warn_line_boards: Dict keyed by warning line, containing a list of
1034 the Board objects with that warning
1035 config: Dictionary keyed by filename - e.g. '.config'. Each
1036 value is itself a dictionary:
1039 show_sizes: Show image size deltas
1040 show_detail: Show detail for each board
1041 show_bloat: Show detail for each function
1042 show_config: Show config changes
1044 def _BoardList(line, line_boards):
1045 """Helper function to get a line of boards containing a line
1048 line: Error line to search for
1050 String containing a list of boards with that error line, or
1051 '' if the user has not requested such a list
1053 if self._list_error_boards:
1055 for board in line_boards[line]:
1056 if not board.target in names:
1057 names.append(board.target)
1058 names_str = '(%s) ' % ','.join(names)
1063 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1068 if line not in base_lines:
1069 worse_lines.append(char + '+' +
1070 _BoardList(line, line_boards) + line)
1071 for line in base_lines:
1072 if line not in lines:
1073 better_lines.append(char + '-' +
1074 _BoardList(line, base_line_boards) + line)
1075 return better_lines, worse_lines
1077 def _CalcConfig(delta, name, config):
1078 """Calculate configuration changes
1081 delta: Type of the delta, e.g. '+'
1082 name: name of the file which changed (e.g. .config)
1083 config: configuration change dictionary
1087 String containing the configuration changes which can be
1091 for key in sorted(config.keys()):
1092 out += '%s=%s ' % (key, config[key])
1093 return '%s %s: %s' % (delta, name, out)
1095 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1096 """Add changes in configuration to a list
1099 lines: list to add to
1100 name: config file name
1101 config_plus: configurations added, dictionary
1104 config_minus: configurations removed, dictionary
1107 config_change: configurations changed, dictionary
1112 lines.append(_CalcConfig('+', name, config_plus))
1114 lines.append(_CalcConfig('-', name, config_minus))
1116 lines.append(_CalcConfig('c', name, config_change))
1118 def _OutputConfigInfo(lines):
1123 col = self.col.GREEN
1124 elif line[0] == '-':
1126 elif line[0] == 'c':
1127 col = self.col.YELLOW
1128 Print(' ' + line, newline=True, colour=col)
1131 better = [] # List of boards fixed since last commit
1132 worse = [] # List of new broken boards since last commit
1133 new = [] # List of boards that didn't exist last time
1134 unknown = [] # List of boards that were not built
1136 for target in board_dict:
1137 if target not in board_selected:
1140 # If the board was built last time, add its outcome to a list
1141 if target in self._base_board_dict:
1142 base_outcome = self._base_board_dict[target].rc
1143 outcome = board_dict[target]
1144 if outcome.rc == OUTCOME_UNKNOWN:
1145 unknown.append(target)
1146 elif outcome.rc < base_outcome:
1147 better.append(target)
1148 elif outcome.rc > base_outcome:
1149 worse.append(target)
1153 # Get a list of errors that have appeared, and disappeared
1154 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1155 self._base_err_line_boards, err_lines, err_line_boards, '')
1156 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1157 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1159 # Display results by arch
1160 if (better or worse or unknown or new or worse_err or better_err
1161 or worse_warn or better_warn):
1163 self.AddOutcome(board_selected, arch_list, better, '',
1165 self.AddOutcome(board_selected, arch_list, worse, '+',
1167 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1168 if self._show_unknown:
1169 self.AddOutcome(board_selected, arch_list, unknown, '?',
1171 for arch, target_list in arch_list.iteritems():
1172 Print('%10s: %s' % (arch, target_list))
1173 self._error_lines += 1
1175 Print('\n'.join(better_err), colour=self.col.GREEN)
1176 self._error_lines += 1
1178 Print('\n'.join(worse_err), colour=self.col.RED)
1179 self._error_lines += 1
1181 Print('\n'.join(better_warn), colour=self.col.CYAN)
1182 self._error_lines += 1
1184 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1185 self._error_lines += 1
1188 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1191 if show_config and self._base_config:
1193 arch_config_plus = {}
1194 arch_config_minus = {}
1195 arch_config_change = {}
1198 for target in board_dict:
1199 if target not in board_selected:
1201 arch = board_selected[target].arch
1202 if arch not in arch_list:
1203 arch_list.append(arch)
1205 for arch in arch_list:
1206 arch_config_plus[arch] = {}
1207 arch_config_minus[arch] = {}
1208 arch_config_change[arch] = {}
1209 for name in self.config_filenames:
1210 arch_config_plus[arch][name] = {}
1211 arch_config_minus[arch][name] = {}
1212 arch_config_change[arch][name] = {}
1214 for target in board_dict:
1215 if target not in board_selected:
1218 arch = board_selected[target].arch
1220 all_config_plus = {}
1221 all_config_minus = {}
1222 all_config_change = {}
1223 tbase = self._base_config[target]
1224 tconfig = config[target]
1226 for name in self.config_filenames:
1227 if not tconfig.config[name]:
1232 base = tbase.config[name]
1233 for key, value in tconfig.config[name].iteritems():
1235 config_plus[key] = value
1236 all_config_plus[key] = value
1237 for key, value in base.iteritems():
1238 if key not in tconfig.config[name]:
1239 config_minus[key] = value
1240 all_config_minus[key] = value
1241 for key, value in base.iteritems():
1242 new_value = tconfig.config.get(key)
1243 if new_value and value != new_value:
1244 desc = '%s -> %s' % (value, new_value)
1245 config_change[key] = desc
1246 all_config_change[key] = desc
1248 arch_config_plus[arch][name].update(config_plus)
1249 arch_config_minus[arch][name].update(config_minus)
1250 arch_config_change[arch][name].update(config_change)
1252 _AddConfig(lines, name, config_plus, config_minus,
1254 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1256 summary[target] = '\n'.join(lines)
1258 lines_by_target = {}
1259 for target, lines in summary.iteritems():
1260 if lines in lines_by_target:
1261 lines_by_target[lines].append(target)
1263 lines_by_target[lines] = [target]
1265 for arch in arch_list:
1270 for name in self.config_filenames:
1271 all_plus.update(arch_config_plus[arch][name])
1272 all_minus.update(arch_config_minus[arch][name])
1273 all_change.update(arch_config_change[arch][name])
1274 _AddConfig(lines, name, arch_config_plus[arch][name],
1275 arch_config_minus[arch][name],
1276 arch_config_change[arch][name])
1277 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1278 #arch_summary[target] = '\n'.join(lines)
1281 _OutputConfigInfo(lines)
1283 for lines, targets in lines_by_target.iteritems():
1286 Print('%s :' % ' '.join(sorted(targets)))
1287 _OutputConfigInfo(lines.split('\n'))
1290 # Save our updated information for the next call to this function
1291 self._base_board_dict = board_dict
1292 self._base_err_lines = err_lines
1293 self._base_warn_lines = warn_lines
1294 self._base_err_line_boards = err_line_boards
1295 self._base_warn_line_boards = warn_line_boards
1296 self._base_config = config
1298 # Get a list of boards that did not get built, if needed
1300 for board in board_selected:
1301 if not board in board_dict:
1302 not_built.append(board)
1304 Print("Boards not built (%d): %s" % (len(not_built),
1305 ', '.join(not_built)))
1307 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1308 (board_dict, err_lines, err_line_boards, warn_lines,
1309 warn_line_boards, config) = self.GetResultSummary(
1310 board_selected, commit_upto,
1311 read_func_sizes=self._show_bloat,
1312 read_config=self._show_config)
1314 msg = '%02d: %s' % (commit_upto + 1,
1315 commits[commit_upto].subject)
1316 Print(msg, colour=self.col.BLUE)
1317 self.PrintResultSummary(board_selected, board_dict,
1318 err_lines if self._show_errors else [], err_line_boards,
1319 warn_lines if self._show_errors else [], warn_line_boards,
1320 config, self._show_sizes, self._show_detail,
1321 self._show_bloat, self._show_config)
1323 def ShowSummary(self, commits, board_selected):
1324 """Show a build summary for U-Boot for a given board list.
1326 Reset the result summary, then repeatedly call GetResultSummary on
1327 each commit's results, then display the differences we see.
1330 commit: Commit objects to summarise
1331 board_selected: Dict containing boards to summarise
1333 self.commit_count = len(commits) if commits else 1
1334 self.commits = commits
1335 self.ResetResultSummary(board_selected)
1336 self._error_lines = 0
1338 for commit_upto in range(0, self.commit_count, self._step):
1339 self.ProduceResultSummary(commit_upto, commits, board_selected)
1340 if not self._error_lines:
1341 Print('(no errors to report)', colour=self.col.GREEN)
1344 def SetupBuild(self, board_selected, commits):
1345 """Set up ready to start a build.
1348 board_selected: Selected boards to build
1349 commits: Selected commits to build
1351 # First work out how many commits we will build
1352 count = (self.commit_count + self._step - 1) / self._step
1353 self.count = len(board_selected) * count
1354 self.upto = self.warned = self.fail = 0
1355 self._timestamps = collections.deque()
1357 def GetThreadDir(self, thread_num):
1358 """Get the directory path to the working dir for a thread.
1361 thread_num: Number of thread to check.
1363 return os.path.join(self._working_dir, '%02d' % thread_num)
1365 def _PrepareThread(self, thread_num, setup_git):
1366 """Prepare the working directory for a thread.
1368 This clones or fetches the repo into the thread's work directory.
1371 thread_num: Thread number (0, 1, ...)
1372 setup_git: True to set up a git repo clone
1374 thread_dir = self.GetThreadDir(thread_num)
1375 builderthread.Mkdir(thread_dir)
1376 git_dir = os.path.join(thread_dir, '.git')
1378 # Clone the repo if it doesn't already exist
1379 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1380 # we have a private index but uses the origin repo's contents?
1381 if setup_git and self.git_dir:
1382 src_dir = os.path.abspath(self.git_dir)
1383 if os.path.exists(git_dir):
1384 gitutil.Fetch(git_dir, thread_dir)
1386 Print('\rCloning repo for thread %d' % thread_num,
1388 gitutil.Clone(src_dir, thread_dir)
1389 Print('\r%s\r' % (' ' * 30), newline=False)
1391 def _PrepareWorkingSpace(self, max_threads, setup_git):
1392 """Prepare the working directory for use.
1394 Set up the git repo for each thread.
1397 max_threads: Maximum number of threads we expect to need.
1398 setup_git: True to set up a git repo clone
1400 builderthread.Mkdir(self._working_dir)
1401 for thread in range(max_threads):
1402 self._PrepareThread(thread, setup_git)
1404 def _PrepareOutputSpace(self):
1405 """Get the output directories ready to receive files.
1407 We delete any output directories which look like ones we need to
1408 create. Having left over directories is confusing when the user wants
1409 to check the output manually.
1411 if not self.commits:
1414 for commit_upto in range(self.commit_count):
1415 dir_list.append(self._GetOutputDir(commit_upto))
1418 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1419 if dirname not in dir_list:
1420 to_remove.append(dirname)
1422 Print('Removing %d old build directories' % len(to_remove),
1424 for dirname in to_remove:
1425 shutil.rmtree(dirname)
1427 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1428 """Build all commits for a list of boards
1431 commits: List of commits to be build, each a Commit object
1432 boards_selected: Dict of selected boards, key is target name,
1433 value is Board object
1434 keep_outputs: True to save build output files
1435 verbose: Display build results as they are completed
1438 - number of boards that failed to build
1439 - number of boards that issued warnings
1441 self.commit_count = len(commits) if commits else 1
1442 self.commits = commits
1443 self._verbose = verbose
1445 self.ResetResultSummary(board_selected)
1446 builderthread.Mkdir(self.base_dir, parents = True)
1447 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1448 commits is not None)
1449 self._PrepareOutputSpace()
1450 Print('\rStarting build...', newline=False)
1451 self.SetupBuild(board_selected, commits)
1452 self.ProcessResult(None)
1454 # Create jobs to build all commits for each board
1455 for brd in board_selected.itervalues():
1456 job = builderthread.BuilderJob()
1458 job.commits = commits
1459 job.keep_outputs = keep_outputs
1460 job.step = self._step
1463 term = threading.Thread(target=self.queue.join)
1464 term.setDaemon(True)
1466 while term.isAlive():
1469 # Wait until we have processed all output
1470 self.out_queue.join()
1473 return (self.fail, self.warned)