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 num_jobs: Number of jobs to run at once (passed to make as -j)
166 num_threads: Number of builder threads to run
167 out_queue: Queue of results to process
168 re_make_err: Compiled regular expression for ignore_lines
169 queue: Queue of jobs to run
170 threads: List of active threads
171 toolchains: Toolchains object to use for building
172 upto: Current commit number we are building (0.count-1)
173 warned: Number of builds that produced at least one warning
174 force_reconfig: Reconfigure U-Boot on each comiit. This disables
175 incremental building, where buildman reconfigures on the first
176 commit for a baord, and then just does an incremental build for
177 the following commits. In fact buildman will reconfigure and
178 retry for any failing commits, so generally the only effect of
179 this option is to slow things down.
180 in_tree: Build U-Boot in-tree instead of specifying an output
181 directory separate from the source code. This option is really
182 only useful for testing in-tree builds.
183 work_in_output: Use the output directory as the work directory and
184 don't write to a separate output directory.
187 _base_board_dict: Last-summarised Dict of boards
188 _base_err_lines: Last-summarised list of errors
189 _base_warn_lines: Last-summarised list of warnings
190 _build_period_us: Time taken for a single build (float object).
191 _complete_delay: Expected delay until completion (timedelta)
192 _next_delay_update: Next time we plan to display a progress update
194 _show_unknown: Show unknown boards (those not built) in summary
195 _start_time: Start time for the build
196 _timestamps: List of timestamps for the completion of the last
197 last _timestamp_count builds. Each is a datetime object.
198 _timestamp_count: Number of timestamps to keep in our list.
199 _working_dir: Base working directory containing all threads
202 """Records a build outcome for a single make invocation
205 rc: Outcome value (OUTCOME_...)
206 err_lines: List of error lines or [] if none
207 sizes: Dictionary of image size information, keyed by filename
208 - Each value is itself a dictionary containing
209 values for 'text', 'data' and 'bss', being the integer
210 size in bytes of each section.
211 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
212 value is itself a dictionary:
214 value: Size of function in bytes
215 config: Dictionary keyed by filename - e.g. '.config'. Each
216 value is itself a dictionary:
219 environment: Dictionary keyed by environment variable, Each
220 value is the value of environment variable.
222 def __init__(self, rc, err_lines, sizes, func_sizes, config,
225 self.err_lines = err_lines
227 self.func_sizes = func_sizes
229 self.environment = environment
231 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
232 gnu_make='make', checkout=True, show_unknown=True, step=1,
233 no_subdirs=False, full_path=False, verbose_build=False,
234 mrproper=False, per_board_out_dir=False,
235 config_only=False, squash_config_y=False,
236 warnings_as_errors=False, work_in_output=False):
237 """Create a new Builder object
240 toolchains: Toolchains object to use for building
241 base_dir: Base directory to use for builder
242 git_dir: Git directory containing source repository
243 num_threads: Number of builder threads to run
244 num_jobs: Number of jobs to run at once (passed to make as -j)
245 gnu_make: the command name of GNU Make.
246 checkout: True to check out source, False to skip that step.
247 This is used for testing.
248 show_unknown: Show unknown boards (those not built) in summary
249 step: 1 to process every commit, n to process every nth commit
250 no_subdirs: Don't create subdirectories when building current
251 source for a single board
252 full_path: Return the full path in CROSS_COMPILE and don't set
254 verbose_build: Run build with V=1 and don't use 'make -s'
255 mrproper: Always run 'make mrproper' when configuring
256 per_board_out_dir: Build in a separate persistent directory per
257 board rather than a thread-specific directory
258 config_only: Only configure each build, don't build it
259 squash_config_y: Convert CONFIG options with the value 'y' to '1'
260 warnings_as_errors: Treat all compiler warnings as errors
261 work_in_output: Use the output directory as the work directory and
262 don't write to a separate output directory.
264 self.toolchains = toolchains
265 self.base_dir = base_dir
267 self._working_dir = base_dir
269 self._working_dir = os.path.join(base_dir, '.bm-work')
271 self.do_make = self.Make
272 self.gnu_make = gnu_make
273 self.checkout = checkout
274 self.num_threads = num_threads
275 self.num_jobs = num_jobs
276 self.already_done = 0
277 self.force_build = False
278 self.git_dir = git_dir
279 self._show_unknown = show_unknown
280 self._timestamp_count = 10
281 self._build_period_us = None
282 self._complete_delay = None
283 self._next_delay_update = datetime.now()
284 self._start_time = datetime.now()
285 self.force_config_on_failure = True
286 self.force_build_failures = False
287 self.force_reconfig = False
290 self._error_lines = 0
291 self.no_subdirs = no_subdirs
292 self.full_path = full_path
293 self.verbose_build = verbose_build
294 self.config_only = config_only
295 self.squash_config_y = squash_config_y
296 self.config_filenames = BASE_CONFIG_FILENAMES
297 self.work_in_output = work_in_output
298 if not self.squash_config_y:
299 self.config_filenames += EXTRA_CONFIG_FILENAMES
301 self.warnings_as_errors = warnings_as_errors
302 self.col = terminal.Color()
304 self._re_function = re.compile('(.*): In function.*')
305 self._re_files = re.compile('In file included from.*')
306 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
307 self._re_dtb_warning = re.compile('(.*): Warning .*')
308 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
309 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
310 re.MULTILINE | re.DOTALL)
312 self.queue = queue.Queue()
313 self.out_queue = queue.Queue()
314 for i in range(self.num_threads):
315 t = builderthread.BuilderThread(self, i, mrproper,
319 self.threads.append(t)
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, filter_dtb_warnings=False,
344 filter_migration_warnings=False):
345 """Setup display options for the builder.
348 show_errors: True to show summarised error/warning info
349 show_sizes: Show size deltas
350 show_detail: Show size delta detail for each board if show_sizes
351 show_bloat: Show detail for each function
352 list_error_boards: Show the boards which caused each error/warning
353 show_config: Show config deltas
354 show_environment: Show environment deltas
355 filter_dtb_warnings: Filter out any warnings from the device-tree
357 filter_migration_warnings: Filter out any warnings about migrating
358 a board to driver model
360 self._show_errors = show_errors
361 self._show_sizes = show_sizes
362 self._show_detail = show_detail
363 self._show_bloat = show_bloat
364 self._list_error_boards = list_error_boards
365 self._show_config = show_config
366 self._show_environment = show_environment
367 self._filter_dtb_warnings = filter_dtb_warnings
368 self._filter_migration_warnings = filter_migration_warnings
370 def _AddTimestamp(self):
371 """Add a new timestamp to the list and record the build period.
373 The build period is the length of time taken to perform a single
374 build (one board, one commit).
377 self._timestamps.append(now)
378 count = len(self._timestamps)
379 delta = self._timestamps[-1] - self._timestamps[0]
380 seconds = delta.total_seconds()
382 # If we have enough data, estimate build period (time taken for a
383 # single build) and therefore completion time.
384 if count > 1 and self._next_delay_update < now:
385 self._next_delay_update = now + timedelta(seconds=2)
387 self._build_period = float(seconds) / count
388 todo = self.count - self.upto
389 self._complete_delay = timedelta(microseconds=
390 self._build_period * todo * 1000000)
392 self._complete_delay -= timedelta(
393 microseconds=self._complete_delay.microseconds)
396 self._timestamps.popleft()
399 def SelectCommit(self, commit, checkout=True):
400 """Checkout the selected commit for this build
403 if checkout and self.checkout:
404 gitutil.Checkout(commit.hash)
406 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
410 commit: Commit object that is being built
411 brd: Board object that is being built
412 stage: Stage that we are at (mrproper, config, build)
413 cwd: Directory where make should be run
414 args: Arguments to pass to make
415 kwargs: Arguments to pass to command.RunPipe()
417 cmd = [self.gnu_make] + list(args)
418 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
419 cwd=cwd, raise_on_error=False, infile='/dev/null', **kwargs)
420 if self.verbose_build:
421 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
422 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
425 def ProcessResult(self, result):
426 """Process the result of a build, showing progress information
429 result: A CommandResult object, which indicates the result for
432 col = terminal.Color()
434 target = result.brd.target
437 if result.return_code != 0:
441 if result.already_done:
442 self.already_done += 1
444 terminal.PrintClear()
445 boards_selected = {target : result.brd}
446 self.ResetResultSummary(boards_selected)
447 self.ProduceResultSummary(result.commit_upto, self.commits,
450 target = '(starting)'
452 # Display separate counts for ok, warned and fail
453 ok = self.upto - self.warned - self.fail
454 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
455 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
456 line += self.col.Color(self.col.RED, '%5d' % self.fail)
458 line += ' /%-5d ' % self.count
459 remaining = self.count - self.upto
461 line += self.col.Color(self.col.MAGENTA, ' -%-5d ' % remaining)
465 # Add our current completion time estimate
467 if self._complete_delay:
468 line += '%s : ' % self._complete_delay
471 terminal.PrintClear()
472 Print(line, newline=False, limit_to_line=True)
474 def _GetOutputDir(self, commit_upto):
475 """Get the name of the output directory for a commit number
477 The output directory is typically .../<branch>/<commit>.
480 commit_upto: Commit number to use (0..self.count-1)
484 commit = self.commits[commit_upto]
485 subject = commit.subject.translate(trans_valid_chars)
486 # See _GetOutputSpaceRemovals() which parses this name
487 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
488 self.commit_count, commit.hash, subject[:20]))
489 elif not self.no_subdirs:
490 commit_dir = 'current'
493 return os.path.join(self.base_dir, commit_dir)
495 def GetBuildDir(self, commit_upto, target):
496 """Get the name of the build directory for a commit number
498 The build directory is typically .../<branch>/<commit>/<target>.
501 commit_upto: Commit number to use (0..self.count-1)
504 output_dir = self._GetOutputDir(commit_upto)
505 return os.path.join(output_dir, target)
507 def GetDoneFile(self, commit_upto, target):
508 """Get the name of the done file for a commit number
511 commit_upto: Commit number to use (0..self.count-1)
514 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
516 def GetSizesFile(self, commit_upto, target):
517 """Get the name of the sizes file for a commit number
520 commit_upto: Commit number to use (0..self.count-1)
523 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
525 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
526 """Get the name of the funcsizes file for a commit number and ELF file
529 commit_upto: Commit number to use (0..self.count-1)
531 elf_fname: Filename of elf image
533 return os.path.join(self.GetBuildDir(commit_upto, target),
534 '%s.sizes' % elf_fname.replace('/', '-'))
536 def GetObjdumpFile(self, commit_upto, target, elf_fname):
537 """Get the name of the objdump file for a commit number and ELF file
540 commit_upto: Commit number to use (0..self.count-1)
542 elf_fname: Filename of elf image
544 return os.path.join(self.GetBuildDir(commit_upto, target),
545 '%s.objdump' % elf_fname.replace('/', '-'))
547 def GetErrFile(self, commit_upto, target):
548 """Get the name of the err file for a commit number
551 commit_upto: Commit number to use (0..self.count-1)
554 output_dir = self.GetBuildDir(commit_upto, target)
555 return os.path.join(output_dir, 'err')
557 def FilterErrors(self, lines):
558 """Filter out errors in which we have no interest
560 We should probably use map().
563 lines: List of error lines, each a string
565 New list with only interesting lines included
568 if self._filter_migration_warnings:
569 text = '\n'.join(lines)
570 text = self._re_migration_warning.sub('', text)
571 lines = text.splitlines()
573 if self.re_make_err.search(line):
575 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
577 out_lines.append(line)
580 def ReadFuncSizes(self, fname, fd):
581 """Read function sizes from the output of 'nm'
584 fd: File containing data to read
585 fname: Filename we are reading from (just for errors)
588 Dictionary containing size of each function in bytes, indexed by
592 for line in fd.readlines():
595 size, type, name = line[:-1].split()
597 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
600 # function names begin with '.' on 64-bit powerpc
602 name = 'static.' + name.split('.')[0]
603 sym[name] = sym.get(name, 0) + int(size, 16)
606 def _ProcessConfig(self, fname):
607 """Read in a .config, autoconf.mk or autoconf.h file
609 This function handles all config file types. It ignores comments and
610 any #defines which don't start with CONFIG_.
613 fname: Filename to read
617 key: Config name (e.g. CONFIG_DM)
618 value: Config value (e.g. 1)
621 if os.path.exists(fname):
622 with open(fname) as fd:
625 if line.startswith('#define'):
626 values = line[8:].split(' ', 1)
631 value = '1' if self.squash_config_y else ''
632 if not key.startswith('CONFIG_'):
634 elif not line or line[0] in ['#', '*', '/']:
637 key, value = line.split('=', 1)
638 if self.squash_config_y and value == 'y':
643 def _ProcessEnvironment(self, fname):
644 """Read in a uboot.env file
646 This function reads in environment variables from a file.
649 fname: Filename to read
653 key: environment variable (e.g. bootlimit)
654 value: value of environment variable (e.g. 1)
657 if os.path.exists(fname):
658 with open(fname) as fd:
659 for line in fd.read().split('\0'):
661 key, value = line.split('=', 1)
662 environment[key] = value
664 # ignore lines we can't parse
668 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
669 read_config, read_environment):
670 """Work out the outcome of a build.
673 commit_upto: Commit number to check (0..n-1)
674 target: Target board to check
675 read_func_sizes: True to read function size information
676 read_config: True to read .config and autoconf.h files
677 read_environment: True to read uboot.env files
682 done_file = self.GetDoneFile(commit_upto, target)
683 sizes_file = self.GetSizesFile(commit_upto, target)
688 if os.path.exists(done_file):
689 with open(done_file, 'r') as fd:
691 return_code = int(fd.readline())
693 # The file may be empty due to running out of disk space.
697 err_file = self.GetErrFile(commit_upto, target)
698 if os.path.exists(err_file):
699 with open(err_file, 'r') as fd:
700 err_lines = self.FilterErrors(fd.readlines())
702 # Decide whether the build was ok, failed or created warnings
710 # Convert size information to our simple format
711 if os.path.exists(sizes_file):
712 with open(sizes_file, 'r') as fd:
713 for line in fd.readlines():
714 values = line.split()
717 rodata = int(values[6], 16)
719 'all' : int(values[0]) + int(values[1]) +
721 'text' : int(values[0]) - rodata,
722 'data' : int(values[1]),
723 'bss' : int(values[2]),
726 sizes[values[5]] = size_dict
729 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
730 for fname in glob.glob(pattern):
731 with open(fname, 'r') as fd:
732 dict_name = os.path.basename(fname).replace('.sizes',
734 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
737 output_dir = self.GetBuildDir(commit_upto, target)
738 for name in self.config_filenames:
739 fname = os.path.join(output_dir, name)
740 config[name] = self._ProcessConfig(fname)
743 output_dir = self.GetBuildDir(commit_upto, target)
744 fname = os.path.join(output_dir, 'uboot.env')
745 environment = self._ProcessEnvironment(fname)
747 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
750 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
752 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
753 read_config, read_environment):
754 """Calculate a summary of the results of building a commit.
757 board_selected: Dict containing boards to summarise
758 commit_upto: Commit number to summarize (0..self.count-1)
759 read_func_sizes: True to read function size information
760 read_config: True to read .config and autoconf.h files
761 read_environment: True to read uboot.env files
765 Dict containing boards which passed building this commit.
766 keyed by board.target
767 List containing a summary of error lines
768 Dict keyed by error line, containing a list of the Board
769 objects with that error
770 List containing a summary of warning lines
771 Dict keyed by error line, containing a list of the Board
772 objects with that warning
773 Dictionary keyed by board.target. Each value is a dictionary:
774 key: filename - e.g. '.config'
775 value is itself a dictionary:
778 Dictionary keyed by board.target. Each value is a dictionary:
779 key: environment variable
780 value: value of environment variable
782 def AddLine(lines_summary, lines_boards, line, board):
784 if line in lines_boards:
785 lines_boards[line].append(board)
787 lines_boards[line] = [board]
788 lines_summary.append(line)
791 err_lines_summary = []
792 err_lines_boards = {}
793 warn_lines_summary = []
794 warn_lines_boards = {}
798 for board in boards_selected.values():
799 outcome = self.GetBuildOutcome(commit_upto, board.target,
800 read_func_sizes, read_config,
802 board_dict[board.target] = outcome
804 last_was_warning = False
805 for line in outcome.err_lines:
807 if (self._re_function.match(line) or
808 self._re_files.match(line)):
811 is_warning = (self._re_warning.match(line) or
812 self._re_dtb_warning.match(line))
813 is_note = self._re_note.match(line)
814 if is_warning or (last_was_warning and is_note):
816 AddLine(warn_lines_summary, warn_lines_boards,
818 AddLine(warn_lines_summary, warn_lines_boards,
822 AddLine(err_lines_summary, err_lines_boards,
824 AddLine(err_lines_summary, err_lines_boards,
826 last_was_warning = is_warning
828 tconfig = Config(self.config_filenames, board.target)
829 for fname in self.config_filenames:
831 for key, value in outcome.config[fname].items():
832 tconfig.Add(fname, key, value)
833 config[board.target] = tconfig
835 tenvironment = Environment(board.target)
836 if outcome.environment:
837 for key, value in outcome.environment.items():
838 tenvironment.Add(key, value)
839 environment[board.target] = tenvironment
841 return (board_dict, err_lines_summary, err_lines_boards,
842 warn_lines_summary, warn_lines_boards, config, environment)
844 def AddOutcome(self, board_dict, arch_list, changes, char, color):
845 """Add an output to our list of outcomes for each architecture
847 This simple function adds failing boards (changes) to the
848 relevant architecture string, so we can print the results out
849 sorted by architecture.
852 board_dict: Dict containing all boards
853 arch_list: Dict keyed by arch name. Value is a string containing
854 a list of board names which failed for that arch.
855 changes: List of boards to add to arch_list
856 color: terminal.Colour object
859 for target in changes:
860 if target in board_dict:
861 arch = board_dict[target].arch
864 str = self.col.Color(color, ' ' + target)
865 if not arch in done_arch:
866 str = ' %s %s' % (self.col.Color(color, char), str)
867 done_arch[arch] = True
868 if not arch in arch_list:
869 arch_list[arch] = str
871 arch_list[arch] += str
874 def ColourNum(self, num):
875 color = self.col.RED if num > 0 else self.col.GREEN
878 return self.col.Color(color, str(num))
880 def ResetResultSummary(self, board_selected):
881 """Reset the results summary ready for use.
883 Set up the base board list to be all those selected, and set the
884 error lines to empty.
886 Following this, calls to PrintResultSummary() will use this
887 information to work out what has changed.
890 board_selected: Dict containing boards to summarise, keyed by
893 self._base_board_dict = {}
894 for board in board_selected:
895 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
897 self._base_err_lines = []
898 self._base_warn_lines = []
899 self._base_err_line_boards = {}
900 self._base_warn_line_boards = {}
901 self._base_config = None
902 self._base_environment = None
904 def PrintFuncSizeDetail(self, fname, old, new):
905 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
906 delta, common = [], {}
913 if name not in common:
916 delta.append([-old[name], name])
919 if name not in common:
922 delta.append([new[name], name])
925 diff = new.get(name, 0) - old.get(name, 0)
927 grow, up = grow + 1, up + diff
929 shrink, down = shrink + 1, down - diff
930 delta.append([diff, name])
935 args = [add, -remove, grow, -shrink, up, -down, up - down]
936 if max(args) == 0 and min(args) == 0:
938 args = [self.ColourNum(x) for x in args]
940 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
941 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
942 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
944 for diff, name in delta:
946 color = self.col.RED if diff > 0 else self.col.GREEN
947 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
948 old.get(name, '-'), new.get(name,'-'), diff)
949 Print(msg, colour=color)
952 def PrintSizeDetail(self, target_list, show_bloat):
953 """Show details size information for each board
956 target_list: List of targets, each a dict containing:
957 'target': Target name
958 'total_diff': Total difference in bytes across all areas
959 <part_name>: Difference for that part
960 show_bloat: Show detail for each function
962 targets_by_diff = sorted(target_list, reverse=True,
963 key=lambda x: x['_total_diff'])
964 for result in targets_by_diff:
965 printed_target = False
966 for name in sorted(result):
968 if name.startswith('_'):
971 color = self.col.RED if diff > 0 else self.col.GREEN
972 msg = ' %s %+d' % (name, diff)
973 if not printed_target:
974 Print('%10s %-15s:' % ('', result['_target']),
976 printed_target = True
977 Print(msg, colour=color, newline=False)
981 target = result['_target']
982 outcome = result['_outcome']
983 base_outcome = self._base_board_dict[target]
984 for fname in outcome.func_sizes:
985 self.PrintFuncSizeDetail(fname,
986 base_outcome.func_sizes[fname],
987 outcome.func_sizes[fname])
990 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
992 """Print a summary of image sizes broken down by section.
994 The summary takes the form of one line per architecture. The
995 line contains deltas for each of the sections (+ means the section
996 got bigger, - means smaller). The numbers are the average number
997 of bytes that a board in this section increased by.
1000 powerpc: (622 boards) text -0.0
1001 arm: (285 boards) text -0.0
1002 nds32: (3 boards) text -8.0
1005 board_selected: Dict containing boards to summarise, keyed by
1007 board_dict: Dict containing boards for which we built this
1008 commit, keyed by board.target. The value is an Outcome object.
1009 show_detail: Show size delta detail for each board
1010 show_bloat: Show detail for each function
1015 # Calculate changes in size for different image parts
1016 # The previous sizes are in Board.sizes, for each board
1017 for target in board_dict:
1018 if target not in board_selected:
1020 base_sizes = self._base_board_dict[target].sizes
1021 outcome = board_dict[target]
1022 sizes = outcome.sizes
1024 # Loop through the list of images, creating a dict of size
1025 # changes for each image/part. We end up with something like
1026 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1027 # which means that U-Boot data increased by 5 bytes and SPL
1028 # text decreased by 4.
1029 err = {'_target' : target}
1031 if image in base_sizes:
1032 base_image = base_sizes[image]
1033 # Loop through the text, data, bss parts
1034 for part in sorted(sizes[image]):
1035 diff = sizes[image][part] - base_image[part]
1038 if image == 'u-boot':
1041 name = image + ':' + part
1043 arch = board_selected[target].arch
1044 if not arch in arch_count:
1045 arch_count[arch] = 1
1047 arch_count[arch] += 1
1049 pass # Only add to our list when we have some stats
1050 elif not arch in arch_list:
1051 arch_list[arch] = [err]
1053 arch_list[arch].append(err)
1055 # We now have a list of image size changes sorted by arch
1056 # Print out a summary of these
1057 for arch, target_list in arch_list.items():
1058 # Get total difference for each type
1060 for result in target_list:
1062 for name, diff in result.items():
1063 if name.startswith('_'):
1067 totals[name] += diff
1070 result['_total_diff'] = total
1071 result['_outcome'] = board_dict[result['_target']]
1073 count = len(target_list)
1074 printed_arch = False
1075 for name in sorted(totals):
1078 # Display the average difference in this name for this
1080 avg_diff = float(diff) / count
1081 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1082 msg = ' %s %+1.1f' % (name, avg_diff)
1083 if not printed_arch:
1084 Print('%10s: (for %d/%d boards)' % (arch, count,
1085 arch_count[arch]), newline=False)
1087 Print(msg, colour=color, newline=False)
1092 self.PrintSizeDetail(target_list, show_bloat)
1095 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1096 err_line_boards, warn_lines, warn_line_boards,
1097 config, environment, show_sizes, show_detail,
1098 show_bloat, show_config, show_environment):
1099 """Compare results with the base results and display delta.
1101 Only boards mentioned in board_selected will be considered. This
1102 function is intended to be called repeatedly with the results of
1103 each commit. It therefore shows a 'diff' between what it saw in
1104 the last call and what it sees now.
1107 board_selected: Dict containing boards to summarise, keyed by
1109 board_dict: Dict containing boards for which we built this
1110 commit, keyed by board.target. The value is an Outcome object.
1111 err_lines: A list of errors for this commit, or [] if there is
1112 none, or we don't want to print errors
1113 err_line_boards: Dict keyed by error line, containing a list of
1114 the Board objects with that error
1115 warn_lines: A list of warnings for this commit, or [] if there is
1116 none, or we don't want to print errors
1117 warn_line_boards: Dict keyed by warning line, containing a list of
1118 the Board objects with that warning
1119 config: Dictionary keyed by filename - e.g. '.config'. Each
1120 value is itself a dictionary:
1123 environment: Dictionary keyed by environment variable, Each
1124 value is the value of environment variable.
1125 show_sizes: Show image size deltas
1126 show_detail: Show size delta detail for each board if show_sizes
1127 show_bloat: Show detail for each function
1128 show_config: Show config changes
1129 show_environment: Show environment changes
1131 def _BoardList(line, line_boards):
1132 """Helper function to get a line of boards containing a line
1135 line: Error line to search for
1136 line_boards: boards to search, each a Board
1138 List of boards with that error line, or [] if the user has not
1139 requested such a list
1143 if self._list_error_boards:
1144 for board in line_boards[line]:
1145 if not board in board_set:
1146 boards.append(board)
1147 board_set.add(board)
1150 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1152 """Calculate the required output based on changes in errors
1155 base_lines: List of errors/warnings for previous commit
1156 base_line_boards: Dict keyed by error line, containing a list
1157 of the Board objects with that error in the previous commit
1158 lines: List of errors/warning for this commit, each a str
1159 line_boards: Dict keyed by error line, containing a list
1160 of the Board objects with that error in this commit
1161 char: Character representing error ('') or warning ('w'). The
1162 broken ('+') or fixed ('-') characters are added in this
1167 List of ErrLine objects for 'better' lines
1168 List of ErrLine objects for 'worse' lines
1173 if line not in base_lines:
1174 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1176 worse_lines.append(errline)
1177 for line in base_lines:
1178 if line not in lines:
1179 errline = ErrLine(char + '-',
1180 _BoardList(line, base_line_boards), line)
1181 better_lines.append(errline)
1182 return better_lines, worse_lines
1184 def _CalcConfig(delta, name, config):
1185 """Calculate configuration changes
1188 delta: Type of the delta, e.g. '+'
1189 name: name of the file which changed (e.g. .config)
1190 config: configuration change dictionary
1194 String containing the configuration changes which can be
1198 for key in sorted(config.keys()):
1199 out += '%s=%s ' % (key, config[key])
1200 return '%s %s: %s' % (delta, name, out)
1202 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1203 """Add changes in configuration to a list
1206 lines: list to add to
1207 name: config file name
1208 config_plus: configurations added, dictionary
1211 config_minus: configurations removed, dictionary
1214 config_change: configurations changed, dictionary
1219 lines.append(_CalcConfig('+', name, config_plus))
1221 lines.append(_CalcConfig('-', name, config_minus))
1223 lines.append(_CalcConfig('c', name, config_change))
1225 def _OutputConfigInfo(lines):
1230 col = self.col.GREEN
1231 elif line[0] == '-':
1233 elif line[0] == 'c':
1234 col = self.col.YELLOW
1235 Print(' ' + line, newline=True, colour=col)
1237 def _OutputErrLines(err_lines, colour):
1238 """Output the line of error/warning lines, if not empty
1240 Also increments self._error_lines if err_lines not empty
1243 err_lines: List of ErrLine objects, each an error or warning
1244 line, possibly including a list of boards with that
1246 colour: Colour to use for output
1250 for line in err_lines:
1252 names = [board.target for board in line.boards]
1253 board_str = ' '.join(names) if names else ''
1255 out = self.col.Color(colour, line.char + '(')
1256 out += self.col.Color(self.col.MAGENTA, board_str,
1258 out += self.col.Color(colour, ') %s' % line.errline)
1260 out = self.col.Color(colour, line.char + line.errline)
1261 out_list.append(out)
1262 Print('\n'.join(out_list))
1263 self._error_lines += 1
1266 ok_boards = [] # List of boards fixed since last commit
1267 warn_boards = [] # List of boards with warnings since last commit
1268 err_boards = [] # List of new broken boards since last commit
1269 new_boards = [] # List of boards that didn't exist last time
1270 unknown_boards = [] # List of boards that were not built
1272 for target in board_dict:
1273 if target not in board_selected:
1276 # If the board was built last time, add its outcome to a list
1277 if target in self._base_board_dict:
1278 base_outcome = self._base_board_dict[target].rc
1279 outcome = board_dict[target]
1280 if outcome.rc == OUTCOME_UNKNOWN:
1281 unknown_boards.append(target)
1282 elif outcome.rc < base_outcome:
1283 if outcome.rc == OUTCOME_WARNING:
1284 warn_boards.append(target)
1286 ok_boards.append(target)
1287 elif outcome.rc > base_outcome:
1288 if outcome.rc == OUTCOME_WARNING:
1289 warn_boards.append(target)
1291 err_boards.append(target)
1293 new_boards.append(target)
1295 # Get a list of errors and warnings that have appeared, and disappeared
1296 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1297 self._base_err_line_boards, err_lines, err_line_boards, '')
1298 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1299 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1301 # Display results by arch
1302 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1303 worse_err, better_err, worse_warn, better_warn)):
1305 self.AddOutcome(board_selected, arch_list, ok_boards, '',
1307 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1309 self.AddOutcome(board_selected, arch_list, err_boards, '+',
1311 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1312 if self._show_unknown:
1313 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
1315 for arch, target_list in arch_list.items():
1316 Print('%10s: %s' % (arch, target_list))
1317 self._error_lines += 1
1318 _OutputErrLines(better_err, colour=self.col.GREEN)
1319 _OutputErrLines(worse_err, colour=self.col.RED)
1320 _OutputErrLines(better_warn, colour=self.col.CYAN)
1321 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
1324 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1327 if show_environment and self._base_environment:
1330 for target in board_dict:
1331 if target not in board_selected:
1334 tbase = self._base_environment[target]
1335 tenvironment = environment[target]
1336 environment_plus = {}
1337 environment_minus = {}
1338 environment_change = {}
1339 base = tbase.environment
1340 for key, value in tenvironment.environment.items():
1342 environment_plus[key] = value
1343 for key, value in base.items():
1344 if key not in tenvironment.environment:
1345 environment_minus[key] = value
1346 for key, value in base.items():
1347 new_value = tenvironment.environment.get(key)
1348 if new_value and value != new_value:
1349 desc = '%s -> %s' % (value, new_value)
1350 environment_change[key] = desc
1352 _AddConfig(lines, target, environment_plus, environment_minus,
1355 _OutputConfigInfo(lines)
1357 if show_config and self._base_config:
1359 arch_config_plus = {}
1360 arch_config_minus = {}
1361 arch_config_change = {}
1364 for target in board_dict:
1365 if target not in board_selected:
1367 arch = board_selected[target].arch
1368 if arch not in arch_list:
1369 arch_list.append(arch)
1371 for arch in arch_list:
1372 arch_config_plus[arch] = {}
1373 arch_config_minus[arch] = {}
1374 arch_config_change[arch] = {}
1375 for name in self.config_filenames:
1376 arch_config_plus[arch][name] = {}
1377 arch_config_minus[arch][name] = {}
1378 arch_config_change[arch][name] = {}
1380 for target in board_dict:
1381 if target not in board_selected:
1384 arch = board_selected[target].arch
1386 all_config_plus = {}
1387 all_config_minus = {}
1388 all_config_change = {}
1389 tbase = self._base_config[target]
1390 tconfig = config[target]
1392 for name in self.config_filenames:
1393 if not tconfig.config[name]:
1398 base = tbase.config[name]
1399 for key, value in tconfig.config[name].items():
1401 config_plus[key] = value
1402 all_config_plus[key] = value
1403 for key, value in base.items():
1404 if key not in tconfig.config[name]:
1405 config_minus[key] = value
1406 all_config_minus[key] = value
1407 for key, value in base.items():
1408 new_value = tconfig.config.get(key)
1409 if new_value and value != new_value:
1410 desc = '%s -> %s' % (value, new_value)
1411 config_change[key] = desc
1412 all_config_change[key] = desc
1414 arch_config_plus[arch][name].update(config_plus)
1415 arch_config_minus[arch][name].update(config_minus)
1416 arch_config_change[arch][name].update(config_change)
1418 _AddConfig(lines, name, config_plus, config_minus,
1420 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1422 summary[target] = '\n'.join(lines)
1424 lines_by_target = {}
1425 for target, lines in summary.items():
1426 if lines in lines_by_target:
1427 lines_by_target[lines].append(target)
1429 lines_by_target[lines] = [target]
1431 for arch in arch_list:
1436 for name in self.config_filenames:
1437 all_plus.update(arch_config_plus[arch][name])
1438 all_minus.update(arch_config_minus[arch][name])
1439 all_change.update(arch_config_change[arch][name])
1440 _AddConfig(lines, name, arch_config_plus[arch][name],
1441 arch_config_minus[arch][name],
1442 arch_config_change[arch][name])
1443 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1444 #arch_summary[target] = '\n'.join(lines)
1447 _OutputConfigInfo(lines)
1449 for lines, targets in lines_by_target.items():
1452 Print('%s :' % ' '.join(sorted(targets)))
1453 _OutputConfigInfo(lines.split('\n'))
1456 # Save our updated information for the next call to this function
1457 self._base_board_dict = board_dict
1458 self._base_err_lines = err_lines
1459 self._base_warn_lines = warn_lines
1460 self._base_err_line_boards = err_line_boards
1461 self._base_warn_line_boards = warn_line_boards
1462 self._base_config = config
1463 self._base_environment = environment
1465 # Get a list of boards that did not get built, if needed
1467 for board in board_selected:
1468 if not board in board_dict:
1469 not_built.append(board)
1471 Print("Boards not built (%d): %s" % (len(not_built),
1472 ', '.join(not_built)))
1474 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1475 (board_dict, err_lines, err_line_boards, warn_lines,
1476 warn_line_boards, config, environment) = self.GetResultSummary(
1477 board_selected, commit_upto,
1478 read_func_sizes=self._show_bloat,
1479 read_config=self._show_config,
1480 read_environment=self._show_environment)
1482 msg = '%02d: %s' % (commit_upto + 1,
1483 commits[commit_upto].subject)
1484 Print(msg, colour=self.col.BLUE)
1485 self.PrintResultSummary(board_selected, board_dict,
1486 err_lines if self._show_errors else [], err_line_boards,
1487 warn_lines if self._show_errors else [], warn_line_boards,
1488 config, environment, self._show_sizes, self._show_detail,
1489 self._show_bloat, self._show_config, self._show_environment)
1491 def ShowSummary(self, commits, board_selected):
1492 """Show a build summary for U-Boot for a given board list.
1494 Reset the result summary, then repeatedly call GetResultSummary on
1495 each commit's results, then display the differences we see.
1498 commit: Commit objects to summarise
1499 board_selected: Dict containing boards to summarise
1501 self.commit_count = len(commits) if commits else 1
1502 self.commits = commits
1503 self.ResetResultSummary(board_selected)
1504 self._error_lines = 0
1506 for commit_upto in range(0, self.commit_count, self._step):
1507 self.ProduceResultSummary(commit_upto, commits, board_selected)
1508 if not self._error_lines:
1509 Print('(no errors to report)', colour=self.col.GREEN)
1512 def SetupBuild(self, board_selected, commits):
1513 """Set up ready to start a build.
1516 board_selected: Selected boards to build
1517 commits: Selected commits to build
1519 # First work out how many commits we will build
1520 count = (self.commit_count + self._step - 1) // self._step
1521 self.count = len(board_selected) * count
1522 self.upto = self.warned = self.fail = 0
1523 self._timestamps = collections.deque()
1525 def GetThreadDir(self, thread_num):
1526 """Get the directory path to the working dir for a thread.
1529 thread_num: Number of thread to check.
1531 if self.work_in_output:
1532 return self._working_dir
1533 return os.path.join(self._working_dir, '%02d' % thread_num)
1535 def _PrepareThread(self, thread_num, setup_git):
1536 """Prepare the working directory for a thread.
1538 This clones or fetches the repo into the thread's work directory.
1541 thread_num: Thread number (0, 1, ...)
1542 setup_git: True to set up a git repo clone
1544 thread_dir = self.GetThreadDir(thread_num)
1545 builderthread.Mkdir(thread_dir)
1546 git_dir = os.path.join(thread_dir, '.git')
1548 # Clone the repo if it doesn't already exist
1549 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1550 # we have a private index but uses the origin repo's contents?
1551 if setup_git and self.git_dir:
1552 src_dir = os.path.abspath(self.git_dir)
1553 if os.path.exists(git_dir):
1554 Print('\rFetching repo for thread %d' % thread_num,
1556 gitutil.Fetch(git_dir, thread_dir)
1557 terminal.PrintClear()
1559 Print('\rCloning repo for thread %d' % thread_num,
1561 gitutil.Clone(src_dir, thread_dir)
1562 terminal.PrintClear()
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)
1615 terminal.PrintClear()
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 msg = 'Completed: %d total built' % self.count
1665 if self.already_done:
1666 msg += ' (%d previously' % self.already_done
1667 if self.already_done != self.count:
1668 msg += ', %d newly' % (self.count - self.already_done)
1670 duration = datetime.now() - self._start_time
1671 if duration > timedelta(microseconds=1000000):
1672 if duration.microseconds >= 500000:
1673 duration = duration + timedelta(seconds=1)
1674 duration = duration - timedelta(microseconds=duration.microseconds)
1675 msg += ', duration %s' % duration
1678 return (self.fail, self.warned)