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_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, **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:
675 return_code = int(fd.readline())
677 err_file = self.GetErrFile(commit_upto, target)
678 if os.path.exists(err_file):
679 with open(err_file, 'r') as fd:
680 err_lines = self.FilterErrors(fd.readlines())
682 # Decide whether the build was ok, failed or created warnings
690 # Convert size information to our simple format
691 if os.path.exists(sizes_file):
692 with open(sizes_file, 'r') as fd:
693 for line in fd.readlines():
694 values = line.split()
697 rodata = int(values[6], 16)
699 'all' : int(values[0]) + int(values[1]) +
701 'text' : int(values[0]) - rodata,
702 'data' : int(values[1]),
703 'bss' : int(values[2]),
706 sizes[values[5]] = size_dict
709 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
710 for fname in glob.glob(pattern):
711 with open(fname, 'r') as fd:
712 dict_name = os.path.basename(fname).replace('.sizes',
714 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
717 output_dir = self.GetBuildDir(commit_upto, target)
718 for name in self.config_filenames:
719 fname = os.path.join(output_dir, name)
720 config[name] = self._ProcessConfig(fname)
723 output_dir = self.GetBuildDir(commit_upto, target)
724 fname = os.path.join(output_dir, 'uboot.env')
725 environment = self._ProcessEnvironment(fname)
727 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
730 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
732 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
733 read_config, read_environment):
734 """Calculate a summary of the results of building a commit.
737 board_selected: Dict containing boards to summarise
738 commit_upto: Commit number to summarize (0..self.count-1)
739 read_func_sizes: True to read function size information
740 read_config: True to read .config and autoconf.h files
741 read_environment: True to read uboot.env files
745 Dict containing boards which passed building this commit.
746 keyed by board.target
747 List containing a summary of error lines
748 Dict keyed by error line, containing a list of the Board
749 objects with that error
750 List containing a summary of warning lines
751 Dict keyed by error line, containing a list of the Board
752 objects with that warning
753 Dictionary keyed by board.target. Each value is a dictionary:
754 key: filename - e.g. '.config'
755 value is itself a dictionary:
758 Dictionary keyed by board.target. Each value is a dictionary:
759 key: environment variable
760 value: value of environment variable
762 def AddLine(lines_summary, lines_boards, line, board):
764 if line in lines_boards:
765 lines_boards[line].append(board)
767 lines_boards[line] = [board]
768 lines_summary.append(line)
771 err_lines_summary = []
772 err_lines_boards = {}
773 warn_lines_summary = []
774 warn_lines_boards = {}
778 for board in boards_selected.itervalues():
779 outcome = self.GetBuildOutcome(commit_upto, board.target,
780 read_func_sizes, read_config,
782 board_dict[board.target] = outcome
784 last_was_warning = False
785 for line in outcome.err_lines:
787 if (self._re_function.match(line) or
788 self._re_files.match(line)):
791 is_warning = self._re_warning.match(line)
792 is_note = self._re_note.match(line)
793 if is_warning or (last_was_warning and is_note):
795 AddLine(warn_lines_summary, warn_lines_boards,
797 AddLine(warn_lines_summary, warn_lines_boards,
801 AddLine(err_lines_summary, err_lines_boards,
803 AddLine(err_lines_summary, err_lines_boards,
805 last_was_warning = is_warning
807 tconfig = Config(self.config_filenames, board.target)
808 for fname in self.config_filenames:
810 for key, value in outcome.config[fname].iteritems():
811 tconfig.Add(fname, key, value)
812 config[board.target] = tconfig
814 tenvironment = Environment(board.target)
815 if outcome.environment:
816 for key, value in outcome.environment.iteritems():
817 tenvironment.Add(key, value)
818 environment[board.target] = tenvironment
820 return (board_dict, err_lines_summary, err_lines_boards,
821 warn_lines_summary, warn_lines_boards, config, environment)
823 def AddOutcome(self, board_dict, arch_list, changes, char, color):
824 """Add an output to our list of outcomes for each architecture
826 This simple function adds failing boards (changes) to the
827 relevant architecture string, so we can print the results out
828 sorted by architecture.
831 board_dict: Dict containing all boards
832 arch_list: Dict keyed by arch name. Value is a string containing
833 a list of board names which failed for that arch.
834 changes: List of boards to add to arch_list
835 color: terminal.Colour object
838 for target in changes:
839 if target in board_dict:
840 arch = board_dict[target].arch
843 str = self.col.Color(color, ' ' + target)
844 if not arch in done_arch:
845 str = ' %s %s' % (self.col.Color(color, char), str)
846 done_arch[arch] = True
847 if not arch in arch_list:
848 arch_list[arch] = str
850 arch_list[arch] += str
853 def ColourNum(self, num):
854 color = self.col.RED if num > 0 else self.col.GREEN
857 return self.col.Color(color, str(num))
859 def ResetResultSummary(self, board_selected):
860 """Reset the results summary ready for use.
862 Set up the base board list to be all those selected, and set the
863 error lines to empty.
865 Following this, calls to PrintResultSummary() will use this
866 information to work out what has changed.
869 board_selected: Dict containing boards to summarise, keyed by
872 self._base_board_dict = {}
873 for board in board_selected:
874 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
876 self._base_err_lines = []
877 self._base_warn_lines = []
878 self._base_err_line_boards = {}
879 self._base_warn_line_boards = {}
880 self._base_config = None
881 self._base_environment = None
883 def PrintFuncSizeDetail(self, fname, old, new):
884 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
885 delta, common = [], {}
892 if name not in common:
895 delta.append([-old[name], name])
898 if name not in common:
901 delta.append([new[name], name])
904 diff = new.get(name, 0) - old.get(name, 0)
906 grow, up = grow + 1, up + diff
908 shrink, down = shrink + 1, down - diff
909 delta.append([diff, name])
914 args = [add, -remove, grow, -shrink, up, -down, up - down]
915 if max(args) == 0 and min(args) == 0:
917 args = [self.ColourNum(x) for x in args]
919 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
920 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
921 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
923 for diff, name in delta:
925 color = self.col.RED if diff > 0 else self.col.GREEN
926 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
927 old.get(name, '-'), new.get(name,'-'), diff)
928 Print(msg, colour=color)
931 def PrintSizeDetail(self, target_list, show_bloat):
932 """Show details size information for each board
935 target_list: List of targets, each a dict containing:
936 'target': Target name
937 'total_diff': Total difference in bytes across all areas
938 <part_name>: Difference for that part
939 show_bloat: Show detail for each function
941 targets_by_diff = sorted(target_list, reverse=True,
942 key=lambda x: x['_total_diff'])
943 for result in targets_by_diff:
944 printed_target = False
945 for name in sorted(result):
947 if name.startswith('_'):
950 color = self.col.RED if diff > 0 else self.col.GREEN
951 msg = ' %s %+d' % (name, diff)
952 if not printed_target:
953 Print('%10s %-15s:' % ('', result['_target']),
955 printed_target = True
956 Print(msg, colour=color, newline=False)
960 target = result['_target']
961 outcome = result['_outcome']
962 base_outcome = self._base_board_dict[target]
963 for fname in outcome.func_sizes:
964 self.PrintFuncSizeDetail(fname,
965 base_outcome.func_sizes[fname],
966 outcome.func_sizes[fname])
969 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
971 """Print a summary of image sizes broken down by section.
973 The summary takes the form of one line per architecture. The
974 line contains deltas for each of the sections (+ means the section
975 got bigger, - means smaller). The nunmbers are the average number
976 of bytes that a board in this section increased by.
979 powerpc: (622 boards) text -0.0
980 arm: (285 boards) text -0.0
981 nds32: (3 boards) text -8.0
984 board_selected: Dict containing boards to summarise, keyed by
986 board_dict: Dict containing boards for which we built this
987 commit, keyed by board.target. The value is an Outcome object.
988 show_detail: Show detail for each board
989 show_bloat: Show detail for each function
994 # Calculate changes in size for different image parts
995 # The previous sizes are in Board.sizes, for each board
996 for target in board_dict:
997 if target not in board_selected:
999 base_sizes = self._base_board_dict[target].sizes
1000 outcome = board_dict[target]
1001 sizes = outcome.sizes
1003 # Loop through the list of images, creating a dict of size
1004 # changes for each image/part. We end up with something like
1005 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1006 # which means that U-Boot data increased by 5 bytes and SPL
1007 # text decreased by 4.
1008 err = {'_target' : target}
1010 if image in base_sizes:
1011 base_image = base_sizes[image]
1012 # Loop through the text, data, bss parts
1013 for part in sorted(sizes[image]):
1014 diff = sizes[image][part] - base_image[part]
1017 if image == 'u-boot':
1020 name = image + ':' + part
1022 arch = board_selected[target].arch
1023 if not arch in arch_count:
1024 arch_count[arch] = 1
1026 arch_count[arch] += 1
1028 pass # Only add to our list when we have some stats
1029 elif not arch in arch_list:
1030 arch_list[arch] = [err]
1032 arch_list[arch].append(err)
1034 # We now have a list of image size changes sorted by arch
1035 # Print out a summary of these
1036 for arch, target_list in arch_list.iteritems():
1037 # Get total difference for each type
1039 for result in target_list:
1041 for name, diff in result.iteritems():
1042 if name.startswith('_'):
1046 totals[name] += diff
1049 result['_total_diff'] = total
1050 result['_outcome'] = board_dict[result['_target']]
1052 count = len(target_list)
1053 printed_arch = False
1054 for name in sorted(totals):
1057 # Display the average difference in this name for this
1059 avg_diff = float(diff) / count
1060 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1061 msg = ' %s %+1.1f' % (name, avg_diff)
1062 if not printed_arch:
1063 Print('%10s: (for %d/%d boards)' % (arch, count,
1064 arch_count[arch]), newline=False)
1066 Print(msg, colour=color, newline=False)
1071 self.PrintSizeDetail(target_list, show_bloat)
1074 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1075 err_line_boards, warn_lines, warn_line_boards,
1076 config, environment, show_sizes, show_detail,
1077 show_bloat, show_config, show_environment):
1078 """Compare results with the base results and display delta.
1080 Only boards mentioned in board_selected will be considered. This
1081 function is intended to be called repeatedly with the results of
1082 each commit. It therefore shows a 'diff' between what it saw in
1083 the last call and what it sees now.
1086 board_selected: Dict containing boards to summarise, keyed by
1088 board_dict: Dict containing boards for which we built this
1089 commit, keyed by board.target. The value is an Outcome object.
1090 err_lines: A list of errors for this commit, or [] if there is
1091 none, or we don't want to print errors
1092 err_line_boards: Dict keyed by error line, containing a list of
1093 the Board objects with that error
1094 warn_lines: A list of warnings for this commit, or [] if there is
1095 none, or we don't want to print errors
1096 warn_line_boards: Dict keyed by warning line, containing a list of
1097 the Board objects with that warning
1098 config: Dictionary keyed by filename - e.g. '.config'. Each
1099 value is itself a dictionary:
1102 environment: Dictionary keyed by environment variable, Each
1103 value is the value of environment variable.
1104 show_sizes: Show image size deltas
1105 show_detail: Show detail for each board
1106 show_bloat: Show detail for each function
1107 show_config: Show config changes
1108 show_environment: Show environment changes
1110 def _BoardList(line, line_boards):
1111 """Helper function to get a line of boards containing a line
1114 line: Error line to search for
1116 String containing a list of boards with that error line, or
1117 '' if the user has not requested such a list
1119 if self._list_error_boards:
1121 for board in line_boards[line]:
1122 if not board.target in names:
1123 names.append(board.target)
1124 names_str = '(%s) ' % ','.join(names)
1129 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1134 if line not in base_lines:
1135 worse_lines.append(char + '+' +
1136 _BoardList(line, line_boards) + line)
1137 for line in base_lines:
1138 if line not in lines:
1139 better_lines.append(char + '-' +
1140 _BoardList(line, base_line_boards) + line)
1141 return better_lines, worse_lines
1143 def _CalcConfig(delta, name, config):
1144 """Calculate configuration changes
1147 delta: Type of the delta, e.g. '+'
1148 name: name of the file which changed (e.g. .config)
1149 config: configuration change dictionary
1153 String containing the configuration changes which can be
1157 for key in sorted(config.keys()):
1158 out += '%s=%s ' % (key, config[key])
1159 return '%s %s: %s' % (delta, name, out)
1161 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1162 """Add changes in configuration to a list
1165 lines: list to add to
1166 name: config file name
1167 config_plus: configurations added, dictionary
1170 config_minus: configurations removed, dictionary
1173 config_change: configurations changed, dictionary
1178 lines.append(_CalcConfig('+', name, config_plus))
1180 lines.append(_CalcConfig('-', name, config_minus))
1182 lines.append(_CalcConfig('c', name, config_change))
1184 def _OutputConfigInfo(lines):
1189 col = self.col.GREEN
1190 elif line[0] == '-':
1192 elif line[0] == 'c':
1193 col = self.col.YELLOW
1194 Print(' ' + line, newline=True, colour=col)
1197 better = [] # List of boards fixed since last commit
1198 worse = [] # List of new broken boards since last commit
1199 new = [] # List of boards that didn't exist last time
1200 unknown = [] # List of boards that were not built
1202 for target in board_dict:
1203 if target not in board_selected:
1206 # If the board was built last time, add its outcome to a list
1207 if target in self._base_board_dict:
1208 base_outcome = self._base_board_dict[target].rc
1209 outcome = board_dict[target]
1210 if outcome.rc == OUTCOME_UNKNOWN:
1211 unknown.append(target)
1212 elif outcome.rc < base_outcome:
1213 better.append(target)
1214 elif outcome.rc > base_outcome:
1215 worse.append(target)
1219 # Get a list of errors that have appeared, and disappeared
1220 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1221 self._base_err_line_boards, err_lines, err_line_boards, '')
1222 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1223 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1225 # Display results by arch
1226 if (better or worse or unknown or new or worse_err or better_err
1227 or worse_warn or better_warn):
1229 self.AddOutcome(board_selected, arch_list, better, '',
1231 self.AddOutcome(board_selected, arch_list, worse, '+',
1233 self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE)
1234 if self._show_unknown:
1235 self.AddOutcome(board_selected, arch_list, unknown, '?',
1237 for arch, target_list in arch_list.iteritems():
1238 Print('%10s: %s' % (arch, target_list))
1239 self._error_lines += 1
1241 Print('\n'.join(better_err), colour=self.col.GREEN)
1242 self._error_lines += 1
1244 Print('\n'.join(worse_err), colour=self.col.RED)
1245 self._error_lines += 1
1247 Print('\n'.join(better_warn), colour=self.col.CYAN)
1248 self._error_lines += 1
1250 Print('\n'.join(worse_warn), colour=self.col.MAGENTA)
1251 self._error_lines += 1
1254 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1257 if show_environment and self._base_environment:
1260 for target in board_dict:
1261 if target not in board_selected:
1264 tbase = self._base_environment[target]
1265 tenvironment = environment[target]
1266 environment_plus = {}
1267 environment_minus = {}
1268 environment_change = {}
1269 base = tbase.environment
1270 for key, value in tenvironment.environment.iteritems():
1272 environment_plus[key] = value
1273 for key, value in base.iteritems():
1274 if key not in tenvironment.environment:
1275 environment_minus[key] = value
1276 for key, value in base.iteritems():
1277 new_value = tenvironment.environment.get(key)
1278 if new_value and value != new_value:
1279 desc = '%s -> %s' % (value, new_value)
1280 environment_change[key] = desc
1282 _AddConfig(lines, target, environment_plus, environment_minus,
1285 _OutputConfigInfo(lines)
1287 if show_config and self._base_config:
1289 arch_config_plus = {}
1290 arch_config_minus = {}
1291 arch_config_change = {}
1294 for target in board_dict:
1295 if target not in board_selected:
1297 arch = board_selected[target].arch
1298 if arch not in arch_list:
1299 arch_list.append(arch)
1301 for arch in arch_list:
1302 arch_config_plus[arch] = {}
1303 arch_config_minus[arch] = {}
1304 arch_config_change[arch] = {}
1305 for name in self.config_filenames:
1306 arch_config_plus[arch][name] = {}
1307 arch_config_minus[arch][name] = {}
1308 arch_config_change[arch][name] = {}
1310 for target in board_dict:
1311 if target not in board_selected:
1314 arch = board_selected[target].arch
1316 all_config_plus = {}
1317 all_config_minus = {}
1318 all_config_change = {}
1319 tbase = self._base_config[target]
1320 tconfig = config[target]
1322 for name in self.config_filenames:
1323 if not tconfig.config[name]:
1328 base = tbase.config[name]
1329 for key, value in tconfig.config[name].iteritems():
1331 config_plus[key] = value
1332 all_config_plus[key] = value
1333 for key, value in base.iteritems():
1334 if key not in tconfig.config[name]:
1335 config_minus[key] = value
1336 all_config_minus[key] = value
1337 for key, value in base.iteritems():
1338 new_value = tconfig.config.get(key)
1339 if new_value and value != new_value:
1340 desc = '%s -> %s' % (value, new_value)
1341 config_change[key] = desc
1342 all_config_change[key] = desc
1344 arch_config_plus[arch][name].update(config_plus)
1345 arch_config_minus[arch][name].update(config_minus)
1346 arch_config_change[arch][name].update(config_change)
1348 _AddConfig(lines, name, config_plus, config_minus,
1350 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1352 summary[target] = '\n'.join(lines)
1354 lines_by_target = {}
1355 for target, lines in summary.iteritems():
1356 if lines in lines_by_target:
1357 lines_by_target[lines].append(target)
1359 lines_by_target[lines] = [target]
1361 for arch in arch_list:
1366 for name in self.config_filenames:
1367 all_plus.update(arch_config_plus[arch][name])
1368 all_minus.update(arch_config_minus[arch][name])
1369 all_change.update(arch_config_change[arch][name])
1370 _AddConfig(lines, name, arch_config_plus[arch][name],
1371 arch_config_minus[arch][name],
1372 arch_config_change[arch][name])
1373 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1374 #arch_summary[target] = '\n'.join(lines)
1377 _OutputConfigInfo(lines)
1379 for lines, targets in lines_by_target.iteritems():
1382 Print('%s :' % ' '.join(sorted(targets)))
1383 _OutputConfigInfo(lines.split('\n'))
1386 # Save our updated information for the next call to this function
1387 self._base_board_dict = board_dict
1388 self._base_err_lines = err_lines
1389 self._base_warn_lines = warn_lines
1390 self._base_err_line_boards = err_line_boards
1391 self._base_warn_line_boards = warn_line_boards
1392 self._base_config = config
1393 self._base_environment = environment
1395 # Get a list of boards that did not get built, if needed
1397 for board in board_selected:
1398 if not board in board_dict:
1399 not_built.append(board)
1401 Print("Boards not built (%d): %s" % (len(not_built),
1402 ', '.join(not_built)))
1404 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1405 (board_dict, err_lines, err_line_boards, warn_lines,
1406 warn_line_boards, config, environment) = self.GetResultSummary(
1407 board_selected, commit_upto,
1408 read_func_sizes=self._show_bloat,
1409 read_config=self._show_config,
1410 read_environment=self._show_environment)
1412 msg = '%02d: %s' % (commit_upto + 1,
1413 commits[commit_upto].subject)
1414 Print(msg, colour=self.col.BLUE)
1415 self.PrintResultSummary(board_selected, board_dict,
1416 err_lines if self._show_errors else [], err_line_boards,
1417 warn_lines if self._show_errors else [], warn_line_boards,
1418 config, environment, self._show_sizes, self._show_detail,
1419 self._show_bloat, self._show_config, self._show_environment)
1421 def ShowSummary(self, commits, board_selected):
1422 """Show a build summary for U-Boot for a given board list.
1424 Reset the result summary, then repeatedly call GetResultSummary on
1425 each commit's results, then display the differences we see.
1428 commit: Commit objects to summarise
1429 board_selected: Dict containing boards to summarise
1431 self.commit_count = len(commits) if commits else 1
1432 self.commits = commits
1433 self.ResetResultSummary(board_selected)
1434 self._error_lines = 0
1436 for commit_upto in range(0, self.commit_count, self._step):
1437 self.ProduceResultSummary(commit_upto, commits, board_selected)
1438 if not self._error_lines:
1439 Print('(no errors to report)', colour=self.col.GREEN)
1442 def SetupBuild(self, board_selected, commits):
1443 """Set up ready to start a build.
1446 board_selected: Selected boards to build
1447 commits: Selected commits to build
1449 # First work out how many commits we will build
1450 count = (self.commit_count + self._step - 1) / self._step
1451 self.count = len(board_selected) * count
1452 self.upto = self.warned = self.fail = 0
1453 self._timestamps = collections.deque()
1455 def GetThreadDir(self, thread_num):
1456 """Get the directory path to the working dir for a thread.
1459 thread_num: Number of thread to check.
1461 return os.path.join(self._working_dir, '%02d' % thread_num)
1463 def _PrepareThread(self, thread_num, setup_git):
1464 """Prepare the working directory for a thread.
1466 This clones or fetches the repo into the thread's work directory.
1469 thread_num: Thread number (0, 1, ...)
1470 setup_git: True to set up a git repo clone
1472 thread_dir = self.GetThreadDir(thread_num)
1473 builderthread.Mkdir(thread_dir)
1474 git_dir = os.path.join(thread_dir, '.git')
1476 # Clone the repo if it doesn't already exist
1477 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1478 # we have a private index but uses the origin repo's contents?
1479 if setup_git and self.git_dir:
1480 src_dir = os.path.abspath(self.git_dir)
1481 if os.path.exists(git_dir):
1482 gitutil.Fetch(git_dir, thread_dir)
1484 Print('\rCloning repo for thread %d' % thread_num,
1486 gitutil.Clone(src_dir, thread_dir)
1487 Print('\r%s\r' % (' ' * 30), newline=False)
1489 def _PrepareWorkingSpace(self, max_threads, setup_git):
1490 """Prepare the working directory for use.
1492 Set up the git repo for each thread.
1495 max_threads: Maximum number of threads we expect to need.
1496 setup_git: True to set up a git repo clone
1498 builderthread.Mkdir(self._working_dir)
1499 for thread in range(max_threads):
1500 self._PrepareThread(thread, setup_git)
1502 def _PrepareOutputSpace(self):
1503 """Get the output directories ready to receive files.
1505 We delete any output directories which look like ones we need to
1506 create. Having left over directories is confusing when the user wants
1507 to check the output manually.
1509 if not self.commits:
1512 for commit_upto in range(self.commit_count):
1513 dir_list.append(self._GetOutputDir(commit_upto))
1516 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1517 if dirname not in dir_list:
1518 to_remove.append(dirname)
1520 Print('Removing %d old build directories' % len(to_remove),
1522 for dirname in to_remove:
1523 shutil.rmtree(dirname)
1525 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1526 """Build all commits for a list of boards
1529 commits: List of commits to be build, each a Commit object
1530 boards_selected: Dict of selected boards, key is target name,
1531 value is Board object
1532 keep_outputs: True to save build output files
1533 verbose: Display build results as they are completed
1536 - number of boards that failed to build
1537 - number of boards that issued warnings
1539 self.commit_count = len(commits) if commits else 1
1540 self.commits = commits
1541 self._verbose = verbose
1543 self.ResetResultSummary(board_selected)
1544 builderthread.Mkdir(self.base_dir, parents = True)
1545 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1546 commits is not None)
1547 self._PrepareOutputSpace()
1548 Print('\rStarting build...', newline=False)
1549 self.SetupBuild(board_selected, commits)
1550 self.ProcessResult(None)
1552 # Create jobs to build all commits for each board
1553 for brd in board_selected.itervalues():
1554 job = builderthread.BuilderJob()
1556 job.commits = commits
1557 job.keep_outputs = keep_outputs
1558 job.step = self._step
1561 term = threading.Thread(target=self.queue.join)
1562 term.setDaemon(True)
1564 while term.isAlive():
1567 # Wait until we have processed all output
1568 self.out_queue.join()
1571 return (self.fail, self.warned)