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 """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'
241 self.toolchains = toolchains
242 self.base_dir = base_dir
243 self._working_dir = os.path.join(base_dir, '.bm-work')
245 self.do_make = self.Make
246 self.gnu_make = gnu_make
247 self.checkout = checkout
248 self.num_threads = num_threads
249 self.num_jobs = num_jobs
250 self.already_done = 0
251 self.force_build = False
252 self.git_dir = git_dir
253 self._show_unknown = show_unknown
254 self._timestamp_count = 10
255 self._build_period_us = None
256 self._complete_delay = None
257 self._next_delay_update = datetime.now()
258 self.force_config_on_failure = True
259 self.force_build_failures = False
260 self.force_reconfig = False
263 self._error_lines = 0
264 self.no_subdirs = no_subdirs
265 self.full_path = full_path
266 self.verbose_build = verbose_build
267 self.config_only = config_only
268 self.squash_config_y = squash_config_y
269 self.config_filenames = BASE_CONFIG_FILENAMES
270 if not self.squash_config_y:
271 self.config_filenames += EXTRA_CONFIG_FILENAMES
273 self.col = terminal.Color()
275 self._re_function = re.compile('(.*): In function.*')
276 self._re_files = re.compile('In file included from.*')
277 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
278 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
280 self.queue = Queue.Queue()
281 self.out_queue = Queue.Queue()
282 for i in range(self.num_threads):
283 t = builderthread.BuilderThread(self, i, incremental,
287 self.threads.append(t)
289 self.last_line_len = 0
290 t = builderthread.ResultThread(self)
293 self.threads.append(t)
295 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
296 self.re_make_err = re.compile('|'.join(ignore_lines))
298 # Handle existing graceful with SIGINT / Ctrl-C
299 signal.signal(signal.SIGINT, self.signal_handler)
302 """Get rid of all threads created by the builder"""
303 for t in self.threads:
306 def signal_handler(self, signal, frame):
309 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
310 show_detail=False, show_bloat=False,
311 list_error_boards=False, show_config=False):
312 """Setup display options for the builder.
314 show_errors: True to show summarised error/warning info
315 show_sizes: Show size deltas
316 show_detail: Show detail for each board
317 show_bloat: Show detail for each function
318 list_error_boards: Show the boards which caused each error/warning
319 show_config: Show config deltas
321 self._show_errors = show_errors
322 self._show_sizes = show_sizes
323 self._show_detail = show_detail
324 self._show_bloat = show_bloat
325 self._list_error_boards = list_error_boards
326 self._show_config = show_config
328 def _AddTimestamp(self):
329 """Add a new timestamp to the list and record the build period.
331 The build period is the length of time taken to perform a single
332 build (one board, one commit).
335 self._timestamps.append(now)
336 count = len(self._timestamps)
337 delta = self._timestamps[-1] - self._timestamps[0]
338 seconds = delta.total_seconds()
340 # If we have enough data, estimate build period (time taken for a
341 # single build) and therefore completion time.
342 if count > 1 and self._next_delay_update < now:
343 self._next_delay_update = now + timedelta(seconds=2)
345 self._build_period = float(seconds) / count
346 todo = self.count - self.upto
347 self._complete_delay = timedelta(microseconds=
348 self._build_period * todo * 1000000)
350 self._complete_delay -= timedelta(
351 microseconds=self._complete_delay.microseconds)
354 self._timestamps.popleft()
357 def ClearLine(self, length):
358 """Clear any characters on the current line
360 Make way for a new line of length 'length', by outputting enough
361 spaces to clear out the old line. Then remember the new length for
365 length: Length of new line, in characters
367 if length < self.last_line_len:
368 Print(' ' * (self.last_line_len - length), newline=False)
369 Print('\r', newline=False)
370 self.last_line_len = length
373 def SelectCommit(self, commit, checkout=True):
374 """Checkout the selected commit for this build
377 if checkout and self.checkout:
378 gitutil.Checkout(commit.hash)
380 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
384 commit: Commit object that is being built
385 brd: Board object that is being built
386 stage: Stage that we are at (mrproper, config, build)
387 cwd: Directory where make should be run
388 args: Arguments to pass to make
389 kwargs: Arguments to pass to command.RunPipe()
391 cmd = [self.gnu_make] + list(args)
392 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
393 cwd=cwd, raise_on_error=False, **kwargs)
394 if self.verbose_build:
395 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
396 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
399 def ProcessResult(self, result):
400 """Process the result of a build, showing progress information
403 result: A CommandResult object, which indicates the result for
406 col = terminal.Color()
408 target = result.brd.target
411 if result.return_code != 0:
415 if result.already_done:
416 self.already_done += 1
418 Print('\r', newline=False)
420 boards_selected = {target : result.brd}
421 self.ResetResultSummary(boards_selected)
422 self.ProduceResultSummary(result.commit_upto, self.commits,
425 target = '(starting)'
427 # Display separate counts for ok, warned and fail
428 ok = self.upto - self.warned - self.fail
429 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
430 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
431 line += self.col.Color(self.col.RED, '%5d' % self.fail)
433 name = ' /%-5d ' % self.count
435 # Add our current completion time estimate
437 if self._complete_delay:
438 name += '%s : ' % self._complete_delay
439 # When building all boards for a commit, we can print a commit
441 if result and result.commit_upto is None:
442 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
446 Print(line + name, newline=False)
447 length = 16 + len(name)
448 self.ClearLine(length)
450 def _GetOutputDir(self, commit_upto):
451 """Get the name of the output directory for a commit number
453 The output directory is typically .../<branch>/<commit>.
456 commit_upto: Commit number to use (0..self.count-1)
460 commit = self.commits[commit_upto]
461 subject = commit.subject.translate(trans_valid_chars)
462 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
463 self.commit_count, commit.hash, subject[:20]))
464 elif not self.no_subdirs:
465 commit_dir = 'current'
468 return os.path.join(self.base_dir, commit_dir)
470 def GetBuildDir(self, commit_upto, target):
471 """Get the name of the build directory for a commit number
473 The build directory is typically .../<branch>/<commit>/<target>.
476 commit_upto: Commit number to use (0..self.count-1)
479 output_dir = self._GetOutputDir(commit_upto)
480 return os.path.join(output_dir, target)
482 def GetDoneFile(self, commit_upto, target):
483 """Get the name of the done file for a commit number
486 commit_upto: Commit number to use (0..self.count-1)
489 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
491 def GetSizesFile(self, commit_upto, target):
492 """Get the name of the sizes file for a commit number
495 commit_upto: Commit number to use (0..self.count-1)
498 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
500 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
501 """Get the name of the funcsizes file for a commit number and ELF file
504 commit_upto: Commit number to use (0..self.count-1)
506 elf_fname: Filename of elf image
508 return os.path.join(self.GetBuildDir(commit_upto, target),
509 '%s.sizes' % elf_fname.replace('/', '-'))
511 def GetObjdumpFile(self, commit_upto, target, elf_fname):
512 """Get the name of the objdump file for a commit number and ELF file
515 commit_upto: Commit number to use (0..self.count-1)
517 elf_fname: Filename of elf image
519 return os.path.join(self.GetBuildDir(commit_upto, target),
520 '%s.objdump' % elf_fname.replace('/', '-'))
522 def GetErrFile(self, commit_upto, target):
523 """Get the name of the err file for a commit number
526 commit_upto: Commit number to use (0..self.count-1)
529 output_dir = self.GetBuildDir(commit_upto, target)
530 return os.path.join(output_dir, 'err')
532 def FilterErrors(self, lines):
533 """Filter out errors in which we have no interest
535 We should probably use map().
538 lines: List of error lines, each a string
540 New list with only interesting lines included
544 if not self.re_make_err.search(line):
545 out_lines.append(line)
548 def ReadFuncSizes(self, fname, fd):
549 """Read function sizes from the output of 'nm'
552 fd: File containing data to read
553 fname: Filename we are reading from (just for errors)
556 Dictionary containing size of each function in bytes, indexed by
560 for line in fd.readlines():
562 size, type, name = line[:-1].split()
564 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
567 # function names begin with '.' on 64-bit powerpc
569 name = 'static.' + name.split('.')[0]
570 sym[name] = sym.get(name, 0) + int(size, 16)
573 def _ProcessConfig(self, fname):
574 """Read in a .config, autoconf.mk or autoconf.h file
576 This function handles all config file types. It ignores comments and
577 any #defines which don't start with CONFIG_.
580 fname: Filename to read
584 key: Config name (e.g. CONFIG_DM)
585 value: Config value (e.g. 1)
588 if os.path.exists(fname):
589 with open(fname) as fd:
592 if line.startswith('#define'):
593 values = line[8:].split(' ', 1)
598 value = '1' if self.squash_config_y else ''
599 if not key.startswith('CONFIG_'):
601 elif not line or line[0] in ['#', '*', '/']:
604 key, value = line.split('=', 1)
605 if self.squash_config_y and value == 'y':
610 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
612 """Work out the outcome of a build.
615 commit_upto: Commit number to check (0..n-1)
616 target: Target board to check
617 read_func_sizes: True to read function size information
618 read_config: True to read .config and autoconf.h files
623 done_file = self.GetDoneFile(commit_upto, target)
624 sizes_file = self.GetSizesFile(commit_upto, target)
628 if os.path.exists(done_file):
629 with open(done_file, 'r') as fd:
630 return_code = int(fd.readline())
632 err_file = self.GetErrFile(commit_upto, target)
633 if os.path.exists(err_file):
634 with open(err_file, 'r') as fd:
635 err_lines = self.FilterErrors(fd.readlines())
637 # Decide whether the build was ok, failed or created warnings
645 # Convert size information to our simple format
646 if os.path.exists(sizes_file):
647 with open(sizes_file, 'r') as fd:
648 for line in fd.readlines():
649 values = line.split()
652 rodata = int(values[6], 16)
654 'all' : int(values[0]) + int(values[1]) +
656 'text' : int(values[0]) - rodata,
657 'data' : int(values[1]),
658 'bss' : int(values[2]),
661 sizes[values[5]] = size_dict
664 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
665 for fname in glob.glob(pattern):
666 with open(fname, 'r') as fd:
667 dict_name = os.path.basename(fname).replace('.sizes',
669 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
672 output_dir = self.GetBuildDir(commit_upto, target)
673 for name in self.config_filenames:
674 fname = os.path.join(output_dir, name)
675 config[name] = self._ProcessConfig(fname)
677 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
679 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
681 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
683 """Calculate a summary of the results of building a commit.
686 board_selected: Dict containing boards to summarise
687 commit_upto: Commit number to summarize (0..self.count-1)
688 read_func_sizes: True to read function size information
689 read_config: True to read .config and autoconf.h files
693 Dict containing boards which passed building this commit.
694 keyed by board.target
695 List containing a summary of error lines
696 Dict keyed by error line, containing a list of the Board
697 objects with that error
698 List containing a summary of warning lines
699 Dict keyed by error line, containing a list of the Board
700 objects with that warning
701 Dictionary keyed by board.target. Each value is a dictionary:
702 key: filename - e.g. '.config'
703 value is itself a dictionary:
707 def AddLine(lines_summary, lines_boards, line, board):
709 if line in lines_boards:
710 lines_boards[line].append(board)
712 lines_boards[line] = [board]
713 lines_summary.append(line)
716 err_lines_summary = []
717 err_lines_boards = {}
718 warn_lines_summary = []
719 warn_lines_boards = {}
722 for board in boards_selected.itervalues():
723 outcome = self.GetBuildOutcome(commit_upto, board.target,
724 read_func_sizes, read_config)
725 board_dict[board.target] = outcome
727 last_was_warning = False
728 for line in outcome.err_lines:
730 if (self._re_function.match(line) or
731 self._re_files.match(line)):
734 is_warning = self._re_warning.match(line)
735 is_note = self._re_note.match(line)
736 if is_warning or (last_was_warning and is_note):
738 AddLine(warn_lines_summary, warn_lines_boards,
740 AddLine(warn_lines_summary, warn_lines_boards,
744 AddLine(err_lines_summary, err_lines_boards,
746 AddLine(err_lines_summary, err_lines_boards,
748 last_was_warning = is_warning
750 tconfig = Config(self.config_filenames, board.target)
751 for fname in self.config_filenames:
753 for key, value in outcome.config[fname].iteritems():
754 tconfig.Add(fname, key, value)
755 config[board.target] = tconfig
757 return (board_dict, err_lines_summary, err_lines_boards,
758 warn_lines_summary, warn_lines_boards, config)
760 def AddOutcome(self, board_dict, arch_list, changes, char, color):
761 """Add an output to our list of outcomes for each architecture
763 This simple function adds failing boards (changes) to the
764 relevant architecture string, so we can print the results out
765 sorted by architecture.
768 board_dict: Dict containing all boards
769 arch_list: Dict keyed by arch name. Value is a string containing
770 a list of board names which failed for that arch.
771 changes: List of boards to add to arch_list
772 color: terminal.Colour object
775 for target in changes:
776 if target in board_dict:
777 arch = board_dict[target].arch
780 str = self.col.Color(color, ' ' + target)
781 if not arch in done_arch:
782 str = ' %s %s' % (self.col.Color(color, char), str)
783 done_arch[arch] = True
784 if not arch in arch_list:
785 arch_list[arch] = str
787 arch_list[arch] += str
790 def ColourNum(self, num):
791 color = self.col.RED if num > 0 else self.col.GREEN
794 return self.col.Color(color, str(num))
796 def ResetResultSummary(self, board_selected):
797 """Reset the results summary ready for use.
799 Set up the base board list to be all those selected, and set the
800 error lines to empty.
802 Following this, calls to PrintResultSummary() will use this
803 information to work out what has changed.
806 board_selected: Dict containing boards to summarise, keyed by
809 self._base_board_dict = {}
810 for board in board_selected:
811 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
812 self._base_err_lines = []
813 self._base_warn_lines = []
814 self._base_err_line_boards = {}
815 self._base_warn_line_boards = {}
816 self._base_config = None
818 def PrintFuncSizeDetail(self, fname, old, new):
819 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
820 delta, common = [], {}
827 if name not in common:
830 delta.append([-old[name], name])
833 if name not in common:
836 delta.append([new[name], name])
839 diff = new.get(name, 0) - old.get(name, 0)
841 grow, up = grow + 1, up + diff
843 shrink, down = shrink + 1, down - diff
844 delta.append([diff, name])
849 args = [add, -remove, grow, -shrink, up, -down, up - down]
852 args = [self.ColourNum(x) for x in args]
854 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
855 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
856 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
858 for diff, name in delta:
860 color = self.col.RED if diff > 0 else self.col.GREEN
861 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
862 old.get(name, '-'), new.get(name,'-'), diff)
863 Print(msg, colour=color)
866 def PrintSizeDetail(self, target_list, show_bloat):
867 """Show details size information for each board
870 target_list: List of targets, each a dict containing:
871 'target': Target name
872 'total_diff': Total difference in bytes across all areas
873 <part_name>: Difference for that part
874 show_bloat: Show detail for each function
876 targets_by_diff = sorted(target_list, reverse=True,
877 key=lambda x: x['_total_diff'])
878 for result in targets_by_diff:
879 printed_target = False
880 for name in sorted(result):
882 if name.startswith('_'):
885 color = self.col.RED if diff > 0 else self.col.GREEN
886 msg = ' %s %+d' % (name, diff)
887 if not printed_target:
888 Print('%10s %-15s:' % ('', result['_target']),
890 printed_target = True
891 Print(msg, colour=color, newline=False)
895 target = result['_target']
896 outcome = result['_outcome']
897 base_outcome = self._base_board_dict[target]
898 for fname in outcome.func_sizes:
899 self.PrintFuncSizeDetail(fname,
900 base_outcome.func_sizes[fname],
901 outcome.func_sizes[fname])
904 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
906 """Print a summary of image sizes broken down by section.
908 The summary takes the form of one line per architecture. The
909 line contains deltas for each of the sections (+ means the section
910 got bigger, - means smaller). The nunmbers are the average number
911 of bytes that a board in this section increased by.
914 powerpc: (622 boards) text -0.0
915 arm: (285 boards) text -0.0
916 nds32: (3 boards) text -8.0
919 board_selected: Dict containing boards to summarise, keyed by
921 board_dict: Dict containing boards for which we built this
922 commit, keyed by board.target. The value is an Outcome object.
923 show_detail: Show detail for each board
924 show_bloat: Show detail for each function
929 # Calculate changes in size for different image parts
930 # The previous sizes are in Board.sizes, for each board
931 for target in board_dict:
932 if target not in board_selected:
934 base_sizes = self._base_board_dict[target].sizes
935 outcome = board_dict[target]
936 sizes = outcome.sizes
938 # Loop through the list of images, creating a dict of size
939 # changes for each image/part. We end up with something like
940 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
941 # which means that U-Boot data increased by 5 bytes and SPL
942 # text decreased by 4.
943 err = {'_target' : target}
945 if image in base_sizes:
946 base_image = base_sizes[image]
947 # Loop through the text, data, bss parts
948 for part in sorted(sizes[image]):
949 diff = sizes[image][part] - base_image[part]
952 if image == 'u-boot':
955 name = image + ':' + part
957 arch = board_selected[target].arch
958 if not arch in arch_count:
961 arch_count[arch] += 1
963 pass # Only add to our list when we have some stats
964 elif not arch in arch_list:
965 arch_list[arch] = [err]
967 arch_list[arch].append(err)
969 # We now have a list of image size changes sorted by arch
970 # Print out a summary of these
971 for arch, target_list in arch_list.iteritems():
972 # Get total difference for each type
974 for result in target_list:
976 for name, diff in result.iteritems():
977 if name.startswith('_'):
984 result['_total_diff'] = total
985 result['_outcome'] = board_dict[result['_target']]
987 count = len(target_list)
989 for name in sorted(totals):
992 # Display the average difference in this name for this
994 avg_diff = float(diff) / count
995 color = self.col.RED if avg_diff > 0 else self.col.GREEN
996 msg = ' %s %+1.1f' % (name, avg_diff)
998 Print('%10s: (for %d/%d boards)' % (arch, count,
999 arch_count[arch]), newline=False)
1001 Print(msg, colour=color, newline=False)
1006 self.PrintSizeDetail(target_list, show_bloat)
1009 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1010 err_line_boards, warn_lines, warn_line_boards,
1011 config, show_sizes, show_detail, show_bloat,
1013 """Compare results with the base results and display delta.
1015 Only boards mentioned in board_selected will be considered. This
1016 function is intended to be called repeatedly with the results of
1017 each commit. It therefore shows a 'diff' between what it saw in
1018 the last call and what it sees now.
1021 board_selected: Dict containing boards to summarise, keyed by
1023 board_dict: Dict containing boards for which we built this
1024 commit, keyed by board.target. The value is an Outcome object.
1025 err_lines: A list of errors for this commit, or [] if there is
1026 none, or we don't want to print errors
1027 err_line_boards: Dict keyed by error line, containing a list of
1028 the Board objects with that error
1029 warn_lines: A list of warnings for this commit, or [] if there is
1030 none, or we don't want to print errors
1031 warn_line_boards: Dict keyed by warning line, containing a list of
1032 the Board objects with that warning
1033 config: Dictionary keyed by filename - e.g. '.config'. Each
1034 value is itself a dictionary:
1037 show_sizes: Show image size deltas
1038 show_detail: Show detail for each board
1039 show_bloat: Show detail for each function
1040 show_config: Show config changes
1042 def _BoardList(line, line_boards):
1043 """Helper function to get a line of boards containing a line
1046 line: Error line to search for
1048 String containing a list of boards with that error line, or
1049 '' if the user has not requested such a list
1051 if self._list_error_boards:
1053 for board in line_boards[line]:
1054 if not board.target in names:
1055 names.append(board.target)
1056 names_str = '(%s) ' % ','.join(names)
1061 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1066 if line not in base_lines:
1067 worse_lines.append(char + '+' +
1068 _BoardList(line, line_boards) + line)
1069 for line in base_lines:
1070 if line not in lines:
1071 better_lines.append(char + '-' +
1072 _BoardList(line, base_line_boards) + line)
1073 return better_lines, worse_lines
1075 def _CalcConfig(delta, name, config):
1076 """Calculate configuration changes
1079 delta: Type of the delta, e.g. '+'
1080 name: name of the file which changed (e.g. .config)
1081 config: configuration change dictionary
1085 String containing the configuration changes which can be
1089 for key in sorted(config.keys()):
1090 out += '%s=%s ' % (key, config[key])
1091 return '%s %s: %s' % (delta, name, out)
1093 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1094 """Add changes in configuration to a list
1097 lines: list to add to
1098 name: config file name
1099 config_plus: configurations added, dictionary
1102 config_minus: configurations removed, dictionary
1105 config_change: configurations changed, dictionary
1110 lines.append(_CalcConfig('+', name, config_plus))
1112 lines.append(_CalcConfig('-', name, config_minus))
1114 lines.append(_CalcConfig('c', name, config_change))
1116 def _OutputConfigInfo(lines):
1121 col = self.col.GREEN
1122 elif line[0] == '-':
1124 elif line[0] == 'c':
1125 col = self.col.YELLOW
1126 Print(' ' + line, newline=True, colour=col)
1129 better = [] # List of boards fixed since last commit
1130 worse = [] # List of new broken boards since last commit
1131 new = [] # List of boards that didn't exist last time
1132 unknown = [] # List of boards that were not built
1134 for target in board_dict:
1135 if target not in board_selected:
1138 # If the board was built last time, add its outcome to a list
1139 if target in self._base_board_dict:
1140 base_outcome = self._base_board_dict[target].rc
1141 outcome = board_dict[target]
1142 if outcome.rc == OUTCOME_UNKNOWN:
1143 unknown.append(target)
1144 elif outcome.rc < base_outcome:
1145 better.append(target)
1146 elif outcome.rc > base_outcome:
1147 worse.append(target)
1151 # Get a list of errors that have appeared, and disappeared
1152 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1153 self._base_err_line_boards, err_lines, err_line_boards, '')
1154 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1155 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1157 # Display results by arch
1158 if (better or worse or unknown or new or worse_err or better_err
1159 or worse_warn or better_warn):
1161 self.AddOutcome(board_selected, arch_list, better, '',
1163 self.AddOutcome(board_selected, arch_list, worse, '+',
1165 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1166 if self._show_unknown:
1167 self.AddOutcome(board_selected, arch_list, unknown, '?',
1169 for arch, target_list in arch_list.iteritems():
1170 Print('%10s: %s' % (arch, target_list))
1171 self._error_lines += 1
1173 Print('\n'.join(better_err), colour=self.col.GREEN)
1174 self._error_lines += 1
1176 Print('\n'.join(worse_err), colour=self.col.RED)
1177 self._error_lines += 1
1179 Print('\n'.join(better_warn), colour=self.col.CYAN)
1180 self._error_lines += 1
1182 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1183 self._error_lines += 1
1186 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1189 if show_config and self._base_config:
1191 arch_config_plus = {}
1192 arch_config_minus = {}
1193 arch_config_change = {}
1196 for target in board_dict:
1197 if target not in board_selected:
1199 arch = board_selected[target].arch
1200 if arch not in arch_list:
1201 arch_list.append(arch)
1203 for arch in arch_list:
1204 arch_config_plus[arch] = {}
1205 arch_config_minus[arch] = {}
1206 arch_config_change[arch] = {}
1207 for name in self.config_filenames:
1208 arch_config_plus[arch][name] = {}
1209 arch_config_minus[arch][name] = {}
1210 arch_config_change[arch][name] = {}
1212 for target in board_dict:
1213 if target not in board_selected:
1216 arch = board_selected[target].arch
1218 all_config_plus = {}
1219 all_config_minus = {}
1220 all_config_change = {}
1221 tbase = self._base_config[target]
1222 tconfig = config[target]
1224 for name in self.config_filenames:
1225 if not tconfig.config[name]:
1230 base = tbase.config[name]
1231 for key, value in tconfig.config[name].iteritems():
1233 config_plus[key] = value
1234 all_config_plus[key] = value
1235 for key, value in base.iteritems():
1236 if key not in tconfig.config[name]:
1237 config_minus[key] = value
1238 all_config_minus[key] = value
1239 for key, value in base.iteritems():
1240 new_value = tconfig.config.get(key)
1241 if new_value and value != new_value:
1242 desc = '%s -> %s' % (value, new_value)
1243 config_change[key] = desc
1244 all_config_change[key] = desc
1246 arch_config_plus[arch][name].update(config_plus)
1247 arch_config_minus[arch][name].update(config_minus)
1248 arch_config_change[arch][name].update(config_change)
1250 _AddConfig(lines, name, config_plus, config_minus,
1252 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1254 summary[target] = '\n'.join(lines)
1256 lines_by_target = {}
1257 for target, lines in summary.iteritems():
1258 if lines in lines_by_target:
1259 lines_by_target[lines].append(target)
1261 lines_by_target[lines] = [target]
1263 for arch in arch_list:
1268 for name in self.config_filenames:
1269 all_plus.update(arch_config_plus[arch][name])
1270 all_minus.update(arch_config_minus[arch][name])
1271 all_change.update(arch_config_change[arch][name])
1272 _AddConfig(lines, name, arch_config_plus[arch][name],
1273 arch_config_minus[arch][name],
1274 arch_config_change[arch][name])
1275 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1276 #arch_summary[target] = '\n'.join(lines)
1279 _OutputConfigInfo(lines)
1281 for lines, targets in lines_by_target.iteritems():
1284 Print('%s :' % ' '.join(sorted(targets)))
1285 _OutputConfigInfo(lines.split('\n'))
1288 # Save our updated information for the next call to this function
1289 self._base_board_dict = board_dict
1290 self._base_err_lines = err_lines
1291 self._base_warn_lines = warn_lines
1292 self._base_err_line_boards = err_line_boards
1293 self._base_warn_line_boards = warn_line_boards
1294 self._base_config = config
1296 # Get a list of boards that did not get built, if needed
1298 for board in board_selected:
1299 if not board in board_dict:
1300 not_built.append(board)
1302 Print("Boards not built (%d): %s" % (len(not_built),
1303 ', '.join(not_built)))
1305 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1306 (board_dict, err_lines, err_line_boards, warn_lines,
1307 warn_line_boards, config) = self.GetResultSummary(
1308 board_selected, commit_upto,
1309 read_func_sizes=self._show_bloat,
1310 read_config=self._show_config)
1312 msg = '%02d: %s' % (commit_upto + 1,
1313 commits[commit_upto].subject)
1314 Print(msg, colour=self.col.BLUE)
1315 self.PrintResultSummary(board_selected, board_dict,
1316 err_lines if self._show_errors else [], err_line_boards,
1317 warn_lines if self._show_errors else [], warn_line_boards,
1318 config, self._show_sizes, self._show_detail,
1319 self._show_bloat, self._show_config)
1321 def ShowSummary(self, commits, board_selected):
1322 """Show a build summary for U-Boot for a given board list.
1324 Reset the result summary, then repeatedly call GetResultSummary on
1325 each commit's results, then display the differences we see.
1328 commit: Commit objects to summarise
1329 board_selected: Dict containing boards to summarise
1331 self.commit_count = len(commits) if commits else 1
1332 self.commits = commits
1333 self.ResetResultSummary(board_selected)
1334 self._error_lines = 0
1336 for commit_upto in range(0, self.commit_count, self._step):
1337 self.ProduceResultSummary(commit_upto, commits, board_selected)
1338 if not self._error_lines:
1339 Print('(no errors to report)', colour=self.col.GREEN)
1342 def SetupBuild(self, board_selected, commits):
1343 """Set up ready to start a build.
1346 board_selected: Selected boards to build
1347 commits: Selected commits to build
1349 # First work out how many commits we will build
1350 count = (self.commit_count + self._step - 1) / self._step
1351 self.count = len(board_selected) * count
1352 self.upto = self.warned = self.fail = 0
1353 self._timestamps = collections.deque()
1355 def GetThreadDir(self, thread_num):
1356 """Get the directory path to the working dir for a thread.
1359 thread_num: Number of thread to check.
1361 return os.path.join(self._working_dir, '%02d' % thread_num)
1363 def _PrepareThread(self, thread_num, setup_git):
1364 """Prepare the working directory for a thread.
1366 This clones or fetches the repo into the thread's work directory.
1369 thread_num: Thread number (0, 1, ...)
1370 setup_git: True to set up a git repo clone
1372 thread_dir = self.GetThreadDir(thread_num)
1373 builderthread.Mkdir(thread_dir)
1374 git_dir = os.path.join(thread_dir, '.git')
1376 # Clone the repo if it doesn't already exist
1377 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1378 # we have a private index but uses the origin repo's contents?
1379 if setup_git and self.git_dir:
1380 src_dir = os.path.abspath(self.git_dir)
1381 if os.path.exists(git_dir):
1382 gitutil.Fetch(git_dir, thread_dir)
1384 Print('\rCloning repo for thread %d' % thread_num,
1386 gitutil.Clone(src_dir, thread_dir)
1387 Print('\r%s\r' % (' ' * 30), newline=False)
1389 def _PrepareWorkingSpace(self, max_threads, setup_git):
1390 """Prepare the working directory for use.
1392 Set up the git repo for each thread.
1395 max_threads: Maximum number of threads we expect to need.
1396 setup_git: True to set up a git repo clone
1398 builderthread.Mkdir(self._working_dir)
1399 for thread in range(max_threads):
1400 self._PrepareThread(thread, setup_git)
1402 def _PrepareOutputSpace(self):
1403 """Get the output directories ready to receive files.
1405 We delete any output directories which look like ones we need to
1406 create. Having left over directories is confusing when the user wants
1407 to check the output manually.
1409 if not self.commits:
1412 for commit_upto in range(self.commit_count):
1413 dir_list.append(self._GetOutputDir(commit_upto))
1416 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1417 if dirname not in dir_list:
1418 to_remove.append(dirname)
1420 Print('Removing %d old build directories' % len(to_remove),
1422 for dirname in to_remove:
1423 shutil.rmtree(dirname)
1425 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1426 """Build all commits for a list of boards
1429 commits: List of commits to be build, each a Commit object
1430 boards_selected: Dict of selected boards, key is target name,
1431 value is Board object
1432 keep_outputs: True to save build output files
1433 verbose: Display build results as they are completed
1436 - number of boards that failed to build
1437 - number of boards that issued warnings
1439 self.commit_count = len(commits) if commits else 1
1440 self.commits = commits
1441 self._verbose = verbose
1443 self.ResetResultSummary(board_selected)
1444 builderthread.Mkdir(self.base_dir, parents = True)
1445 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1446 commits is not None)
1447 self._PrepareOutputSpace()
1448 Print('\rStarting build...', newline=False)
1449 self.SetupBuild(board_selected, commits)
1450 self.ProcessResult(None)
1452 # Create jobs to build all commits for each board
1453 for brd in board_selected.itervalues():
1454 job = builderthread.BuilderJob()
1456 job.commits = commits
1457 job.keep_outputs = keep_outputs
1458 job.step = self._step
1461 term = threading.Thread(target=self.queue.join)
1462 term.setDaemon(True)
1464 while term.isAlive():
1467 # Wait until we have processed all output
1468 self.out_queue.join()
1471 return (self.fail, self.warned)