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'
108 """Class for building U-Boot for a particular commit.
110 Public members: (many should ->private)
111 active: True if the builder is active and has not been stopped
112 already_done: Number of builds already completed
113 base_dir: Base directory to use for builder
114 checkout: True to check out source, False to skip that step.
115 This is used for testing.
116 col: terminal.Color() object
117 count: Number of commits to build
118 do_make: Method to call to invoke Make
119 fail: Number of builds that failed due to error
120 force_build: Force building even if a build already exists
121 force_config_on_failure: If a commit fails for a board, disable
122 incremental building for the next commit we build for that
123 board, so that we will see all warnings/errors again.
124 force_build_failures: If a previously-built build (i.e. built on
125 a previous run of buildman) is marked as failed, rebuild it.
126 git_dir: Git directory containing source repository
127 last_line_len: Length of the last line we printed (used for erasing
128 it with new progress information)
129 num_jobs: Number of jobs to run at once (passed to make as -j)
130 num_threads: Number of builder threads to run
131 out_queue: Queue of results to process
132 re_make_err: Compiled regular expression for ignore_lines
133 queue: Queue of jobs to run
134 threads: List of active threads
135 toolchains: Toolchains object to use for building
136 upto: Current commit number we are building (0.count-1)
137 warned: Number of builds that produced at least one warning
138 force_reconfig: Reconfigure U-Boot on each comiit. This disables
139 incremental building, where buildman reconfigures on the first
140 commit for a baord, and then just does an incremental build for
141 the following commits. In fact buildman will reconfigure and
142 retry for any failing commits, so generally the only effect of
143 this option is to slow things down.
144 in_tree: Build U-Boot in-tree instead of specifying an output
145 directory separate from the source code. This option is really
146 only useful for testing in-tree builds.
149 _base_board_dict: Last-summarised Dict of boards
150 _base_err_lines: Last-summarised list of errors
151 _base_warn_lines: Last-summarised list of warnings
152 _build_period_us: Time taken for a single build (float object).
153 _complete_delay: Expected delay until completion (timedelta)
154 _next_delay_update: Next time we plan to display a progress update
156 _show_unknown: Show unknown boards (those not built) in summary
157 _timestamps: List of timestamps for the completion of the last
158 last _timestamp_count builds. Each is a datetime object.
159 _timestamp_count: Number of timestamps to keep in our list.
160 _working_dir: Base working directory containing all threads
163 """Records a build outcome for a single make invocation
166 rc: Outcome value (OUTCOME_...)
167 err_lines: List of error lines or [] if none
168 sizes: Dictionary of image size information, keyed by filename
169 - Each value is itself a dictionary containing
170 values for 'text', 'data' and 'bss', being the integer
171 size in bytes of each section.
172 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
173 value is itself a dictionary:
175 value: Size of function in bytes
176 config: Dictionary keyed by filename - e.g. '.config'. Each
177 value is itself a dictionary:
181 def __init__(self, rc, err_lines, sizes, func_sizes, config):
183 self.err_lines = err_lines
185 self.func_sizes = func_sizes
188 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
189 gnu_make='make', checkout=True, show_unknown=True, step=1,
190 no_subdirs=False, full_path=False, verbose_build=False):
191 """Create a new Builder object
194 toolchains: Toolchains object to use for building
195 base_dir: Base directory to use for builder
196 git_dir: Git directory containing source repository
197 num_threads: Number of builder threads to run
198 num_jobs: Number of jobs to run at once (passed to make as -j)
199 gnu_make: the command name of GNU Make.
200 checkout: True to check out source, False to skip that step.
201 This is used for testing.
202 show_unknown: Show unknown boards (those not built) in summary
203 step: 1 to process every commit, n to process every nth commit
204 no_subdirs: Don't create subdirectories when building current
205 source for a single board
206 full_path: Return the full path in CROSS_COMPILE and don't set
208 verbose_build: Run build with V=1 and don't use 'make -s'
210 self.toolchains = toolchains
211 self.base_dir = base_dir
212 self._working_dir = os.path.join(base_dir, '.bm-work')
215 self.do_make = self.Make
216 self.gnu_make = gnu_make
217 self.checkout = checkout
218 self.num_threads = num_threads
219 self.num_jobs = num_jobs
220 self.already_done = 0
221 self.force_build = False
222 self.git_dir = git_dir
223 self._show_unknown = show_unknown
224 self._timestamp_count = 10
225 self._build_period_us = None
226 self._complete_delay = None
227 self._next_delay_update = datetime.now()
228 self.force_config_on_failure = True
229 self.force_build_failures = False
230 self.force_reconfig = False
233 self._error_lines = 0
234 self.no_subdirs = no_subdirs
235 self.full_path = full_path
236 self.verbose_build = verbose_build
238 self.col = terminal.Color()
240 self._re_function = re.compile('(.*): In function.*')
241 self._re_files = re.compile('In file included from.*')
242 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
243 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
245 self.queue = Queue.Queue()
246 self.out_queue = Queue.Queue()
247 for i in range(self.num_threads):
248 t = builderthread.BuilderThread(self, i)
251 self.threads.append(t)
253 self.last_line_len = 0
254 t = builderthread.ResultThread(self)
257 self.threads.append(t)
259 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
260 self.re_make_err = re.compile('|'.join(ignore_lines))
263 """Get rid of all threads created by the builder"""
264 for t in self.threads:
267 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
268 show_detail=False, show_bloat=False,
269 list_error_boards=False, show_config=False):
270 """Setup display options for the builder.
272 show_errors: True to show summarised error/warning info
273 show_sizes: Show size deltas
274 show_detail: Show detail for each board
275 show_bloat: Show detail for each function
276 list_error_boards: Show the boards which caused each error/warning
277 show_config: Show config deltas
279 self._show_errors = show_errors
280 self._show_sizes = show_sizes
281 self._show_detail = show_detail
282 self._show_bloat = show_bloat
283 self._list_error_boards = list_error_boards
284 self._show_config = show_config
286 def _AddTimestamp(self):
287 """Add a new timestamp to the list and record the build period.
289 The build period is the length of time taken to perform a single
290 build (one board, one commit).
293 self._timestamps.append(now)
294 count = len(self._timestamps)
295 delta = self._timestamps[-1] - self._timestamps[0]
296 seconds = delta.total_seconds()
298 # If we have enough data, estimate build period (time taken for a
299 # single build) and therefore completion time.
300 if count > 1 and self._next_delay_update < now:
301 self._next_delay_update = now + timedelta(seconds=2)
303 self._build_period = float(seconds) / count
304 todo = self.count - self.upto
305 self._complete_delay = timedelta(microseconds=
306 self._build_period * todo * 1000000)
308 self._complete_delay -= timedelta(
309 microseconds=self._complete_delay.microseconds)
312 self._timestamps.popleft()
315 def ClearLine(self, length):
316 """Clear any characters on the current line
318 Make way for a new line of length 'length', by outputting enough
319 spaces to clear out the old line. Then remember the new length for
323 length: Length of new line, in characters
325 if length < self.last_line_len:
326 Print(' ' * (self.last_line_len - length), newline=False)
327 Print('\r', newline=False)
328 self.last_line_len = length
331 def SelectCommit(self, commit, checkout=True):
332 """Checkout the selected commit for this build
335 if checkout and self.checkout:
336 gitutil.Checkout(commit.hash)
338 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
342 commit: Commit object that is being built
343 brd: Board object that is being built
344 stage: Stage that we are at (mrproper, config, build)
345 cwd: Directory where make should be run
346 args: Arguments to pass to make
347 kwargs: Arguments to pass to command.RunPipe()
349 cmd = [self.gnu_make] + list(args)
350 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
351 cwd=cwd, raise_on_error=False, **kwargs)
352 if self.verbose_build:
353 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
354 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
357 def ProcessResult(self, result):
358 """Process the result of a build, showing progress information
361 result: A CommandResult object, which indicates the result for
364 col = terminal.Color()
366 target = result.brd.target
368 if result.return_code < 0:
374 if result.return_code != 0:
378 if result.already_done:
379 self.already_done += 1
381 Print('\r', newline=False)
383 boards_selected = {target : result.brd}
384 self.ResetResultSummary(boards_selected)
385 self.ProduceResultSummary(result.commit_upto, self.commits,
388 target = '(starting)'
390 # Display separate counts for ok, warned and fail
391 ok = self.upto - self.warned - self.fail
392 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
393 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
394 line += self.col.Color(self.col.RED, '%5d' % self.fail)
396 name = ' /%-5d ' % self.count
398 # Add our current completion time estimate
400 if self._complete_delay:
401 name += '%s : ' % self._complete_delay
402 # When building all boards for a commit, we can print a commit
404 if result and result.commit_upto is None:
405 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
409 Print(line + name, newline=False)
410 length = 14 + len(name)
411 self.ClearLine(length)
413 def _GetOutputDir(self, commit_upto):
414 """Get the name of the output directory for a commit number
416 The output directory is typically .../<branch>/<commit>.
419 commit_upto: Commit number to use (0..self.count-1)
423 commit = self.commits[commit_upto]
424 subject = commit.subject.translate(trans_valid_chars)
425 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
426 self.commit_count, commit.hash, subject[:20]))
427 elif not self.no_subdirs:
428 commit_dir = 'current'
431 return os.path.join(self.base_dir, commit_dir)
433 def GetBuildDir(self, commit_upto, target):
434 """Get the name of the build directory for a commit number
436 The build directory is typically .../<branch>/<commit>/<target>.
439 commit_upto: Commit number to use (0..self.count-1)
442 output_dir = self._GetOutputDir(commit_upto)
443 return os.path.join(output_dir, target)
445 def GetDoneFile(self, commit_upto, target):
446 """Get the name of the done file for a commit number
449 commit_upto: Commit number to use (0..self.count-1)
452 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
454 def GetSizesFile(self, commit_upto, target):
455 """Get the name of the sizes file for a commit number
458 commit_upto: Commit number to use (0..self.count-1)
461 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
463 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
464 """Get the name of the funcsizes file for a commit number and ELF file
467 commit_upto: Commit number to use (0..self.count-1)
469 elf_fname: Filename of elf image
471 return os.path.join(self.GetBuildDir(commit_upto, target),
472 '%s.sizes' % elf_fname.replace('/', '-'))
474 def GetObjdumpFile(self, commit_upto, target, elf_fname):
475 """Get the name of the objdump file for a commit number and ELF file
478 commit_upto: Commit number to use (0..self.count-1)
480 elf_fname: Filename of elf image
482 return os.path.join(self.GetBuildDir(commit_upto, target),
483 '%s.objdump' % elf_fname.replace('/', '-'))
485 def GetErrFile(self, commit_upto, target):
486 """Get the name of the err file for a commit number
489 commit_upto: Commit number to use (0..self.count-1)
492 output_dir = self.GetBuildDir(commit_upto, target)
493 return os.path.join(output_dir, 'err')
495 def FilterErrors(self, lines):
496 """Filter out errors in which we have no interest
498 We should probably use map().
501 lines: List of error lines, each a string
503 New list with only interesting lines included
507 if not self.re_make_err.search(line):
508 out_lines.append(line)
511 def ReadFuncSizes(self, fname, fd):
512 """Read function sizes from the output of 'nm'
515 fd: File containing data to read
516 fname: Filename we are reading from (just for errors)
519 Dictionary containing size of each function in bytes, indexed by
523 for line in fd.readlines():
525 size, type, name = line[:-1].split()
527 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
530 # function names begin with '.' on 64-bit powerpc
532 name = 'static.' + name.split('.')[0]
533 sym[name] = sym.get(name, 0) + int(size, 16)
536 def _ProcessConfig(self, fname):
537 """Read in a .config, autoconf.mk or autoconf.h file
539 This function handles all config file types. It ignores comments and
540 any #defines which don't start with CONFIG_.
543 fname: Filename to read
547 key: Config name (e.g. CONFIG_DM)
548 value: Config value (e.g. 1)
551 if os.path.exists(fname):
552 with open(fname) as fd:
555 if line.startswith('#define'):
556 values = line[8:].split(' ', 1)
562 if not key.startswith('CONFIG_'):
564 elif not line or line[0] in ['#', '*', '/']:
567 key, value = line.split('=', 1)
571 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
573 """Work out the outcome of a build.
576 commit_upto: Commit number to check (0..n-1)
577 target: Target board to check
578 read_func_sizes: True to read function size information
579 read_config: True to read .config and autoconf.h files
584 done_file = self.GetDoneFile(commit_upto, target)
585 sizes_file = self.GetSizesFile(commit_upto, target)
589 if os.path.exists(done_file):
590 with open(done_file, 'r') as fd:
591 return_code = int(fd.readline())
593 err_file = self.GetErrFile(commit_upto, target)
594 if os.path.exists(err_file):
595 with open(err_file, 'r') as fd:
596 err_lines = self.FilterErrors(fd.readlines())
598 # Decide whether the build was ok, failed or created warnings
606 # Convert size information to our simple format
607 if os.path.exists(sizes_file):
608 with open(sizes_file, 'r') as fd:
609 for line in fd.readlines():
610 values = line.split()
613 rodata = int(values[6], 16)
615 'all' : int(values[0]) + int(values[1]) +
617 'text' : int(values[0]) - rodata,
618 'data' : int(values[1]),
619 'bss' : int(values[2]),
622 sizes[values[5]] = size_dict
625 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
626 for fname in glob.glob(pattern):
627 with open(fname, 'r') as fd:
628 dict_name = os.path.basename(fname).replace('.sizes',
630 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
633 output_dir = self.GetBuildDir(commit_upto, target)
634 for name in CONFIG_FILENAMES:
635 fname = os.path.join(output_dir, name)
636 config[name] = self._ProcessConfig(fname)
638 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config)
640 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {})
642 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
644 """Calculate a summary of the results of building a commit.
647 board_selected: Dict containing boards to summarise
648 commit_upto: Commit number to summarize (0..self.count-1)
649 read_func_sizes: True to read function size information
650 read_config: True to read .config and autoconf.h files
654 Dict containing boards which passed building this commit.
655 keyed by board.target
656 List containing a summary of error lines
657 Dict keyed by error line, containing a list of the Board
658 objects with that error
659 List containing a summary of warning lines
660 Dict keyed by error line, containing a list of the Board
661 objects with that warning
662 Dictionary keyed by filename - e.g. '.config'. Each
663 value is itself a dictionary:
667 def AddLine(lines_summary, lines_boards, line, board):
669 if line in lines_boards:
670 lines_boards[line].append(board)
672 lines_boards[line] = [board]
673 lines_summary.append(line)
676 err_lines_summary = []
677 err_lines_boards = {}
678 warn_lines_summary = []
679 warn_lines_boards = {}
681 for fname in CONFIG_FILENAMES:
684 for board in boards_selected.itervalues():
685 outcome = self.GetBuildOutcome(commit_upto, board.target,
686 read_func_sizes, read_config)
687 board_dict[board.target] = outcome
689 last_was_warning = False
690 for line in outcome.err_lines:
692 if (self._re_function.match(line) or
693 self._re_files.match(line)):
696 is_warning = self._re_warning.match(line)
697 is_note = self._re_note.match(line)
698 if is_warning or (last_was_warning and is_note):
700 AddLine(warn_lines_summary, warn_lines_boards,
702 AddLine(warn_lines_summary, warn_lines_boards,
706 AddLine(err_lines_summary, err_lines_boards,
708 AddLine(err_lines_summary, err_lines_boards,
710 last_was_warning = is_warning
712 for fname in CONFIG_FILENAMES:
715 for key, value in outcome.config[fname].iteritems():
716 config[fname][key] = value
718 return (board_dict, err_lines_summary, err_lines_boards,
719 warn_lines_summary, warn_lines_boards, config)
721 def AddOutcome(self, board_dict, arch_list, changes, char, color):
722 """Add an output to our list of outcomes for each architecture
724 This simple function adds failing boards (changes) to the
725 relevant architecture string, so we can print the results out
726 sorted by architecture.
729 board_dict: Dict containing all boards
730 arch_list: Dict keyed by arch name. Value is a string containing
731 a list of board names which failed for that arch.
732 changes: List of boards to add to arch_list
733 color: terminal.Colour object
736 for target in changes:
737 if target in board_dict:
738 arch = board_dict[target].arch
741 str = self.col.Color(color, ' ' + target)
742 if not arch in done_arch:
743 str = ' %s %s' % (self.col.Color(color, char), str)
744 done_arch[arch] = True
745 if not arch in arch_list:
746 arch_list[arch] = str
748 arch_list[arch] += str
751 def ColourNum(self, num):
752 color = self.col.RED if num > 0 else self.col.GREEN
755 return self.col.Color(color, str(num))
757 def ResetResultSummary(self, board_selected):
758 """Reset the results summary ready for use.
760 Set up the base board list to be all those selected, and set the
761 error lines to empty.
763 Following this, calls to PrintResultSummary() will use this
764 information to work out what has changed.
767 board_selected: Dict containing boards to summarise, keyed by
770 self._base_board_dict = {}
771 for board in board_selected:
772 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {})
773 self._base_err_lines = []
774 self._base_warn_lines = []
775 self._base_err_line_boards = {}
776 self._base_warn_line_boards = {}
777 self._base_config = {}
778 for fname in CONFIG_FILENAMES:
779 self._base_config[fname] = {}
781 def PrintFuncSizeDetail(self, fname, old, new):
782 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
783 delta, common = [], {}
790 if name not in common:
793 delta.append([-old[name], name])
796 if name not in common:
799 delta.append([new[name], name])
802 diff = new.get(name, 0) - old.get(name, 0)
804 grow, up = grow + 1, up + diff
806 shrink, down = shrink + 1, down - diff
807 delta.append([diff, name])
812 args = [add, -remove, grow, -shrink, up, -down, up - down]
815 args = [self.ColourNum(x) for x in args]
817 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
818 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
819 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
821 for diff, name in delta:
823 color = self.col.RED if diff > 0 else self.col.GREEN
824 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
825 old.get(name, '-'), new.get(name,'-'), diff)
826 Print(msg, colour=color)
829 def PrintSizeDetail(self, target_list, show_bloat):
830 """Show details size information for each board
833 target_list: List of targets, each a dict containing:
834 'target': Target name
835 'total_diff': Total difference in bytes across all areas
836 <part_name>: Difference for that part
837 show_bloat: Show detail for each function
839 targets_by_diff = sorted(target_list, reverse=True,
840 key=lambda x: x['_total_diff'])
841 for result in targets_by_diff:
842 printed_target = False
843 for name in sorted(result):
845 if name.startswith('_'):
848 color = self.col.RED if diff > 0 else self.col.GREEN
849 msg = ' %s %+d' % (name, diff)
850 if not printed_target:
851 Print('%10s %-15s:' % ('', result['_target']),
853 printed_target = True
854 Print(msg, colour=color, newline=False)
858 target = result['_target']
859 outcome = result['_outcome']
860 base_outcome = self._base_board_dict[target]
861 for fname in outcome.func_sizes:
862 self.PrintFuncSizeDetail(fname,
863 base_outcome.func_sizes[fname],
864 outcome.func_sizes[fname])
867 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
869 """Print a summary of image sizes broken down by section.
871 The summary takes the form of one line per architecture. The
872 line contains deltas for each of the sections (+ means the section
873 got bigger, - means smaller). The nunmbers are the average number
874 of bytes that a board in this section increased by.
877 powerpc: (622 boards) text -0.0
878 arm: (285 boards) text -0.0
879 nds32: (3 boards) text -8.0
882 board_selected: Dict containing boards to summarise, keyed by
884 board_dict: Dict containing boards for which we built this
885 commit, keyed by board.target. The value is an Outcome object.
886 show_detail: Show detail for each board
887 show_bloat: Show detail for each function
892 # Calculate changes in size for different image parts
893 # The previous sizes are in Board.sizes, for each board
894 for target in board_dict:
895 if target not in board_selected:
897 base_sizes = self._base_board_dict[target].sizes
898 outcome = board_dict[target]
899 sizes = outcome.sizes
901 # Loop through the list of images, creating a dict of size
902 # changes for each image/part. We end up with something like
903 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
904 # which means that U-Boot data increased by 5 bytes and SPL
905 # text decreased by 4.
906 err = {'_target' : target}
908 if image in base_sizes:
909 base_image = base_sizes[image]
910 # Loop through the text, data, bss parts
911 for part in sorted(sizes[image]):
912 diff = sizes[image][part] - base_image[part]
915 if image == 'u-boot':
918 name = image + ':' + part
920 arch = board_selected[target].arch
921 if not arch in arch_count:
924 arch_count[arch] += 1
926 pass # Only add to our list when we have some stats
927 elif not arch in arch_list:
928 arch_list[arch] = [err]
930 arch_list[arch].append(err)
932 # We now have a list of image size changes sorted by arch
933 # Print out a summary of these
934 for arch, target_list in arch_list.iteritems():
935 # Get total difference for each type
937 for result in target_list:
939 for name, diff in result.iteritems():
940 if name.startswith('_'):
947 result['_total_diff'] = total
948 result['_outcome'] = board_dict[result['_target']]
950 count = len(target_list)
952 for name in sorted(totals):
955 # Display the average difference in this name for this
957 avg_diff = float(diff) / count
958 color = self.col.RED if avg_diff > 0 else self.col.GREEN
959 msg = ' %s %+1.1f' % (name, avg_diff)
961 Print('%10s: (for %d/%d boards)' % (arch, count,
962 arch_count[arch]), newline=False)
964 Print(msg, colour=color, newline=False)
969 self.PrintSizeDetail(target_list, show_bloat)
972 def PrintResultSummary(self, board_selected, board_dict, err_lines,
973 err_line_boards, warn_lines, warn_line_boards,
974 config, show_sizes, show_detail, show_bloat,
976 """Compare results with the base results and display delta.
978 Only boards mentioned in board_selected will be considered. This
979 function is intended to be called repeatedly with the results of
980 each commit. It therefore shows a 'diff' between what it saw in
981 the last call and what it sees now.
984 board_selected: Dict containing boards to summarise, keyed by
986 board_dict: Dict containing boards for which we built this
987 commit, keyed by board.target. The value is an Outcome object.
988 err_lines: A list of errors for this commit, or [] if there is
989 none, or we don't want to print errors
990 err_line_boards: Dict keyed by error line, containing a list of
991 the Board objects with that error
992 warn_lines: A list of warnings for this commit, or [] if there is
993 none, or we don't want to print errors
994 warn_line_boards: Dict keyed by warning line, containing a list of
995 the Board objects with that warning
996 config: Dictionary keyed by filename - e.g. '.config'. Each
997 value is itself a dictionary:
1000 show_sizes: Show image size deltas
1001 show_detail: Show detail for each board
1002 show_bloat: Show detail for each function
1003 show_config: Show config changes
1005 def _BoardList(line, line_boards):
1006 """Helper function to get a line of boards containing a line
1009 line: Error line to search for
1011 String containing a list of boards with that error line, or
1012 '' if the user has not requested such a list
1014 if self._list_error_boards:
1016 for board in line_boards[line]:
1017 if not board.target in names:
1018 names.append(board.target)
1019 names_str = '(%s) ' % ','.join(names)
1024 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1029 if line not in base_lines:
1030 worse_lines.append(char + '+' +
1031 _BoardList(line, line_boards) + line)
1032 for line in base_lines:
1033 if line not in lines:
1034 better_lines.append(char + '-' +
1035 _BoardList(line, base_line_boards) + line)
1036 return better_lines, worse_lines
1038 def _CalcConfig(delta, name, config):
1039 """Calculate configuration changes
1042 delta: Type of the delta, e.g. '+'
1043 name: name of the file which changed (e.g. .config)
1044 config: configuration change dictionary
1048 String containing the configuration changes which can be
1052 for key in sorted(config.keys()):
1053 out += '%s=%s ' % (key, config[key])
1054 return '%5s %s: %s' % (delta, name, out)
1056 def _ShowConfig(name, config_plus, config_minus, config_change):
1057 """Show changes in configuration
1060 config_plus: configurations added, dictionary
1063 config_minus: configurations removed, dictionary
1066 config_change: configurations changed, dictionary
1071 Print(_CalcConfig('+', name, config_plus),
1072 colour=self.col.GREEN)
1074 Print(_CalcConfig('-', name, config_minus),
1075 colour=self.col.RED)
1077 Print(_CalcConfig('+/-', name, config_change),
1078 colour=self.col.YELLOW)
1080 better = [] # List of boards fixed since last commit
1081 worse = [] # List of new broken boards since last commit
1082 new = [] # List of boards that didn't exist last time
1083 unknown = [] # List of boards that were not built
1085 for target in board_dict:
1086 if target not in board_selected:
1089 # If the board was built last time, add its outcome to a list
1090 if target in self._base_board_dict:
1091 base_outcome = self._base_board_dict[target].rc
1092 outcome = board_dict[target]
1093 if outcome.rc == OUTCOME_UNKNOWN:
1094 unknown.append(target)
1095 elif outcome.rc < base_outcome:
1096 better.append(target)
1097 elif outcome.rc > base_outcome:
1098 worse.append(target)
1102 # Get a list of errors that have appeared, and disappeared
1103 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1104 self._base_err_line_boards, err_lines, err_line_boards, '')
1105 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1106 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1108 # Display results by arch
1109 if (better or worse or unknown or new or worse_err or better_err
1110 or worse_warn or better_warn):
1112 self.AddOutcome(board_selected, arch_list, better, '',
1114 self.AddOutcome(board_selected, arch_list, worse, '+',
1116 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1117 if self._show_unknown:
1118 self.AddOutcome(board_selected, arch_list, unknown, '?',
1120 for arch, target_list in arch_list.iteritems():
1121 Print('%10s: %s' % (arch, target_list))
1122 self._error_lines += 1
1124 Print('\n'.join(better_err), colour=self.col.GREEN)
1125 self._error_lines += 1
1127 Print('\n'.join(worse_err), colour=self.col.RED)
1128 self._error_lines += 1
1130 Print('\n'.join(better_warn), colour=self.col.CYAN)
1131 self._error_lines += 1
1133 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1134 self._error_lines += 1
1137 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1141 all_config_plus = {}
1142 all_config_minus = {}
1143 all_config_change = {}
1144 for name in CONFIG_FILENAMES:
1145 if not config[name]:
1150 base = self._base_config[name]
1151 for key, value in config[name].iteritems():
1153 config_plus[key] = value
1154 all_config_plus[key] = value
1155 for key, value in base.iteritems():
1156 if key not in config[name]:
1157 config_minus[key] = value
1158 all_config_minus[key] = value
1159 for key, value in base.iteritems():
1160 new_value = base[key]
1161 if key in config[name] and value != new_value:
1162 desc = '%s -> %s' % (value, new_value)
1163 config_change[key] = desc
1164 all_config_change[key] = desc
1165 _ShowConfig(name, config_plus, config_minus, config_change)
1166 _ShowConfig('all', all_config_plus, all_config_minus,
1169 # Save our updated information for the next call to this function
1170 self._base_board_dict = board_dict
1171 self._base_err_lines = err_lines
1172 self._base_warn_lines = warn_lines
1173 self._base_err_line_boards = err_line_boards
1174 self._base_warn_line_boards = warn_line_boards
1175 self._base_config = config
1177 # Get a list of boards that did not get built, if needed
1179 for board in board_selected:
1180 if not board in board_dict:
1181 not_built.append(board)
1183 Print("Boards not built (%d): %s" % (len(not_built),
1184 ', '.join(not_built)))
1186 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1187 (board_dict, err_lines, err_line_boards, warn_lines,
1188 warn_line_boards, config) = self.GetResultSummary(
1189 board_selected, commit_upto,
1190 read_func_sizes=self._show_bloat,
1191 read_config=self._show_config)
1193 msg = '%02d: %s' % (commit_upto + 1,
1194 commits[commit_upto].subject)
1195 Print(msg, colour=self.col.BLUE)
1196 self.PrintResultSummary(board_selected, board_dict,
1197 err_lines if self._show_errors else [], err_line_boards,
1198 warn_lines if self._show_errors else [], warn_line_boards,
1199 config, self._show_sizes, self._show_detail,
1200 self._show_bloat, self._show_config)
1202 def ShowSummary(self, commits, board_selected):
1203 """Show a build summary for U-Boot for a given board list.
1205 Reset the result summary, then repeatedly call GetResultSummary on
1206 each commit's results, then display the differences we see.
1209 commit: Commit objects to summarise
1210 board_selected: Dict containing boards to summarise
1212 self.commit_count = len(commits) if commits else 1
1213 self.commits = commits
1214 self.ResetResultSummary(board_selected)
1215 self._error_lines = 0
1217 for commit_upto in range(0, self.commit_count, self._step):
1218 self.ProduceResultSummary(commit_upto, commits, board_selected)
1219 if not self._error_lines:
1220 Print('(no errors to report)', colour=self.col.GREEN)
1223 def SetupBuild(self, board_selected, commits):
1224 """Set up ready to start a build.
1227 board_selected: Selected boards to build
1228 commits: Selected commits to build
1230 # First work out how many commits we will build
1231 count = (self.commit_count + self._step - 1) / self._step
1232 self.count = len(board_selected) * count
1233 self.upto = self.warned = self.fail = 0
1234 self._timestamps = collections.deque()
1236 def GetThreadDir(self, thread_num):
1237 """Get the directory path to the working dir for a thread.
1240 thread_num: Number of thread to check.
1242 return os.path.join(self._working_dir, '%02d' % thread_num)
1244 def _PrepareThread(self, thread_num, setup_git):
1245 """Prepare the working directory for a thread.
1247 This clones or fetches the repo into the thread's work directory.
1250 thread_num: Thread number (0, 1, ...)
1251 setup_git: True to set up a git repo clone
1253 thread_dir = self.GetThreadDir(thread_num)
1254 builderthread.Mkdir(thread_dir)
1255 git_dir = os.path.join(thread_dir, '.git')
1257 # Clone the repo if it doesn't already exist
1258 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1259 # we have a private index but uses the origin repo's contents?
1260 if setup_git and self.git_dir:
1261 src_dir = os.path.abspath(self.git_dir)
1262 if os.path.exists(git_dir):
1263 gitutil.Fetch(git_dir, thread_dir)
1265 Print('Cloning repo for thread %d' % thread_num)
1266 gitutil.Clone(src_dir, thread_dir)
1268 def _PrepareWorkingSpace(self, max_threads, setup_git):
1269 """Prepare the working directory for use.
1271 Set up the git repo for each thread.
1274 max_threads: Maximum number of threads we expect to need.
1275 setup_git: True to set up a git repo clone
1277 builderthread.Mkdir(self._working_dir)
1278 for thread in range(max_threads):
1279 self._PrepareThread(thread, setup_git)
1281 def _PrepareOutputSpace(self):
1282 """Get the output directories ready to receive files.
1284 We delete any output directories which look like ones we need to
1285 create. Having left over directories is confusing when the user wants
1286 to check the output manually.
1288 if not self.commits:
1291 for commit_upto in range(self.commit_count):
1292 dir_list.append(self._GetOutputDir(commit_upto))
1294 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1295 if dirname not in dir_list:
1296 shutil.rmtree(dirname)
1298 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1299 """Build all commits for a list of boards
1302 commits: List of commits to be build, each a Commit object
1303 boards_selected: Dict of selected boards, key is target name,
1304 value is Board object
1305 keep_outputs: True to save build output files
1306 verbose: Display build results as they are completed
1309 - number of boards that failed to build
1310 - number of boards that issued warnings
1312 self.commit_count = len(commits) if commits else 1
1313 self.commits = commits
1314 self._verbose = verbose
1316 self.ResetResultSummary(board_selected)
1317 builderthread.Mkdir(self.base_dir, parents = True)
1318 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1319 commits is not None)
1320 self._PrepareOutputSpace()
1321 self.SetupBuild(board_selected, commits)
1322 self.ProcessResult(None)
1324 # Create jobs to build all commits for each board
1325 for brd in board_selected.itervalues():
1326 job = builderthread.BuilderJob()
1328 job.commits = commits
1329 job.keep_outputs = keep_outputs
1330 job.step = self._step
1333 # Wait until all jobs are started
1336 # Wait until we have processed all output
1337 self.out_queue.join()
1340 return (self.fail, self.warned)