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 incremental=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 incremental: Always perform incremental builds; don't run make
256 mrproper when configuring
257 per_board_out_dir: Build in a separate persistent directory per
258 board rather than a thread-specific directory
259 config_only: Only configure each build, don't build it
260 squash_config_y: Convert CONFIG options with the value 'y' to '1'
261 warnings_as_errors: Treat all compiler warnings as errors
262 work_in_output: Use the output directory as the work directory and
263 don't write to a separate output directory.
265 self.toolchains = toolchains
266 self.base_dir = base_dir
268 self._working_dir = base_dir
270 self._working_dir = os.path.join(base_dir, '.bm-work')
272 self.do_make = self.Make
273 self.gnu_make = gnu_make
274 self.checkout = checkout
275 self.num_threads = num_threads
276 self.num_jobs = num_jobs
277 self.already_done = 0
278 self.force_build = False
279 self.git_dir = git_dir
280 self._show_unknown = show_unknown
281 self._timestamp_count = 10
282 self._build_period_us = None
283 self._complete_delay = None
284 self._next_delay_update = datetime.now()
285 self._start_time = 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 t = builderthread.ResultThread(self)
323 self.threads.append(t)
325 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
326 self.re_make_err = re.compile('|'.join(ignore_lines))
328 # Handle existing graceful with SIGINT / Ctrl-C
329 signal.signal(signal.SIGINT, self.signal_handler)
332 """Get rid of all threads created by the builder"""
333 for t in self.threads:
336 def signal_handler(self, signal, frame):
339 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
340 show_detail=False, show_bloat=False,
341 list_error_boards=False, show_config=False,
342 show_environment=False):
343 """Setup display options for the builder.
345 show_errors: True to show summarised error/warning info
346 show_sizes: Show size deltas
347 show_detail: Show size delta detail for each board if show_sizes
348 show_bloat: Show detail for each function
349 list_error_boards: Show the boards which caused each error/warning
350 show_config: Show config deltas
351 show_environment: Show environment deltas
353 self._show_errors = show_errors
354 self._show_sizes = show_sizes
355 self._show_detail = show_detail
356 self._show_bloat = show_bloat
357 self._list_error_boards = list_error_boards
358 self._show_config = show_config
359 self._show_environment = show_environment
361 def _AddTimestamp(self):
362 """Add a new timestamp to the list and record the build period.
364 The build period is the length of time taken to perform a single
365 build (one board, one commit).
368 self._timestamps.append(now)
369 count = len(self._timestamps)
370 delta = self._timestamps[-1] - self._timestamps[0]
371 seconds = delta.total_seconds()
373 # If we have enough data, estimate build period (time taken for a
374 # single build) and therefore completion time.
375 if count > 1 and self._next_delay_update < now:
376 self._next_delay_update = now + timedelta(seconds=2)
378 self._build_period = float(seconds) / count
379 todo = self.count - self.upto
380 self._complete_delay = timedelta(microseconds=
381 self._build_period * todo * 1000000)
383 self._complete_delay -= timedelta(
384 microseconds=self._complete_delay.microseconds)
387 self._timestamps.popleft()
390 def SelectCommit(self, commit, checkout=True):
391 """Checkout the selected commit for this build
394 if checkout and self.checkout:
395 gitutil.Checkout(commit.hash)
397 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
401 commit: Commit object that is being built
402 brd: Board object that is being built
403 stage: Stage that we are at (mrproper, config, build)
404 cwd: Directory where make should be run
405 args: Arguments to pass to make
406 kwargs: Arguments to pass to command.RunPipe()
408 cmd = [self.gnu_make] + list(args)
409 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
410 cwd=cwd, raise_on_error=False, infile='/dev/null', **kwargs)
411 if self.verbose_build:
412 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
413 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
416 def ProcessResult(self, result):
417 """Process the result of a build, showing progress information
420 result: A CommandResult object, which indicates the result for
423 col = terminal.Color()
425 target = result.brd.target
428 if result.return_code != 0:
432 if result.already_done:
433 self.already_done += 1
435 terminal.PrintClear()
436 boards_selected = {target : result.brd}
437 self.ResetResultSummary(boards_selected)
438 self.ProduceResultSummary(result.commit_upto, self.commits,
441 target = '(starting)'
443 # Display separate counts for ok, warned and fail
444 ok = self.upto - self.warned - self.fail
445 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
446 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
447 line += self.col.Color(self.col.RED, '%5d' % self.fail)
449 line += ' /%-5d ' % self.count
450 remaining = self.count - self.upto
452 line += self.col.Color(self.col.MAGENTA, ' -%-5d ' % remaining)
456 # Add our current completion time estimate
458 if self._complete_delay:
459 line += '%s : ' % self._complete_delay
462 terminal.PrintClear()
463 Print(line, newline=False, limit_to_line=True)
465 def _GetOutputDir(self, commit_upto):
466 """Get the name of the output directory for a commit number
468 The output directory is typically .../<branch>/<commit>.
471 commit_upto: Commit number to use (0..self.count-1)
475 commit = self.commits[commit_upto]
476 subject = commit.subject.translate(trans_valid_chars)
477 # See _GetOutputSpaceRemovals() which parses this name
478 commit_dir = ('%02d_of_%02d_g%s_%s' % (commit_upto + 1,
479 self.commit_count, commit.hash, subject[:20]))
480 elif not self.no_subdirs:
481 commit_dir = 'current'
484 return os.path.join(self.base_dir, commit_dir)
486 def GetBuildDir(self, commit_upto, target):
487 """Get the name of the build directory for a commit number
489 The build directory is typically .../<branch>/<commit>/<target>.
492 commit_upto: Commit number to use (0..self.count-1)
495 output_dir = self._GetOutputDir(commit_upto)
496 return os.path.join(output_dir, target)
498 def GetDoneFile(self, commit_upto, target):
499 """Get the name of the done file for a commit number
502 commit_upto: Commit number to use (0..self.count-1)
505 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
507 def GetSizesFile(self, commit_upto, target):
508 """Get the name of the sizes 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), 'sizes')
516 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
517 """Get the name of the funcsizes file for a commit number and ELF file
520 commit_upto: Commit number to use (0..self.count-1)
522 elf_fname: Filename of elf image
524 return os.path.join(self.GetBuildDir(commit_upto, target),
525 '%s.sizes' % elf_fname.replace('/', '-'))
527 def GetObjdumpFile(self, commit_upto, target, elf_fname):
528 """Get the name of the objdump file for a commit number and ELF file
531 commit_upto: Commit number to use (0..self.count-1)
533 elf_fname: Filename of elf image
535 return os.path.join(self.GetBuildDir(commit_upto, target),
536 '%s.objdump' % elf_fname.replace('/', '-'))
538 def GetErrFile(self, commit_upto, target):
539 """Get the name of the err file for a commit number
542 commit_upto: Commit number to use (0..self.count-1)
545 output_dir = self.GetBuildDir(commit_upto, target)
546 return os.path.join(output_dir, 'err')
548 def FilterErrors(self, lines):
549 """Filter out errors in which we have no interest
551 We should probably use map().
554 lines: List of error lines, each a string
556 New list with only interesting lines included
560 if not self.re_make_err.search(line):
561 out_lines.append(line)
564 def ReadFuncSizes(self, fname, fd):
565 """Read function sizes from the output of 'nm'
568 fd: File containing data to read
569 fname: Filename we are reading from (just for errors)
572 Dictionary containing size of each function in bytes, indexed by
576 for line in fd.readlines():
579 size, type, name = line[:-1].split()
581 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
584 # function names begin with '.' on 64-bit powerpc
586 name = 'static.' + name.split('.')[0]
587 sym[name] = sym.get(name, 0) + int(size, 16)
590 def _ProcessConfig(self, fname):
591 """Read in a .config, autoconf.mk or autoconf.h file
593 This function handles all config file types. It ignores comments and
594 any #defines which don't start with CONFIG_.
597 fname: Filename to read
601 key: Config name (e.g. CONFIG_DM)
602 value: Config value (e.g. 1)
605 if os.path.exists(fname):
606 with open(fname) as fd:
609 if line.startswith('#define'):
610 values = line[8:].split(' ', 1)
615 value = '1' if self.squash_config_y else ''
616 if not key.startswith('CONFIG_'):
618 elif not line or line[0] in ['#', '*', '/']:
621 key, value = line.split('=', 1)
622 if self.squash_config_y and value == 'y':
627 def _ProcessEnvironment(self, fname):
628 """Read in a uboot.env file
630 This function reads in environment variables from a file.
633 fname: Filename to read
637 key: environment variable (e.g. bootlimit)
638 value: value of environment variable (e.g. 1)
641 if os.path.exists(fname):
642 with open(fname) as fd:
643 for line in fd.read().split('\0'):
645 key, value = line.split('=', 1)
646 environment[key] = value
648 # ignore lines we can't parse
652 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
653 read_config, read_environment):
654 """Work out the outcome of a build.
657 commit_upto: Commit number to check (0..n-1)
658 target: Target board to check
659 read_func_sizes: True to read function size information
660 read_config: True to read .config and autoconf.h files
661 read_environment: True to read uboot.env files
666 done_file = self.GetDoneFile(commit_upto, target)
667 sizes_file = self.GetSizesFile(commit_upto, target)
672 if os.path.exists(done_file):
673 with open(done_file, 'r') as fd:
675 return_code = int(fd.readline())
677 # The file may be empty due to running out of disk space.
681 err_file = self.GetErrFile(commit_upto, target)
682 if os.path.exists(err_file):
683 with open(err_file, 'r') as fd:
684 err_lines = self.FilterErrors(fd.readlines())
686 # Decide whether the build was ok, failed or created warnings
694 # Convert size information to our simple format
695 if os.path.exists(sizes_file):
696 with open(sizes_file, 'r') as fd:
697 for line in fd.readlines():
698 values = line.split()
701 rodata = int(values[6], 16)
703 'all' : int(values[0]) + int(values[1]) +
705 'text' : int(values[0]) - rodata,
706 'data' : int(values[1]),
707 'bss' : int(values[2]),
710 sizes[values[5]] = size_dict
713 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
714 for fname in glob.glob(pattern):
715 with open(fname, 'r') as fd:
716 dict_name = os.path.basename(fname).replace('.sizes',
718 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
721 output_dir = self.GetBuildDir(commit_upto, target)
722 for name in self.config_filenames:
723 fname = os.path.join(output_dir, name)
724 config[name] = self._ProcessConfig(fname)
727 output_dir = self.GetBuildDir(commit_upto, target)
728 fname = os.path.join(output_dir, 'uboot.env')
729 environment = self._ProcessEnvironment(fname)
731 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
734 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
736 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
737 read_config, read_environment):
738 """Calculate a summary of the results of building a commit.
741 board_selected: Dict containing boards to summarise
742 commit_upto: Commit number to summarize (0..self.count-1)
743 read_func_sizes: True to read function size information
744 read_config: True to read .config and autoconf.h files
745 read_environment: True to read uboot.env files
749 Dict containing boards which passed building this commit.
750 keyed by board.target
751 List containing a summary of error lines
752 Dict keyed by error line, containing a list of the Board
753 objects with that error
754 List containing a summary of warning lines
755 Dict keyed by error line, containing a list of the Board
756 objects with that warning
757 Dictionary keyed by board.target. Each value is a dictionary:
758 key: filename - e.g. '.config'
759 value is itself a dictionary:
762 Dictionary keyed by board.target. Each value is a dictionary:
763 key: environment variable
764 value: value of environment variable
766 def AddLine(lines_summary, lines_boards, line, board):
768 if line in lines_boards:
769 lines_boards[line].append(board)
771 lines_boards[line] = [board]
772 lines_summary.append(line)
775 err_lines_summary = []
776 err_lines_boards = {}
777 warn_lines_summary = []
778 warn_lines_boards = {}
782 for board in boards_selected.values():
783 outcome = self.GetBuildOutcome(commit_upto, board.target,
784 read_func_sizes, read_config,
786 board_dict[board.target] = outcome
788 last_was_warning = False
789 for line in outcome.err_lines:
791 if (self._re_function.match(line) or
792 self._re_files.match(line)):
795 is_warning = (self._re_warning.match(line) or
796 self._re_dtb_warning.match(line))
797 is_note = self._re_note.match(line)
798 if is_warning or (last_was_warning and is_note):
800 AddLine(warn_lines_summary, warn_lines_boards,
802 AddLine(warn_lines_summary, warn_lines_boards,
806 AddLine(err_lines_summary, err_lines_boards,
808 AddLine(err_lines_summary, err_lines_boards,
810 last_was_warning = is_warning
812 tconfig = Config(self.config_filenames, board.target)
813 for fname in self.config_filenames:
815 for key, value in outcome.config[fname].items():
816 tconfig.Add(fname, key, value)
817 config[board.target] = tconfig
819 tenvironment = Environment(board.target)
820 if outcome.environment:
821 for key, value in outcome.environment.items():
822 tenvironment.Add(key, value)
823 environment[board.target] = tenvironment
825 return (board_dict, err_lines_summary, err_lines_boards,
826 warn_lines_summary, warn_lines_boards, config, environment)
828 def AddOutcome(self, board_dict, arch_list, changes, char, color):
829 """Add an output to our list of outcomes for each architecture
831 This simple function adds failing boards (changes) to the
832 relevant architecture string, so we can print the results out
833 sorted by architecture.
836 board_dict: Dict containing all boards
837 arch_list: Dict keyed by arch name. Value is a string containing
838 a list of board names which failed for that arch.
839 changes: List of boards to add to arch_list
840 color: terminal.Colour object
843 for target in changes:
844 if target in board_dict:
845 arch = board_dict[target].arch
848 str = self.col.Color(color, ' ' + target)
849 if not arch in done_arch:
850 str = ' %s %s' % (self.col.Color(color, char), str)
851 done_arch[arch] = True
852 if not arch in arch_list:
853 arch_list[arch] = str
855 arch_list[arch] += str
858 def ColourNum(self, num):
859 color = self.col.RED if num > 0 else self.col.GREEN
862 return self.col.Color(color, str(num))
864 def ResetResultSummary(self, board_selected):
865 """Reset the results summary ready for use.
867 Set up the base board list to be all those selected, and set the
868 error lines to empty.
870 Following this, calls to PrintResultSummary() will use this
871 information to work out what has changed.
874 board_selected: Dict containing boards to summarise, keyed by
877 self._base_board_dict = {}
878 for board in board_selected:
879 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
881 self._base_err_lines = []
882 self._base_warn_lines = []
883 self._base_err_line_boards = {}
884 self._base_warn_line_boards = {}
885 self._base_config = None
886 self._base_environment = None
888 def PrintFuncSizeDetail(self, fname, old, new):
889 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
890 delta, common = [], {}
897 if name not in common:
900 delta.append([-old[name], name])
903 if name not in common:
906 delta.append([new[name], name])
909 diff = new.get(name, 0) - old.get(name, 0)
911 grow, up = grow + 1, up + diff
913 shrink, down = shrink + 1, down - diff
914 delta.append([diff, name])
919 args = [add, -remove, grow, -shrink, up, -down, up - down]
920 if max(args) == 0 and min(args) == 0:
922 args = [self.ColourNum(x) for x in args]
924 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
925 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
926 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
928 for diff, name in delta:
930 color = self.col.RED if diff > 0 else self.col.GREEN
931 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
932 old.get(name, '-'), new.get(name,'-'), diff)
933 Print(msg, colour=color)
936 def PrintSizeDetail(self, target_list, show_bloat):
937 """Show details size information for each board
940 target_list: List of targets, each a dict containing:
941 'target': Target name
942 'total_diff': Total difference in bytes across all areas
943 <part_name>: Difference for that part
944 show_bloat: Show detail for each function
946 targets_by_diff = sorted(target_list, reverse=True,
947 key=lambda x: x['_total_diff'])
948 for result in targets_by_diff:
949 printed_target = False
950 for name in sorted(result):
952 if name.startswith('_'):
955 color = self.col.RED if diff > 0 else self.col.GREEN
956 msg = ' %s %+d' % (name, diff)
957 if not printed_target:
958 Print('%10s %-15s:' % ('', result['_target']),
960 printed_target = True
961 Print(msg, colour=color, newline=False)
965 target = result['_target']
966 outcome = result['_outcome']
967 base_outcome = self._base_board_dict[target]
968 for fname in outcome.func_sizes:
969 self.PrintFuncSizeDetail(fname,
970 base_outcome.func_sizes[fname],
971 outcome.func_sizes[fname])
974 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
976 """Print a summary of image sizes broken down by section.
978 The summary takes the form of one line per architecture. The
979 line contains deltas for each of the sections (+ means the section
980 got bigger, - means smaller). The numbers are the average number
981 of bytes that a board in this section increased by.
984 powerpc: (622 boards) text -0.0
985 arm: (285 boards) text -0.0
986 nds32: (3 boards) text -8.0
989 board_selected: Dict containing boards to summarise, keyed by
991 board_dict: Dict containing boards for which we built this
992 commit, keyed by board.target. The value is an Outcome object.
993 show_detail: Show size delta detail for each board
994 show_bloat: Show detail for each function
999 # Calculate changes in size for different image parts
1000 # The previous sizes are in Board.sizes, for each board
1001 for target in board_dict:
1002 if target not in board_selected:
1004 base_sizes = self._base_board_dict[target].sizes
1005 outcome = board_dict[target]
1006 sizes = outcome.sizes
1008 # Loop through the list of images, creating a dict of size
1009 # changes for each image/part. We end up with something like
1010 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1011 # which means that U-Boot data increased by 5 bytes and SPL
1012 # text decreased by 4.
1013 err = {'_target' : target}
1015 if image in base_sizes:
1016 base_image = base_sizes[image]
1017 # Loop through the text, data, bss parts
1018 for part in sorted(sizes[image]):
1019 diff = sizes[image][part] - base_image[part]
1022 if image == 'u-boot':
1025 name = image + ':' + part
1027 arch = board_selected[target].arch
1028 if not arch in arch_count:
1029 arch_count[arch] = 1
1031 arch_count[arch] += 1
1033 pass # Only add to our list when we have some stats
1034 elif not arch in arch_list:
1035 arch_list[arch] = [err]
1037 arch_list[arch].append(err)
1039 # We now have a list of image size changes sorted by arch
1040 # Print out a summary of these
1041 for arch, target_list in arch_list.items():
1042 # Get total difference for each type
1044 for result in target_list:
1046 for name, diff in result.items():
1047 if name.startswith('_'):
1051 totals[name] += diff
1054 result['_total_diff'] = total
1055 result['_outcome'] = board_dict[result['_target']]
1057 count = len(target_list)
1058 printed_arch = False
1059 for name in sorted(totals):
1062 # Display the average difference in this name for this
1064 avg_diff = float(diff) / count
1065 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1066 msg = ' %s %+1.1f' % (name, avg_diff)
1067 if not printed_arch:
1068 Print('%10s: (for %d/%d boards)' % (arch, count,
1069 arch_count[arch]), newline=False)
1071 Print(msg, colour=color, newline=False)
1076 self.PrintSizeDetail(target_list, show_bloat)
1079 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1080 err_line_boards, warn_lines, warn_line_boards,
1081 config, environment, show_sizes, show_detail,
1082 show_bloat, show_config, show_environment):
1083 """Compare results with the base results and display delta.
1085 Only boards mentioned in board_selected will be considered. This
1086 function is intended to be called repeatedly with the results of
1087 each commit. It therefore shows a 'diff' between what it saw in
1088 the last call and what it sees now.
1091 board_selected: Dict containing boards to summarise, keyed by
1093 board_dict: Dict containing boards for which we built this
1094 commit, keyed by board.target. The value is an Outcome object.
1095 err_lines: A list of errors for this commit, or [] if there is
1096 none, or we don't want to print errors
1097 err_line_boards: Dict keyed by error line, containing a list of
1098 the Board objects with that error
1099 warn_lines: A list of warnings for this commit, or [] if there is
1100 none, or we don't want to print errors
1101 warn_line_boards: Dict keyed by warning line, containing a list of
1102 the Board objects with that warning
1103 config: Dictionary keyed by filename - e.g. '.config'. Each
1104 value is itself a dictionary:
1107 environment: Dictionary keyed by environment variable, Each
1108 value is the value of environment variable.
1109 show_sizes: Show image size deltas
1110 show_detail: Show size delta detail for each board if show_sizes
1111 show_bloat: Show detail for each function
1112 show_config: Show config changes
1113 show_environment: Show environment changes
1115 def _BoardList(line, line_boards):
1116 """Helper function to get a line of boards containing a line
1119 line: Error line to search for
1120 line_boards: boards to search, each a Board
1122 List of boards with that error line, or [] if the user has not
1123 requested such a list
1127 if self._list_error_boards:
1128 for board in line_boards[line]:
1129 if not board in board_set:
1130 boards.append(board)
1131 board_set.add(board)
1134 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1136 """Calculate the required output based on changes in errors
1139 base_lines: List of errors/warnings for previous commit
1140 base_line_boards: Dict keyed by error line, containing a list
1141 of the Board objects with that error in the previous commit
1142 lines: List of errors/warning for this commit, each a str
1143 line_boards: Dict keyed by error line, containing a list
1144 of the Board objects with that error in this commit
1145 char: Character representing error ('') or warning ('w'). The
1146 broken ('+') or fixed ('-') characters are added in this
1151 List of ErrLine objects for 'better' lines
1152 List of ErrLine objects for 'worse' lines
1157 if line not in base_lines:
1158 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1160 worse_lines.append(errline)
1161 for line in base_lines:
1162 if line not in lines:
1163 errline = ErrLine(char + '-',
1164 _BoardList(line, base_line_boards), line)
1165 better_lines.append(errline)
1166 return better_lines, worse_lines
1168 def _CalcConfig(delta, name, config):
1169 """Calculate configuration changes
1172 delta: Type of the delta, e.g. '+'
1173 name: name of the file which changed (e.g. .config)
1174 config: configuration change dictionary
1178 String containing the configuration changes which can be
1182 for key in sorted(config.keys()):
1183 out += '%s=%s ' % (key, config[key])
1184 return '%s %s: %s' % (delta, name, out)
1186 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1187 """Add changes in configuration to a list
1190 lines: list to add to
1191 name: config file name
1192 config_plus: configurations added, dictionary
1195 config_minus: configurations removed, dictionary
1198 config_change: configurations changed, dictionary
1203 lines.append(_CalcConfig('+', name, config_plus))
1205 lines.append(_CalcConfig('-', name, config_minus))
1207 lines.append(_CalcConfig('c', name, config_change))
1209 def _OutputConfigInfo(lines):
1214 col = self.col.GREEN
1215 elif line[0] == '-':
1217 elif line[0] == 'c':
1218 col = self.col.YELLOW
1219 Print(' ' + line, newline=True, colour=col)
1221 def _OutputErrLines(err_lines, colour):
1222 """Output the line of error/warning lines, if not empty
1224 Also increments self._error_lines if err_lines not empty
1227 err_lines: List of ErrLine objects, each an error or warning
1228 line, possibly including a list of boards with that
1230 colour: Colour to use for output
1234 for line in err_lines:
1236 names = [board.target for board in line.boards]
1237 board_str = ' '.join(names) if names else ''
1239 out = self.col.Color(colour, line.char + '(')
1240 out += self.col.Color(self.col.MAGENTA, board_str,
1242 out += self.col.Color(colour, ') %s' % line.errline)
1244 out = self.col.Color(colour, line.char + line.errline)
1245 out_list.append(out)
1246 Print('\n'.join(out_list))
1247 self._error_lines += 1
1250 ok_boards = [] # List of boards fixed since last commit
1251 warn_boards = [] # List of boards with warnings since last commit
1252 err_boards = [] # List of new broken boards since last commit
1253 new_boards = [] # List of boards that didn't exist last time
1254 unknown_boards = [] # List of boards that were not built
1256 for target in board_dict:
1257 if target not in board_selected:
1260 # If the board was built last time, add its outcome to a list
1261 if target in self._base_board_dict:
1262 base_outcome = self._base_board_dict[target].rc
1263 outcome = board_dict[target]
1264 if outcome.rc == OUTCOME_UNKNOWN:
1265 unknown_boards.append(target)
1266 elif outcome.rc < base_outcome:
1267 if outcome.rc == OUTCOME_WARNING:
1268 warn_boards.append(target)
1270 ok_boards.append(target)
1271 elif outcome.rc > base_outcome:
1272 if outcome.rc == OUTCOME_WARNING:
1273 warn_boards.append(target)
1275 err_boards.append(target)
1277 new_boards.append(target)
1279 # Get a list of errors and warnings that have appeared, and disappeared
1280 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1281 self._base_err_line_boards, err_lines, err_line_boards, '')
1282 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1283 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1285 # Display results by arch
1286 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1287 worse_err, better_err, worse_warn, better_warn)):
1289 self.AddOutcome(board_selected, arch_list, ok_boards, '',
1291 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1293 self.AddOutcome(board_selected, arch_list, err_boards, '+',
1295 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1296 if self._show_unknown:
1297 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
1299 for arch, target_list in arch_list.items():
1300 Print('%10s: %s' % (arch, target_list))
1301 self._error_lines += 1
1302 _OutputErrLines(better_err, colour=self.col.GREEN)
1303 _OutputErrLines(worse_err, colour=self.col.RED)
1304 _OutputErrLines(better_warn, colour=self.col.CYAN)
1305 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
1308 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1311 if show_environment and self._base_environment:
1314 for target in board_dict:
1315 if target not in board_selected:
1318 tbase = self._base_environment[target]
1319 tenvironment = environment[target]
1320 environment_plus = {}
1321 environment_minus = {}
1322 environment_change = {}
1323 base = tbase.environment
1324 for key, value in tenvironment.environment.items():
1326 environment_plus[key] = value
1327 for key, value in base.items():
1328 if key not in tenvironment.environment:
1329 environment_minus[key] = value
1330 for key, value in base.items():
1331 new_value = tenvironment.environment.get(key)
1332 if new_value and value != new_value:
1333 desc = '%s -> %s' % (value, new_value)
1334 environment_change[key] = desc
1336 _AddConfig(lines, target, environment_plus, environment_minus,
1339 _OutputConfigInfo(lines)
1341 if show_config and self._base_config:
1343 arch_config_plus = {}
1344 arch_config_minus = {}
1345 arch_config_change = {}
1348 for target in board_dict:
1349 if target not in board_selected:
1351 arch = board_selected[target].arch
1352 if arch not in arch_list:
1353 arch_list.append(arch)
1355 for arch in arch_list:
1356 arch_config_plus[arch] = {}
1357 arch_config_minus[arch] = {}
1358 arch_config_change[arch] = {}
1359 for name in self.config_filenames:
1360 arch_config_plus[arch][name] = {}
1361 arch_config_minus[arch][name] = {}
1362 arch_config_change[arch][name] = {}
1364 for target in board_dict:
1365 if target not in board_selected:
1368 arch = board_selected[target].arch
1370 all_config_plus = {}
1371 all_config_minus = {}
1372 all_config_change = {}
1373 tbase = self._base_config[target]
1374 tconfig = config[target]
1376 for name in self.config_filenames:
1377 if not tconfig.config[name]:
1382 base = tbase.config[name]
1383 for key, value in tconfig.config[name].items():
1385 config_plus[key] = value
1386 all_config_plus[key] = value
1387 for key, value in base.items():
1388 if key not in tconfig.config[name]:
1389 config_minus[key] = value
1390 all_config_minus[key] = value
1391 for key, value in base.items():
1392 new_value = tconfig.config.get(key)
1393 if new_value and value != new_value:
1394 desc = '%s -> %s' % (value, new_value)
1395 config_change[key] = desc
1396 all_config_change[key] = desc
1398 arch_config_plus[arch][name].update(config_plus)
1399 arch_config_minus[arch][name].update(config_minus)
1400 arch_config_change[arch][name].update(config_change)
1402 _AddConfig(lines, name, config_plus, config_minus,
1404 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1406 summary[target] = '\n'.join(lines)
1408 lines_by_target = {}
1409 for target, lines in summary.items():
1410 if lines in lines_by_target:
1411 lines_by_target[lines].append(target)
1413 lines_by_target[lines] = [target]
1415 for arch in arch_list:
1420 for name in self.config_filenames:
1421 all_plus.update(arch_config_plus[arch][name])
1422 all_minus.update(arch_config_minus[arch][name])
1423 all_change.update(arch_config_change[arch][name])
1424 _AddConfig(lines, name, arch_config_plus[arch][name],
1425 arch_config_minus[arch][name],
1426 arch_config_change[arch][name])
1427 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1428 #arch_summary[target] = '\n'.join(lines)
1431 _OutputConfigInfo(lines)
1433 for lines, targets in lines_by_target.items():
1436 Print('%s :' % ' '.join(sorted(targets)))
1437 _OutputConfigInfo(lines.split('\n'))
1440 # Save our updated information for the next call to this function
1441 self._base_board_dict = board_dict
1442 self._base_err_lines = err_lines
1443 self._base_warn_lines = warn_lines
1444 self._base_err_line_boards = err_line_boards
1445 self._base_warn_line_boards = warn_line_boards
1446 self._base_config = config
1447 self._base_environment = environment
1449 # Get a list of boards that did not get built, if needed
1451 for board in board_selected:
1452 if not board in board_dict:
1453 not_built.append(board)
1455 Print("Boards not built (%d): %s" % (len(not_built),
1456 ', '.join(not_built)))
1458 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1459 (board_dict, err_lines, err_line_boards, warn_lines,
1460 warn_line_boards, config, environment) = self.GetResultSummary(
1461 board_selected, commit_upto,
1462 read_func_sizes=self._show_bloat,
1463 read_config=self._show_config,
1464 read_environment=self._show_environment)
1466 msg = '%02d: %s' % (commit_upto + 1,
1467 commits[commit_upto].subject)
1468 Print(msg, colour=self.col.BLUE)
1469 self.PrintResultSummary(board_selected, board_dict,
1470 err_lines if self._show_errors else [], err_line_boards,
1471 warn_lines if self._show_errors else [], warn_line_boards,
1472 config, environment, self._show_sizes, self._show_detail,
1473 self._show_bloat, self._show_config, self._show_environment)
1475 def ShowSummary(self, commits, board_selected):
1476 """Show a build summary for U-Boot for a given board list.
1478 Reset the result summary, then repeatedly call GetResultSummary on
1479 each commit's results, then display the differences we see.
1482 commit: Commit objects to summarise
1483 board_selected: Dict containing boards to summarise
1485 self.commit_count = len(commits) if commits else 1
1486 self.commits = commits
1487 self.ResetResultSummary(board_selected)
1488 self._error_lines = 0
1490 for commit_upto in range(0, self.commit_count, self._step):
1491 self.ProduceResultSummary(commit_upto, commits, board_selected)
1492 if not self._error_lines:
1493 Print('(no errors to report)', colour=self.col.GREEN)
1496 def SetupBuild(self, board_selected, commits):
1497 """Set up ready to start a build.
1500 board_selected: Selected boards to build
1501 commits: Selected commits to build
1503 # First work out how many commits we will build
1504 count = (self.commit_count + self._step - 1) // self._step
1505 self.count = len(board_selected) * count
1506 self.upto = self.warned = self.fail = 0
1507 self._timestamps = collections.deque()
1509 def GetThreadDir(self, thread_num):
1510 """Get the directory path to the working dir for a thread.
1513 thread_num: Number of thread to check.
1515 if self.work_in_output:
1516 return self._working_dir
1517 return os.path.join(self._working_dir, '%02d' % thread_num)
1519 def _PrepareThread(self, thread_num, setup_git):
1520 """Prepare the working directory for a thread.
1522 This clones or fetches the repo into the thread's work directory.
1525 thread_num: Thread number (0, 1, ...)
1526 setup_git: True to set up a git repo clone
1528 thread_dir = self.GetThreadDir(thread_num)
1529 builderthread.Mkdir(thread_dir)
1530 git_dir = os.path.join(thread_dir, '.git')
1532 # Clone the repo if it doesn't already exist
1533 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1534 # we have a private index but uses the origin repo's contents?
1535 if setup_git and self.git_dir:
1536 src_dir = os.path.abspath(self.git_dir)
1537 if os.path.exists(git_dir):
1538 Print('\rFetching repo for thread %d' % thread_num,
1540 gitutil.Fetch(git_dir, thread_dir)
1541 terminal.PrintClear()
1543 Print('\rCloning repo for thread %d' % thread_num,
1545 gitutil.Clone(src_dir, thread_dir)
1546 terminal.PrintClear()
1548 def _PrepareWorkingSpace(self, max_threads, setup_git):
1549 """Prepare the working directory for use.
1551 Set up the git repo for each thread.
1554 max_threads: Maximum number of threads we expect to need.
1555 setup_git: True to set up a git repo clone
1557 builderthread.Mkdir(self._working_dir)
1558 for thread in range(max_threads):
1559 self._PrepareThread(thread, setup_git)
1561 def _GetOutputSpaceRemovals(self):
1562 """Get the output directories ready to receive files.
1564 Figure out what needs to be deleted in the output directory before it
1565 can be used. We only delete old buildman directories which have the
1566 expected name pattern. See _GetOutputDir().
1569 List of full paths of directories to remove
1571 if not self.commits:
1574 for commit_upto in range(self.commit_count):
1575 dir_list.append(self._GetOutputDir(commit_upto))
1578 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1579 if dirname not in dir_list:
1580 leaf = dirname[len(self.base_dir) + 1:]
1581 m = re.match('[0-9]+_of_[0-9]+_g[0-9a-f]+_.*', leaf)
1583 to_remove.append(dirname)
1586 def _PrepareOutputSpace(self):
1587 """Get the output directories ready to receive files.
1589 We delete any output directories which look like ones we need to
1590 create. Having left over directories is confusing when the user wants
1591 to check the output manually.
1593 to_remove = self._GetOutputSpaceRemovals()
1595 Print('Removing %d old build directories...' % len(to_remove),
1597 for dirname in to_remove:
1598 shutil.rmtree(dirname)
1599 terminal.PrintClear()
1601 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1602 """Build all commits for a list of boards
1605 commits: List of commits to be build, each a Commit object
1606 boards_selected: Dict of selected boards, key is target name,
1607 value is Board object
1608 keep_outputs: True to save build output files
1609 verbose: Display build results as they are completed
1612 - number of boards that failed to build
1613 - number of boards that issued warnings
1615 self.commit_count = len(commits) if commits else 1
1616 self.commits = commits
1617 self._verbose = verbose
1619 self.ResetResultSummary(board_selected)
1620 builderthread.Mkdir(self.base_dir, parents = True)
1621 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1622 commits is not None)
1623 self._PrepareOutputSpace()
1624 Print('\rStarting build...', newline=False)
1625 self.SetupBuild(board_selected, commits)
1626 self.ProcessResult(None)
1628 # Create jobs to build all commits for each board
1629 for brd in board_selected.values():
1630 job = builderthread.BuilderJob()
1632 job.commits = commits
1633 job.keep_outputs = keep_outputs
1634 job.work_in_output = self.work_in_output
1635 job.step = self._step
1638 term = threading.Thread(target=self.queue.join)
1639 term.setDaemon(True)
1641 while term.isAlive():
1644 # Wait until we have processed all output
1645 self.out_queue.join()
1648 msg = 'Completed: %d total built' % self.count
1649 if self.already_done:
1650 msg += ' (%d previously' % self.already_done
1651 if self.already_done != self.count:
1652 msg += ', %d newly' % (self.count - self.already_done)
1654 duration = datetime.now() - self._start_time
1655 if duration > timedelta(microseconds=1000000):
1656 if duration.microseconds >= 500000:
1657 duration = duration + timedelta(seconds=1)
1658 duration = duration - timedelta(microseconds=duration.microseconds)
1659 msg += ', duration %s' % duration
1662 return (self.fail, self.warned)