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
30 Please see README for user documentation, and you should be familiar with
31 that before trying to make sense of this.
33 Buildman works by keeping the machine as busy as possible, building different
34 commits for different boards on multiple CPUs at once.
36 The source repo (self.git_dir) contains all the commits to be built. Each
37 thread works on a single board at a time. It checks out the first commit,
38 configures it for that board, then builds it. Then it checks out the next
39 commit and builds it (typically without re-configuring). When it runs out
40 of commits, it gets another job from the builder and starts again with that
43 Clearly the builder threads could work either way - they could check out a
44 commit and then built it for all boards. Using separate directories for each
45 commit/board pair they could leave their build product around afterwards
48 The intent behind building a single board for multiple commits, is to make
49 use of incremental builds. Since each commit is built incrementally from
50 the previous one, builds are faster. Reconfiguring for a different board
51 removes all intermediate object files.
53 Many threads can be working at once, but each has its own working directory.
54 When a thread finishes a build, it puts the output files into a result
57 The base directory used by buildman is normally '../<branch>', i.e.
58 a directory higher than the source repository and named after the branch
61 Within the base directory, we have one subdirectory for each commit. Within
62 that is one subdirectory for each board. Within that is the build output for
63 that commit/board combination.
65 Buildman also create working directories for each thread, in a .bm-work/
66 subdirectory in the base dir.
68 As an example, say we are building branch 'us-net' for boards 'sandbox' and
69 'seaboard', and say that us-net has two commits. We will have directories
72 us-net/ base directory
73 01_of_02_g4ed4ebc_net--Add-tftp-speed-/
78 02_of_02_g4ed4ebc_net--Check-tftp-comp/
84 00/ working directory for thread 0 (contains source checkout)
86 01/ working directory for thread 1
89 u-boot/ source directory
93 """Holds information about a particular error line we are outputing
95 char: Character representation: '+': error, '-': fixed error, 'w+': warning,
97 boards: List of Board objects which have line in the error/warning output
98 errline: The text of the error line
100 ErrLine = collections.namedtuple('ErrLine', 'char,boards,errline')
102 # Possible build outcomes
103 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4))
105 # Translate a commit subject into a valid filename (and handle unicode)
106 trans_valid_chars = str.maketrans('/: ', '---')
108 BASE_CONFIG_FILENAMES = [
109 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
112 EXTRA_CONFIG_FILENAMES = [
113 '.config', '.config-spl', '.config-tpl',
114 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
115 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
119 """Holds information about configuration settings for a board."""
120 def __init__(self, config_filename, target):
123 for fname in config_filename:
124 self.config[fname] = {}
126 def Add(self, fname, key, value):
127 self.config[fname][key] = value
131 for fname in self.config:
132 for key, value in self.config[fname].items():
134 val = val ^ hash(key) & hash(value)
138 """Holds information about environment variables for a board."""
139 def __init__(self, target):
141 self.environment = {}
143 def Add(self, key, value):
144 self.environment[key] = value
147 """Class for building U-Boot for a particular commit.
149 Public members: (many should ->private)
150 already_done: Number of builds already completed
151 base_dir: Base directory to use for builder
152 checkout: True to check out source, False to skip that step.
153 This is used for testing.
154 col: terminal.Color() object
155 count: Number of commits to build
156 do_make: Method to call to invoke Make
157 fail: Number of builds that failed due to error
158 force_build: Force building even if a build already exists
159 force_config_on_failure: If a commit fails for a board, disable
160 incremental building for the next commit we build for that
161 board, so that we will see all warnings/errors again.
162 force_build_failures: If a previously-built build (i.e. built on
163 a previous run of buildman) is marked as failed, rebuild it.
164 git_dir: Git directory containing source repository
165 last_line_len: Length of the last line we printed (used for erasing
166 it with new progress information)
167 num_jobs: Number of jobs to run at once (passed to make as -j)
168 num_threads: Number of builder threads to run
169 out_queue: Queue of results to process
170 re_make_err: Compiled regular expression for ignore_lines
171 queue: Queue of jobs to run
172 threads: List of active threads
173 toolchains: Toolchains object to use for building
174 upto: Current commit number we are building (0.count-1)
175 warned: Number of builds that produced at least one warning
176 force_reconfig: Reconfigure U-Boot on each comiit. This disables
177 incremental building, where buildman reconfigures on the first
178 commit for a baord, and then just does an incremental build for
179 the following commits. In fact buildman will reconfigure and
180 retry for any failing commits, so generally the only effect of
181 this option is to slow things down.
182 in_tree: Build U-Boot in-tree instead of specifying an output
183 directory separate from the source code. This option is really
184 only useful for testing in-tree builds.
185 work_in_output: Use the output directory as the work directory and
186 don't write to a separate output directory.
189 _base_board_dict: Last-summarised Dict of boards
190 _base_err_lines: Last-summarised list of errors
191 _base_warn_lines: Last-summarised list of warnings
192 _build_period_us: Time taken for a single build (float object).
193 _complete_delay: Expected delay until completion (timedelta)
194 _next_delay_update: Next time we plan to display a progress update
196 _show_unknown: Show unknown boards (those not built) in summary
197 _timestamps: List of timestamps for the completion of the last
198 last _timestamp_count builds. Each is a datetime object.
199 _timestamp_count: Number of timestamps to keep in our list.
200 _working_dir: Base working directory containing all threads
203 """Records a build outcome for a single make invocation
206 rc: Outcome value (OUTCOME_...)
207 err_lines: List of error lines or [] if none
208 sizes: Dictionary of image size information, keyed by filename
209 - Each value is itself a dictionary containing
210 values for 'text', 'data' and 'bss', being the integer
211 size in bytes of each section.
212 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
213 value is itself a dictionary:
215 value: Size of function in bytes
216 config: Dictionary keyed by filename - e.g. '.config'. Each
217 value is itself a dictionary:
220 environment: Dictionary keyed by environment variable, Each
221 value is the value of environment variable.
223 def __init__(self, rc, err_lines, sizes, func_sizes, config,
226 self.err_lines = err_lines
228 self.func_sizes = func_sizes
230 self.environment = environment
232 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
233 gnu_make='make', checkout=True, show_unknown=True, step=1,
234 no_subdirs=False, full_path=False, verbose_build=False,
235 incremental=False, per_board_out_dir=False,
236 config_only=False, squash_config_y=False,
237 warnings_as_errors=False, work_in_output=False):
238 """Create a new Builder object
241 toolchains: Toolchains object to use for building
242 base_dir: Base directory to use for builder
243 git_dir: Git directory containing source repository
244 num_threads: Number of builder threads to run
245 num_jobs: Number of jobs to run at once (passed to make as -j)
246 gnu_make: the command name of GNU Make.
247 checkout: True to check out source, False to skip that step.
248 This is used for testing.
249 show_unknown: Show unknown boards (those not built) in summary
250 step: 1 to process every commit, n to process every nth commit
251 no_subdirs: Don't create subdirectories when building current
252 source for a single board
253 full_path: Return the full path in CROSS_COMPILE and don't set
255 verbose_build: Run build with V=1 and don't use 'make -s'
256 incremental: Always perform incremental builds; don't run make
257 mrproper when configuring
258 per_board_out_dir: Build in a separate persistent directory per
259 board rather than a thread-specific directory
260 config_only: Only configure each build, don't build it
261 squash_config_y: Convert CONFIG options with the value 'y' to '1'
262 warnings_as_errors: Treat all compiler warnings as errors
263 work_in_output: Use the output directory as the work directory and
264 don't write to a separate output directory.
266 self.toolchains = toolchains
267 self.base_dir = base_dir
269 self._working_dir = base_dir
271 self._working_dir = os.path.join(base_dir, '.bm-work')
273 self.do_make = self.Make
274 self.gnu_make = gnu_make
275 self.checkout = checkout
276 self.num_threads = num_threads
277 self.num_jobs = num_jobs
278 self.already_done = 0
279 self.force_build = False
280 self.git_dir = git_dir
281 self._show_unknown = show_unknown
282 self._timestamp_count = 10
283 self._build_period_us = None
284 self._complete_delay = None
285 self._next_delay_update = datetime.now()
286 self.force_config_on_failure = True
287 self.force_build_failures = False
288 self.force_reconfig = False
291 self._error_lines = 0
292 self.no_subdirs = no_subdirs
293 self.full_path = full_path
294 self.verbose_build = verbose_build
295 self.config_only = config_only
296 self.squash_config_y = squash_config_y
297 self.config_filenames = BASE_CONFIG_FILENAMES
298 self.work_in_output = work_in_output
299 if not self.squash_config_y:
300 self.config_filenames += EXTRA_CONFIG_FILENAMES
302 self.warnings_as_errors = warnings_as_errors
303 self.col = terminal.Color()
305 self._re_function = re.compile('(.*): In function.*')
306 self._re_files = re.compile('In file included from.*')
307 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
308 self._re_dtb_warning = re.compile('(.*): Warning .*')
309 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
311 self.queue = queue.Queue()
312 self.out_queue = queue.Queue()
313 for i in range(self.num_threads):
314 t = builderthread.BuilderThread(self, i, incremental,
318 self.threads.append(t)
320 self.last_line_len = 0
321 t = builderthread.ResultThread(self)
324 self.threads.append(t)
326 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
327 self.re_make_err = re.compile('|'.join(ignore_lines))
329 # Handle existing graceful with SIGINT / Ctrl-C
330 signal.signal(signal.SIGINT, self.signal_handler)
333 """Get rid of all threads created by the builder"""
334 for t in self.threads:
337 def signal_handler(self, signal, frame):
340 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
341 show_detail=False, show_bloat=False,
342 list_error_boards=False, show_config=False,
343 show_environment=False):
344 """Setup display options for the builder.
346 show_errors: True to show summarised error/warning info
347 show_sizes: Show size deltas
348 show_detail: Show size delta detail for each board if show_sizes
349 show_bloat: Show detail for each function
350 list_error_boards: Show the boards which caused each error/warning
351 show_config: Show config deltas
352 show_environment: Show environment deltas
354 self._show_errors = show_errors
355 self._show_sizes = show_sizes
356 self._show_detail = show_detail
357 self._show_bloat = show_bloat
358 self._list_error_boards = list_error_boards
359 self._show_config = show_config
360 self._show_environment = show_environment
362 def _AddTimestamp(self):
363 """Add a new timestamp to the list and record the build period.
365 The build period is the length of time taken to perform a single
366 build (one board, one commit).
369 self._timestamps.append(now)
370 count = len(self._timestamps)
371 delta = self._timestamps[-1] - self._timestamps[0]
372 seconds = delta.total_seconds()
374 # If we have enough data, estimate build period (time taken for a
375 # single build) and therefore completion time.
376 if count > 1 and self._next_delay_update < now:
377 self._next_delay_update = now + timedelta(seconds=2)
379 self._build_period = float(seconds) / count
380 todo = self.count - self.upto
381 self._complete_delay = timedelta(microseconds=
382 self._build_period * todo * 1000000)
384 self._complete_delay -= timedelta(
385 microseconds=self._complete_delay.microseconds)
388 self._timestamps.popleft()
391 def ClearLine(self, length):
392 """Clear any characters on the current line
394 Make way for a new line of length 'length', by outputting enough
395 spaces to clear out the old line. Then remember the new length for
399 length: Length of new line, in characters
401 if length < self.last_line_len:
402 Print(' ' * (self.last_line_len - length), newline=False)
403 Print('\r', newline=False)
404 self.last_line_len = length
407 def SelectCommit(self, commit, checkout=True):
408 """Checkout the selected commit for this build
411 if checkout and self.checkout:
412 gitutil.Checkout(commit.hash)
414 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
418 commit: Commit object that is being built
419 brd: Board object that is being built
420 stage: Stage that we are at (mrproper, config, build)
421 cwd: Directory where make should be run
422 args: Arguments to pass to make
423 kwargs: Arguments to pass to command.RunPipe()
425 cmd = [self.gnu_make] + list(args)
426 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
427 cwd=cwd, raise_on_error=False, infile='/dev/null', **kwargs)
428 if self.verbose_build:
429 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
430 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
433 def ProcessResult(self, result):
434 """Process the result of a build, showing progress information
437 result: A CommandResult object, which indicates the result for
440 col = terminal.Color()
442 target = result.brd.target
445 if result.return_code != 0:
449 if result.already_done:
450 self.already_done += 1
452 Print('\r', newline=False)
454 boards_selected = {target : result.brd}
455 self.ResetResultSummary(boards_selected)
456 self.ProduceResultSummary(result.commit_upto, self.commits,
459 target = '(starting)'
461 # Display separate counts for ok, warned and fail
462 ok = self.upto - self.warned - self.fail
463 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
464 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
465 line += self.col.Color(self.col.RED, '%5d' % self.fail)
467 name = ' /%-5d ' % self.count
469 # Add our current completion time estimate
471 if self._complete_delay:
472 name += '%s : ' % self._complete_delay
473 # When building all boards for a commit, we can print a commit
475 if result and result.commit_upto is None:
476 name += 'commit %2d/%-3d' % (self.commit_upto + 1,
480 Print(line + name, newline=False)
481 length = 16 + len(name)
482 self.ClearLine(length)
484 def _GetOutputDir(self, commit_upto):
485 """Get the name of the output directory for a commit number
487 The output directory is typically .../<branch>/<commit>.
490 commit_upto: Commit number to use (0..self.count-1)
494 commit = self.commits[commit_upto]
495 subject = commit.subject.translate(trans_valid_chars)
496 # See _GetOutputSpaceRemovals() which parses this name
497 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
498 self.commit_count, commit.hash, subject[:20]))
499 elif not self.no_subdirs:
500 commit_dir = 'current'
503 return os.path.join(self.base_dir, commit_dir)
505 def GetBuildDir(self, commit_upto, target):
506 """Get the name of the build directory for a commit number
508 The build directory is typically .../<branch>/<commit>/<target>.
511 commit_upto: Commit number to use (0..self.count-1)
514 output_dir = self._GetOutputDir(commit_upto)
515 return os.path.join(output_dir, target)
517 def GetDoneFile(self, commit_upto, target):
518 """Get the name of the done file for a commit number
521 commit_upto: Commit number to use (0..self.count-1)
524 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
526 def GetSizesFile(self, commit_upto, target):
527 """Get the name of the sizes file for a commit number
530 commit_upto: Commit number to use (0..self.count-1)
533 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
535 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
536 """Get the name of the funcsizes file for a commit number and ELF file
539 commit_upto: Commit number to use (0..self.count-1)
541 elf_fname: Filename of elf image
543 return os.path.join(self.GetBuildDir(commit_upto, target),
544 '%s.sizes' % elf_fname.replace('/', '-'))
546 def GetObjdumpFile(self, commit_upto, target, elf_fname):
547 """Get the name of the objdump file for a commit number and ELF file
550 commit_upto: Commit number to use (0..self.count-1)
552 elf_fname: Filename of elf image
554 return os.path.join(self.GetBuildDir(commit_upto, target),
555 '%s.objdump' % elf_fname.replace('/', '-'))
557 def GetErrFile(self, commit_upto, target):
558 """Get the name of the err file for a commit number
561 commit_upto: Commit number to use (0..self.count-1)
564 output_dir = self.GetBuildDir(commit_upto, target)
565 return os.path.join(output_dir, 'err')
567 def FilterErrors(self, lines):
568 """Filter out errors in which we have no interest
570 We should probably use map().
573 lines: List of error lines, each a string
575 New list with only interesting lines included
579 if not self.re_make_err.search(line):
580 out_lines.append(line)
583 def ReadFuncSizes(self, fname, fd):
584 """Read function sizes from the output of 'nm'
587 fd: File containing data to read
588 fname: Filename we are reading from (just for errors)
591 Dictionary containing size of each function in bytes, indexed by
595 for line in fd.readlines():
598 size, type, name = line[:-1].split()
600 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
603 # function names begin with '.' on 64-bit powerpc
605 name = 'static.' + name.split('.')[0]
606 sym[name] = sym.get(name, 0) + int(size, 16)
609 def _ProcessConfig(self, fname):
610 """Read in a .config, autoconf.mk or autoconf.h file
612 This function handles all config file types. It ignores comments and
613 any #defines which don't start with CONFIG_.
616 fname: Filename to read
620 key: Config name (e.g. CONFIG_DM)
621 value: Config value (e.g. 1)
624 if os.path.exists(fname):
625 with open(fname) as fd:
628 if line.startswith('#define'):
629 values = line[8:].split(' ', 1)
634 value = '1' if self.squash_config_y else ''
635 if not key.startswith('CONFIG_'):
637 elif not line or line[0] in ['#', '*', '/']:
640 key, value = line.split('=', 1)
641 if self.squash_config_y and value == 'y':
646 def _ProcessEnvironment(self, fname):
647 """Read in a uboot.env file
649 This function reads in environment variables from a file.
652 fname: Filename to read
656 key: environment variable (e.g. bootlimit)
657 value: value of environment variable (e.g. 1)
660 if os.path.exists(fname):
661 with open(fname) as fd:
662 for line in fd.read().split('\0'):
664 key, value = line.split('=', 1)
665 environment[key] = value
667 # ignore lines we can't parse
671 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
672 read_config, read_environment):
673 """Work out the outcome of a build.
676 commit_upto: Commit number to check (0..n-1)
677 target: Target board to check
678 read_func_sizes: True to read function size information
679 read_config: True to read .config and autoconf.h files
680 read_environment: True to read uboot.env files
685 done_file = self.GetDoneFile(commit_upto, target)
686 sizes_file = self.GetSizesFile(commit_upto, target)
691 if os.path.exists(done_file):
692 with open(done_file, 'r') as fd:
694 return_code = int(fd.readline())
696 # The file may be empty due to running out of disk space.
700 err_file = self.GetErrFile(commit_upto, target)
701 if os.path.exists(err_file):
702 with open(err_file, 'r') as fd:
703 err_lines = self.FilterErrors(fd.readlines())
705 # Decide whether the build was ok, failed or created warnings
713 # Convert size information to our simple format
714 if os.path.exists(sizes_file):
715 with open(sizes_file, 'r') as fd:
716 for line in fd.readlines():
717 values = line.split()
720 rodata = int(values[6], 16)
722 'all' : int(values[0]) + int(values[1]) +
724 'text' : int(values[0]) - rodata,
725 'data' : int(values[1]),
726 'bss' : int(values[2]),
729 sizes[values[5]] = size_dict
732 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
733 for fname in glob.glob(pattern):
734 with open(fname, 'r') as fd:
735 dict_name = os.path.basename(fname).replace('.sizes',
737 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
740 output_dir = self.GetBuildDir(commit_upto, target)
741 for name in self.config_filenames:
742 fname = os.path.join(output_dir, name)
743 config[name] = self._ProcessConfig(fname)
746 output_dir = self.GetBuildDir(commit_upto, target)
747 fname = os.path.join(output_dir, 'uboot.env')
748 environment = self._ProcessEnvironment(fname)
750 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
753 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
755 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
756 read_config, read_environment):
757 """Calculate a summary of the results of building a commit.
760 board_selected: Dict containing boards to summarise
761 commit_upto: Commit number to summarize (0..self.count-1)
762 read_func_sizes: True to read function size information
763 read_config: True to read .config and autoconf.h files
764 read_environment: True to read uboot.env files
768 Dict containing boards which passed building this commit.
769 keyed by board.target
770 List containing a summary of error lines
771 Dict keyed by error line, containing a list of the Board
772 objects with that error
773 List containing a summary of warning lines
774 Dict keyed by error line, containing a list of the Board
775 objects with that warning
776 Dictionary keyed by board.target. Each value is a dictionary:
777 key: filename - e.g. '.config'
778 value is itself a dictionary:
781 Dictionary keyed by board.target. Each value is a dictionary:
782 key: environment variable
783 value: value of environment variable
785 def AddLine(lines_summary, lines_boards, line, board):
787 if line in lines_boards:
788 lines_boards[line].append(board)
790 lines_boards[line] = [board]
791 lines_summary.append(line)
794 err_lines_summary = []
795 err_lines_boards = {}
796 warn_lines_summary = []
797 warn_lines_boards = {}
801 for board in boards_selected.values():
802 outcome = self.GetBuildOutcome(commit_upto, board.target,
803 read_func_sizes, read_config,
805 board_dict[board.target] = outcome
807 last_was_warning = False
808 for line in outcome.err_lines:
810 if (self._re_function.match(line) or
811 self._re_files.match(line)):
814 is_warning = (self._re_warning.match(line) or
815 self._re_dtb_warning.match(line))
816 is_note = self._re_note.match(line)
817 if is_warning or (last_was_warning and is_note):
819 AddLine(warn_lines_summary, warn_lines_boards,
821 AddLine(warn_lines_summary, warn_lines_boards,
825 AddLine(err_lines_summary, err_lines_boards,
827 AddLine(err_lines_summary, err_lines_boards,
829 last_was_warning = is_warning
831 tconfig = Config(self.config_filenames, board.target)
832 for fname in self.config_filenames:
834 for key, value in outcome.config[fname].items():
835 tconfig.Add(fname, key, value)
836 config[board.target] = tconfig
838 tenvironment = Environment(board.target)
839 if outcome.environment:
840 for key, value in outcome.environment.items():
841 tenvironment.Add(key, value)
842 environment[board.target] = tenvironment
844 return (board_dict, err_lines_summary, err_lines_boards,
845 warn_lines_summary, warn_lines_boards, config, environment)
847 def AddOutcome(self, board_dict, arch_list, changes, char, color):
848 """Add an output to our list of outcomes for each architecture
850 This simple function adds failing boards (changes) to the
851 relevant architecture string, so we can print the results out
852 sorted by architecture.
855 board_dict: Dict containing all boards
856 arch_list: Dict keyed by arch name. Value is a string containing
857 a list of board names which failed for that arch.
858 changes: List of boards to add to arch_list
859 color: terminal.Colour object
862 for target in changes:
863 if target in board_dict:
864 arch = board_dict[target].arch
867 str = self.col.Color(color, ' ' + target)
868 if not arch in done_arch:
869 str = ' %s %s' % (self.col.Color(color, char), str)
870 done_arch[arch] = True
871 if not arch in arch_list:
872 arch_list[arch] = str
874 arch_list[arch] += str
877 def ColourNum(self, num):
878 color = self.col.RED if num > 0 else self.col.GREEN
881 return self.col.Color(color, str(num))
883 def ResetResultSummary(self, board_selected):
884 """Reset the results summary ready for use.
886 Set up the base board list to be all those selected, and set the
887 error lines to empty.
889 Following this, calls to PrintResultSummary() will use this
890 information to work out what has changed.
893 board_selected: Dict containing boards to summarise, keyed by
896 self._base_board_dict = {}
897 for board in board_selected:
898 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
900 self._base_err_lines = []
901 self._base_warn_lines = []
902 self._base_err_line_boards = {}
903 self._base_warn_line_boards = {}
904 self._base_config = None
905 self._base_environment = None
907 def PrintFuncSizeDetail(self, fname, old, new):
908 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
909 delta, common = [], {}
916 if name not in common:
919 delta.append([-old[name], name])
922 if name not in common:
925 delta.append([new[name], name])
928 diff = new.get(name, 0) - old.get(name, 0)
930 grow, up = grow + 1, up + diff
932 shrink, down = shrink + 1, down - diff
933 delta.append([diff, name])
938 args = [add, -remove, grow, -shrink, up, -down, up - down]
939 if max(args) == 0 and min(args) == 0:
941 args = [self.ColourNum(x) for x in args]
943 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
944 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
945 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
947 for diff, name in delta:
949 color = self.col.RED if diff > 0 else self.col.GREEN
950 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
951 old.get(name, '-'), new.get(name,'-'), diff)
952 Print(msg, colour=color)
955 def PrintSizeDetail(self, target_list, show_bloat):
956 """Show details size information for each board
959 target_list: List of targets, each a dict containing:
960 'target': Target name
961 'total_diff': Total difference in bytes across all areas
962 <part_name>: Difference for that part
963 show_bloat: Show detail for each function
965 targets_by_diff = sorted(target_list, reverse=True,
966 key=lambda x: x['_total_diff'])
967 for result in targets_by_diff:
968 printed_target = False
969 for name in sorted(result):
971 if name.startswith('_'):
974 color = self.col.RED if diff > 0 else self.col.GREEN
975 msg = ' %s %+d' % (name, diff)
976 if not printed_target:
977 Print('%10s %-15s:' % ('', result['_target']),
979 printed_target = True
980 Print(msg, colour=color, newline=False)
984 target = result['_target']
985 outcome = result['_outcome']
986 base_outcome = self._base_board_dict[target]
987 for fname in outcome.func_sizes:
988 self.PrintFuncSizeDetail(fname,
989 base_outcome.func_sizes[fname],
990 outcome.func_sizes[fname])
993 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
995 """Print a summary of image sizes broken down by section.
997 The summary takes the form of one line per architecture. The
998 line contains deltas for each of the sections (+ means the section
999 got bigger, - means smaller). The numbers are the average number
1000 of bytes that a board in this section increased by.
1003 powerpc: (622 boards) text -0.0
1004 arm: (285 boards) text -0.0
1005 nds32: (3 boards) text -8.0
1008 board_selected: Dict containing boards to summarise, keyed by
1010 board_dict: Dict containing boards for which we built this
1011 commit, keyed by board.target. The value is an Outcome object.
1012 show_detail: Show size delta detail for each board
1013 show_bloat: Show detail for each function
1018 # Calculate changes in size for different image parts
1019 # The previous sizes are in Board.sizes, for each board
1020 for target in board_dict:
1021 if target not in board_selected:
1023 base_sizes = self._base_board_dict[target].sizes
1024 outcome = board_dict[target]
1025 sizes = outcome.sizes
1027 # Loop through the list of images, creating a dict of size
1028 # changes for each image/part. We end up with something like
1029 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1030 # which means that U-Boot data increased by 5 bytes and SPL
1031 # text decreased by 4.
1032 err = {'_target' : target}
1034 if image in base_sizes:
1035 base_image = base_sizes[image]
1036 # Loop through the text, data, bss parts
1037 for part in sorted(sizes[image]):
1038 diff = sizes[image][part] - base_image[part]
1041 if image == 'u-boot':
1044 name = image + ':' + part
1046 arch = board_selected[target].arch
1047 if not arch in arch_count:
1048 arch_count[arch] = 1
1050 arch_count[arch] += 1
1052 pass # Only add to our list when we have some stats
1053 elif not arch in arch_list:
1054 arch_list[arch] = [err]
1056 arch_list[arch].append(err)
1058 # We now have a list of image size changes sorted by arch
1059 # Print out a summary of these
1060 for arch, target_list in arch_list.items():
1061 # Get total difference for each type
1063 for result in target_list:
1065 for name, diff in result.items():
1066 if name.startswith('_'):
1070 totals[name] += diff
1073 result['_total_diff'] = total
1074 result['_outcome'] = board_dict[result['_target']]
1076 count = len(target_list)
1077 printed_arch = False
1078 for name in sorted(totals):
1081 # Display the average difference in this name for this
1083 avg_diff = float(diff) / count
1084 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1085 msg = ' %s %+1.1f' % (name, avg_diff)
1086 if not printed_arch:
1087 Print('%10s: (for %d/%d boards)' % (arch, count,
1088 arch_count[arch]), newline=False)
1090 Print(msg, colour=color, newline=False)
1095 self.PrintSizeDetail(target_list, show_bloat)
1098 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1099 err_line_boards, warn_lines, warn_line_boards,
1100 config, environment, show_sizes, show_detail,
1101 show_bloat, show_config, show_environment):
1102 """Compare results with the base results and display delta.
1104 Only boards mentioned in board_selected will be considered. This
1105 function is intended to be called repeatedly with the results of
1106 each commit. It therefore shows a 'diff' between what it saw in
1107 the last call and what it sees now.
1110 board_selected: Dict containing boards to summarise, keyed by
1112 board_dict: Dict containing boards for which we built this
1113 commit, keyed by board.target. The value is an Outcome object.
1114 err_lines: A list of errors for this commit, or [] if there is
1115 none, or we don't want to print errors
1116 err_line_boards: Dict keyed by error line, containing a list of
1117 the Board objects with that error
1118 warn_lines: A list of warnings for this commit, or [] if there is
1119 none, or we don't want to print errors
1120 warn_line_boards: Dict keyed by warning line, containing a list of
1121 the Board objects with that warning
1122 config: Dictionary keyed by filename - e.g. '.config'. Each
1123 value is itself a dictionary:
1126 environment: Dictionary keyed by environment variable, Each
1127 value is the value of environment variable.
1128 show_sizes: Show image size deltas
1129 show_detail: Show size delta detail for each board if show_sizes
1130 show_bloat: Show detail for each function
1131 show_config: Show config changes
1132 show_environment: Show environment changes
1134 def _BoardList(line, line_boards):
1135 """Helper function to get a line of boards containing a line
1138 line: Error line to search for
1139 line_boards: boards to search, each a Board
1141 List of boards with that error line, or [] if the user has not
1142 requested such a list
1146 if self._list_error_boards:
1147 for board in line_boards[line]:
1148 if not board in board_set:
1149 boards.append(board)
1150 board_set.add(board)
1153 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1155 """Calculate the required output based on changes in errors
1158 base_lines: List of errors/warnings for previous commit
1159 base_line_boards: Dict keyed by error line, containing a list
1160 of the Board objects with that error in the previous commit
1161 lines: List of errors/warning for this commit, each a str
1162 line_boards: Dict keyed by error line, containing a list
1163 of the Board objects with that error in this commit
1164 char: Character representing error ('') or warning ('w'). The
1165 broken ('+') or fixed ('-') characters are added in this
1170 List of ErrLine objects for 'better' lines
1171 List of ErrLine objects for 'worse' lines
1176 if line not in base_lines:
1177 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1179 worse_lines.append(errline)
1180 for line in base_lines:
1181 if line not in lines:
1182 errline = ErrLine(char + '-',
1183 _BoardList(line, base_line_boards), line)
1184 better_lines.append(errline)
1185 return better_lines, worse_lines
1187 def _CalcConfig(delta, name, config):
1188 """Calculate configuration changes
1191 delta: Type of the delta, e.g. '+'
1192 name: name of the file which changed (e.g. .config)
1193 config: configuration change dictionary
1197 String containing the configuration changes which can be
1201 for key in sorted(config.keys()):
1202 out += '%s=%s ' % (key, config[key])
1203 return '%s %s: %s' % (delta, name, out)
1205 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1206 """Add changes in configuration to a list
1209 lines: list to add to
1210 name: config file name
1211 config_plus: configurations added, dictionary
1214 config_minus: configurations removed, dictionary
1217 config_change: configurations changed, dictionary
1222 lines.append(_CalcConfig('+', name, config_plus))
1224 lines.append(_CalcConfig('-', name, config_minus))
1226 lines.append(_CalcConfig('c', name, config_change))
1228 def _OutputConfigInfo(lines):
1233 col = self.col.GREEN
1234 elif line[0] == '-':
1236 elif line[0] == 'c':
1237 col = self.col.YELLOW
1238 Print(' ' + line, newline=True, colour=col)
1240 def _OutputErrLines(err_lines, colour):
1241 """Output the line of error/warning lines, if not empty
1243 Also increments self._error_lines if err_lines not empty
1246 err_lines: List of ErrLine objects, each an error or warning
1247 line, possibly including a list of boards with that
1249 colour: Colour to use for output
1253 for line in err_lines:
1255 names = [board.target for board in line.boards]
1256 board_str = ','.join(names) if names else ''
1258 out = self.col.Color(colour, line.char + '(')
1259 out += self.col.Color(self.col.MAGENTA, board_str,
1261 out += self.col.Color(colour, ') %s' % line.errline)
1263 out = self.col.Color(colour, line.char + line.errline)
1264 out_list.append(out)
1265 Print('\n'.join(out_list))
1266 self._error_lines += 1
1269 ok_boards = [] # List of boards fixed since last commit
1270 warn_boards = [] # List of boards with warnings since last commit
1271 err_boards = [] # List of new broken boards since last commit
1272 new_boards = [] # List of boards that didn't exist last time
1273 unknown_boards = [] # List of boards that were not built
1275 for target in board_dict:
1276 if target not in board_selected:
1279 # If the board was built last time, add its outcome to a list
1280 if target in self._base_board_dict:
1281 base_outcome = self._base_board_dict[target].rc
1282 outcome = board_dict[target]
1283 if outcome.rc == OUTCOME_UNKNOWN:
1284 unknown_boards.append(target)
1285 elif outcome.rc < base_outcome:
1286 if outcome.rc == OUTCOME_WARNING:
1287 warn_boards.append(target)
1289 ok_boards.append(target)
1290 elif outcome.rc > base_outcome:
1291 if outcome.rc == OUTCOME_WARNING:
1292 warn_boards.append(target)
1294 err_boards.append(target)
1296 new_boards.append(target)
1298 # Get a list of errors and warnings that have appeared, and disappeared
1299 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1300 self._base_err_line_boards, err_lines, err_line_boards, '')
1301 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1302 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1304 # Display results by arch
1305 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1306 worse_err, better_err, worse_warn, better_warn)):
1308 self.AddOutcome(board_selected, arch_list, ok_boards, '',
1310 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1312 self.AddOutcome(board_selected, arch_list, err_boards, '+',
1314 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1315 if self._show_unknown:
1316 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
1318 for arch, target_list in arch_list.items():
1319 Print('%10s: %s' % (arch, target_list))
1320 self._error_lines += 1
1321 _OutputErrLines(better_err, colour=self.col.GREEN)
1322 _OutputErrLines(worse_err, colour=self.col.RED)
1323 _OutputErrLines(better_warn, colour=self.col.CYAN)
1324 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
1327 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1330 if show_environment and self._base_environment:
1333 for target in board_dict:
1334 if target not in board_selected:
1337 tbase = self._base_environment[target]
1338 tenvironment = environment[target]
1339 environment_plus = {}
1340 environment_minus = {}
1341 environment_change = {}
1342 base = tbase.environment
1343 for key, value in tenvironment.environment.items():
1345 environment_plus[key] = value
1346 for key, value in base.items():
1347 if key not in tenvironment.environment:
1348 environment_minus[key] = value
1349 for key, value in base.items():
1350 new_value = tenvironment.environment.get(key)
1351 if new_value and value != new_value:
1352 desc = '%s -> %s' % (value, new_value)
1353 environment_change[key] = desc
1355 _AddConfig(lines, target, environment_plus, environment_minus,
1358 _OutputConfigInfo(lines)
1360 if show_config and self._base_config:
1362 arch_config_plus = {}
1363 arch_config_minus = {}
1364 arch_config_change = {}
1367 for target in board_dict:
1368 if target not in board_selected:
1370 arch = board_selected[target].arch
1371 if arch not in arch_list:
1372 arch_list.append(arch)
1374 for arch in arch_list:
1375 arch_config_plus[arch] = {}
1376 arch_config_minus[arch] = {}
1377 arch_config_change[arch] = {}
1378 for name in self.config_filenames:
1379 arch_config_plus[arch][name] = {}
1380 arch_config_minus[arch][name] = {}
1381 arch_config_change[arch][name] = {}
1383 for target in board_dict:
1384 if target not in board_selected:
1387 arch = board_selected[target].arch
1389 all_config_plus = {}
1390 all_config_minus = {}
1391 all_config_change = {}
1392 tbase = self._base_config[target]
1393 tconfig = config[target]
1395 for name in self.config_filenames:
1396 if not tconfig.config[name]:
1401 base = tbase.config[name]
1402 for key, value in tconfig.config[name].items():
1404 config_plus[key] = value
1405 all_config_plus[key] = value
1406 for key, value in base.items():
1407 if key not in tconfig.config[name]:
1408 config_minus[key] = value
1409 all_config_minus[key] = value
1410 for key, value in base.items():
1411 new_value = tconfig.config.get(key)
1412 if new_value and value != new_value:
1413 desc = '%s -> %s' % (value, new_value)
1414 config_change[key] = desc
1415 all_config_change[key] = desc
1417 arch_config_plus[arch][name].update(config_plus)
1418 arch_config_minus[arch][name].update(config_minus)
1419 arch_config_change[arch][name].update(config_change)
1421 _AddConfig(lines, name, config_plus, config_minus,
1423 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1425 summary[target] = '\n'.join(lines)
1427 lines_by_target = {}
1428 for target, lines in summary.items():
1429 if lines in lines_by_target:
1430 lines_by_target[lines].append(target)
1432 lines_by_target[lines] = [target]
1434 for arch in arch_list:
1439 for name in self.config_filenames:
1440 all_plus.update(arch_config_plus[arch][name])
1441 all_minus.update(arch_config_minus[arch][name])
1442 all_change.update(arch_config_change[arch][name])
1443 _AddConfig(lines, name, arch_config_plus[arch][name],
1444 arch_config_minus[arch][name],
1445 arch_config_change[arch][name])
1446 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1447 #arch_summary[target] = '\n'.join(lines)
1450 _OutputConfigInfo(lines)
1452 for lines, targets in lines_by_target.items():
1455 Print('%s :' % ' '.join(sorted(targets)))
1456 _OutputConfigInfo(lines.split('\n'))
1459 # Save our updated information for the next call to this function
1460 self._base_board_dict = board_dict
1461 self._base_err_lines = err_lines
1462 self._base_warn_lines = warn_lines
1463 self._base_err_line_boards = err_line_boards
1464 self._base_warn_line_boards = warn_line_boards
1465 self._base_config = config
1466 self._base_environment = environment
1468 # Get a list of boards that did not get built, if needed
1470 for board in board_selected:
1471 if not board in board_dict:
1472 not_built.append(board)
1474 Print("Boards not built (%d): %s" % (len(not_built),
1475 ', '.join(not_built)))
1477 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1478 (board_dict, err_lines, err_line_boards, warn_lines,
1479 warn_line_boards, config, environment) = self.GetResultSummary(
1480 board_selected, commit_upto,
1481 read_func_sizes=self._show_bloat,
1482 read_config=self._show_config,
1483 read_environment=self._show_environment)
1485 msg = '%02d: %s' % (commit_upto + 1,
1486 commits[commit_upto].subject)
1487 Print(msg, colour=self.col.BLUE)
1488 self.PrintResultSummary(board_selected, board_dict,
1489 err_lines if self._show_errors else [], err_line_boards,
1490 warn_lines if self._show_errors else [], warn_line_boards,
1491 config, environment, self._show_sizes, self._show_detail,
1492 self._show_bloat, self._show_config, self._show_environment)
1494 def ShowSummary(self, commits, board_selected):
1495 """Show a build summary for U-Boot for a given board list.
1497 Reset the result summary, then repeatedly call GetResultSummary on
1498 each commit's results, then display the differences we see.
1501 commit: Commit objects to summarise
1502 board_selected: Dict containing boards to summarise
1504 self.commit_count = len(commits) if commits else 1
1505 self.commits = commits
1506 self.ResetResultSummary(board_selected)
1507 self._error_lines = 0
1509 for commit_upto in range(0, self.commit_count, self._step):
1510 self.ProduceResultSummary(commit_upto, commits, board_selected)
1511 if not self._error_lines:
1512 Print('(no errors to report)', colour=self.col.GREEN)
1515 def SetupBuild(self, board_selected, commits):
1516 """Set up ready to start a build.
1519 board_selected: Selected boards to build
1520 commits: Selected commits to build
1522 # First work out how many commits we will build
1523 count = (self.commit_count + self._step - 1) // self._step
1524 self.count = len(board_selected) * count
1525 self.upto = self.warned = self.fail = 0
1526 self._timestamps = collections.deque()
1528 def GetThreadDir(self, thread_num):
1529 """Get the directory path to the working dir for a thread.
1532 thread_num: Number of thread to check.
1534 if self.work_in_output:
1535 return self._working_dir
1536 return os.path.join(self._working_dir, '%02d' % thread_num)
1538 def _PrepareThread(self, thread_num, setup_git):
1539 """Prepare the working directory for a thread.
1541 This clones or fetches the repo into the thread's work directory.
1544 thread_num: Thread number (0, 1, ...)
1545 setup_git: True to set up a git repo clone
1547 thread_dir = self.GetThreadDir(thread_num)
1548 builderthread.Mkdir(thread_dir)
1549 git_dir = os.path.join(thread_dir, '.git')
1551 # Clone the repo if it doesn't already exist
1552 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1553 # we have a private index but uses the origin repo's contents?
1554 if setup_git and self.git_dir:
1555 src_dir = os.path.abspath(self.git_dir)
1556 if os.path.exists(git_dir):
1557 gitutil.Fetch(git_dir, thread_dir)
1559 Print('\rCloning repo for thread %d' % thread_num,
1561 gitutil.Clone(src_dir, thread_dir)
1562 Print('\r%s\r' % (' ' * 30), newline=False)
1564 def _PrepareWorkingSpace(self, max_threads, setup_git):
1565 """Prepare the working directory for use.
1567 Set up the git repo for each thread.
1570 max_threads: Maximum number of threads we expect to need.
1571 setup_git: True to set up a git repo clone
1573 builderthread.Mkdir(self._working_dir)
1574 for thread in range(max_threads):
1575 self._PrepareThread(thread, setup_git)
1577 def _GetOutputSpaceRemovals(self):
1578 """Get the output directories ready to receive files.
1580 Figure out what needs to be deleted in the output directory before it
1581 can be used. We only delete old buildman directories which have the
1582 expected name pattern. See _GetOutputDir().
1585 List of full paths of directories to remove
1587 if not self.commits:
1590 for commit_upto in range(self.commit_count):
1591 dir_list.append(self._GetOutputDir(commit_upto))
1594 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1595 if dirname not in dir_list:
1596 leaf = dirname[len(self.base_dir) + 1:]
1597 m = re.match('[0-9]+_of_[0-9]+_g[0-9a-f]+_.*', leaf)
1599 to_remove.append(dirname)
1602 def _PrepareOutputSpace(self):
1603 """Get the output directories ready to receive files.
1605 We delete any output directories which look like ones we need to
1606 create. Having left over directories is confusing when the user wants
1607 to check the output manually.
1609 to_remove = self._GetOutputSpaceRemovals()
1611 Print('Removing %d old build directories...' % len(to_remove),
1613 for dirname in to_remove:
1614 shutil.rmtree(dirname)
1617 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1618 """Build all commits for a list of boards
1621 commits: List of commits to be build, each a Commit object
1622 boards_selected: Dict of selected boards, key is target name,
1623 value is Board object
1624 keep_outputs: True to save build output files
1625 verbose: Display build results as they are completed
1628 - number of boards that failed to build
1629 - number of boards that issued warnings
1631 self.commit_count = len(commits) if commits else 1
1632 self.commits = commits
1633 self._verbose = verbose
1635 self.ResetResultSummary(board_selected)
1636 builderthread.Mkdir(self.base_dir, parents = True)
1637 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1638 commits is not None)
1639 self._PrepareOutputSpace()
1640 Print('\rStarting build...', newline=False)
1641 self.SetupBuild(board_selected, commits)
1642 self.ProcessResult(None)
1644 # Create jobs to build all commits for each board
1645 for brd in board_selected.values():
1646 job = builderthread.BuilderJob()
1648 job.commits = commits
1649 job.keep_outputs = keep_outputs
1650 job.work_in_output = self.work_in_output
1651 job.step = self._step
1654 term = threading.Thread(target=self.queue.join)
1655 term.setDaemon(True)
1657 while term.isAlive():
1660 # Wait until we have processed all output
1661 self.out_queue.join()
1664 return (self.fail, self.warned)