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():
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:
677 return_code = int(fd.readline())
679 # The file may be empty due to running out of disk space.
683 err_file = self.GetErrFile(commit_upto, target)
684 if os.path.exists(err_file):
685 with open(err_file, 'r') as fd:
686 err_lines = self.FilterErrors(fd.readlines())
688 # Decide whether the build was ok, failed or created warnings
696 # Convert size information to our simple format
697 if os.path.exists(sizes_file):
698 with open(sizes_file, 'r') as fd:
699 for line in fd.readlines():
700 values = line.split()
703 rodata = int(values[6], 16)
705 'all' : int(values[0]) + int(values[1]) +
707 'text' : int(values[0]) - rodata,
708 'data' : int(values[1]),
709 'bss' : int(values[2]),
712 sizes[values[5]] = size_dict
715 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
716 for fname in glob.glob(pattern):
717 with open(fname, 'r') as fd:
718 dict_name = os.path.basename(fname).replace('.sizes',
720 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
723 output_dir = self.GetBuildDir(commit_upto, target)
724 for name in self.config_filenames:
725 fname = os.path.join(output_dir, name)
726 config[name] = self._ProcessConfig(fname)
729 output_dir = self.GetBuildDir(commit_upto, target)
730 fname = os.path.join(output_dir, 'uboot.env')
731 environment = self._ProcessEnvironment(fname)
733 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
736 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
738 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
739 read_config, read_environment):
740 """Calculate a summary of the results of building a commit.
743 board_selected: Dict containing boards to summarise
744 commit_upto: Commit number to summarize (0..self.count-1)
745 read_func_sizes: True to read function size information
746 read_config: True to read .config and autoconf.h files
747 read_environment: True to read uboot.env files
751 Dict containing boards which passed building this commit.
752 keyed by board.target
753 List containing a summary of error lines
754 Dict keyed by error line, containing a list of the Board
755 objects with that error
756 List containing a summary of warning lines
757 Dict keyed by error line, containing a list of the Board
758 objects with that warning
759 Dictionary keyed by board.target. Each value is a dictionary:
760 key: filename - e.g. '.config'
761 value is itself a dictionary:
764 Dictionary keyed by board.target. Each value is a dictionary:
765 key: environment variable
766 value: value of environment variable
768 def AddLine(lines_summary, lines_boards, line, board):
770 if line in lines_boards:
771 lines_boards[line].append(board)
773 lines_boards[line] = [board]
774 lines_summary.append(line)
777 err_lines_summary = []
778 err_lines_boards = {}
779 warn_lines_summary = []
780 warn_lines_boards = {}
784 for board in boards_selected.values():
785 outcome = self.GetBuildOutcome(commit_upto, board.target,
786 read_func_sizes, read_config,
788 board_dict[board.target] = outcome
790 last_was_warning = False
791 for line in outcome.err_lines:
793 if (self._re_function.match(line) or
794 self._re_files.match(line)):
797 is_warning = (self._re_warning.match(line) or
798 self._re_dtb_warning.match(line))
799 is_note = self._re_note.match(line)
800 if is_warning or (last_was_warning and is_note):
802 AddLine(warn_lines_summary, warn_lines_boards,
804 AddLine(warn_lines_summary, warn_lines_boards,
808 AddLine(err_lines_summary, err_lines_boards,
810 AddLine(err_lines_summary, err_lines_boards,
812 last_was_warning = is_warning
814 tconfig = Config(self.config_filenames, board.target)
815 for fname in self.config_filenames:
817 for key, value in outcome.config[fname].items():
818 tconfig.Add(fname, key, value)
819 config[board.target] = tconfig
821 tenvironment = Environment(board.target)
822 if outcome.environment:
823 for key, value in outcome.environment.items():
824 tenvironment.Add(key, value)
825 environment[board.target] = tenvironment
827 return (board_dict, err_lines_summary, err_lines_boards,
828 warn_lines_summary, warn_lines_boards, config, environment)
830 def AddOutcome(self, board_dict, arch_list, changes, char, color):
831 """Add an output to our list of outcomes for each architecture
833 This simple function adds failing boards (changes) to the
834 relevant architecture string, so we can print the results out
835 sorted by architecture.
838 board_dict: Dict containing all boards
839 arch_list: Dict keyed by arch name. Value is a string containing
840 a list of board names which failed for that arch.
841 changes: List of boards to add to arch_list
842 color: terminal.Colour object
845 for target in changes:
846 if target in board_dict:
847 arch = board_dict[target].arch
850 str = self.col.Color(color, ' ' + target)
851 if not arch in done_arch:
852 str = ' %s %s' % (self.col.Color(color, char), str)
853 done_arch[arch] = True
854 if not arch in arch_list:
855 arch_list[arch] = str
857 arch_list[arch] += str
860 def ColourNum(self, num):
861 color = self.col.RED if num > 0 else self.col.GREEN
864 return self.col.Color(color, str(num))
866 def ResetResultSummary(self, board_selected):
867 """Reset the results summary ready for use.
869 Set up the base board list to be all those selected, and set the
870 error lines to empty.
872 Following this, calls to PrintResultSummary() will use this
873 information to work out what has changed.
876 board_selected: Dict containing boards to summarise, keyed by
879 self._base_board_dict = {}
880 for board in board_selected:
881 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
883 self._base_err_lines = []
884 self._base_warn_lines = []
885 self._base_err_line_boards = {}
886 self._base_warn_line_boards = {}
887 self._base_config = None
888 self._base_environment = None
890 def PrintFuncSizeDetail(self, fname, old, new):
891 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
892 delta, common = [], {}
899 if name not in common:
902 delta.append([-old[name], name])
905 if name not in common:
908 delta.append([new[name], name])
911 diff = new.get(name, 0) - old.get(name, 0)
913 grow, up = grow + 1, up + diff
915 shrink, down = shrink + 1, down - diff
916 delta.append([diff, name])
921 args = [add, -remove, grow, -shrink, up, -down, up - down]
922 if max(args) == 0 and min(args) == 0:
924 args = [self.ColourNum(x) for x in args]
926 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
927 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
928 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
930 for diff, name in delta:
932 color = self.col.RED if diff > 0 else self.col.GREEN
933 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
934 old.get(name, '-'), new.get(name,'-'), diff)
935 Print(msg, colour=color)
938 def PrintSizeDetail(self, target_list, show_bloat):
939 """Show details size information for each board
942 target_list: List of targets, each a dict containing:
943 'target': Target name
944 'total_diff': Total difference in bytes across all areas
945 <part_name>: Difference for that part
946 show_bloat: Show detail for each function
948 targets_by_diff = sorted(target_list, reverse=True,
949 key=lambda x: x['_total_diff'])
950 for result in targets_by_diff:
951 printed_target = False
952 for name in sorted(result):
954 if name.startswith('_'):
957 color = self.col.RED if diff > 0 else self.col.GREEN
958 msg = ' %s %+d' % (name, diff)
959 if not printed_target:
960 Print('%10s %-15s:' % ('', result['_target']),
962 printed_target = True
963 Print(msg, colour=color, newline=False)
967 target = result['_target']
968 outcome = result['_outcome']
969 base_outcome = self._base_board_dict[target]
970 for fname in outcome.func_sizes:
971 self.PrintFuncSizeDetail(fname,
972 base_outcome.func_sizes[fname],
973 outcome.func_sizes[fname])
976 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
978 """Print a summary of image sizes broken down by section.
980 The summary takes the form of one line per architecture. The
981 line contains deltas for each of the sections (+ means the section
982 got bigger, - means smaller). The nunmbers are the average number
983 of bytes that a board in this section increased by.
986 powerpc: (622 boards) text -0.0
987 arm: (285 boards) text -0.0
988 nds32: (3 boards) text -8.0
991 board_selected: Dict containing boards to summarise, keyed by
993 board_dict: Dict containing boards for which we built this
994 commit, keyed by board.target. The value is an Outcome object.
995 show_detail: Show detail for each board
996 show_bloat: Show detail for each function
1001 # Calculate changes in size for different image parts
1002 # The previous sizes are in Board.sizes, for each board
1003 for target in board_dict:
1004 if target not in board_selected:
1006 base_sizes = self._base_board_dict[target].sizes
1007 outcome = board_dict[target]
1008 sizes = outcome.sizes
1010 # Loop through the list of images, creating a dict of size
1011 # changes for each image/part. We end up with something like
1012 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1013 # which means that U-Boot data increased by 5 bytes and SPL
1014 # text decreased by 4.
1015 err = {'_target' : target}
1017 if image in base_sizes:
1018 base_image = base_sizes[image]
1019 # Loop through the text, data, bss parts
1020 for part in sorted(sizes[image]):
1021 diff = sizes[image][part] - base_image[part]
1024 if image == 'u-boot':
1027 name = image + ':' + part
1029 arch = board_selected[target].arch
1030 if not arch in arch_count:
1031 arch_count[arch] = 1
1033 arch_count[arch] += 1
1035 pass # Only add to our list when we have some stats
1036 elif not arch in arch_list:
1037 arch_list[arch] = [err]
1039 arch_list[arch].append(err)
1041 # We now have a list of image size changes sorted by arch
1042 # Print out a summary of these
1043 for arch, target_list in arch_list.items():
1044 # Get total difference for each type
1046 for result in target_list:
1048 for name, diff in result.items():
1049 if name.startswith('_'):
1053 totals[name] += diff
1056 result['_total_diff'] = total
1057 result['_outcome'] = board_dict[result['_target']]
1059 count = len(target_list)
1060 printed_arch = False
1061 for name in sorted(totals):
1064 # Display the average difference in this name for this
1066 avg_diff = float(diff) / count
1067 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1068 msg = ' %s %+1.1f' % (name, avg_diff)
1069 if not printed_arch:
1070 Print('%10s: (for %d/%d boards)' % (arch, count,
1071 arch_count[arch]), newline=False)
1073 Print(msg, colour=color, newline=False)
1078 self.PrintSizeDetail(target_list, show_bloat)
1081 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1082 err_line_boards, warn_lines, warn_line_boards,
1083 config, environment, show_sizes, show_detail,
1084 show_bloat, show_config, show_environment):
1085 """Compare results with the base results and display delta.
1087 Only boards mentioned in board_selected will be considered. This
1088 function is intended to be called repeatedly with the results of
1089 each commit. It therefore shows a 'diff' between what it saw in
1090 the last call and what it sees now.
1093 board_selected: Dict containing boards to summarise, keyed by
1095 board_dict: Dict containing boards for which we built this
1096 commit, keyed by board.target. The value is an Outcome object.
1097 err_lines: A list of errors for this commit, or [] if there is
1098 none, or we don't want to print errors
1099 err_line_boards: Dict keyed by error line, containing a list of
1100 the Board objects with that error
1101 warn_lines: A list of warnings for this commit, or [] if there is
1102 none, or we don't want to print errors
1103 warn_line_boards: Dict keyed by warning line, containing a list of
1104 the Board objects with that warning
1105 config: Dictionary keyed by filename - e.g. '.config'. Each
1106 value is itself a dictionary:
1109 environment: Dictionary keyed by environment variable, Each
1110 value is the value of environment variable.
1111 show_sizes: Show image size deltas
1112 show_detail: Show detail for each board
1113 show_bloat: Show detail for each function
1114 show_config: Show config changes
1115 show_environment: Show environment changes
1117 def _BoardList(line, line_boards):
1118 """Helper function to get a line of boards containing a line
1121 line: Error line to search for
1123 String containing a list of boards with that error line, or
1124 '' if the user has not requested such a list
1126 if self._list_error_boards:
1128 for board in line_boards[line]:
1129 if not board.target in names:
1130 names.append(board.target)
1131 names_str = '(%s) ' % ','.join(names)
1136 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1141 if line not in base_lines:
1142 worse_lines.append(char + '+' +
1143 _BoardList(line, line_boards) + line)
1144 for line in base_lines:
1145 if line not in lines:
1146 better_lines.append(char + '-' +
1147 _BoardList(line, base_line_boards) + line)
1148 return better_lines, worse_lines
1150 def _CalcConfig(delta, name, config):
1151 """Calculate configuration changes
1154 delta: Type of the delta, e.g. '+'
1155 name: name of the file which changed (e.g. .config)
1156 config: configuration change dictionary
1160 String containing the configuration changes which can be
1164 for key in sorted(config.keys()):
1165 out += '%s=%s ' % (key, config[key])
1166 return '%s %s: %s' % (delta, name, out)
1168 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1169 """Add changes in configuration to a list
1172 lines: list to add to
1173 name: config file name
1174 config_plus: configurations added, dictionary
1177 config_minus: configurations removed, dictionary
1180 config_change: configurations changed, dictionary
1185 lines.append(_CalcConfig('+', name, config_plus))
1187 lines.append(_CalcConfig('-', name, config_minus))
1189 lines.append(_CalcConfig('c', name, config_change))
1191 def _OutputConfigInfo(lines):
1196 col = self.col.GREEN
1197 elif line[0] == '-':
1199 elif line[0] == 'c':
1200 col = self.col.YELLOW
1201 Print(' ' + line, newline=True, colour=col)
1204 ok_boards = [] # List of boards fixed since last commit
1205 warn_boards = [] # List of boards with warnings since last commit
1206 err_boards = [] # List of new broken boards since last commit
1207 new_boards = [] # List of boards that didn't exist last time
1208 unknown_boards = [] # List of boards that were not built
1210 for target in board_dict:
1211 if target not in board_selected:
1214 # If the board was built last time, add its outcome to a list
1215 if target in self._base_board_dict:
1216 base_outcome = self._base_board_dict[target].rc
1217 outcome = board_dict[target]
1218 if outcome.rc == OUTCOME_UNKNOWN:
1219 unknown_boards.append(target)
1220 elif outcome.rc < base_outcome:
1221 if outcome.rc == OUTCOME_WARNING:
1222 warn_boards.append(target)
1224 ok_boards.append(target)
1225 elif outcome.rc > base_outcome:
1226 if outcome.rc == OUTCOME_WARNING:
1227 warn_boards.append(target)
1229 err_boards.append(target)
1231 new_boards.append(target)
1233 # Get a list of errors that have appeared, and disappeared
1234 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1235 self._base_err_line_boards, err_lines, err_line_boards, '')
1236 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1237 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1239 # Display results by arch
1240 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1241 worse_err, better_err, worse_warn, better_warn)):
1243 self.AddOutcome(board_selected, arch_list, ok_boards, '',
1245 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1247 self.AddOutcome(board_selected, arch_list, err_boards, '+',
1249 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1250 if self._show_unknown:
1251 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
1253 for arch, target_list in arch_list.items():
1254 Print('%10s: %s' % (arch, target_list))
1255 self._error_lines += 1
1257 Print('\n'.join(better_err), colour=self.col.GREEN)
1258 self._error_lines += 1
1260 Print('\n'.join(worse_err), colour=self.col.RED)
1261 self._error_lines += 1
1263 Print('\n'.join(better_warn), colour=self.col.CYAN)
1264 self._error_lines += 1
1266 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1267 self._error_lines += 1
1270 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1273 if show_environment and self._base_environment:
1276 for target in board_dict:
1277 if target not in board_selected:
1280 tbase = self._base_environment[target]
1281 tenvironment = environment[target]
1282 environment_plus = {}
1283 environment_minus = {}
1284 environment_change = {}
1285 base = tbase.environment
1286 for key, value in tenvironment.environment.items():
1288 environment_plus[key] = value
1289 for key, value in base.items():
1290 if key not in tenvironment.environment:
1291 environment_minus[key] = value
1292 for key, value in base.items():
1293 new_value = tenvironment.environment.get(key)
1294 if new_value and value != new_value:
1295 desc = '%s -> %s' % (value, new_value)
1296 environment_change[key] = desc
1298 _AddConfig(lines, target, environment_plus, environment_minus,
1301 _OutputConfigInfo(lines)
1303 if show_config and self._base_config:
1305 arch_config_plus = {}
1306 arch_config_minus = {}
1307 arch_config_change = {}
1310 for target in board_dict:
1311 if target not in board_selected:
1313 arch = board_selected[target].arch
1314 if arch not in arch_list:
1315 arch_list.append(arch)
1317 for arch in arch_list:
1318 arch_config_plus[arch] = {}
1319 arch_config_minus[arch] = {}
1320 arch_config_change[arch] = {}
1321 for name in self.config_filenames:
1322 arch_config_plus[arch][name] = {}
1323 arch_config_minus[arch][name] = {}
1324 arch_config_change[arch][name] = {}
1326 for target in board_dict:
1327 if target not in board_selected:
1330 arch = board_selected[target].arch
1332 all_config_plus = {}
1333 all_config_minus = {}
1334 all_config_change = {}
1335 tbase = self._base_config[target]
1336 tconfig = config[target]
1338 for name in self.config_filenames:
1339 if not tconfig.config[name]:
1344 base = tbase.config[name]
1345 for key, value in tconfig.config[name].items():
1347 config_plus[key] = value
1348 all_config_plus[key] = value
1349 for key, value in base.items():
1350 if key not in tconfig.config[name]:
1351 config_minus[key] = value
1352 all_config_minus[key] = value
1353 for key, value in base.items():
1354 new_value = tconfig.config.get(key)
1355 if new_value and value != new_value:
1356 desc = '%s -> %s' % (value, new_value)
1357 config_change[key] = desc
1358 all_config_change[key] = desc
1360 arch_config_plus[arch][name].update(config_plus)
1361 arch_config_minus[arch][name].update(config_minus)
1362 arch_config_change[arch][name].update(config_change)
1364 _AddConfig(lines, name, config_plus, config_minus,
1366 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1368 summary[target] = '\n'.join(lines)
1370 lines_by_target = {}
1371 for target, lines in summary.items():
1372 if lines in lines_by_target:
1373 lines_by_target[lines].append(target)
1375 lines_by_target[lines] = [target]
1377 for arch in arch_list:
1382 for name in self.config_filenames:
1383 all_plus.update(arch_config_plus[arch][name])
1384 all_minus.update(arch_config_minus[arch][name])
1385 all_change.update(arch_config_change[arch][name])
1386 _AddConfig(lines, name, arch_config_plus[arch][name],
1387 arch_config_minus[arch][name],
1388 arch_config_change[arch][name])
1389 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1390 #arch_summary[target] = '\n'.join(lines)
1393 _OutputConfigInfo(lines)
1395 for lines, targets in lines_by_target.items():
1398 Print('%s :' % ' '.join(sorted(targets)))
1399 _OutputConfigInfo(lines.split('\n'))
1402 # Save our updated information for the next call to this function
1403 self._base_board_dict = board_dict
1404 self._base_err_lines = err_lines
1405 self._base_warn_lines = warn_lines
1406 self._base_err_line_boards = err_line_boards
1407 self._base_warn_line_boards = warn_line_boards
1408 self._base_config = config
1409 self._base_environment = environment
1411 # Get a list of boards that did not get built, if needed
1413 for board in board_selected:
1414 if not board in board_dict:
1415 not_built.append(board)
1417 Print("Boards not built (%d): %s" % (len(not_built),
1418 ', '.join(not_built)))
1420 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1421 (board_dict, err_lines, err_line_boards, warn_lines,
1422 warn_line_boards, config, environment) = self.GetResultSummary(
1423 board_selected, commit_upto,
1424 read_func_sizes=self._show_bloat,
1425 read_config=self._show_config,
1426 read_environment=self._show_environment)
1428 msg = '%02d: %s' % (commit_upto + 1,
1429 commits[commit_upto].subject)
1430 Print(msg, colour=self.col.BLUE)
1431 self.PrintResultSummary(board_selected, board_dict,
1432 err_lines if self._show_errors else [], err_line_boards,
1433 warn_lines if self._show_errors else [], warn_line_boards,
1434 config, environment, self._show_sizes, self._show_detail,
1435 self._show_bloat, self._show_config, self._show_environment)
1437 def ShowSummary(self, commits, board_selected):
1438 """Show a build summary for U-Boot for a given board list.
1440 Reset the result summary, then repeatedly call GetResultSummary on
1441 each commit's results, then display the differences we see.
1444 commit: Commit objects to summarise
1445 board_selected: Dict containing boards to summarise
1447 self.commit_count = len(commits) if commits else 1
1448 self.commits = commits
1449 self.ResetResultSummary(board_selected)
1450 self._error_lines = 0
1452 for commit_upto in range(0, self.commit_count, self._step):
1453 self.ProduceResultSummary(commit_upto, commits, board_selected)
1454 if not self._error_lines:
1455 Print('(no errors to report)', colour=self.col.GREEN)
1458 def SetupBuild(self, board_selected, commits):
1459 """Set up ready to start a build.
1462 board_selected: Selected boards to build
1463 commits: Selected commits to build
1465 # First work out how many commits we will build
1466 count = (self.commit_count + self._step - 1) // self._step
1467 self.count = len(board_selected) * count
1468 self.upto = self.warned = self.fail = 0
1469 self._timestamps = collections.deque()
1471 def GetThreadDir(self, thread_num):
1472 """Get the directory path to the working dir for a thread.
1475 thread_num: Number of thread to check.
1477 return os.path.join(self._working_dir, '%02d' % thread_num)
1479 def _PrepareThread(self, thread_num, setup_git):
1480 """Prepare the working directory for a thread.
1482 This clones or fetches the repo into the thread's work directory.
1485 thread_num: Thread number (0, 1, ...)
1486 setup_git: True to set up a git repo clone
1488 thread_dir = self.GetThreadDir(thread_num)
1489 builderthread.Mkdir(thread_dir)
1490 git_dir = os.path.join(thread_dir, '.git')
1492 # Clone the repo if it doesn't already exist
1493 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1494 # we have a private index but uses the origin repo's contents?
1495 if setup_git and self.git_dir:
1496 src_dir = os.path.abspath(self.git_dir)
1497 if os.path.exists(git_dir):
1498 gitutil.Fetch(git_dir, thread_dir)
1500 Print('\rCloning repo for thread %d' % thread_num,
1502 gitutil.Clone(src_dir, thread_dir)
1503 Print('\r%s\r' % (' ' * 30), newline=False)
1505 def _PrepareWorkingSpace(self, max_threads, setup_git):
1506 """Prepare the working directory for use.
1508 Set up the git repo for each thread.
1511 max_threads: Maximum number of threads we expect to need.
1512 setup_git: True to set up a git repo clone
1514 builderthread.Mkdir(self._working_dir)
1515 for thread in range(max_threads):
1516 self._PrepareThread(thread, setup_git)
1518 def _PrepareOutputSpace(self):
1519 """Get the output directories ready to receive files.
1521 We delete any output directories which look like ones we need to
1522 create. Having left over directories is confusing when the user wants
1523 to check the output manually.
1525 if not self.commits:
1528 for commit_upto in range(self.commit_count):
1529 dir_list.append(self._GetOutputDir(commit_upto))
1532 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1533 if dirname not in dir_list:
1534 to_remove.append(dirname)
1536 Print('Removing %d old build directories' % len(to_remove),
1538 for dirname in to_remove:
1539 shutil.rmtree(dirname)
1541 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1542 """Build all commits for a list of boards
1545 commits: List of commits to be build, each a Commit object
1546 boards_selected: Dict of selected boards, key is target name,
1547 value is Board object
1548 keep_outputs: True to save build output files
1549 verbose: Display build results as they are completed
1552 - number of boards that failed to build
1553 - number of boards that issued warnings
1555 self.commit_count = len(commits) if commits else 1
1556 self.commits = commits
1557 self._verbose = verbose
1559 self.ResetResultSummary(board_selected)
1560 builderthread.Mkdir(self.base_dir, parents = True)
1561 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1562 commits is not None)
1563 self._PrepareOutputSpace()
1564 Print('\rStarting build...', newline=False)
1565 self.SetupBuild(board_selected, commits)
1566 self.ProcessResult(None)
1568 # Create jobs to build all commits for each board
1569 for brd in board_selected.values():
1570 job = builderthread.BuilderJob()
1572 job.commits = commits
1573 job.keep_outputs = keep_outputs
1574 job.step = self._step
1577 term = threading.Thread(target=self.queue.join)
1578 term.setDaemon(True)
1580 while term.isAlive():
1583 # Wait until we have processed all output
1584 self.out_queue.join()
1587 return (self.fail, self.warned)