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 (and handle unicode)
99 trans_valid_chars = string.maketrans('/: ', '---')
100 trans_valid_chars = trans_valid_chars.decode('latin-1')
102 BASE_CONFIG_FILENAMES = [
103 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
106 EXTRA_CONFIG_FILENAMES = [
107 '.config', '.config-spl', '.config-tpl',
108 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
109 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
113 """Holds information about configuration settings for a board."""
114 def __init__(self, config_filename, target):
117 for fname in config_filename:
118 self.config[fname] = {}
120 def Add(self, fname, key, value):
121 self.config[fname][key] = value
125 for fname in self.config:
126 for key, value in self.config[fname].iteritems():
128 val = val ^ hash(key) & hash(value)
132 """Class for building U-Boot for a particular commit.
134 Public members: (many should ->private)
135 already_done: Number of builds already completed
136 base_dir: Base directory to use for builder
137 checkout: True to check out source, False to skip that step.
138 This is used for testing.
139 col: terminal.Color() object
140 count: Number of commits to build
141 do_make: Method to call to invoke Make
142 fail: Number of builds that failed due to error
143 force_build: Force building even if a build already exists
144 force_config_on_failure: If a commit fails for a board, disable
145 incremental building for the next commit we build for that
146 board, so that we will see all warnings/errors again.
147 force_build_failures: If a previously-built build (i.e. built on
148 a previous run of buildman) is marked as failed, rebuild it.
149 git_dir: Git directory containing source repository
150 last_line_len: Length of the last line we printed (used for erasing
151 it with new progress information)
152 num_jobs: Number of jobs to run at once (passed to make as -j)
153 num_threads: Number of builder threads to run
154 out_queue: Queue of results to process
155 re_make_err: Compiled regular expression for ignore_lines
156 queue: Queue of jobs to run
157 threads: List of active threads
158 toolchains: Toolchains object to use for building
159 upto: Current commit number we are building (0.count-1)
160 warned: Number of builds that produced at least one warning
161 force_reconfig: Reconfigure U-Boot on each comiit. This disables
162 incremental building, where buildman reconfigures on the first
163 commit for a baord, and then just does an incremental build for
164 the following commits. In fact buildman will reconfigure and
165 retry for any failing commits, so generally the only effect of
166 this option is to slow things down.
167 in_tree: Build U-Boot in-tree instead of specifying an output
168 directory separate from the source code. This option is really
169 only useful for testing in-tree builds.
172 _base_board_dict: Last-summarised Dict of boards
173 _base_err_lines: Last-summarised list of errors
174 _base_warn_lines: Last-summarised list of warnings
175 _build_period_us: Time taken for a single build (float object).
176 _complete_delay: Expected delay until completion (timedelta)
177 _next_delay_update: Next time we plan to display a progress update
179 _show_unknown: Show unknown boards (those not built) in summary
180 _timestamps: List of timestamps for the completion of the last
181 last _timestamp_count builds. Each is a datetime object.
182 _timestamp_count: Number of timestamps to keep in our list.
183 _working_dir: Base working directory containing all threads
186 """Records a build outcome for a single make invocation
189 rc: Outcome value (OUTCOME_...)
190 err_lines: List of error lines or [] if none
191 sizes: Dictionary of image size information, keyed by filename
192 - Each value is itself a dictionary containing
193 values for 'text', 'data' and 'bss', being the integer
194 size in bytes of each section.
195 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
196 value is itself a dictionary:
198 value: Size of function in bytes
199 config: Dictionary keyed by filename - e.g. '.config'. Each
200 value is itself a dictionary:
204 def __init__(self, rc, err_lines, sizes, func_sizes, config):
206 self.err_lines = err_lines
208 self.func_sizes = func_sizes
211 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
212 gnu_make='make', checkout=True, show_unknown=True, step=1,
213 no_subdirs=False, full_path=False, verbose_build=False,
214 incremental=False, per_board_out_dir=False,
215 config_only=False, squash_config_y=False,
216 warnings_as_errors=False):
217 """Create a new Builder object
220 toolchains: Toolchains object to use for building
221 base_dir: Base directory to use for builder
222 git_dir: Git directory containing source repository
223 num_threads: Number of builder threads to run
224 num_jobs: Number of jobs to run at once (passed to make as -j)
225 gnu_make: the command name of GNU Make.
226 checkout: True to check out source, False to skip that step.
227 This is used for testing.
228 show_unknown: Show unknown boards (those not built) in summary
229 step: 1 to process every commit, n to process every nth commit
230 no_subdirs: Don't create subdirectories when building current
231 source for a single board
232 full_path: Return the full path in CROSS_COMPILE and don't set
234 verbose_build: Run build with V=1 and don't use 'make -s'
235 incremental: Always perform incremental builds; don't run make
236 mrproper when configuring
237 per_board_out_dir: Build in a separate persistent directory per
238 board rather than a thread-specific directory
239 config_only: Only configure each build, don't build it
240 squash_config_y: Convert CONFIG options with the value 'y' to '1'
241 warnings_as_errors: Treat all compiler warnings as errors
243 self.toolchains = toolchains
244 self.base_dir = base_dir
245 self._working_dir = os.path.join(base_dir, '.bm-work')
247 self.do_make = self.Make
248 self.gnu_make = gnu_make
249 self.checkout = checkout
250 self.num_threads = num_threads
251 self.num_jobs = num_jobs
252 self.already_done = 0
253 self.force_build = False
254 self.git_dir = git_dir
255 self._show_unknown = show_unknown
256 self._timestamp_count = 10
257 self._build_period_us = None
258 self._complete_delay = None
259 self._next_delay_update = datetime.now()
260 self.force_config_on_failure = True
261 self.force_build_failures = False
262 self.force_reconfig = False
265 self._error_lines = 0
266 self.no_subdirs = no_subdirs
267 self.full_path = full_path
268 self.verbose_build = verbose_build
269 self.config_only = config_only
270 self.squash_config_y = squash_config_y
271 self.config_filenames = BASE_CONFIG_FILENAMES
272 if not self.squash_config_y:
273 self.config_filenames += EXTRA_CONFIG_FILENAMES
275 self.warnings_as_errors = warnings_as_errors
276 self.col = terminal.Color()
278 self._re_function = re.compile('(.*): In function.*')
279 self._re_files = re.compile('In file included from.*')
280 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
281 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
283 self.queue = Queue.Queue()
284 self.out_queue = Queue.Queue()
285 for i in range(self.num_threads):
286 t = builderthread.BuilderThread(self, i, incremental,
290 self.threads.append(t)
292 self.last_line_len = 0
293 t = builderthread.ResultThread(self)
296 self.threads.append(t)
298 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
299 self.re_make_err = re.compile('|'.join(ignore_lines))
301 # Handle existing graceful with SIGINT / Ctrl-C
302 signal.signal(signal.SIGINT, self.signal_handler)
305 """Get rid of all threads created by the builder"""
306 for t in self.threads:
309 def signal_handler(self, signal, frame):
312 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
313 show_detail=False, show_bloat=False,
314 list_error_boards=False, show_config=False):
315 """Setup display options for the builder.
317 show_errors: True to show summarised error/warning info
318 show_sizes: Show size deltas
319 show_detail: Show detail for each board
320 show_bloat: Show detail for each function
321 list_error_boards: Show the boards which caused each error/warning
322 show_config: Show config deltas
324 self._show_errors = show_errors
325 self._show_sizes = show_sizes
326 self._show_detail = show_detail
327 self._show_bloat = show_bloat
328 self._list_error_boards = list_error_boards
329 self._show_config = show_config
331 def _AddTimestamp(self):
332 """Add a new timestamp to the list and record the build period.
334 The build period is the length of time taken to perform a single
335 build (one board, one commit).
338 self._timestamps.append(now)
339 count = len(self._timestamps)
340 delta = self._timestamps[-1] - self._timestamps[0]
341 seconds = delta.total_seconds()
343 # If we have enough data, estimate build period (time taken for a
344 # single build) and therefore completion time.
345 if count > 1 and self._next_delay_update < now:
346 self._next_delay_update = now + timedelta(seconds=2)
348 self._build_period = float(seconds) / count
349 todo = self.count - self.upto
350 self._complete_delay = timedelta(microseconds=
351 self._build_period * todo * 1000000)
353 self._complete_delay -= timedelta(
354 microseconds=self._complete_delay.microseconds)
357 self._timestamps.popleft()
360 def ClearLine(self, length):
361 """Clear any characters on the current line
363 Make way for a new line of length 'length', by outputting enough
364 spaces to clear out the old line. Then remember the new length for
368 length: Length of new line, in characters
370 if length < self.last_line_len:
371 Print(' ' * (self.last_line_len - length), newline=False)
372 Print('\r', newline=False)
373 self.last_line_len = length
376 def SelectCommit(self, commit, checkout=True):
377 """Checkout the selected commit for this build
380 if checkout and self.checkout:
381 gitutil.Checkout(commit.hash)
383 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
387 commit: Commit object that is being built
388 brd: Board object that is being built
389 stage: Stage that we are at (mrproper, config, build)
390 cwd: Directory where make should be run
391 args: Arguments to pass to make
392 kwargs: Arguments to pass to command.RunPipe()
394 cmd = [self.gnu_make] + list(args)
395 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
396 cwd=cwd, raise_on_error=False, **kwargs)
397 if self.verbose_build:
398 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
399 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
402 def ProcessResult(self, result):
403 """Process the result of a build, showing progress information
406 result: A CommandResult object, which indicates the result for
409 col = terminal.Color()
411 target = result.brd.target
414 if result.return_code != 0:
418 if result.already_done:
419 self.already_done += 1
421 Print('\r', newline=False)
423 boards_selected = {target : result.brd}
424 self.ResetResultSummary(boards_selected)
425 self.ProduceResultSummary(result.commit_upto, self.commits,
428 target = '(starting)'
430 # Display separate counts for ok, warned and fail
431 ok = self.upto - self.warned - self.fail
432 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
433 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
434 line += self.col.Color(self.col.RED, '%5d' % self.fail)
436 name = ' /%-5d ' % self.count
438 # Add our current completion time estimate
440 if self._complete_delay:
441 name += '%s : ' % self._complete_delay
442 # When building all boards for a commit, we can print a commit
444 if result and result.commit_upto is None:
445 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
449 Print(line + name, newline=False)
450 length = 16 + len(name)
451 self.ClearLine(length)
453 def _GetOutputDir(self, commit_upto):
454 """Get the name of the output directory for a commit number
456 The output directory is typically .../<branch>/<commit>.
459 commit_upto: Commit number to use (0..self.count-1)
463 commit = self.commits[commit_upto]
464 subject = commit.subject.translate(trans_valid_chars)
465 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
466 self.commit_count, commit.hash, subject[:20]))
467 elif not self.no_subdirs:
468 commit_dir = 'current'
471 return os.path.join(self.base_dir, commit_dir)
473 def GetBuildDir(self, commit_upto, target):
474 """Get the name of the build directory for a commit number
476 The build directory is typically .../<branch>/<commit>/<target>.
479 commit_upto: Commit number to use (0..self.count-1)
482 output_dir = self._GetOutputDir(commit_upto)
483 return os.path.join(output_dir, target)
485 def GetDoneFile(self, commit_upto, target):
486 """Get the name of the done file for a commit number
489 commit_upto: Commit number to use (0..self.count-1)
492 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
494 def GetSizesFile(self, commit_upto, target):
495 """Get the name of the sizes file for a commit number
498 commit_upto: Commit number to use (0..self.count-1)
501 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
503 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
504 """Get the name of the funcsizes file for a commit number and ELF file
507 commit_upto: Commit number to use (0..self.count-1)
509 elf_fname: Filename of elf image
511 return os.path.join(self.GetBuildDir(commit_upto, target),
512 '%s.sizes' % elf_fname.replace('/', '-'))
514 def GetObjdumpFile(self, commit_upto, target, elf_fname):
515 """Get the name of the objdump file for a commit number and ELF file
518 commit_upto: Commit number to use (0..self.count-1)
520 elf_fname: Filename of elf image
522 return os.path.join(self.GetBuildDir(commit_upto, target),
523 '%s.objdump' % elf_fname.replace('/', '-'))
525 def GetErrFile(self, commit_upto, target):
526 """Get the name of the err file for a commit number
529 commit_upto: Commit number to use (0..self.count-1)
532 output_dir = self.GetBuildDir(commit_upto, target)
533 return os.path.join(output_dir, 'err')
535 def FilterErrors(self, lines):
536 """Filter out errors in which we have no interest
538 We should probably use map().
541 lines: List of error lines, each a string
543 New list with only interesting lines included
547 if not self.re_make_err.search(line):
548 out_lines.append(line)
551 def ReadFuncSizes(self, fname, fd):
552 """Read function sizes from the output of 'nm'
555 fd: File containing data to read
556 fname: Filename we are reading from (just for errors)
559 Dictionary containing size of each function in bytes, indexed by
563 for line in fd.readlines():
565 size, type, name = line[:-1].split()
567 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
570 # function names begin with '.' on 64-bit powerpc
572 name = 'static.' + name.split('.')[0]
573 sym[name] = sym.get(name, 0) + int(size, 16)
576 def _ProcessConfig(self, fname):
577 """Read in a .config, autoconf.mk or autoconf.h file
579 This function handles all config file types. It ignores comments and
580 any #defines which don't start with CONFIG_.
583 fname: Filename to read
587 key: Config name (e.g. CONFIG_DM)
588 value: Config value (e.g. 1)
591 if os.path.exists(fname):
592 with open(fname) as fd:
595 if line.startswith('#define'):
596 values = line[8:].split(' ', 1)
601 value = '1' if self.squash_config_y else ''
602 if not key.startswith('CONFIG_'):
604 elif not line or line[0] in ['#', '*', '/']:
607 key, value = line.split('=', 1)
608 if self.squash_config_y and value == 'y':
613 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
615 """Work out the outcome of a build.
618 commit_upto: Commit number to check (0..n-1)
619 target: Target board to check
620 read_func_sizes: True to read function size information
621 read_config: True to read .config and autoconf.h files
626 done_file = self.GetDoneFile(commit_upto, target)
627 sizes_file = self.GetSizesFile(commit_upto, target)
631 if os.path.exists(done_file):
632 with open(done_file, 'r') as fd:
633 return_code = int(fd.readline())
635 err_file = self.GetErrFile(commit_upto, target)
636 if os.path.exists(err_file):
637 with open(err_file, 'r') as fd:
638 err_lines = self.FilterErrors(fd.readlines())
640 # Decide whether the build was ok, failed or created warnings
648 # Convert size information to our simple format
649 if os.path.exists(sizes_file):
650 with open(sizes_file, 'r') as fd:
651 for line in fd.readlines():
652 values = line.split()
655 rodata = int(values[6], 16)
657 'all' : int(values[0]) + int(values[1]) +
659 'text' : int(values[0]) - rodata,
660 'data' : int(values[1]),
661 'bss' : int(values[2]),
664 sizes[values[5]] = size_dict
667 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
668 for fname in glob.glob(pattern):
669 with open(fname, 'r') as fd:
670 dict_name = os.path.basename(fname).replace('.sizes',
672 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
675 output_dir = self.GetBuildDir(commit_upto, target)
676 for name in self.config_filenames:
677 fname = os.path.join(output_dir, name)
678 config[name] = self._ProcessConfig(fname)
680 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
682 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
684 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
686 """Calculate a summary of the results of building a commit.
689 board_selected: Dict containing boards to summarise
690 commit_upto: Commit number to summarize (0..self.count-1)
691 read_func_sizes: True to read function size information
692 read_config: True to read .config and autoconf.h files
696 Dict containing boards which passed building this commit.
697 keyed by board.target
698 List containing a summary of error lines
699 Dict keyed by error line, containing a list of the Board
700 objects with that error
701 List containing a summary of warning lines
702 Dict keyed by error line, containing a list of the Board
703 objects with that warning
704 Dictionary keyed by board.target. Each value is a dictionary:
705 key: filename - e.g. '.config'
706 value is itself a dictionary:
710 def AddLine(lines_summary, lines_boards, line, board):
712 if line in lines_boards:
713 lines_boards[line].append(board)
715 lines_boards[line] = [board]
716 lines_summary.append(line)
719 err_lines_summary = []
720 err_lines_boards = {}
721 warn_lines_summary = []
722 warn_lines_boards = {}
725 for board in boards_selected.itervalues():
726 outcome = self.GetBuildOutcome(commit_upto, board.target,
727 read_func_sizes, read_config)
728 board_dict[board.target] = outcome
730 last_was_warning = False
731 for line in outcome.err_lines:
733 if (self._re_function.match(line) or
734 self._re_files.match(line)):
737 is_warning = self._re_warning.match(line)
738 is_note = self._re_note.match(line)
739 if is_warning or (last_was_warning and is_note):
741 AddLine(warn_lines_summary, warn_lines_boards,
743 AddLine(warn_lines_summary, warn_lines_boards,
747 AddLine(err_lines_summary, err_lines_boards,
749 AddLine(err_lines_summary, err_lines_boards,
751 last_was_warning = is_warning
753 tconfig = Config(self.config_filenames, board.target)
754 for fname in self.config_filenames:
756 for key, value in outcome.config[fname].iteritems():
757 tconfig.Add(fname, key, value)
758 config[board.target] = tconfig
760 return (board_dict, err_lines_summary, err_lines_boards,
761 warn_lines_summary, warn_lines_boards, config)
763 def AddOutcome(self, board_dict, arch_list, changes, char, color):
764 """Add an output to our list of outcomes for each architecture
766 This simple function adds failing boards (changes) to the
767 relevant architecture string, so we can print the results out
768 sorted by architecture.
771 board_dict: Dict containing all boards
772 arch_list: Dict keyed by arch name. Value is a string containing
773 a list of board names which failed for that arch.
774 changes: List of boards to add to arch_list
775 color: terminal.Colour object
778 for target in changes:
779 if target in board_dict:
780 arch = board_dict[target].arch
783 str = self.col.Color(color, ' ' + target)
784 if not arch in done_arch:
785 str = ' %s %s' % (self.col.Color(color, char), str)
786 done_arch[arch] = True
787 if not arch in arch_list:
788 arch_list[arch] = str
790 arch_list[arch] += str
793 def ColourNum(self, num):
794 color = self.col.RED if num > 0 else self.col.GREEN
797 return self.col.Color(color, str(num))
799 def ResetResultSummary(self, board_selected):
800 """Reset the results summary ready for use.
802 Set up the base board list to be all those selected, and set the
803 error lines to empty.
805 Following this, calls to PrintResultSummary() will use this
806 information to work out what has changed.
809 board_selected: Dict containing boards to summarise, keyed by
812 self._base_board_dict = {}
813 for board in board_selected:
814 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
815 self._base_err_lines = []
816 self._base_warn_lines = []
817 self._base_err_line_boards = {}
818 self._base_warn_line_boards = {}
819 self._base_config = None
821 def PrintFuncSizeDetail(self, fname, old, new):
822 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
823 delta, common = [], {}
830 if name not in common:
833 delta.append([-old[name], name])
836 if name not in common:
839 delta.append([new[name], name])
842 diff = new.get(name, 0) - old.get(name, 0)
844 grow, up = grow + 1, up + diff
846 shrink, down = shrink + 1, down - diff
847 delta.append([diff, name])
852 args = [add, -remove, grow, -shrink, up, -down, up - down]
853 if max(args) == 0 and min(args) == 0:
855 args = [self.ColourNum(x) for x in args]
857 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
858 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
859 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
861 for diff, name in delta:
863 color = self.col.RED if diff > 0 else self.col.GREEN
864 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
865 old.get(name, '-'), new.get(name,'-'), diff)
866 Print(msg, colour=color)
869 def PrintSizeDetail(self, target_list, show_bloat):
870 """Show details size information for each board
873 target_list: List of targets, each a dict containing:
874 'target': Target name
875 'total_diff': Total difference in bytes across all areas
876 <part_name>: Difference for that part
877 show_bloat: Show detail for each function
879 targets_by_diff = sorted(target_list, reverse=True,
880 key=lambda x: x['_total_diff'])
881 for result in targets_by_diff:
882 printed_target = False
883 for name in sorted(result):
885 if name.startswith('_'):
888 color = self.col.RED if diff > 0 else self.col.GREEN
889 msg = ' %s %+d' % (name, diff)
890 if not printed_target:
891 Print('%10s %-15s:' % ('', result['_target']),
893 printed_target = True
894 Print(msg, colour=color, newline=False)
898 target = result['_target']
899 outcome = result['_outcome']
900 base_outcome = self._base_board_dict[target]
901 for fname in outcome.func_sizes:
902 self.PrintFuncSizeDetail(fname,
903 base_outcome.func_sizes[fname],
904 outcome.func_sizes[fname])
907 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
909 """Print a summary of image sizes broken down by section.
911 The summary takes the form of one line per architecture. The
912 line contains deltas for each of the sections (+ means the section
913 got bigger, - means smaller). The nunmbers are the average number
914 of bytes that a board in this section increased by.
917 powerpc: (622 boards) text -0.0
918 arm: (285 boards) text -0.0
919 nds32: (3 boards) text -8.0
922 board_selected: Dict containing boards to summarise, keyed by
924 board_dict: Dict containing boards for which we built this
925 commit, keyed by board.target. The value is an Outcome object.
926 show_detail: Show detail for each board
927 show_bloat: Show detail for each function
932 # Calculate changes in size for different image parts
933 # The previous sizes are in Board.sizes, for each board
934 for target in board_dict:
935 if target not in board_selected:
937 base_sizes = self._base_board_dict[target].sizes
938 outcome = board_dict[target]
939 sizes = outcome.sizes
941 # Loop through the list of images, creating a dict of size
942 # changes for each image/part. We end up with something like
943 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
944 # which means that U-Boot data increased by 5 bytes and SPL
945 # text decreased by 4.
946 err = {'_target' : target}
948 if image in base_sizes:
949 base_image = base_sizes[image]
950 # Loop through the text, data, bss parts
951 for part in sorted(sizes[image]):
952 diff = sizes[image][part] - base_image[part]
955 if image == 'u-boot':
958 name = image + ':' + part
960 arch = board_selected[target].arch
961 if not arch in arch_count:
964 arch_count[arch] += 1
966 pass # Only add to our list when we have some stats
967 elif not arch in arch_list:
968 arch_list[arch] = [err]
970 arch_list[arch].append(err)
972 # We now have a list of image size changes sorted by arch
973 # Print out a summary of these
974 for arch, target_list in arch_list.iteritems():
975 # Get total difference for each type
977 for result in target_list:
979 for name, diff in result.iteritems():
980 if name.startswith('_'):
987 result['_total_diff'] = total
988 result['_outcome'] = board_dict[result['_target']]
990 count = len(target_list)
992 for name in sorted(totals):
995 # Display the average difference in this name for this
997 avg_diff = float(diff) / count
998 color = self.col.RED if avg_diff > 0 else self.col.GREEN
999 msg = ' %s %+1.1f' % (name, avg_diff)
1000 if not printed_arch:
1001 Print('%10s: (for %d/%d boards)' % (arch, count,
1002 arch_count[arch]), newline=False)
1004 Print(msg, colour=color, newline=False)
1009 self.PrintSizeDetail(target_list, show_bloat)
1012 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1013 err_line_boards, warn_lines, warn_line_boards,
1014 config, show_sizes, show_detail, show_bloat,
1016 """Compare results with the base results and display delta.
1018 Only boards mentioned in board_selected will be considered. This
1019 function is intended to be called repeatedly with the results of
1020 each commit. It therefore shows a 'diff' between what it saw in
1021 the last call and what it sees now.
1024 board_selected: Dict containing boards to summarise, keyed by
1026 board_dict: Dict containing boards for which we built this
1027 commit, keyed by board.target. The value is an Outcome object.
1028 err_lines: A list of errors for this commit, or [] if there is
1029 none, or we don't want to print errors
1030 err_line_boards: Dict keyed by error line, containing a list of
1031 the Board objects with that error
1032 warn_lines: A list of warnings for this commit, or [] if there is
1033 none, or we don't want to print errors
1034 warn_line_boards: Dict keyed by warning line, containing a list of
1035 the Board objects with that warning
1036 config: Dictionary keyed by filename - e.g. '.config'. Each
1037 value is itself a dictionary:
1040 show_sizes: Show image size deltas
1041 show_detail: Show detail for each board
1042 show_bloat: Show detail for each function
1043 show_config: Show config changes
1045 def _BoardList(line, line_boards):
1046 """Helper function to get a line of boards containing a line
1049 line: Error line to search for
1051 String containing a list of boards with that error line, or
1052 '' if the user has not requested such a list
1054 if self._list_error_boards:
1056 for board in line_boards[line]:
1057 if not board.target in names:
1058 names.append(board.target)
1059 names_str = '(%s) ' % ','.join(names)
1064 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1069 if line not in base_lines:
1070 worse_lines.append(char + '+' +
1071 _BoardList(line, line_boards) + line)
1072 for line in base_lines:
1073 if line not in lines:
1074 better_lines.append(char + '-' +
1075 _BoardList(line, base_line_boards) + line)
1076 return better_lines, worse_lines
1078 def _CalcConfig(delta, name, config):
1079 """Calculate configuration changes
1082 delta: Type of the delta, e.g. '+'
1083 name: name of the file which changed (e.g. .config)
1084 config: configuration change dictionary
1088 String containing the configuration changes which can be
1092 for key in sorted(config.keys()):
1093 out += '%s=%s ' % (key, config[key])
1094 return '%s %s: %s' % (delta, name, out)
1096 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1097 """Add changes in configuration to a list
1100 lines: list to add to
1101 name: config file name
1102 config_plus: configurations added, dictionary
1105 config_minus: configurations removed, dictionary
1108 config_change: configurations changed, dictionary
1113 lines.append(_CalcConfig('+', name, config_plus))
1115 lines.append(_CalcConfig('-', name, config_minus))
1117 lines.append(_CalcConfig('c', name, config_change))
1119 def _OutputConfigInfo(lines):
1124 col = self.col.GREEN
1125 elif line[0] == '-':
1127 elif line[0] == 'c':
1128 col = self.col.YELLOW
1129 Print(' ' + line, newline=True, colour=col)
1132 better = [] # List of boards fixed since last commit
1133 worse = [] # List of new broken boards since last commit
1134 new = [] # List of boards that didn't exist last time
1135 unknown = [] # List of boards that were not built
1137 for target in board_dict:
1138 if target not in board_selected:
1141 # If the board was built last time, add its outcome to a list
1142 if target in self._base_board_dict:
1143 base_outcome = self._base_board_dict[target].rc
1144 outcome = board_dict[target]
1145 if outcome.rc == OUTCOME_UNKNOWN:
1146 unknown.append(target)
1147 elif outcome.rc < base_outcome:
1148 better.append(target)
1149 elif outcome.rc > base_outcome:
1150 worse.append(target)
1154 # Get a list of errors that have appeared, and disappeared
1155 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1156 self._base_err_line_boards, err_lines, err_line_boards, '')
1157 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1158 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1160 # Display results by arch
1161 if (better or worse or unknown or new or worse_err or better_err
1162 or worse_warn or better_warn):
1164 self.AddOutcome(board_selected, arch_list, better, '',
1166 self.AddOutcome(board_selected, arch_list, worse, '+',
1168 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1169 if self._show_unknown:
1170 self.AddOutcome(board_selected, arch_list, unknown, '?',
1172 for arch, target_list in arch_list.iteritems():
1173 Print('%10s: %s' % (arch, target_list))
1174 self._error_lines += 1
1176 Print('\n'.join(better_err), colour=self.col.GREEN)
1177 self._error_lines += 1
1179 Print('\n'.join(worse_err), colour=self.col.RED)
1180 self._error_lines += 1
1182 Print('\n'.join(better_warn), colour=self.col.CYAN)
1183 self._error_lines += 1
1185 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1186 self._error_lines += 1
1189 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1192 if show_config and self._base_config:
1194 arch_config_plus = {}
1195 arch_config_minus = {}
1196 arch_config_change = {}
1199 for target in board_dict:
1200 if target not in board_selected:
1202 arch = board_selected[target].arch
1203 if arch not in arch_list:
1204 arch_list.append(arch)
1206 for arch in arch_list:
1207 arch_config_plus[arch] = {}
1208 arch_config_minus[arch] = {}
1209 arch_config_change[arch] = {}
1210 for name in self.config_filenames:
1211 arch_config_plus[arch][name] = {}
1212 arch_config_minus[arch][name] = {}
1213 arch_config_change[arch][name] = {}
1215 for target in board_dict:
1216 if target not in board_selected:
1219 arch = board_selected[target].arch
1221 all_config_plus = {}
1222 all_config_minus = {}
1223 all_config_change = {}
1224 tbase = self._base_config[target]
1225 tconfig = config[target]
1227 for name in self.config_filenames:
1228 if not tconfig.config[name]:
1233 base = tbase.config[name]
1234 for key, value in tconfig.config[name].iteritems():
1236 config_plus[key] = value
1237 all_config_plus[key] = value
1238 for key, value in base.iteritems():
1239 if key not in tconfig.config[name]:
1240 config_minus[key] = value
1241 all_config_minus[key] = value
1242 for key, value in base.iteritems():
1243 new_value = tconfig.config.get(key)
1244 if new_value and value != new_value:
1245 desc = '%s -> %s' % (value, new_value)
1246 config_change[key] = desc
1247 all_config_change[key] = desc
1249 arch_config_plus[arch][name].update(config_plus)
1250 arch_config_minus[arch][name].update(config_minus)
1251 arch_config_change[arch][name].update(config_change)
1253 _AddConfig(lines, name, config_plus, config_minus,
1255 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1257 summary[target] = '\n'.join(lines)
1259 lines_by_target = {}
1260 for target, lines in summary.iteritems():
1261 if lines in lines_by_target:
1262 lines_by_target[lines].append(target)
1264 lines_by_target[lines] = [target]
1266 for arch in arch_list:
1271 for name in self.config_filenames:
1272 all_plus.update(arch_config_plus[arch][name])
1273 all_minus.update(arch_config_minus[arch][name])
1274 all_change.update(arch_config_change[arch][name])
1275 _AddConfig(lines, name, arch_config_plus[arch][name],
1276 arch_config_minus[arch][name],
1277 arch_config_change[arch][name])
1278 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1279 #arch_summary[target] = '\n'.join(lines)
1282 _OutputConfigInfo(lines)
1284 for lines, targets in lines_by_target.iteritems():
1287 Print('%s :' % ' '.join(sorted(targets)))
1288 _OutputConfigInfo(lines.split('\n'))
1291 # Save our updated information for the next call to this function
1292 self._base_board_dict = board_dict
1293 self._base_err_lines = err_lines
1294 self._base_warn_lines = warn_lines
1295 self._base_err_line_boards = err_line_boards
1296 self._base_warn_line_boards = warn_line_boards
1297 self._base_config = config
1299 # Get a list of boards that did not get built, if needed
1301 for board in board_selected:
1302 if not board in board_dict:
1303 not_built.append(board)
1305 Print("Boards not built (%d): %s" % (len(not_built),
1306 ', '.join(not_built)))
1308 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1309 (board_dict, err_lines, err_line_boards, warn_lines,
1310 warn_line_boards, config) = self.GetResultSummary(
1311 board_selected, commit_upto,
1312 read_func_sizes=self._show_bloat,
1313 read_config=self._show_config)
1315 msg = '%02d: %s' % (commit_upto + 1,
1316 commits[commit_upto].subject)
1317 Print(msg, colour=self.col.BLUE)
1318 self.PrintResultSummary(board_selected, board_dict,
1319 err_lines if self._show_errors else [], err_line_boards,
1320 warn_lines if self._show_errors else [], warn_line_boards,
1321 config, self._show_sizes, self._show_detail,
1322 self._show_bloat, self._show_config)
1324 def ShowSummary(self, commits, board_selected):
1325 """Show a build summary for U-Boot for a given board list.
1327 Reset the result summary, then repeatedly call GetResultSummary on
1328 each commit's results, then display the differences we see.
1331 commit: Commit objects to summarise
1332 board_selected: Dict containing boards to summarise
1334 self.commit_count = len(commits) if commits else 1
1335 self.commits = commits
1336 self.ResetResultSummary(board_selected)
1337 self._error_lines = 0
1339 for commit_upto in range(0, self.commit_count, self._step):
1340 self.ProduceResultSummary(commit_upto, commits, board_selected)
1341 if not self._error_lines:
1342 Print('(no errors to report)', colour=self.col.GREEN)
1345 def SetupBuild(self, board_selected, commits):
1346 """Set up ready to start a build.
1349 board_selected: Selected boards to build
1350 commits: Selected commits to build
1352 # First work out how many commits we will build
1353 count = (self.commit_count + self._step - 1) / self._step
1354 self.count = len(board_selected) * count
1355 self.upto = self.warned = self.fail = 0
1356 self._timestamps = collections.deque()
1358 def GetThreadDir(self, thread_num):
1359 """Get the directory path to the working dir for a thread.
1362 thread_num: Number of thread to check.
1364 return os.path.join(self._working_dir, '%02d' % thread_num)
1366 def _PrepareThread(self, thread_num, setup_git):
1367 """Prepare the working directory for a thread.
1369 This clones or fetches the repo into the thread's work directory.
1372 thread_num: Thread number (0, 1, ...)
1373 setup_git: True to set up a git repo clone
1375 thread_dir = self.GetThreadDir(thread_num)
1376 builderthread.Mkdir(thread_dir)
1377 git_dir = os.path.join(thread_dir, '.git')
1379 # Clone the repo if it doesn't already exist
1380 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1381 # we have a private index but uses the origin repo's contents?
1382 if setup_git and self.git_dir:
1383 src_dir = os.path.abspath(self.git_dir)
1384 if os.path.exists(git_dir):
1385 gitutil.Fetch(git_dir, thread_dir)
1387 Print('\rCloning repo for thread %d' % thread_num,
1389 gitutil.Clone(src_dir, thread_dir)
1390 Print('\r%s\r' % (' ' * 30), newline=False)
1392 def _PrepareWorkingSpace(self, max_threads, setup_git):
1393 """Prepare the working directory for use.
1395 Set up the git repo for each thread.
1398 max_threads: Maximum number of threads we expect to need.
1399 setup_git: True to set up a git repo clone
1401 builderthread.Mkdir(self._working_dir)
1402 for thread in range(max_threads):
1403 self._PrepareThread(thread, setup_git)
1405 def _PrepareOutputSpace(self):
1406 """Get the output directories ready to receive files.
1408 We delete any output directories which look like ones we need to
1409 create. Having left over directories is confusing when the user wants
1410 to check the output manually.
1412 if not self.commits:
1415 for commit_upto in range(self.commit_count):
1416 dir_list.append(self._GetOutputDir(commit_upto))
1419 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1420 if dirname not in dir_list:
1421 to_remove.append(dirname)
1423 Print('Removing %d old build directories' % len(to_remove),
1425 for dirname in to_remove:
1426 shutil.rmtree(dirname)
1428 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1429 """Build all commits for a list of boards
1432 commits: List of commits to be build, each a Commit object
1433 boards_selected: Dict of selected boards, key is target name,
1434 value is Board object
1435 keep_outputs: True to save build output files
1436 verbose: Display build results as they are completed
1439 - number of boards that failed to build
1440 - number of boards that issued warnings
1442 self.commit_count = len(commits) if commits else 1
1443 self.commits = commits
1444 self._verbose = verbose
1446 self.ResetResultSummary(board_selected)
1447 builderthread.Mkdir(self.base_dir, parents = True)
1448 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1449 commits is not None)
1450 self._PrepareOutputSpace()
1451 Print('\rStarting build...', newline=False)
1452 self.SetupBuild(board_selected, commits)
1453 self.ProcessResult(None)
1455 # Create jobs to build all commits for each board
1456 for brd in board_selected.itervalues():
1457 job = builderthread.BuilderJob()
1459 job.commits = commits
1460 job.keep_outputs = keep_outputs
1461 job.step = self._step
1464 term = threading.Thread(target=self.queue.join)
1465 term.setDaemon(True)
1467 while term.isAlive():
1470 # Wait until we have processed all output
1471 self.out_queue.join()
1474 return (self.fail, self.warned)