1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2013 The Chromium OS Authors.
4 # Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com>
8 from datetime import datetime, timedelta
24 from terminal import Print
31 Please see README for user documentation, and you should be familiar with
32 that before trying to make sense of this.
34 Buildman works by keeping the machine as busy as possible, building different
35 commits for different boards on multiple CPUs at once.
37 The source repo (self.git_dir) contains all the commits to be built. Each
38 thread works on a single board at a time. It checks out the first commit,
39 configures it for that board, then builds it. Then it checks out the next
40 commit and builds it (typically without re-configuring). When it runs out
41 of commits, it gets another job from the builder and starts again with that
44 Clearly the builder threads could work either way - they could check out a
45 commit and then built it for all boards. Using separate directories for each
46 commit/board pair they could leave their build product around afterwards
49 The intent behind building a single board for multiple commits, is to make
50 use of incremental builds. Since each commit is built incrementally from
51 the previous one, builds are faster. Reconfiguring for a different board
52 removes all intermediate object files.
54 Many threads can be working at once, but each has its own working directory.
55 When a thread finishes a build, it puts the output files into a result
58 The base directory used by buildman is normally '../<branch>', i.e.
59 a directory higher than the source repository and named after the branch
62 Within the base directory, we have one subdirectory for each commit. Within
63 that is one subdirectory for each board. Within that is the build output for
64 that commit/board combination.
66 Buildman also create working directories for each thread, in a .bm-work/
67 subdirectory in the base dir.
69 As an example, say we are building branch 'us-net' for boards 'sandbox' and
70 'seaboard', and say that us-net has two commits. We will have directories
73 us-net/ base directory
74 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
79 02_of_02_g4ed4ebc_net--Check-tftp-comp/
85 00/ working directory for thread 0 (contains source checkout)
87 01/ working directory for thread 1
90 u-boot/ source directory
94 # Possible build outcomes
95 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = range(4)
97 # Translate a commit subject into a valid filename (and handle unicode)
98 trans_valid_chars = string.maketrans('/: ', '---')
99 trans_valid_chars = trans_valid_chars.decode('latin-1')
101 BASE_CONFIG_FILENAMES = [
102 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
105 EXTRA_CONFIG_FILENAMES = [
106 '.config', '.config-spl', '.config-tpl',
107 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
108 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
112 """Holds information about configuration settings for a board."""
113 def __init__(self, config_filename, target):
116 for fname in config_filename:
117 self.config[fname] = {}
119 def Add(self, fname, key, value):
120 self.config[fname][key] = value
124 for fname in self.config:
125 for key, value in self.config[fname].iteritems():
127 val = val ^ hash(key) & hash(value)
131 """Holds information about environment variables for a board."""
132 def __init__(self, target):
134 self.environment = {}
136 def Add(self, key, value):
137 self.environment[key] = value
140 """Class for building U-Boot for a particular commit.
142 Public members: (many should ->private)
143 already_done: Number of builds already completed
144 base_dir: Base directory to use for builder
145 checkout: True to check out source, False to skip that step.
146 This is used for testing.
147 col: terminal.Color() object
148 count: Number of commits to build
149 do_make: Method to call to invoke Make
150 fail: Number of builds that failed due to error
151 force_build: Force building even if a build already exists
152 force_config_on_failure: If a commit fails for a board, disable
153 incremental building for the next commit we build for that
154 board, so that we will see all warnings/errors again.
155 force_build_failures: If a previously-built build (i.e. built on
156 a previous run of buildman) is marked as failed, rebuild it.
157 git_dir: Git directory containing source repository
158 last_line_len: Length of the last line we printed (used for erasing
159 it with new progress information)
160 num_jobs: Number of jobs to run at once (passed to make as -j)
161 num_threads: Number of builder threads to run
162 out_queue: Queue of results to process
163 re_make_err: Compiled regular expression for ignore_lines
164 queue: Queue of jobs to run
165 threads: List of active threads
166 toolchains: Toolchains object to use for building
167 upto: Current commit number we are building (0.count-1)
168 warned: Number of builds that produced at least one warning
169 force_reconfig: Reconfigure U-Boot on each comiit. This disables
170 incremental building, where buildman reconfigures on the first
171 commit for a baord, and then just does an incremental build for
172 the following commits. In fact buildman will reconfigure and
173 retry for any failing commits, so generally the only effect of
174 this option is to slow things down.
175 in_tree: Build U-Boot in-tree instead of specifying an output
176 directory separate from the source code. This option is really
177 only useful for testing in-tree builds.
180 _base_board_dict: Last-summarised Dict of boards
181 _base_err_lines: Last-summarised list of errors
182 _base_warn_lines: Last-summarised list of warnings
183 _build_period_us: Time taken for a single build (float object).
184 _complete_delay: Expected delay until completion (timedelta)
185 _next_delay_update: Next time we plan to display a progress update
187 _show_unknown: Show unknown boards (those not built) in summary
188 _timestamps: List of timestamps for the completion of the last
189 last _timestamp_count builds. Each is a datetime object.
190 _timestamp_count: Number of timestamps to keep in our list.
191 _working_dir: Base working directory containing all threads
194 """Records a build outcome for a single make invocation
197 rc: Outcome value (OUTCOME_...)
198 err_lines: List of error lines or [] if none
199 sizes: Dictionary of image size information, keyed by filename
200 - Each value is itself a dictionary containing
201 values for 'text', 'data' and 'bss', being the integer
202 size in bytes of each section.
203 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
204 value is itself a dictionary:
206 value: Size of function in bytes
207 config: Dictionary keyed by filename - e.g. '.config'. Each
208 value is itself a dictionary:
211 environment: Dictionary keyed by environment variable, Each
212 value is the value of environment variable.
214 def __init__(self, rc, err_lines, sizes, func_sizes, config,
217 self.err_lines = err_lines
219 self.func_sizes = func_sizes
221 self.environment = environment
223 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
224 gnu_make='make', checkout=True, show_unknown=True, step=1,
225 no_subdirs=False, full_path=False, verbose_build=False,
226 incremental=False, per_board_out_dir=False,
227 config_only=False, squash_config_y=False,
228 warnings_as_errors=False):
229 """Create a new Builder object
232 toolchains: Toolchains object to use for building
233 base_dir: Base directory to use for builder
234 git_dir: Git directory containing source repository
235 num_threads: Number of builder threads to run
236 num_jobs: Number of jobs to run at once (passed to make as -j)
237 gnu_make: the command name of GNU Make.
238 checkout: True to check out source, False to skip that step.
239 This is used for testing.
240 show_unknown: Show unknown boards (those not built) in summary
241 step: 1 to process every commit, n to process every nth commit
242 no_subdirs: Don't create subdirectories when building current
243 source for a single board
244 full_path: Return the full path in CROSS_COMPILE and don't set
246 verbose_build: Run build with V=1 and don't use 'make -s'
247 incremental: Always perform incremental builds; don't run make
248 mrproper when configuring
249 per_board_out_dir: Build in a separate persistent directory per
250 board rather than a thread-specific directory
251 config_only: Only configure each build, don't build it
252 squash_config_y: Convert CONFIG options with the value 'y' to '1'
253 warnings_as_errors: Treat all compiler warnings as errors
255 self.toolchains = toolchains
256 self.base_dir = base_dir
257 self._working_dir = os.path.join(base_dir, '.bm-work')
259 self.do_make = self.Make
260 self.gnu_make = gnu_make
261 self.checkout = checkout
262 self.num_threads = num_threads
263 self.num_jobs = num_jobs
264 self.already_done = 0
265 self.force_build = False
266 self.git_dir = git_dir
267 self._show_unknown = show_unknown
268 self._timestamp_count = 10
269 self._build_period_us = None
270 self._complete_delay = None
271 self._next_delay_update = datetime.now()
272 self.force_config_on_failure = True
273 self.force_build_failures = False
274 self.force_reconfig = False
277 self._error_lines = 0
278 self.no_subdirs = no_subdirs
279 self.full_path = full_path
280 self.verbose_build = verbose_build
281 self.config_only = config_only
282 self.squash_config_y = squash_config_y
283 self.config_filenames = BASE_CONFIG_FILENAMES
284 if not self.squash_config_y:
285 self.config_filenames += EXTRA_CONFIG_FILENAMES
287 self.warnings_as_errors = warnings_as_errors
288 self.col = terminal.Color()
290 self._re_function = re.compile('(.*): In function.*')
291 self._re_files = re.compile('In file included from.*')
292 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
293 self._re_dtb_warning = re.compile('(.*): Warning .*')
294 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
296 self.queue = Queue.Queue()
297 self.out_queue = Queue.Queue()
298 for i in range(self.num_threads):
299 t = builderthread.BuilderThread(self, i, incremental,
303 self.threads.append(t)
305 self.last_line_len = 0
306 t = builderthread.ResultThread(self)
309 self.threads.append(t)
311 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
312 self.re_make_err = re.compile('|'.join(ignore_lines))
314 # Handle existing graceful with SIGINT / Ctrl-C
315 signal.signal(signal.SIGINT, self.signal_handler)
318 """Get rid of all threads created by the builder"""
319 for t in self.threads:
322 def signal_handler(self, signal, frame):
325 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
326 show_detail=False, show_bloat=False,
327 list_error_boards=False, show_config=False,
328 show_environment=False):
329 """Setup display options for the builder.
331 show_errors: True to show summarised error/warning info
332 show_sizes: Show size deltas
333 show_detail: Show detail for each board
334 show_bloat: Show detail for each function
335 list_error_boards: Show the boards which caused each error/warning
336 show_config: Show config deltas
337 show_environment: Show environment deltas
339 self._show_errors = show_errors
340 self._show_sizes = show_sizes
341 self._show_detail = show_detail
342 self._show_bloat = show_bloat
343 self._list_error_boards = list_error_boards
344 self._show_config = show_config
345 self._show_environment = show_environment
347 def _AddTimestamp(self):
348 """Add a new timestamp to the list and record the build period.
350 The build period is the length of time taken to perform a single
351 build (one board, one commit).
354 self._timestamps.append(now)
355 count = len(self._timestamps)
356 delta = self._timestamps[-1] - self._timestamps[0]
357 seconds = delta.total_seconds()
359 # If we have enough data, estimate build period (time taken for a
360 # single build) and therefore completion time.
361 if count > 1 and self._next_delay_update < now:
362 self._next_delay_update = now + timedelta(seconds=2)
364 self._build_period = float(seconds) / count
365 todo = self.count - self.upto
366 self._complete_delay = timedelta(microseconds=
367 self._build_period * todo * 1000000)
369 self._complete_delay -= timedelta(
370 microseconds=self._complete_delay.microseconds)
373 self._timestamps.popleft()
376 def ClearLine(self, length):
377 """Clear any characters on the current line
379 Make way for a new line of length 'length', by outputting enough
380 spaces to clear out the old line. Then remember the new length for
384 length: Length of new line, in characters
386 if length < self.last_line_len:
387 Print(' ' * (self.last_line_len - length), newline=False)
388 Print('\r', newline=False)
389 self.last_line_len = length
392 def SelectCommit(self, commit, checkout=True):
393 """Checkout the selected commit for this build
396 if checkout and self.checkout:
397 gitutil.Checkout(commit.hash)
399 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
403 commit: Commit object that is being built
404 brd: Board object that is being built
405 stage: Stage that we are at (mrproper, config, build)
406 cwd: Directory where make should be run
407 args: Arguments to pass to make
408 kwargs: Arguments to pass to command.RunPipe()
410 cmd = [self.gnu_make] + list(args)
411 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
412 cwd=cwd, raise_on_error=False, infile='/dev/null', **kwargs)
413 if self.verbose_build:
414 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
415 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
418 def ProcessResult(self, result):
419 """Process the result of a build, showing progress information
422 result: A CommandResult object, which indicates the result for
425 col = terminal.Color()
427 target = result.brd.target
430 if result.return_code != 0:
434 if result.already_done:
435 self.already_done += 1
437 Print('\r', newline=False)
439 boards_selected = {target : result.brd}
440 self.ResetResultSummary(boards_selected)
441 self.ProduceResultSummary(result.commit_upto, self.commits,
444 target = '(starting)'
446 # Display separate counts for ok, warned and fail
447 ok = self.upto - self.warned - self.fail
448 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
449 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
450 line += self.col.Color(self.col.RED, '%5d' % self.fail)
452 name = ' /%-5d ' % self.count
454 # Add our current completion time estimate
456 if self._complete_delay:
457 name += '%s : ' % self._complete_delay
458 # When building all boards for a commit, we can print a commit
460 if result and result.commit_upto is None:
461 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
465 Print(line + name, newline=False)
466 length = 16 + len(name)
467 self.ClearLine(length)
469 def _GetOutputDir(self, commit_upto):
470 """Get the name of the output directory for a commit number
472 The output directory is typically .../<branch>/<commit>.
475 commit_upto: Commit number to use (0..self.count-1)
479 commit = self.commits[commit_upto]
480 subject = commit.subject.translate(trans_valid_chars)
481 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
482 self.commit_count, commit.hash, subject[:20]))
483 elif not self.no_subdirs:
484 commit_dir = 'current'
487 return os.path.join(self.base_dir, commit_dir)
489 def GetBuildDir(self, commit_upto, target):
490 """Get the name of the build directory for a commit number
492 The build directory is typically .../<branch>/<commit>/<target>.
495 commit_upto: Commit number to use (0..self.count-1)
498 output_dir = self._GetOutputDir(commit_upto)
499 return os.path.join(output_dir, target)
501 def GetDoneFile(self, commit_upto, target):
502 """Get the name of the done file for a commit number
505 commit_upto: Commit number to use (0..self.count-1)
508 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
510 def GetSizesFile(self, commit_upto, target):
511 """Get the name of the sizes file for a commit number
514 commit_upto: Commit number to use (0..self.count-1)
517 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
519 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
520 """Get the name of the funcsizes file for a commit number and ELF file
523 commit_upto: Commit number to use (0..self.count-1)
525 elf_fname: Filename of elf image
527 return os.path.join(self.GetBuildDir(commit_upto, target),
528 '%s.sizes' % elf_fname.replace('/', '-'))
530 def GetObjdumpFile(self, commit_upto, target, elf_fname):
531 """Get the name of the objdump file for a commit number and ELF file
534 commit_upto: Commit number to use (0..self.count-1)
536 elf_fname: Filename of elf image
538 return os.path.join(self.GetBuildDir(commit_upto, target),
539 '%s.objdump' % elf_fname.replace('/', '-'))
541 def GetErrFile(self, commit_upto, target):
542 """Get the name of the err file for a commit number
545 commit_upto: Commit number to use (0..self.count-1)
548 output_dir = self.GetBuildDir(commit_upto, target)
549 return os.path.join(output_dir, 'err')
551 def FilterErrors(self, lines):
552 """Filter out errors in which we have no interest
554 We should probably use map().
557 lines: List of error lines, each a string
559 New list with only interesting lines included
563 if not self.re_make_err.search(line):
564 out_lines.append(line)
567 def ReadFuncSizes(self, fname, fd):
568 """Read function sizes from the output of 'nm'
571 fd: File containing data to read
572 fname: Filename we are reading from (just for errors)
575 Dictionary containing size of each function in bytes, indexed by
579 for line in fd.readlines():
581 size, type, name = line[:-1].split()
583 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
586 # function names begin with '.' on 64-bit powerpc
588 name = 'static.' + name.split('.')[0]
589 sym[name] = sym.get(name, 0) + int(size, 16)
592 def _ProcessConfig(self, fname):
593 """Read in a .config, autoconf.mk or autoconf.h file
595 This function handles all config file types. It ignores comments and
596 any #defines which don't start with CONFIG_.
599 fname: Filename to read
603 key: Config name (e.g. CONFIG_DM)
604 value: Config value (e.g. 1)
607 if os.path.exists(fname):
608 with open(fname) as fd:
611 if line.startswith('#define'):
612 values = line[8:].split(' ', 1)
617 value = '1' if self.squash_config_y else ''
618 if not key.startswith('CONFIG_'):
620 elif not line or line[0] in ['#', '*', '/']:
623 key, value = line.split('=', 1)
624 if self.squash_config_y and value == 'y':
629 def _ProcessEnvironment(self, fname):
630 """Read in a uboot.env file
632 This function reads in environment variables from a file.
635 fname: Filename to read
639 key: environment variable (e.g. bootlimit)
640 value: value of environment variable (e.g. 1)
643 if os.path.exists(fname):
644 with open(fname) as fd:
645 for line in fd.read().split('\0'):
647 key, value = line.split('=', 1)
648 environment[key] = value
650 # ignore lines we can't parse
654 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
655 read_config, read_environment):
656 """Work out the outcome of a build.
659 commit_upto: Commit number to check (0..n-1)
660 target: Target board to check
661 read_func_sizes: True to read function size information
662 read_config: True to read .config and autoconf.h files
663 read_environment: True to read uboot.env files
668 done_file = self.GetDoneFile(commit_upto, target)
669 sizes_file = self.GetSizesFile(commit_upto, target)
674 if os.path.exists(done_file):
675 with open(done_file, 'r') as fd:
676 return_code = int(fd.readline())
678 err_file = self.GetErrFile(commit_upto, target)
679 if os.path.exists(err_file):
680 with open(err_file, 'r') as fd:
681 err_lines = self.FilterErrors(fd.readlines())
683 # Decide whether the build was ok, failed or created warnings
691 # Convert size information to our simple format
692 if os.path.exists(sizes_file):
693 with open(sizes_file, 'r') as fd:
694 for line in fd.readlines():
695 values = line.split()
698 rodata = int(values[6], 16)
700 'all' : int(values[0]) + int(values[1]) +
702 'text' : int(values[0]) - rodata,
703 'data' : int(values[1]),
704 'bss' : int(values[2]),
707 sizes[values[5]] = size_dict
710 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
711 for fname in glob.glob(pattern):
712 with open(fname, 'r') as fd:
713 dict_name = os.path.basename(fname).replace('.sizes',
715 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
718 output_dir = self.GetBuildDir(commit_upto, target)
719 for name in self.config_filenames:
720 fname = os.path.join(output_dir, name)
721 config[name] = self._ProcessConfig(fname)
724 output_dir = self.GetBuildDir(commit_upto, target)
725 fname = os.path.join(output_dir, 'uboot.env')
726 environment = self._ProcessEnvironment(fname)
728 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
731 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
733 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
734 read_config, read_environment):
735 """Calculate a summary of the results of building a commit.
738 board_selected: Dict containing boards to summarise
739 commit_upto: Commit number to summarize (0..self.count-1)
740 read_func_sizes: True to read function size information
741 read_config: True to read .config and autoconf.h files
742 read_environment: True to read uboot.env files
746 Dict containing boards which passed building this commit.
747 keyed by board.target
748 List containing a summary of error lines
749 Dict keyed by error line, containing a list of the Board
750 objects with that error
751 List containing a summary of warning lines
752 Dict keyed by error line, containing a list of the Board
753 objects with that warning
754 Dictionary keyed by board.target. Each value is a dictionary:
755 key: filename - e.g. '.config'
756 value is itself a dictionary:
759 Dictionary keyed by board.target. Each value is a dictionary:
760 key: environment variable
761 value: value of environment variable
763 def AddLine(lines_summary, lines_boards, line, board):
765 if line in lines_boards:
766 lines_boards[line].append(board)
768 lines_boards[line] = [board]
769 lines_summary.append(line)
772 err_lines_summary = []
773 err_lines_boards = {}
774 warn_lines_summary = []
775 warn_lines_boards = {}
779 for board in boards_selected.itervalues():
780 outcome = self.GetBuildOutcome(commit_upto, board.target,
781 read_func_sizes, read_config,
783 board_dict[board.target] = outcome
785 last_was_warning = False
786 for line in outcome.err_lines:
788 if (self._re_function.match(line) or
789 self._re_files.match(line)):
792 is_warning = (self._re_warning.match(line) or
793 self._re_dtb_warning.match(line))
794 is_note = self._re_note.match(line)
795 if is_warning or (last_was_warning and is_note):
797 AddLine(warn_lines_summary, warn_lines_boards,
799 AddLine(warn_lines_summary, warn_lines_boards,
803 AddLine(err_lines_summary, err_lines_boards,
805 AddLine(err_lines_summary, err_lines_boards,
807 last_was_warning = is_warning
809 tconfig = Config(self.config_filenames, board.target)
810 for fname in self.config_filenames:
812 for key, value in outcome.config[fname].iteritems():
813 tconfig.Add(fname, key, value)
814 config[board.target] = tconfig
816 tenvironment = Environment(board.target)
817 if outcome.environment:
818 for key, value in outcome.environment.iteritems():
819 tenvironment.Add(key, value)
820 environment[board.target] = tenvironment
822 return (board_dict, err_lines_summary, err_lines_boards,
823 warn_lines_summary, warn_lines_boards, config, environment)
825 def AddOutcome(self, board_dict, arch_list, changes, char, color):
826 """Add an output to our list of outcomes for each architecture
828 This simple function adds failing boards (changes) to the
829 relevant architecture string, so we can print the results out
830 sorted by architecture.
833 board_dict: Dict containing all boards
834 arch_list: Dict keyed by arch name. Value is a string containing
835 a list of board names which failed for that arch.
836 changes: List of boards to add to arch_list
837 color: terminal.Colour object
840 for target in changes:
841 if target in board_dict:
842 arch = board_dict[target].arch
845 str = self.col.Color(color, ' ' + target)
846 if not arch in done_arch:
847 str = ' %s %s' % (self.col.Color(color, char), str)
848 done_arch[arch] = True
849 if not arch in arch_list:
850 arch_list[arch] = str
852 arch_list[arch] += str
855 def ColourNum(self, num):
856 color = self.col.RED if num > 0 else self.col.GREEN
859 return self.col.Color(color, str(num))
861 def ResetResultSummary(self, board_selected):
862 """Reset the results summary ready for use.
864 Set up the base board list to be all those selected, and set the
865 error lines to empty.
867 Following this, calls to PrintResultSummary() will use this
868 information to work out what has changed.
871 board_selected: Dict containing boards to summarise, keyed by
874 self._base_board_dict = {}
875 for board in board_selected:
876 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
878 self._base_err_lines = []
879 self._base_warn_lines = []
880 self._base_err_line_boards = {}
881 self._base_warn_line_boards = {}
882 self._base_config = None
883 self._base_environment = None
885 def PrintFuncSizeDetail(self, fname, old, new):
886 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
887 delta, common = [], {}
894 if name not in common:
897 delta.append([-old[name], name])
900 if name not in common:
903 delta.append([new[name], name])
906 diff = new.get(name, 0) - old.get(name, 0)
908 grow, up = grow + 1, up + diff
910 shrink, down = shrink + 1, down - diff
911 delta.append([diff, name])
916 args = [add, -remove, grow, -shrink, up, -down, up - down]
917 if max(args) == 0 and min(args) == 0:
919 args = [self.ColourNum(x) for x in args]
921 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
922 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
923 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
925 for diff, name in delta:
927 color = self.col.RED if diff > 0 else self.col.GREEN
928 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
929 old.get(name, '-'), new.get(name,'-'), diff)
930 Print(msg, colour=color)
933 def PrintSizeDetail(self, target_list, show_bloat):
934 """Show details size information for each board
937 target_list: List of targets, each a dict containing:
938 'target': Target name
939 'total_diff': Total difference in bytes across all areas
940 <part_name>: Difference for that part
941 show_bloat: Show detail for each function
943 targets_by_diff = sorted(target_list, reverse=True,
944 key=lambda x: x['_total_diff'])
945 for result in targets_by_diff:
946 printed_target = False
947 for name in sorted(result):
949 if name.startswith('_'):
952 color = self.col.RED if diff > 0 else self.col.GREEN
953 msg = ' %s %+d' % (name, diff)
954 if not printed_target:
955 Print('%10s %-15s:' % ('', result['_target']),
957 printed_target = True
958 Print(msg, colour=color, newline=False)
962 target = result['_target']
963 outcome = result['_outcome']
964 base_outcome = self._base_board_dict[target]
965 for fname in outcome.func_sizes:
966 self.PrintFuncSizeDetail(fname,
967 base_outcome.func_sizes[fname],
968 outcome.func_sizes[fname])
971 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
973 """Print a summary of image sizes broken down by section.
975 The summary takes the form of one line per architecture. The
976 line contains deltas for each of the sections (+ means the section
977 got bigger, - means smaller). The nunmbers are the average number
978 of bytes that a board in this section increased by.
981 powerpc: (622 boards) text -0.0
982 arm: (285 boards) text -0.0
983 nds32: (3 boards) text -8.0
986 board_selected: Dict containing boards to summarise, keyed by
988 board_dict: Dict containing boards for which we built this
989 commit, keyed by board.target. The value is an Outcome object.
990 show_detail: Show detail for each board
991 show_bloat: Show detail for each function
996 # Calculate changes in size for different image parts
997 # The previous sizes are in Board.sizes, for each board
998 for target in board_dict:
999 if target not in board_selected:
1001 base_sizes = self._base_board_dict[target].sizes
1002 outcome = board_dict[target]
1003 sizes = outcome.sizes
1005 # Loop through the list of images, creating a dict of size
1006 # changes for each image/part. We end up with something like
1007 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1008 # which means that U-Boot data increased by 5 bytes and SPL
1009 # text decreased by 4.
1010 err = {'_target' : target}
1012 if image in base_sizes:
1013 base_image = base_sizes[image]
1014 # Loop through the text, data, bss parts
1015 for part in sorted(sizes[image]):
1016 diff = sizes[image][part] - base_image[part]
1019 if image == 'u-boot':
1022 name = image + ':' + part
1024 arch = board_selected[target].arch
1025 if not arch in arch_count:
1026 arch_count[arch] = 1
1028 arch_count[arch] += 1
1030 pass # Only add to our list when we have some stats
1031 elif not arch in arch_list:
1032 arch_list[arch] = [err]
1034 arch_list[arch].append(err)
1036 # We now have a list of image size changes sorted by arch
1037 # Print out a summary of these
1038 for arch, target_list in arch_list.iteritems():
1039 # Get total difference for each type
1041 for result in target_list:
1043 for name, diff in result.iteritems():
1044 if name.startswith('_'):
1048 totals[name] += diff
1051 result['_total_diff'] = total
1052 result['_outcome'] = board_dict[result['_target']]
1054 count = len(target_list)
1055 printed_arch = False
1056 for name in sorted(totals):
1059 # Display the average difference in this name for this
1061 avg_diff = float(diff) / count
1062 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1063 msg = ' %s %+1.1f' % (name, avg_diff)
1064 if not printed_arch:
1065 Print('%10s: (for %d/%d boards)' % (arch, count,
1066 arch_count[arch]), newline=False)
1068 Print(msg, colour=color, newline=False)
1073 self.PrintSizeDetail(target_list, show_bloat)
1076 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1077 err_line_boards, warn_lines, warn_line_boards,
1078 config, environment, show_sizes, show_detail,
1079 show_bloat, show_config, show_environment):
1080 """Compare results with the base results and display delta.
1082 Only boards mentioned in board_selected will be considered. This
1083 function is intended to be called repeatedly with the results of
1084 each commit. It therefore shows a 'diff' between what it saw in
1085 the last call and what it sees now.
1088 board_selected: Dict containing boards to summarise, keyed by
1090 board_dict: Dict containing boards for which we built this
1091 commit, keyed by board.target. The value is an Outcome object.
1092 err_lines: A list of errors for this commit, or [] if there is
1093 none, or we don't want to print errors
1094 err_line_boards: Dict keyed by error line, containing a list of
1095 the Board objects with that error
1096 warn_lines: A list of warnings for this commit, or [] if there is
1097 none, or we don't want to print errors
1098 warn_line_boards: Dict keyed by warning line, containing a list of
1099 the Board objects with that warning
1100 config: Dictionary keyed by filename - e.g. '.config'. Each
1101 value is itself a dictionary:
1104 environment: Dictionary keyed by environment variable, Each
1105 value is the value of environment variable.
1106 show_sizes: Show image size deltas
1107 show_detail: Show detail for each board
1108 show_bloat: Show detail for each function
1109 show_config: Show config changes
1110 show_environment: Show environment changes
1112 def _BoardList(line, line_boards):
1113 """Helper function to get a line of boards containing a line
1116 line: Error line to search for
1118 String containing a list of boards with that error line, or
1119 '' if the user has not requested such a list
1121 if self._list_error_boards:
1123 for board in line_boards[line]:
1124 if not board.target in names:
1125 names.append(board.target)
1126 names_str = '(%s) ' % ','.join(names)
1131 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1136 if line not in base_lines:
1137 worse_lines.append(char + '+' +
1138 _BoardList(line, line_boards) + line)
1139 for line in base_lines:
1140 if line not in lines:
1141 better_lines.append(char + '-' +
1142 _BoardList(line, base_line_boards) + line)
1143 return better_lines, worse_lines
1145 def _CalcConfig(delta, name, config):
1146 """Calculate configuration changes
1149 delta: Type of the delta, e.g. '+'
1150 name: name of the file which changed (e.g. .config)
1151 config: configuration change dictionary
1155 String containing the configuration changes which can be
1159 for key in sorted(config.keys()):
1160 out += '%s=%s ' % (key, config[key])
1161 return '%s %s: %s' % (delta, name, out)
1163 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1164 """Add changes in configuration to a list
1167 lines: list to add to
1168 name: config file name
1169 config_plus: configurations added, dictionary
1172 config_minus: configurations removed, dictionary
1175 config_change: configurations changed, dictionary
1180 lines.append(_CalcConfig('+', name, config_plus))
1182 lines.append(_CalcConfig('-', name, config_minus))
1184 lines.append(_CalcConfig('c', name, config_change))
1186 def _OutputConfigInfo(lines):
1191 col = self.col.GREEN
1192 elif line[0] == '-':
1194 elif line[0] == 'c':
1195 col = self.col.YELLOW
1196 Print(' ' + line, newline=True, colour=col)
1199 ok_boards = [] # List of boards fixed since last commit
1200 warn_boards = [] # List of boards with warnings since last commit
1201 err_boards = [] # List of new broken boards since last commit
1202 new_boards = [] # List of boards that didn't exist last time
1203 unknown_boards = [] # List of boards that were not built
1205 for target in board_dict:
1206 if target not in board_selected:
1209 # If the board was built last time, add its outcome to a list
1210 if target in self._base_board_dict:
1211 base_outcome = self._base_board_dict[target].rc
1212 outcome = board_dict[target]
1213 if outcome.rc == OUTCOME_UNKNOWN:
1214 unknown_boards.append(target)
1215 elif outcome.rc < base_outcome:
1216 if outcome.rc == OUTCOME_WARNING:
1217 warn_boards.append(target)
1219 ok_boards.append(target)
1220 elif outcome.rc > base_outcome:
1221 if outcome.rc == OUTCOME_WARNING:
1222 warn_boards.append(target)
1224 err_boards.append(target)
1226 new_boards.append(target)
1228 # Get a list of errors that have appeared, and disappeared
1229 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1230 self._base_err_line_boards, err_lines, err_line_boards, '')
1231 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1232 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1234 # Display results by arch
1235 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1236 worse_err, better_err, worse_warn, better_warn)):
1238 self.AddOutcome(board_selected, arch_list, ok_boards, '',
1240 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1242 self.AddOutcome(board_selected, arch_list, err_boards, '+',
1244 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1245 if self._show_unknown:
1246 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
1248 for arch, target_list in arch_list.iteritems():
1249 Print('%10s: %s' % (arch, target_list))
1250 self._error_lines += 1
1252 Print('\n'.join(better_err), colour=self.col.GREEN)
1253 self._error_lines += 1
1255 Print('\n'.join(worse_err), colour=self.col.RED)
1256 self._error_lines += 1
1258 Print('\n'.join(better_warn), colour=self.col.CYAN)
1259 self._error_lines += 1
1261 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1262 self._error_lines += 1
1265 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1268 if show_environment and self._base_environment:
1271 for target in board_dict:
1272 if target not in board_selected:
1275 tbase = self._base_environment[target]
1276 tenvironment = environment[target]
1277 environment_plus = {}
1278 environment_minus = {}
1279 environment_change = {}
1280 base = tbase.environment
1281 for key, value in tenvironment.environment.iteritems():
1283 environment_plus[key] = value
1284 for key, value in base.iteritems():
1285 if key not in tenvironment.environment:
1286 environment_minus[key] = value
1287 for key, value in base.iteritems():
1288 new_value = tenvironment.environment.get(key)
1289 if new_value and value != new_value:
1290 desc = '%s -> %s' % (value, new_value)
1291 environment_change[key] = desc
1293 _AddConfig(lines, target, environment_plus, environment_minus,
1296 _OutputConfigInfo(lines)
1298 if show_config and self._base_config:
1300 arch_config_plus = {}
1301 arch_config_minus = {}
1302 arch_config_change = {}
1305 for target in board_dict:
1306 if target not in board_selected:
1308 arch = board_selected[target].arch
1309 if arch not in arch_list:
1310 arch_list.append(arch)
1312 for arch in arch_list:
1313 arch_config_plus[arch] = {}
1314 arch_config_minus[arch] = {}
1315 arch_config_change[arch] = {}
1316 for name in self.config_filenames:
1317 arch_config_plus[arch][name] = {}
1318 arch_config_minus[arch][name] = {}
1319 arch_config_change[arch][name] = {}
1321 for target in board_dict:
1322 if target not in board_selected:
1325 arch = board_selected[target].arch
1327 all_config_plus = {}
1328 all_config_minus = {}
1329 all_config_change = {}
1330 tbase = self._base_config[target]
1331 tconfig = config[target]
1333 for name in self.config_filenames:
1334 if not tconfig.config[name]:
1339 base = tbase.config[name]
1340 for key, value in tconfig.config[name].iteritems():
1342 config_plus[key] = value
1343 all_config_plus[key] = value
1344 for key, value in base.iteritems():
1345 if key not in tconfig.config[name]:
1346 config_minus[key] = value
1347 all_config_minus[key] = value
1348 for key, value in base.iteritems():
1349 new_value = tconfig.config.get(key)
1350 if new_value and value != new_value:
1351 desc = '%s -> %s' % (value, new_value)
1352 config_change[key] = desc
1353 all_config_change[key] = desc
1355 arch_config_plus[arch][name].update(config_plus)
1356 arch_config_minus[arch][name].update(config_minus)
1357 arch_config_change[arch][name].update(config_change)
1359 _AddConfig(lines, name, config_plus, config_minus,
1361 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1363 summary[target] = '\n'.join(lines)
1365 lines_by_target = {}
1366 for target, lines in summary.iteritems():
1367 if lines in lines_by_target:
1368 lines_by_target[lines].append(target)
1370 lines_by_target[lines] = [target]
1372 for arch in arch_list:
1377 for name in self.config_filenames:
1378 all_plus.update(arch_config_plus[arch][name])
1379 all_minus.update(arch_config_minus[arch][name])
1380 all_change.update(arch_config_change[arch][name])
1381 _AddConfig(lines, name, arch_config_plus[arch][name],
1382 arch_config_minus[arch][name],
1383 arch_config_change[arch][name])
1384 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1385 #arch_summary[target] = '\n'.join(lines)
1388 _OutputConfigInfo(lines)
1390 for lines, targets in lines_by_target.iteritems():
1393 Print('%s :' % ' '.join(sorted(targets)))
1394 _OutputConfigInfo(lines.split('\n'))
1397 # Save our updated information for the next call to this function
1398 self._base_board_dict = board_dict
1399 self._base_err_lines = err_lines
1400 self._base_warn_lines = warn_lines
1401 self._base_err_line_boards = err_line_boards
1402 self._base_warn_line_boards = warn_line_boards
1403 self._base_config = config
1404 self._base_environment = environment
1406 # Get a list of boards that did not get built, if needed
1408 for board in board_selected:
1409 if not board in board_dict:
1410 not_built.append(board)
1412 Print("Boards not built (%d): %s" % (len(not_built),
1413 ', '.join(not_built)))
1415 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1416 (board_dict, err_lines, err_line_boards, warn_lines,
1417 warn_line_boards, config, environment) = self.GetResultSummary(
1418 board_selected, commit_upto,
1419 read_func_sizes=self._show_bloat,
1420 read_config=self._show_config,
1421 read_environment=self._show_environment)
1423 msg = '%02d: %s' % (commit_upto + 1,
1424 commits[commit_upto].subject)
1425 Print(msg, colour=self.col.BLUE)
1426 self.PrintResultSummary(board_selected, board_dict,
1427 err_lines if self._show_errors else [], err_line_boards,
1428 warn_lines if self._show_errors else [], warn_line_boards,
1429 config, environment, self._show_sizes, self._show_detail,
1430 self._show_bloat, self._show_config, self._show_environment)
1432 def ShowSummary(self, commits, board_selected):
1433 """Show a build summary for U-Boot for a given board list.
1435 Reset the result summary, then repeatedly call GetResultSummary on
1436 each commit's results, then display the differences we see.
1439 commit: Commit objects to summarise
1440 board_selected: Dict containing boards to summarise
1442 self.commit_count = len(commits) if commits else 1
1443 self.commits = commits
1444 self.ResetResultSummary(board_selected)
1445 self._error_lines = 0
1447 for commit_upto in range(0, self.commit_count, self._step):
1448 self.ProduceResultSummary(commit_upto, commits, board_selected)
1449 if not self._error_lines:
1450 Print('(no errors to report)', colour=self.col.GREEN)
1453 def SetupBuild(self, board_selected, commits):
1454 """Set up ready to start a build.
1457 board_selected: Selected boards to build
1458 commits: Selected commits to build
1460 # First work out how many commits we will build
1461 count = (self.commit_count + self._step - 1) / self._step
1462 self.count = len(board_selected) * count
1463 self.upto = self.warned = self.fail = 0
1464 self._timestamps = collections.deque()
1466 def GetThreadDir(self, thread_num):
1467 """Get the directory path to the working dir for a thread.
1470 thread_num: Number of thread to check.
1472 return os.path.join(self._working_dir, '%02d' % thread_num)
1474 def _PrepareThread(self, thread_num, setup_git):
1475 """Prepare the working directory for a thread.
1477 This clones or fetches the repo into the thread's work directory.
1480 thread_num: Thread number (0, 1, ...)
1481 setup_git: True to set up a git repo clone
1483 thread_dir = self.GetThreadDir(thread_num)
1484 builderthread.Mkdir(thread_dir)
1485 git_dir = os.path.join(thread_dir, '.git')
1487 # Clone the repo if it doesn't already exist
1488 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1489 # we have a private index but uses the origin repo's contents?
1490 if setup_git and self.git_dir:
1491 src_dir = os.path.abspath(self.git_dir)
1492 if os.path.exists(git_dir):
1493 gitutil.Fetch(git_dir, thread_dir)
1495 Print('\rCloning repo for thread %d' % thread_num,
1497 gitutil.Clone(src_dir, thread_dir)
1498 Print('\r%s\r' % (' ' * 30), newline=False)
1500 def _PrepareWorkingSpace(self, max_threads, setup_git):
1501 """Prepare the working directory for use.
1503 Set up the git repo for each thread.
1506 max_threads: Maximum number of threads we expect to need.
1507 setup_git: True to set up a git repo clone
1509 builderthread.Mkdir(self._working_dir)
1510 for thread in range(max_threads):
1511 self._PrepareThread(thread, setup_git)
1513 def _PrepareOutputSpace(self):
1514 """Get the output directories ready to receive files.
1516 We delete any output directories which look like ones we need to
1517 create. Having left over directories is confusing when the user wants
1518 to check the output manually.
1520 if not self.commits:
1523 for commit_upto in range(self.commit_count):
1524 dir_list.append(self._GetOutputDir(commit_upto))
1527 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1528 if dirname not in dir_list:
1529 to_remove.append(dirname)
1531 Print('Removing %d old build directories' % len(to_remove),
1533 for dirname in to_remove:
1534 shutil.rmtree(dirname)
1536 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1537 """Build all commits for a list of boards
1540 commits: List of commits to be build, each a Commit object
1541 boards_selected: Dict of selected boards, key is target name,
1542 value is Board object
1543 keep_outputs: True to save build output files
1544 verbose: Display build results as they are completed
1547 - number of boards that failed to build
1548 - number of boards that issued warnings
1550 self.commit_count = len(commits) if commits else 1
1551 self.commits = commits
1552 self._verbose = verbose
1554 self.ResetResultSummary(board_selected)
1555 builderthread.Mkdir(self.base_dir, parents = True)
1556 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1557 commits is not None)
1558 self._PrepareOutputSpace()
1559 Print('\rStarting build...', newline=False)
1560 self.SetupBuild(board_selected, commits)
1561 self.ProcessResult(None)
1563 # Create jobs to build all commits for each board
1564 for brd in board_selected.itervalues():
1565 job = builderthread.BuilderJob()
1567 job.commits = commits
1568 job.keep_outputs = keep_outputs
1569 job.step = self._step
1572 term = threading.Thread(target=self.queue.join)
1573 term.setDaemon(True)
1575 while term.isAlive():
1578 # Wait until we have processed all output
1579 self.out_queue.join()
1582 return (self.fail, self.warned)