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