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 = list(range(4))
97 # Translate a commit subject into a valid filename (and handle unicode)
98 trans_valid_chars = str.maketrans('/: ', '---')
100 BASE_CONFIG_FILENAMES = [
101 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
104 EXTRA_CONFIG_FILENAMES = [
105 '.config', '.config-spl', '.config-tpl',
106 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
107 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
111 """Holds information about configuration settings for a board."""
112 def __init__(self, config_filename, target):
115 for fname in config_filename:
116 self.config[fname] = {}
118 def Add(self, fname, key, value):
119 self.config[fname][key] = value
123 for fname in self.config:
124 for key, value in self.config[fname].items():
126 val = val ^ hash(key) & hash(value)
130 """Holds information about environment variables for a board."""
131 def __init__(self, target):
133 self.environment = {}
135 def Add(self, key, value):
136 self.environment[key] = value
139 """Class for building U-Boot for a particular commit.
141 Public members: (many should ->private)
142 already_done: Number of builds already completed
143 base_dir: Base directory to use for builder
144 checkout: True to check out source, False to skip that step.
145 This is used for testing.
146 col: terminal.Color() object
147 count: Number of commits to build
148 do_make: Method to call to invoke Make
149 fail: Number of builds that failed due to error
150 force_build: Force building even if a build already exists
151 force_config_on_failure: If a commit fails for a board, disable
152 incremental building for the next commit we build for that
153 board, so that we will see all warnings/errors again.
154 force_build_failures: If a previously-built build (i.e. built on
155 a previous run of buildman) is marked as failed, rebuild it.
156 git_dir: Git directory containing source repository
157 last_line_len: Length of the last line we printed (used for erasing
158 it with new progress information)
159 num_jobs: Number of jobs to run at once (passed to make as -j)
160 num_threads: Number of builder threads to run
161 out_queue: Queue of results to process
162 re_make_err: Compiled regular expression for ignore_lines
163 queue: Queue of jobs to run
164 threads: List of active threads
165 toolchains: Toolchains object to use for building
166 upto: Current commit number we are building (0.count-1)
167 warned: Number of builds that produced at least one warning
168 force_reconfig: Reconfigure U-Boot on each comiit. This disables
169 incremental building, where buildman reconfigures on the first
170 commit for a baord, and then just does an incremental build for
171 the following commits. In fact buildman will reconfigure and
172 retry for any failing commits, so generally the only effect of
173 this option is to slow things down.
174 in_tree: Build U-Boot in-tree instead of specifying an output
175 directory separate from the source code. This option is really
176 only useful for testing in-tree builds.
179 _base_board_dict: Last-summarised Dict of boards
180 _base_err_lines: Last-summarised list of errors
181 _base_warn_lines: Last-summarised list of warnings
182 _build_period_us: Time taken for a single build (float object).
183 _complete_delay: Expected delay until completion (timedelta)
184 _next_delay_update: Next time we plan to display a progress update
186 _show_unknown: Show unknown boards (those not built) in summary
187 _timestamps: List of timestamps for the completion of the last
188 last _timestamp_count builds. Each is a datetime object.
189 _timestamp_count: Number of timestamps to keep in our list.
190 _working_dir: Base working directory containing all threads
193 """Records a build outcome for a single make invocation
196 rc: Outcome value (OUTCOME_...)
197 err_lines: List of error lines or [] if none
198 sizes: Dictionary of image size information, keyed by filename
199 - Each value is itself a dictionary containing
200 values for 'text', 'data' and 'bss', being the integer
201 size in bytes of each section.
202 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
203 value is itself a dictionary:
205 value: Size of function in bytes
206 config: Dictionary keyed by filename - e.g. '.config'. Each
207 value is itself a dictionary:
210 environment: Dictionary keyed by environment variable, Each
211 value is the value of environment variable.
213 def __init__(self, rc, err_lines, sizes, func_sizes, config,
216 self.err_lines = err_lines
218 self.func_sizes = func_sizes
220 self.environment = environment
222 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
223 gnu_make='make', checkout=True, show_unknown=True, step=1,
224 no_subdirs=False, full_path=False, verbose_build=False,
225 incremental=False, per_board_out_dir=False,
226 config_only=False, squash_config_y=False,
227 warnings_as_errors=False):
228 """Create a new Builder object
231 toolchains: Toolchains object to use for building
232 base_dir: Base directory to use for builder
233 git_dir: Git directory containing source repository
234 num_threads: Number of builder threads to run
235 num_jobs: Number of jobs to run at once (passed to make as -j)
236 gnu_make: the command name of GNU Make.
237 checkout: True to check out source, False to skip that step.
238 This is used for testing.
239 show_unknown: Show unknown boards (those not built) in summary
240 step: 1 to process every commit, n to process every nth commit
241 no_subdirs: Don't create subdirectories when building current
242 source for a single board
243 full_path: Return the full path in CROSS_COMPILE and don't set
245 verbose_build: Run build with V=1 and don't use 'make -s'
246 incremental: Always perform incremental builds; don't run make
247 mrproper when configuring
248 per_board_out_dir: Build in a separate persistent directory per
249 board rather than a thread-specific directory
250 config_only: Only configure each build, don't build it
251 squash_config_y: Convert CONFIG options with the value 'y' to '1'
252 warnings_as_errors: Treat all compiler warnings as errors
254 self.toolchains = toolchains
255 self.base_dir = base_dir
256 self._working_dir = os.path.join(base_dir, '.bm-work')
258 self.do_make = self.Make
259 self.gnu_make = gnu_make
260 self.checkout = checkout
261 self.num_threads = num_threads
262 self.num_jobs = num_jobs
263 self.already_done = 0
264 self.force_build = False
265 self.git_dir = git_dir
266 self._show_unknown = show_unknown
267 self._timestamp_count = 10
268 self._build_period_us = None
269 self._complete_delay = None
270 self._next_delay_update = datetime.now()
271 self.force_config_on_failure = True
272 self.force_build_failures = False
273 self.force_reconfig = False
276 self._error_lines = 0
277 self.no_subdirs = no_subdirs
278 self.full_path = full_path
279 self.verbose_build = verbose_build
280 self.config_only = config_only
281 self.squash_config_y = squash_config_y
282 self.config_filenames = BASE_CONFIG_FILENAMES
283 if not self.squash_config_y:
284 self.config_filenames += EXTRA_CONFIG_FILENAMES
286 self.warnings_as_errors = warnings_as_errors
287 self.col = terminal.Color()
289 self._re_function = re.compile('(.*): In function.*')
290 self._re_files = re.compile('In file included from.*')
291 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
292 self._re_dtb_warning = re.compile('(.*): Warning .*')
293 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
295 self.queue = queue.Queue()
296 self.out_queue = queue.Queue()
297 for i in range(self.num_threads):
298 t = builderthread.BuilderThread(self, i, incremental,
302 self.threads.append(t)
304 self.last_line_len = 0
305 t = builderthread.ResultThread(self)
308 self.threads.append(t)
310 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
311 self.re_make_err = re.compile('|'.join(ignore_lines))
313 # Handle existing graceful with SIGINT / Ctrl-C
314 signal.signal(signal.SIGINT, self.signal_handler)
317 """Get rid of all threads created by the builder"""
318 for t in self.threads:
321 def signal_handler(self, signal, frame):
324 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
325 show_detail=False, show_bloat=False,
326 list_error_boards=False, show_config=False,
327 show_environment=False):
328 """Setup display options for the builder.
330 show_errors: True to show summarised error/warning info
331 show_sizes: Show size deltas
332 show_detail: Show detail for each board
333 show_bloat: Show detail for each function
334 list_error_boards: Show the boards which caused each error/warning
335 show_config: Show config deltas
336 show_environment: Show environment deltas
338 self._show_errors = show_errors
339 self._show_sizes = show_sizes
340 self._show_detail = show_detail
341 self._show_bloat = show_bloat
342 self._list_error_boards = list_error_boards
343 self._show_config = show_config
344 self._show_environment = show_environment
346 def _AddTimestamp(self):
347 """Add a new timestamp to the list and record the build period.
349 The build period is the length of time taken to perform a single
350 build (one board, one commit).
353 self._timestamps.append(now)
354 count = len(self._timestamps)
355 delta = self._timestamps[-1] - self._timestamps[0]
356 seconds = delta.total_seconds()
358 # If we have enough data, estimate build period (time taken for a
359 # single build) and therefore completion time.
360 if count > 1 and self._next_delay_update < now:
361 self._next_delay_update = now + timedelta(seconds=2)
363 self._build_period = float(seconds) / count
364 todo = self.count - self.upto
365 self._complete_delay = timedelta(microseconds=
366 self._build_period * todo * 1000000)
368 self._complete_delay -= timedelta(
369 microseconds=self._complete_delay.microseconds)
372 self._timestamps.popleft()
375 def ClearLine(self, length):
376 """Clear any characters on the current line
378 Make way for a new line of length 'length', by outputting enough
379 spaces to clear out the old line. Then remember the new length for
383 length: Length of new line, in characters
385 if length < self.last_line_len:
386 Print(' ' * (self.last_line_len - length), newline=False)
387 Print('\r', newline=False)
388 self.last_line_len = length
391 def SelectCommit(self, commit, checkout=True):
392 """Checkout the selected commit for this build
395 if checkout and self.checkout:
396 gitutil.Checkout(commit.hash)
398 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
402 commit: Commit object that is being built
403 brd: Board object that is being built
404 stage: Stage that we are at (mrproper, config, build)
405 cwd: Directory where make should be run
406 args: Arguments to pass to make
407 kwargs: Arguments to pass to command.RunPipe()
409 cmd = [self.gnu_make] + list(args)
410 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
411 cwd=cwd, raise_on_error=False, infile='/dev/null', **kwargs)
412 if self.verbose_build:
413 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
414 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
417 def ProcessResult(self, result):
418 """Process the result of a build, showing progress information
421 result: A CommandResult object, which indicates the result for
424 col = terminal.Color()
426 target = result.brd.target
429 if result.return_code != 0:
433 if result.already_done:
434 self.already_done += 1
436 Print('\r', newline=False)
438 boards_selected = {target : result.brd}
439 self.ResetResultSummary(boards_selected)
440 self.ProduceResultSummary(result.commit_upto, self.commits,
443 target = '(starting)'
445 # Display separate counts for ok, warned and fail
446 ok = self.upto - self.warned - self.fail
447 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
448 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
449 line += self.col.Color(self.col.RED, '%5d' % self.fail)
451 name = ' /%-5d ' % self.count
453 # Add our current completion time estimate
455 if self._complete_delay:
456 name += '%s : ' % self._complete_delay
457 # When building all boards for a commit, we can print a commit
459 if result and result.commit_upto is None:
460 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
464 Print(line + name, newline=False)
465 length = 16 + len(name)
466 self.ClearLine(length)
468 def _GetOutputDir(self, commit_upto):
469 """Get the name of the output directory for a commit number
471 The output directory is typically .../<branch>/<commit>.
474 commit_upto: Commit number to use (0..self.count-1)
478 commit = self.commits[commit_upto]
479 subject = commit.subject.translate(trans_valid_chars)
480 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
481 self.commit_count, commit.hash, subject[:20]))
482 elif not self.no_subdirs:
483 commit_dir = 'current'
486 return os.path.join(self.base_dir, commit_dir)
488 def GetBuildDir(self, commit_upto, target):
489 """Get the name of the build directory for a commit number
491 The build directory is typically .../<branch>/<commit>/<target>.
494 commit_upto: Commit number to use (0..self.count-1)
497 output_dir = self._GetOutputDir(commit_upto)
498 return os.path.join(output_dir, target)
500 def GetDoneFile(self, commit_upto, target):
501 """Get the name of the done file for a commit number
504 commit_upto: Commit number to use (0..self.count-1)
507 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
509 def GetSizesFile(self, commit_upto, target):
510 """Get the name of the sizes file for a commit number
513 commit_upto: Commit number to use (0..self.count-1)
516 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
518 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
519 """Get the name of the funcsizes file for a commit number and ELF file
522 commit_upto: Commit number to use (0..self.count-1)
524 elf_fname: Filename of elf image
526 return os.path.join(self.GetBuildDir(commit_upto, target),
527 '%s.sizes' % elf_fname.replace('/', '-'))
529 def GetObjdumpFile(self, commit_upto, target, elf_fname):
530 """Get the name of the objdump file for a commit number and ELF file
533 commit_upto: Commit number to use (0..self.count-1)
535 elf_fname: Filename of elf image
537 return os.path.join(self.GetBuildDir(commit_upto, target),
538 '%s.objdump' % elf_fname.replace('/', '-'))
540 def GetErrFile(self, commit_upto, target):
541 """Get the name of the err file for a commit number
544 commit_upto: Commit number to use (0..self.count-1)
547 output_dir = self.GetBuildDir(commit_upto, target)
548 return os.path.join(output_dir, 'err')
550 def FilterErrors(self, lines):
551 """Filter out errors in which we have no interest
553 We should probably use map().
556 lines: List of error lines, each a string
558 New list with only interesting lines included
562 if not self.re_make_err.search(line):
563 out_lines.append(line)
566 def ReadFuncSizes(self, fname, fd):
567 """Read function sizes from the output of 'nm'
570 fd: File containing data to read
571 fname: Filename we are reading from (just for errors)
574 Dictionary containing size of each function in bytes, indexed by
578 for line in fd.readlines():
580 size, type, name = line[:-1].split()
582 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
585 # function names begin with '.' on 64-bit powerpc
587 name = 'static.' + name.split('.')[0]
588 sym[name] = sym.get(name, 0) + int(size, 16)
591 def _ProcessConfig(self, fname):
592 """Read in a .config, autoconf.mk or autoconf.h file
594 This function handles all config file types. It ignores comments and
595 any #defines which don't start with CONFIG_.
598 fname: Filename to read
602 key: Config name (e.g. CONFIG_DM)
603 value: Config value (e.g. 1)
606 if os.path.exists(fname):
607 with open(fname) as fd:
610 if line.startswith('#define'):
611 values = line[8:].split(' ', 1)
616 value = '1' if self.squash_config_y else ''
617 if not key.startswith('CONFIG_'):
619 elif not line or line[0] in ['#', '*', '/']:
622 key, value = line.split('=', 1)
623 if self.squash_config_y and value == 'y':
628 def _ProcessEnvironment(self, fname):
629 """Read in a uboot.env file
631 This function reads in environment variables from a file.
634 fname: Filename to read
638 key: environment variable (e.g. bootlimit)
639 value: value of environment variable (e.g. 1)
642 if os.path.exists(fname):
643 with open(fname) as fd:
644 for line in fd.read().split('\0'):
646 key, value = line.split('=', 1)
647 environment[key] = value
649 # ignore lines we can't parse
653 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
654 read_config, read_environment):
655 """Work out the outcome of a build.
658 commit_upto: Commit number to check (0..n-1)
659 target: Target board to check
660 read_func_sizes: True to read function size information
661 read_config: True to read .config and autoconf.h files
662 read_environment: True to read uboot.env files
667 done_file = self.GetDoneFile(commit_upto, target)
668 sizes_file = self.GetSizesFile(commit_upto, target)
673 if os.path.exists(done_file):
674 with open(done_file, 'r') as fd:
676 return_code = int(fd.readline())
678 # The file may be empty due to running out of disk space.
682 err_file = self.GetErrFile(commit_upto, target)
683 if os.path.exists(err_file):
684 with open(err_file, 'r') as fd:
685 err_lines = self.FilterErrors(fd.readlines())
687 # Decide whether the build was ok, failed or created warnings
695 # Convert size information to our simple format
696 if os.path.exists(sizes_file):
697 with open(sizes_file, 'r') as fd:
698 for line in fd.readlines():
699 values = line.split()
702 rodata = int(values[6], 16)
704 'all' : int(values[0]) + int(values[1]) +
706 'text' : int(values[0]) - rodata,
707 'data' : int(values[1]),
708 'bss' : int(values[2]),
711 sizes[values[5]] = size_dict
714 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
715 for fname in glob.glob(pattern):
716 with open(fname, 'r') as fd:
717 dict_name = os.path.basename(fname).replace('.sizes',
719 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
722 output_dir = self.GetBuildDir(commit_upto, target)
723 for name in self.config_filenames:
724 fname = os.path.join(output_dir, name)
725 config[name] = self._ProcessConfig(fname)
728 output_dir = self.GetBuildDir(commit_upto, target)
729 fname = os.path.join(output_dir, 'uboot.env')
730 environment = self._ProcessEnvironment(fname)
732 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
735 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
737 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
738 read_config, read_environment):
739 """Calculate a summary of the results of building a commit.
742 board_selected: Dict containing boards to summarise
743 commit_upto: Commit number to summarize (0..self.count-1)
744 read_func_sizes: True to read function size information
745 read_config: True to read .config and autoconf.h files
746 read_environment: True to read uboot.env files
750 Dict containing boards which passed building this commit.
751 keyed by board.target
752 List containing a summary of error lines
753 Dict keyed by error line, containing a list of the Board
754 objects with that error
755 List containing a summary of warning lines
756 Dict keyed by error line, containing a list of the Board
757 objects with that warning
758 Dictionary keyed by board.target. Each value is a dictionary:
759 key: filename - e.g. '.config'
760 value is itself a dictionary:
763 Dictionary keyed by board.target. Each value is a dictionary:
764 key: environment variable
765 value: value of environment variable
767 def AddLine(lines_summary, lines_boards, line, board):
769 if line in lines_boards:
770 lines_boards[line].append(board)
772 lines_boards[line] = [board]
773 lines_summary.append(line)
776 err_lines_summary = []
777 err_lines_boards = {}
778 warn_lines_summary = []
779 warn_lines_boards = {}
783 for board in boards_selected.values():
784 outcome = self.GetBuildOutcome(commit_upto, board.target,
785 read_func_sizes, read_config,
787 board_dict[board.target] = outcome
789 last_was_warning = False
790 for line in outcome.err_lines:
792 if (self._re_function.match(line) or
793 self._re_files.match(line)):
796 is_warning = (self._re_warning.match(line) or
797 self._re_dtb_warning.match(line))
798 is_note = self._re_note.match(line)
799 if is_warning or (last_was_warning and is_note):
801 AddLine(warn_lines_summary, warn_lines_boards,
803 AddLine(warn_lines_summary, warn_lines_boards,
807 AddLine(err_lines_summary, err_lines_boards,
809 AddLine(err_lines_summary, err_lines_boards,
811 last_was_warning = is_warning
813 tconfig = Config(self.config_filenames, board.target)
814 for fname in self.config_filenames:
816 for key, value in outcome.config[fname].items():
817 tconfig.Add(fname, key, value)
818 config[board.target] = tconfig
820 tenvironment = Environment(board.target)
821 if outcome.environment:
822 for key, value in outcome.environment.items():
823 tenvironment.Add(key, value)
824 environment[board.target] = tenvironment
826 return (board_dict, err_lines_summary, err_lines_boards,
827 warn_lines_summary, warn_lines_boards, config, environment)
829 def AddOutcome(self, board_dict, arch_list, changes, char, color):
830 """Add an output to our list of outcomes for each architecture
832 This simple function adds failing boards (changes) to the
833 relevant architecture string, so we can print the results out
834 sorted by architecture.
837 board_dict: Dict containing all boards
838 arch_list: Dict keyed by arch name. Value is a string containing
839 a list of board names which failed for that arch.
840 changes: List of boards to add to arch_list
841 color: terminal.Colour object
844 for target in changes:
845 if target in board_dict:
846 arch = board_dict[target].arch
849 str = self.col.Color(color, ' ' + target)
850 if not arch in done_arch:
851 str = ' %s %s' % (self.col.Color(color, char), str)
852 done_arch[arch] = True
853 if not arch in arch_list:
854 arch_list[arch] = str
856 arch_list[arch] += str
859 def ColourNum(self, num):
860 color = self.col.RED if num > 0 else self.col.GREEN
863 return self.col.Color(color, str(num))
865 def ResetResultSummary(self, board_selected):
866 """Reset the results summary ready for use.
868 Set up the base board list to be all those selected, and set the
869 error lines to empty.
871 Following this, calls to PrintResultSummary() will use this
872 information to work out what has changed.
875 board_selected: Dict containing boards to summarise, keyed by
878 self._base_board_dict = {}
879 for board in board_selected:
880 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
882 self._base_err_lines = []
883 self._base_warn_lines = []
884 self._base_err_line_boards = {}
885 self._base_warn_line_boards = {}
886 self._base_config = None
887 self._base_environment = None
889 def PrintFuncSizeDetail(self, fname, old, new):
890 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
891 delta, common = [], {}
898 if name not in common:
901 delta.append([-old[name], name])
904 if name not in common:
907 delta.append([new[name], name])
910 diff = new.get(name, 0) - old.get(name, 0)
912 grow, up = grow + 1, up + diff
914 shrink, down = shrink + 1, down - diff
915 delta.append([diff, name])
920 args = [add, -remove, grow, -shrink, up, -down, up - down]
921 if max(args) == 0 and min(args) == 0:
923 args = [self.ColourNum(x) for x in args]
925 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
926 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
927 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
929 for diff, name in delta:
931 color = self.col.RED if diff > 0 else self.col.GREEN
932 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
933 old.get(name, '-'), new.get(name,'-'), diff)
934 Print(msg, colour=color)
937 def PrintSizeDetail(self, target_list, show_bloat):
938 """Show details size information for each board
941 target_list: List of targets, each a dict containing:
942 'target': Target name
943 'total_diff': Total difference in bytes across all areas
944 <part_name>: Difference for that part
945 show_bloat: Show detail for each function
947 targets_by_diff = sorted(target_list, reverse=True,
948 key=lambda x: x['_total_diff'])
949 for result in targets_by_diff:
950 printed_target = False
951 for name in sorted(result):
953 if name.startswith('_'):
956 color = self.col.RED if diff > 0 else self.col.GREEN
957 msg = ' %s %+d' % (name, diff)
958 if not printed_target:
959 Print('%10s %-15s:' % ('', result['_target']),
961 printed_target = True
962 Print(msg, colour=color, newline=False)
966 target = result['_target']
967 outcome = result['_outcome']
968 base_outcome = self._base_board_dict[target]
969 for fname in outcome.func_sizes:
970 self.PrintFuncSizeDetail(fname,
971 base_outcome.func_sizes[fname],
972 outcome.func_sizes[fname])
975 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
977 """Print a summary of image sizes broken down by section.
979 The summary takes the form of one line per architecture. The
980 line contains deltas for each of the sections (+ means the section
981 got bigger, - means smaller). The nunmbers are the average number
982 of bytes that a board in this section increased by.
985 powerpc: (622 boards) text -0.0
986 arm: (285 boards) text -0.0
987 nds32: (3 boards) text -8.0
990 board_selected: Dict containing boards to summarise, keyed by
992 board_dict: Dict containing boards for which we built this
993 commit, keyed by board.target. The value is an Outcome object.
994 show_detail: Show detail for each board
995 show_bloat: Show detail for each function
1000 # Calculate changes in size for different image parts
1001 # The previous sizes are in Board.sizes, for each board
1002 for target in board_dict:
1003 if target not in board_selected:
1005 base_sizes = self._base_board_dict[target].sizes
1006 outcome = board_dict[target]
1007 sizes = outcome.sizes
1009 # Loop through the list of images, creating a dict of size
1010 # changes for each image/part. We end up with something like
1011 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1012 # which means that U-Boot data increased by 5 bytes and SPL
1013 # text decreased by 4.
1014 err = {'_target' : target}
1016 if image in base_sizes:
1017 base_image = base_sizes[image]
1018 # Loop through the text, data, bss parts
1019 for part in sorted(sizes[image]):
1020 diff = sizes[image][part] - base_image[part]
1023 if image == 'u-boot':
1026 name = image + ':' + part
1028 arch = board_selected[target].arch
1029 if not arch in arch_count:
1030 arch_count[arch] = 1
1032 arch_count[arch] += 1
1034 pass # Only add to our list when we have some stats
1035 elif not arch in arch_list:
1036 arch_list[arch] = [err]
1038 arch_list[arch].append(err)
1040 # We now have a list of image size changes sorted by arch
1041 # Print out a summary of these
1042 for arch, target_list in arch_list.items():
1043 # Get total difference for each type
1045 for result in target_list:
1047 for name, diff in result.items():
1048 if name.startswith('_'):
1052 totals[name] += diff
1055 result['_total_diff'] = total
1056 result['_outcome'] = board_dict[result['_target']]
1058 count = len(target_list)
1059 printed_arch = False
1060 for name in sorted(totals):
1063 # Display the average difference in this name for this
1065 avg_diff = float(diff) / count
1066 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1067 msg = ' %s %+1.1f' % (name, avg_diff)
1068 if not printed_arch:
1069 Print('%10s: (for %d/%d boards)' % (arch, count,
1070 arch_count[arch]), newline=False)
1072 Print(msg, colour=color, newline=False)
1077 self.PrintSizeDetail(target_list, show_bloat)
1080 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1081 err_line_boards, warn_lines, warn_line_boards,
1082 config, environment, show_sizes, show_detail,
1083 show_bloat, show_config, show_environment):
1084 """Compare results with the base results and display delta.
1086 Only boards mentioned in board_selected will be considered. This
1087 function is intended to be called repeatedly with the results of
1088 each commit. It therefore shows a 'diff' between what it saw in
1089 the last call and what it sees now.
1092 board_selected: Dict containing boards to summarise, keyed by
1094 board_dict: Dict containing boards for which we built this
1095 commit, keyed by board.target. The value is an Outcome object.
1096 err_lines: A list of errors for this commit, or [] if there is
1097 none, or we don't want to print errors
1098 err_line_boards: Dict keyed by error line, containing a list of
1099 the Board objects with that error
1100 warn_lines: A list of warnings for this commit, or [] if there is
1101 none, or we don't want to print errors
1102 warn_line_boards: Dict keyed by warning line, containing a list of
1103 the Board objects with that warning
1104 config: Dictionary keyed by filename - e.g. '.config'. Each
1105 value is itself a dictionary:
1108 environment: Dictionary keyed by environment variable, Each
1109 value is the value of environment variable.
1110 show_sizes: Show image size deltas
1111 show_detail: Show detail for each board
1112 show_bloat: Show detail for each function
1113 show_config: Show config changes
1114 show_environment: Show environment changes
1116 def _BoardList(line, line_boards):
1117 """Helper function to get a line of boards containing a line
1120 line: Error line to search for
1122 String containing a list of boards with that error line, or
1123 '' if the user has not requested such a list
1125 if self._list_error_boards:
1127 for board in line_boards[line]:
1128 if not board.target in names:
1129 names.append(board.target)
1130 names_str = '(%s) ' % ','.join(names)
1135 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1140 if line not in base_lines:
1141 worse_lines.append(char + '+' +
1142 _BoardList(line, line_boards) + line)
1143 for line in base_lines:
1144 if line not in lines:
1145 better_lines.append(char + '-' +
1146 _BoardList(line, base_line_boards) + line)
1147 return better_lines, worse_lines
1149 def _CalcConfig(delta, name, config):
1150 """Calculate configuration changes
1153 delta: Type of the delta, e.g. '+'
1154 name: name of the file which changed (e.g. .config)
1155 config: configuration change dictionary
1159 String containing the configuration changes which can be
1163 for key in sorted(config.keys()):
1164 out += '%s=%s ' % (key, config[key])
1165 return '%s %s: %s' % (delta, name, out)
1167 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1168 """Add changes in configuration to a list
1171 lines: list to add to
1172 name: config file name
1173 config_plus: configurations added, dictionary
1176 config_minus: configurations removed, dictionary
1179 config_change: configurations changed, dictionary
1184 lines.append(_CalcConfig('+', name, config_plus))
1186 lines.append(_CalcConfig('-', name, config_minus))
1188 lines.append(_CalcConfig('c', name, config_change))
1190 def _OutputConfigInfo(lines):
1195 col = self.col.GREEN
1196 elif line[0] == '-':
1198 elif line[0] == 'c':
1199 col = self.col.YELLOW
1200 Print(' ' + line, newline=True, colour=col)
1203 ok_boards = [] # List of boards fixed since last commit
1204 warn_boards = [] # List of boards with warnings since last commit
1205 err_boards = [] # List of new broken boards since last commit
1206 new_boards = [] # List of boards that didn't exist last time
1207 unknown_boards = [] # List of boards that were not built
1209 for target in board_dict:
1210 if target not in board_selected:
1213 # If the board was built last time, add its outcome to a list
1214 if target in self._base_board_dict:
1215 base_outcome = self._base_board_dict[target].rc
1216 outcome = board_dict[target]
1217 if outcome.rc == OUTCOME_UNKNOWN:
1218 unknown_boards.append(target)
1219 elif outcome.rc < base_outcome:
1220 if outcome.rc == OUTCOME_WARNING:
1221 warn_boards.append(target)
1223 ok_boards.append(target)
1224 elif outcome.rc > base_outcome:
1225 if outcome.rc == OUTCOME_WARNING:
1226 warn_boards.append(target)
1228 err_boards.append(target)
1230 new_boards.append(target)
1232 # Get a list of errors that have appeared, and disappeared
1233 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1234 self._base_err_line_boards, err_lines, err_line_boards, '')
1235 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1236 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1238 # Display results by arch
1239 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1240 worse_err, better_err, worse_warn, better_warn)):
1242 self.AddOutcome(board_selected, arch_list, ok_boards, '',
1244 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1246 self.AddOutcome(board_selected, arch_list, err_boards, '+',
1248 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1249 if self._show_unknown:
1250 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
1252 for arch, target_list in arch_list.items():
1253 Print('%10s: %s' % (arch, target_list))
1254 self._error_lines += 1
1256 Print('\n'.join(better_err), colour=self.col.GREEN)
1257 self._error_lines += 1
1259 Print('\n'.join(worse_err), colour=self.col.RED)
1260 self._error_lines += 1
1262 Print('\n'.join(better_warn), colour=self.col.CYAN)
1263 self._error_lines += 1
1265 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1266 self._error_lines += 1
1269 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1272 if show_environment and self._base_environment:
1275 for target in board_dict:
1276 if target not in board_selected:
1279 tbase = self._base_environment[target]
1280 tenvironment = environment[target]
1281 environment_plus = {}
1282 environment_minus = {}
1283 environment_change = {}
1284 base = tbase.environment
1285 for key, value in tenvironment.environment.items():
1287 environment_plus[key] = value
1288 for key, value in base.items():
1289 if key not in tenvironment.environment:
1290 environment_minus[key] = value
1291 for key, value in base.items():
1292 new_value = tenvironment.environment.get(key)
1293 if new_value and value != new_value:
1294 desc = '%s -> %s' % (value, new_value)
1295 environment_change[key] = desc
1297 _AddConfig(lines, target, environment_plus, environment_minus,
1300 _OutputConfigInfo(lines)
1302 if show_config and self._base_config:
1304 arch_config_plus = {}
1305 arch_config_minus = {}
1306 arch_config_change = {}
1309 for target in board_dict:
1310 if target not in board_selected:
1312 arch = board_selected[target].arch
1313 if arch not in arch_list:
1314 arch_list.append(arch)
1316 for arch in arch_list:
1317 arch_config_plus[arch] = {}
1318 arch_config_minus[arch] = {}
1319 arch_config_change[arch] = {}
1320 for name in self.config_filenames:
1321 arch_config_plus[arch][name] = {}
1322 arch_config_minus[arch][name] = {}
1323 arch_config_change[arch][name] = {}
1325 for target in board_dict:
1326 if target not in board_selected:
1329 arch = board_selected[target].arch
1331 all_config_plus = {}
1332 all_config_minus = {}
1333 all_config_change = {}
1334 tbase = self._base_config[target]
1335 tconfig = config[target]
1337 for name in self.config_filenames:
1338 if not tconfig.config[name]:
1343 base = tbase.config[name]
1344 for key, value in tconfig.config[name].items():
1346 config_plus[key] = value
1347 all_config_plus[key] = value
1348 for key, value in base.items():
1349 if key not in tconfig.config[name]:
1350 config_minus[key] = value
1351 all_config_minus[key] = value
1352 for key, value in base.items():
1353 new_value = tconfig.config.get(key)
1354 if new_value and value != new_value:
1355 desc = '%s -> %s' % (value, new_value)
1356 config_change[key] = desc
1357 all_config_change[key] = desc
1359 arch_config_plus[arch][name].update(config_plus)
1360 arch_config_minus[arch][name].update(config_minus)
1361 arch_config_change[arch][name].update(config_change)
1363 _AddConfig(lines, name, config_plus, config_minus,
1365 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1367 summary[target] = '\n'.join(lines)
1369 lines_by_target = {}
1370 for target, lines in summary.items():
1371 if lines in lines_by_target:
1372 lines_by_target[lines].append(target)
1374 lines_by_target[lines] = [target]
1376 for arch in arch_list:
1381 for name in self.config_filenames:
1382 all_plus.update(arch_config_plus[arch][name])
1383 all_minus.update(arch_config_minus[arch][name])
1384 all_change.update(arch_config_change[arch][name])
1385 _AddConfig(lines, name, arch_config_plus[arch][name],
1386 arch_config_minus[arch][name],
1387 arch_config_change[arch][name])
1388 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1389 #arch_summary[target] = '\n'.join(lines)
1392 _OutputConfigInfo(lines)
1394 for lines, targets in lines_by_target.items():
1397 Print('%s :' % ' '.join(sorted(targets)))
1398 _OutputConfigInfo(lines.split('\n'))
1401 # Save our updated information for the next call to this function
1402 self._base_board_dict = board_dict
1403 self._base_err_lines = err_lines
1404 self._base_warn_lines = warn_lines
1405 self._base_err_line_boards = err_line_boards
1406 self._base_warn_line_boards = warn_line_boards
1407 self._base_config = config
1408 self._base_environment = environment
1410 # Get a list of boards that did not get built, if needed
1412 for board in board_selected:
1413 if not board in board_dict:
1414 not_built.append(board)
1416 Print("Boards not built (%d): %s" % (len(not_built),
1417 ', '.join(not_built)))
1419 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1420 (board_dict, err_lines, err_line_boards, warn_lines,
1421 warn_line_boards, config, environment) = self.GetResultSummary(
1422 board_selected, commit_upto,
1423 read_func_sizes=self._show_bloat,
1424 read_config=self._show_config,
1425 read_environment=self._show_environment)
1427 msg = '%02d: %s' % (commit_upto + 1,
1428 commits[commit_upto].subject)
1429 Print(msg, colour=self.col.BLUE)
1430 self.PrintResultSummary(board_selected, board_dict,
1431 err_lines if self._show_errors else [], err_line_boards,
1432 warn_lines if self._show_errors else [], warn_line_boards,
1433 config, environment, self._show_sizes, self._show_detail,
1434 self._show_bloat, self._show_config, self._show_environment)
1436 def ShowSummary(self, commits, board_selected):
1437 """Show a build summary for U-Boot for a given board list.
1439 Reset the result summary, then repeatedly call GetResultSummary on
1440 each commit's results, then display the differences we see.
1443 commit: Commit objects to summarise
1444 board_selected: Dict containing boards to summarise
1446 self.commit_count = len(commits) if commits else 1
1447 self.commits = commits
1448 self.ResetResultSummary(board_selected)
1449 self._error_lines = 0
1451 for commit_upto in range(0, self.commit_count, self._step):
1452 self.ProduceResultSummary(commit_upto, commits, board_selected)
1453 if not self._error_lines:
1454 Print('(no errors to report)', colour=self.col.GREEN)
1457 def SetupBuild(self, board_selected, commits):
1458 """Set up ready to start a build.
1461 board_selected: Selected boards to build
1462 commits: Selected commits to build
1464 # First work out how many commits we will build
1465 count = (self.commit_count + self._step - 1) // self._step
1466 self.count = len(board_selected) * count
1467 self.upto = self.warned = self.fail = 0
1468 self._timestamps = collections.deque()
1470 def GetThreadDir(self, thread_num):
1471 """Get the directory path to the working dir for a thread.
1474 thread_num: Number of thread to check.
1476 return os.path.join(self._working_dir, '%02d' % thread_num)
1478 def _PrepareThread(self, thread_num, setup_git):
1479 """Prepare the working directory for a thread.
1481 This clones or fetches the repo into the thread's work directory.
1484 thread_num: Thread number (0, 1, ...)
1485 setup_git: True to set up a git repo clone
1487 thread_dir = self.GetThreadDir(thread_num)
1488 builderthread.Mkdir(thread_dir)
1489 git_dir = os.path.join(thread_dir, '.git')
1491 # Clone the repo if it doesn't already exist
1492 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1493 # we have a private index but uses the origin repo's contents?
1494 if setup_git and self.git_dir:
1495 src_dir = os.path.abspath(self.git_dir)
1496 if os.path.exists(git_dir):
1497 gitutil.Fetch(git_dir, thread_dir)
1499 Print('\rCloning repo for thread %d' % thread_num,
1501 gitutil.Clone(src_dir, thread_dir)
1502 Print('\r%s\r' % (' ' * 30), newline=False)
1504 def _PrepareWorkingSpace(self, max_threads, setup_git):
1505 """Prepare the working directory for use.
1507 Set up the git repo for each thread.
1510 max_threads: Maximum number of threads we expect to need.
1511 setup_git: True to set up a git repo clone
1513 builderthread.Mkdir(self._working_dir)
1514 for thread in range(max_threads):
1515 self._PrepareThread(thread, setup_git)
1517 def _PrepareOutputSpace(self):
1518 """Get the output directories ready to receive files.
1520 We delete any output directories which look like ones we need to
1521 create. Having left over directories is confusing when the user wants
1522 to check the output manually.
1524 if not self.commits:
1527 for commit_upto in range(self.commit_count):
1528 dir_list.append(self._GetOutputDir(commit_upto))
1531 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1532 if dirname not in dir_list:
1533 to_remove.append(dirname)
1535 Print('Removing %d old build directories' % len(to_remove),
1537 for dirname in to_remove:
1538 shutil.rmtree(dirname)
1540 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1541 """Build all commits for a list of boards
1544 commits: List of commits to be build, each a Commit object
1545 boards_selected: Dict of selected boards, key is target name,
1546 value is Board object
1547 keep_outputs: True to save build output files
1548 verbose: Display build results as they are completed
1551 - number of boards that failed to build
1552 - number of boards that issued warnings
1554 self.commit_count = len(commits) if commits else 1
1555 self.commits = commits
1556 self._verbose = verbose
1558 self.ResetResultSummary(board_selected)
1559 builderthread.Mkdir(self.base_dir, parents = True)
1560 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1561 commits is not None)
1562 self._PrepareOutputSpace()
1563 Print('\rStarting build...', newline=False)
1564 self.SetupBuild(board_selected, commits)
1565 self.ProcessResult(None)
1567 # Create jobs to build all commits for each board
1568 for brd in board_selected.values():
1569 job = builderthread.BuilderJob()
1571 job.commits = commits
1572 job.keep_outputs = keep_outputs
1573 job.step = self._step
1576 term = threading.Thread(target=self.queue.join)
1577 term.setDaemon(True)
1579 while term.isAlive():
1582 # Wait until we have processed all output
1583 self.out_queue.join()
1586 return (self.fail, self.warned)