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
20 from buildman import builderthread
21 from buildman import toolchain
22 from patman import command
23 from patman import gitutil
24 from patman import terminal
25 from patman.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_g4ed4ebc_net--Add-tftp-speed-/
78 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)
482 if self.work_in_output:
483 return self._working_dir
487 commit = self.commits[commit_upto]
488 subject = commit.subject.translate(trans_valid_chars)
489 # See _GetOutputSpaceRemovals() which parses this name
490 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
491 commit.hash, subject[:20]))
492 elif not self.no_subdirs:
493 commit_dir = 'current'
496 return os.path.join(self.base_dir, commit_dir)
498 def GetBuildDir(self, commit_upto, target):
499 """Get the name of the build directory for a commit number
501 The build directory is typically .../<branch>/<commit>/<target>.
504 commit_upto: Commit number to use (0..self.count-1)
507 output_dir = self._GetOutputDir(commit_upto)
508 if self.work_in_output:
510 return os.path.join(output_dir, target)
512 def GetDoneFile(self, commit_upto, target):
513 """Get the name of the done file for a commit number
516 commit_upto: Commit number to use (0..self.count-1)
519 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
521 def GetSizesFile(self, commit_upto, target):
522 """Get the name of the sizes file for a commit number
525 commit_upto: Commit number to use (0..self.count-1)
528 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
530 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
531 """Get the name of the funcsizes file for a commit number and ELF file
534 commit_upto: Commit number to use (0..self.count-1)
536 elf_fname: Filename of elf image
538 return os.path.join(self.GetBuildDir(commit_upto, target),
539 '%s.sizes' % elf_fname.replace('/', '-'))
541 def GetObjdumpFile(self, commit_upto, target, elf_fname):
542 """Get the name of the objdump file for a commit number and ELF file
545 commit_upto: Commit number to use (0..self.count-1)
547 elf_fname: Filename of elf image
549 return os.path.join(self.GetBuildDir(commit_upto, target),
550 '%s.objdump' % elf_fname.replace('/', '-'))
552 def GetErrFile(self, commit_upto, target):
553 """Get the name of the err file for a commit number
556 commit_upto: Commit number to use (0..self.count-1)
559 output_dir = self.GetBuildDir(commit_upto, target)
560 return os.path.join(output_dir, 'err')
562 def FilterErrors(self, lines):
563 """Filter out errors in which we have no interest
565 We should probably use map().
568 lines: List of error lines, each a string
570 New list with only interesting lines included
573 if self._filter_migration_warnings:
574 text = '\n'.join(lines)
575 text = self._re_migration_warning.sub('', text)
576 lines = text.splitlines()
578 if self.re_make_err.search(line):
580 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
582 out_lines.append(line)
585 def ReadFuncSizes(self, fname, fd):
586 """Read function sizes from the output of 'nm'
589 fd: File containing data to read
590 fname: Filename we are reading from (just for errors)
593 Dictionary containing size of each function in bytes, indexed by
597 for line in fd.readlines():
600 size, type, name = line[:-1].split()
602 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
605 # function names begin with '.' on 64-bit powerpc
607 name = 'static.' + name.split('.')[0]
608 sym[name] = sym.get(name, 0) + int(size, 16)
611 def _ProcessConfig(self, fname):
612 """Read in a .config, autoconf.mk or autoconf.h file
614 This function handles all config file types. It ignores comments and
615 any #defines which don't start with CONFIG_.
618 fname: Filename to read
622 key: Config name (e.g. CONFIG_DM)
623 value: Config value (e.g. 1)
626 if os.path.exists(fname):
627 with open(fname) as fd:
630 if line.startswith('#define'):
631 values = line[8:].split(' ', 1)
636 value = '1' if self.squash_config_y else ''
637 if not key.startswith('CONFIG_'):
639 elif not line or line[0] in ['#', '*', '/']:
642 key, value = line.split('=', 1)
643 if self.squash_config_y and value == 'y':
648 def _ProcessEnvironment(self, fname):
649 """Read in a uboot.env file
651 This function reads in environment variables from a file.
654 fname: Filename to read
658 key: environment variable (e.g. bootlimit)
659 value: value of environment variable (e.g. 1)
662 if os.path.exists(fname):
663 with open(fname) as fd:
664 for line in fd.read().split('\0'):
666 key, value = line.split('=', 1)
667 environment[key] = value
669 # ignore lines we can't parse
673 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
674 read_config, read_environment):
675 """Work out the outcome of a build.
678 commit_upto: Commit number to check (0..n-1)
679 target: Target board to check
680 read_func_sizes: True to read function size information
681 read_config: True to read .config and autoconf.h files
682 read_environment: True to read uboot.env files
687 done_file = self.GetDoneFile(commit_upto, target)
688 sizes_file = self.GetSizesFile(commit_upto, target)
693 if os.path.exists(done_file):
694 with open(done_file, 'r') as fd:
696 return_code = int(fd.readline())
698 # The file may be empty due to running out of disk space.
702 err_file = self.GetErrFile(commit_upto, target)
703 if os.path.exists(err_file):
704 with open(err_file, 'r') as fd:
705 err_lines = self.FilterErrors(fd.readlines())
707 # Decide whether the build was ok, failed or created warnings
715 # Convert size information to our simple format
716 if os.path.exists(sizes_file):
717 with open(sizes_file, 'r') as fd:
718 for line in fd.readlines():
719 values = line.split()
722 rodata = int(values[6], 16)
724 'all' : int(values[0]) + int(values[1]) +
726 'text' : int(values[0]) - rodata,
727 'data' : int(values[1]),
728 'bss' : int(values[2]),
731 sizes[values[5]] = size_dict
734 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
735 for fname in glob.glob(pattern):
736 with open(fname, 'r') as fd:
737 dict_name = os.path.basename(fname).replace('.sizes',
739 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
742 output_dir = self.GetBuildDir(commit_upto, target)
743 for name in self.config_filenames:
744 fname = os.path.join(output_dir, name)
745 config[name] = self._ProcessConfig(fname)
748 output_dir = self.GetBuildDir(commit_upto, target)
749 fname = os.path.join(output_dir, 'uboot.env')
750 environment = self._ProcessEnvironment(fname)
752 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
755 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
757 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
758 read_config, read_environment):
759 """Calculate a summary of the results of building a commit.
762 board_selected: Dict containing boards to summarise
763 commit_upto: Commit number to summarize (0..self.count-1)
764 read_func_sizes: True to read function size information
765 read_config: True to read .config and autoconf.h files
766 read_environment: True to read uboot.env files
770 Dict containing boards which passed building this commit.
771 keyed by board.target
772 List containing a summary of error lines
773 Dict keyed by error line, containing a list of the Board
774 objects with that error
775 List containing a summary of warning lines
776 Dict keyed by error line, containing a list of the Board
777 objects with that warning
778 Dictionary keyed by board.target. Each value is a dictionary:
779 key: filename - e.g. '.config'
780 value is itself a dictionary:
783 Dictionary keyed by board.target. Each value is a dictionary:
784 key: environment variable
785 value: value of environment variable
787 def AddLine(lines_summary, lines_boards, line, board):
789 if line in lines_boards:
790 lines_boards[line].append(board)
792 lines_boards[line] = [board]
793 lines_summary.append(line)
796 err_lines_summary = []
797 err_lines_boards = {}
798 warn_lines_summary = []
799 warn_lines_boards = {}
803 for board in boards_selected.values():
804 outcome = self.GetBuildOutcome(commit_upto, board.target,
805 read_func_sizes, read_config,
807 board_dict[board.target] = outcome
809 last_was_warning = False
810 for line in outcome.err_lines:
812 if (self._re_function.match(line) or
813 self._re_files.match(line)):
816 is_warning = (self._re_warning.match(line) or
817 self._re_dtb_warning.match(line))
818 is_note = self._re_note.match(line)
819 if is_warning or (last_was_warning and is_note):
821 AddLine(warn_lines_summary, warn_lines_boards,
823 AddLine(warn_lines_summary, warn_lines_boards,
827 AddLine(err_lines_summary, err_lines_boards,
829 AddLine(err_lines_summary, err_lines_boards,
831 last_was_warning = is_warning
833 tconfig = Config(self.config_filenames, board.target)
834 for fname in self.config_filenames:
836 for key, value in outcome.config[fname].items():
837 tconfig.Add(fname, key, value)
838 config[board.target] = tconfig
840 tenvironment = Environment(board.target)
841 if outcome.environment:
842 for key, value in outcome.environment.items():
843 tenvironment.Add(key, value)
844 environment[board.target] = tenvironment
846 return (board_dict, err_lines_summary, err_lines_boards,
847 warn_lines_summary, warn_lines_boards, config, environment)
849 def AddOutcome(self, board_dict, arch_list, changes, char, color):
850 """Add an output to our list of outcomes for each architecture
852 This simple function adds failing boards (changes) to the
853 relevant architecture string, so we can print the results out
854 sorted by architecture.
857 board_dict: Dict containing all boards
858 arch_list: Dict keyed by arch name. Value is a string containing
859 a list of board names which failed for that arch.
860 changes: List of boards to add to arch_list
861 color: terminal.Colour object
864 for target in changes:
865 if target in board_dict:
866 arch = board_dict[target].arch
869 str = self.col.Color(color, ' ' + target)
870 if not arch in done_arch:
871 str = ' %s %s' % (self.col.Color(color, char), str)
872 done_arch[arch] = True
873 if not arch in arch_list:
874 arch_list[arch] = str
876 arch_list[arch] += str
879 def ColourNum(self, num):
880 color = self.col.RED if num > 0 else self.col.GREEN
883 return self.col.Color(color, str(num))
885 def ResetResultSummary(self, board_selected):
886 """Reset the results summary ready for use.
888 Set up the base board list to be all those selected, and set the
889 error lines to empty.
891 Following this, calls to PrintResultSummary() will use this
892 information to work out what has changed.
895 board_selected: Dict containing boards to summarise, keyed by
898 self._base_board_dict = {}
899 for board in board_selected:
900 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
902 self._base_err_lines = []
903 self._base_warn_lines = []
904 self._base_err_line_boards = {}
905 self._base_warn_line_boards = {}
906 self._base_config = None
907 self._base_environment = None
909 def PrintFuncSizeDetail(self, fname, old, new):
910 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
911 delta, common = [], {}
918 if name not in common:
921 delta.append([-old[name], name])
924 if name not in common:
927 delta.append([new[name], name])
930 diff = new.get(name, 0) - old.get(name, 0)
932 grow, up = grow + 1, up + diff
934 shrink, down = shrink + 1, down - diff
935 delta.append([diff, name])
940 args = [add, -remove, grow, -shrink, up, -down, up - down]
941 if max(args) == 0 and min(args) == 0:
943 args = [self.ColourNum(x) for x in args]
945 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
946 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
947 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
949 for diff, name in delta:
951 color = self.col.RED if diff > 0 else self.col.GREEN
952 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
953 old.get(name, '-'), new.get(name,'-'), diff)
954 Print(msg, colour=color)
957 def PrintSizeDetail(self, target_list, show_bloat):
958 """Show details size information for each board
961 target_list: List of targets, each a dict containing:
962 'target': Target name
963 'total_diff': Total difference in bytes across all areas
964 <part_name>: Difference for that part
965 show_bloat: Show detail for each function
967 targets_by_diff = sorted(target_list, reverse=True,
968 key=lambda x: x['_total_diff'])
969 for result in targets_by_diff:
970 printed_target = False
971 for name in sorted(result):
973 if name.startswith('_'):
976 color = self.col.RED if diff > 0 else self.col.GREEN
977 msg = ' %s %+d' % (name, diff)
978 if not printed_target:
979 Print('%10s %-15s:' % ('', result['_target']),
981 printed_target = True
982 Print(msg, colour=color, newline=False)
986 target = result['_target']
987 outcome = result['_outcome']
988 base_outcome = self._base_board_dict[target]
989 for fname in outcome.func_sizes:
990 self.PrintFuncSizeDetail(fname,
991 base_outcome.func_sizes[fname],
992 outcome.func_sizes[fname])
995 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
997 """Print a summary of image sizes broken down by section.
999 The summary takes the form of one line per architecture. The
1000 line contains deltas for each of the sections (+ means the section
1001 got bigger, - means smaller). The numbers are the average number
1002 of bytes that a board in this section increased by.
1005 powerpc: (622 boards) text -0.0
1006 arm: (285 boards) text -0.0
1007 nds32: (3 boards) text -8.0
1010 board_selected: Dict containing boards to summarise, keyed by
1012 board_dict: Dict containing boards for which we built this
1013 commit, keyed by board.target. The value is an Outcome object.
1014 show_detail: Show size delta detail for each board
1015 show_bloat: Show detail for each function
1020 # Calculate changes in size for different image parts
1021 # The previous sizes are in Board.sizes, for each board
1022 for target in board_dict:
1023 if target not in board_selected:
1025 base_sizes = self._base_board_dict[target].sizes
1026 outcome = board_dict[target]
1027 sizes = outcome.sizes
1029 # Loop through the list of images, creating a dict of size
1030 # changes for each image/part. We end up with something like
1031 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1032 # which means that U-Boot data increased by 5 bytes and SPL
1033 # text decreased by 4.
1034 err = {'_target' : target}
1036 if image in base_sizes:
1037 base_image = base_sizes[image]
1038 # Loop through the text, data, bss parts
1039 for part in sorted(sizes[image]):
1040 diff = sizes[image][part] - base_image[part]
1043 if image == 'u-boot':
1046 name = image + ':' + part
1048 arch = board_selected[target].arch
1049 if not arch in arch_count:
1050 arch_count[arch] = 1
1052 arch_count[arch] += 1
1054 pass # Only add to our list when we have some stats
1055 elif not arch in arch_list:
1056 arch_list[arch] = [err]
1058 arch_list[arch].append(err)
1060 # We now have a list of image size changes sorted by arch
1061 # Print out a summary of these
1062 for arch, target_list in arch_list.items():
1063 # Get total difference for each type
1065 for result in target_list:
1067 for name, diff in result.items():
1068 if name.startswith('_'):
1072 totals[name] += diff
1075 result['_total_diff'] = total
1076 result['_outcome'] = board_dict[result['_target']]
1078 count = len(target_list)
1079 printed_arch = False
1080 for name in sorted(totals):
1083 # Display the average difference in this name for this
1085 avg_diff = float(diff) / count
1086 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1087 msg = ' %s %+1.1f' % (name, avg_diff)
1088 if not printed_arch:
1089 Print('%10s: (for %d/%d boards)' % (arch, count,
1090 arch_count[arch]), newline=False)
1092 Print(msg, colour=color, newline=False)
1097 self.PrintSizeDetail(target_list, show_bloat)
1100 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1101 err_line_boards, warn_lines, warn_line_boards,
1102 config, environment, show_sizes, show_detail,
1103 show_bloat, show_config, show_environment):
1104 """Compare results with the base results and display delta.
1106 Only boards mentioned in board_selected will be considered. This
1107 function is intended to be called repeatedly with the results of
1108 each commit. It therefore shows a 'diff' between what it saw in
1109 the last call and what it sees now.
1112 board_selected: Dict containing boards to summarise, keyed by
1114 board_dict: Dict containing boards for which we built this
1115 commit, keyed by board.target. The value is an Outcome object.
1116 err_lines: A list of errors for this commit, or [] if there is
1117 none, or we don't want to print errors
1118 err_line_boards: Dict keyed by error line, containing a list of
1119 the Board objects with that error
1120 warn_lines: A list of warnings for this commit, or [] if there is
1121 none, or we don't want to print errors
1122 warn_line_boards: Dict keyed by warning line, containing a list of
1123 the Board objects with that warning
1124 config: Dictionary keyed by filename - e.g. '.config'. Each
1125 value is itself a dictionary:
1128 environment: Dictionary keyed by environment variable, Each
1129 value is the value of environment variable.
1130 show_sizes: Show image size deltas
1131 show_detail: Show size delta detail for each board if show_sizes
1132 show_bloat: Show detail for each function
1133 show_config: Show config changes
1134 show_environment: Show environment changes
1136 def _BoardList(line, line_boards):
1137 """Helper function to get a line of boards containing a line
1140 line: Error line to search for
1141 line_boards: boards to search, each a Board
1143 List of boards with that error line, or [] if the user has not
1144 requested such a list
1148 if self._list_error_boards:
1149 for board in line_boards[line]:
1150 if not board in board_set:
1151 boards.append(board)
1152 board_set.add(board)
1155 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1157 """Calculate the required output based on changes in errors
1160 base_lines: List of errors/warnings for previous commit
1161 base_line_boards: Dict keyed by error line, containing a list
1162 of the Board objects with that error in the previous commit
1163 lines: List of errors/warning for this commit, each a str
1164 line_boards: Dict keyed by error line, containing a list
1165 of the Board objects with that error in this commit
1166 char: Character representing error ('') or warning ('w'). The
1167 broken ('+') or fixed ('-') characters are added in this
1172 List of ErrLine objects for 'better' lines
1173 List of ErrLine objects for 'worse' lines
1178 if line not in base_lines:
1179 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1181 worse_lines.append(errline)
1182 for line in base_lines:
1183 if line not in lines:
1184 errline = ErrLine(char + '-',
1185 _BoardList(line, base_line_boards), line)
1186 better_lines.append(errline)
1187 return better_lines, worse_lines
1189 def _CalcConfig(delta, name, config):
1190 """Calculate configuration changes
1193 delta: Type of the delta, e.g. '+'
1194 name: name of the file which changed (e.g. .config)
1195 config: configuration change dictionary
1199 String containing the configuration changes which can be
1203 for key in sorted(config.keys()):
1204 out += '%s=%s ' % (key, config[key])
1205 return '%s %s: %s' % (delta, name, out)
1207 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1208 """Add changes in configuration to a list
1211 lines: list to add to
1212 name: config file name
1213 config_plus: configurations added, dictionary
1216 config_minus: configurations removed, dictionary
1219 config_change: configurations changed, dictionary
1224 lines.append(_CalcConfig('+', name, config_plus))
1226 lines.append(_CalcConfig('-', name, config_minus))
1228 lines.append(_CalcConfig('c', name, config_change))
1230 def _OutputConfigInfo(lines):
1235 col = self.col.GREEN
1236 elif line[0] == '-':
1238 elif line[0] == 'c':
1239 col = self.col.YELLOW
1240 Print(' ' + line, newline=True, colour=col)
1242 def _OutputErrLines(err_lines, colour):
1243 """Output the line of error/warning lines, if not empty
1245 Also increments self._error_lines if err_lines not empty
1248 err_lines: List of ErrLine objects, each an error or warning
1249 line, possibly including a list of boards with that
1251 colour: Colour to use for output
1255 for line in err_lines:
1257 names = [board.target for board in line.boards]
1258 board_str = ' '.join(names) if names else ''
1260 out = self.col.Color(colour, line.char + '(')
1261 out += self.col.Color(self.col.MAGENTA, board_str,
1263 out += self.col.Color(colour, ') %s' % line.errline)
1265 out = self.col.Color(colour, line.char + line.errline)
1266 out_list.append(out)
1267 Print('\n'.join(out_list))
1268 self._error_lines += 1
1271 ok_boards = [] # List of boards fixed since last commit
1272 warn_boards = [] # List of boards with warnings since last commit
1273 err_boards = [] # List of new broken boards since last commit
1274 new_boards = [] # List of boards that didn't exist last time
1275 unknown_boards = [] # List of boards that were not built
1277 for target in board_dict:
1278 if target not in board_selected:
1281 # If the board was built last time, add its outcome to a list
1282 if target in self._base_board_dict:
1283 base_outcome = self._base_board_dict[target].rc
1284 outcome = board_dict[target]
1285 if outcome.rc == OUTCOME_UNKNOWN:
1286 unknown_boards.append(target)
1287 elif outcome.rc < base_outcome:
1288 if outcome.rc == OUTCOME_WARNING:
1289 warn_boards.append(target)
1291 ok_boards.append(target)
1292 elif outcome.rc > base_outcome:
1293 if outcome.rc == OUTCOME_WARNING:
1294 warn_boards.append(target)
1296 err_boards.append(target)
1298 new_boards.append(target)
1300 # Get a list of errors and warnings that have appeared, and disappeared
1301 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1302 self._base_err_line_boards, err_lines, err_line_boards, '')
1303 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1304 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1306 # Display results by arch
1307 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1308 worse_err, better_err, worse_warn, better_warn)):
1310 self.AddOutcome(board_selected, arch_list, ok_boards, '',
1312 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1314 self.AddOutcome(board_selected, arch_list, err_boards, '+',
1316 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1317 if self._show_unknown:
1318 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
1320 for arch, target_list in arch_list.items():
1321 Print('%10s: %s' % (arch, target_list))
1322 self._error_lines += 1
1323 _OutputErrLines(better_err, colour=self.col.GREEN)
1324 _OutputErrLines(worse_err, colour=self.col.RED)
1325 _OutputErrLines(better_warn, colour=self.col.CYAN)
1326 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
1329 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1332 if show_environment and self._base_environment:
1335 for target in board_dict:
1336 if target not in board_selected:
1339 tbase = self._base_environment[target]
1340 tenvironment = environment[target]
1341 environment_plus = {}
1342 environment_minus = {}
1343 environment_change = {}
1344 base = tbase.environment
1345 for key, value in tenvironment.environment.items():
1347 environment_plus[key] = value
1348 for key, value in base.items():
1349 if key not in tenvironment.environment:
1350 environment_minus[key] = value
1351 for key, value in base.items():
1352 new_value = tenvironment.environment.get(key)
1353 if new_value and value != new_value:
1354 desc = '%s -> %s' % (value, new_value)
1355 environment_change[key] = desc
1357 _AddConfig(lines, target, environment_plus, environment_minus,
1360 _OutputConfigInfo(lines)
1362 if show_config and self._base_config:
1364 arch_config_plus = {}
1365 arch_config_minus = {}
1366 arch_config_change = {}
1369 for target in board_dict:
1370 if target not in board_selected:
1372 arch = board_selected[target].arch
1373 if arch not in arch_list:
1374 arch_list.append(arch)
1376 for arch in arch_list:
1377 arch_config_plus[arch] = {}
1378 arch_config_minus[arch] = {}
1379 arch_config_change[arch] = {}
1380 for name in self.config_filenames:
1381 arch_config_plus[arch][name] = {}
1382 arch_config_minus[arch][name] = {}
1383 arch_config_change[arch][name] = {}
1385 for target in board_dict:
1386 if target not in board_selected:
1389 arch = board_selected[target].arch
1391 all_config_plus = {}
1392 all_config_minus = {}
1393 all_config_change = {}
1394 tbase = self._base_config[target]
1395 tconfig = config[target]
1397 for name in self.config_filenames:
1398 if not tconfig.config[name]:
1403 base = tbase.config[name]
1404 for key, value in tconfig.config[name].items():
1406 config_plus[key] = value
1407 all_config_plus[key] = value
1408 for key, value in base.items():
1409 if key not in tconfig.config[name]:
1410 config_minus[key] = value
1411 all_config_minus[key] = value
1412 for key, value in base.items():
1413 new_value = tconfig.config.get(key)
1414 if new_value and value != new_value:
1415 desc = '%s -> %s' % (value, new_value)
1416 config_change[key] = desc
1417 all_config_change[key] = desc
1419 arch_config_plus[arch][name].update(config_plus)
1420 arch_config_minus[arch][name].update(config_minus)
1421 arch_config_change[arch][name].update(config_change)
1423 _AddConfig(lines, name, config_plus, config_minus,
1425 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1427 summary[target] = '\n'.join(lines)
1429 lines_by_target = {}
1430 for target, lines in summary.items():
1431 if lines in lines_by_target:
1432 lines_by_target[lines].append(target)
1434 lines_by_target[lines] = [target]
1436 for arch in arch_list:
1441 for name in self.config_filenames:
1442 all_plus.update(arch_config_plus[arch][name])
1443 all_minus.update(arch_config_minus[arch][name])
1444 all_change.update(arch_config_change[arch][name])
1445 _AddConfig(lines, name, arch_config_plus[arch][name],
1446 arch_config_minus[arch][name],
1447 arch_config_change[arch][name])
1448 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1449 #arch_summary[target] = '\n'.join(lines)
1452 _OutputConfigInfo(lines)
1454 for lines, targets in lines_by_target.items():
1457 Print('%s :' % ' '.join(sorted(targets)))
1458 _OutputConfigInfo(lines.split('\n'))
1461 # Save our updated information for the next call to this function
1462 self._base_board_dict = board_dict
1463 self._base_err_lines = err_lines
1464 self._base_warn_lines = warn_lines
1465 self._base_err_line_boards = err_line_boards
1466 self._base_warn_line_boards = warn_line_boards
1467 self._base_config = config
1468 self._base_environment = environment
1470 # Get a list of boards that did not get built, if needed
1472 for board in board_selected:
1473 if not board in board_dict:
1474 not_built.append(board)
1476 Print("Boards not built (%d): %s" % (len(not_built),
1477 ', '.join(not_built)))
1479 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1480 (board_dict, err_lines, err_line_boards, warn_lines,
1481 warn_line_boards, config, environment) = self.GetResultSummary(
1482 board_selected, commit_upto,
1483 read_func_sizes=self._show_bloat,
1484 read_config=self._show_config,
1485 read_environment=self._show_environment)
1487 msg = '%02d: %s' % (commit_upto + 1,
1488 commits[commit_upto].subject)
1489 Print(msg, colour=self.col.BLUE)
1490 self.PrintResultSummary(board_selected, board_dict,
1491 err_lines if self._show_errors else [], err_line_boards,
1492 warn_lines if self._show_errors else [], warn_line_boards,
1493 config, environment, self._show_sizes, self._show_detail,
1494 self._show_bloat, self._show_config, self._show_environment)
1496 def ShowSummary(self, commits, board_selected):
1497 """Show a build summary for U-Boot for a given board list.
1499 Reset the result summary, then repeatedly call GetResultSummary on
1500 each commit's results, then display the differences we see.
1503 commit: Commit objects to summarise
1504 board_selected: Dict containing boards to summarise
1506 self.commit_count = len(commits) if commits else 1
1507 self.commits = commits
1508 self.ResetResultSummary(board_selected)
1509 self._error_lines = 0
1511 for commit_upto in range(0, self.commit_count, self._step):
1512 self.ProduceResultSummary(commit_upto, commits, board_selected)
1513 if not self._error_lines:
1514 Print('(no errors to report)', colour=self.col.GREEN)
1517 def SetupBuild(self, board_selected, commits):
1518 """Set up ready to start a build.
1521 board_selected: Selected boards to build
1522 commits: Selected commits to build
1524 # First work out how many commits we will build
1525 count = (self.commit_count + self._step - 1) // self._step
1526 self.count = len(board_selected) * count
1527 self.upto = self.warned = self.fail = 0
1528 self._timestamps = collections.deque()
1530 def GetThreadDir(self, thread_num):
1531 """Get the directory path to the working dir for a thread.
1534 thread_num: Number of thread to check.
1536 if self.work_in_output:
1537 return self._working_dir
1538 return os.path.join(self._working_dir, '%02d' % thread_num)
1540 def _PrepareThread(self, thread_num, setup_git):
1541 """Prepare the working directory for a thread.
1543 This clones or fetches the repo into the thread's work directory.
1546 thread_num: Thread number (0, 1, ...)
1547 setup_git: True to set up a git repo clone
1549 thread_dir = self.GetThreadDir(thread_num)
1550 builderthread.Mkdir(thread_dir)
1551 git_dir = os.path.join(thread_dir, '.git')
1553 # Clone the repo if it doesn't already exist
1554 # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so
1555 # we have a private index but uses the origin repo's contents?
1556 if setup_git and self.git_dir:
1557 src_dir = os.path.abspath(self.git_dir)
1558 if os.path.exists(git_dir):
1559 Print('\rFetching repo for thread %d' % thread_num,
1561 gitutil.Fetch(git_dir, thread_dir)
1562 terminal.PrintClear()
1564 Print('\rCloning repo for thread %d' % thread_num,
1566 gitutil.Clone(src_dir, thread_dir)
1567 terminal.PrintClear()
1569 def _PrepareWorkingSpace(self, max_threads, setup_git):
1570 """Prepare the working directory for use.
1572 Set up the git repo for each thread.
1575 max_threads: Maximum number of threads we expect to need.
1576 setup_git: True to set up a git repo clone
1578 builderthread.Mkdir(self._working_dir)
1579 for thread in range(max_threads):
1580 self._PrepareThread(thread, setup_git)
1582 def _GetOutputSpaceRemovals(self):
1583 """Get the output directories ready to receive files.
1585 Figure out what needs to be deleted in the output directory before it
1586 can be used. We only delete old buildman directories which have the
1587 expected name pattern. See _GetOutputDir().
1590 List of full paths of directories to remove
1592 if not self.commits:
1595 for commit_upto in range(self.commit_count):
1596 dir_list.append(self._GetOutputDir(commit_upto))
1599 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1600 if dirname not in dir_list:
1601 leaf = dirname[len(self.base_dir) + 1:]
1602 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
1604 to_remove.append(dirname)
1607 def _PrepareOutputSpace(self):
1608 """Get the output directories ready to receive files.
1610 We delete any output directories which look like ones we need to
1611 create. Having left over directories is confusing when the user wants
1612 to check the output manually.
1614 to_remove = self._GetOutputSpaceRemovals()
1616 Print('Removing %d old build directories...' % len(to_remove),
1618 for dirname in to_remove:
1619 shutil.rmtree(dirname)
1620 terminal.PrintClear()
1622 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1623 """Build all commits for a list of boards
1626 commits: List of commits to be build, each a Commit object
1627 boards_selected: Dict of selected boards, key is target name,
1628 value is Board object
1629 keep_outputs: True to save build output files
1630 verbose: Display build results as they are completed
1633 - number of boards that failed to build
1634 - number of boards that issued warnings
1636 self.commit_count = len(commits) if commits else 1
1637 self.commits = commits
1638 self._verbose = verbose
1640 self.ResetResultSummary(board_selected)
1641 builderthread.Mkdir(self.base_dir, parents = True)
1642 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1643 commits is not None)
1644 self._PrepareOutputSpace()
1645 Print('\rStarting build...', newline=False)
1646 self.SetupBuild(board_selected, commits)
1647 self.ProcessResult(None)
1649 # Create jobs to build all commits for each board
1650 for brd in board_selected.values():
1651 job = builderthread.BuilderJob()
1653 job.commits = commits
1654 job.keep_outputs = keep_outputs
1655 job.work_in_output = self.work_in_output
1656 job.step = self._step
1659 term = threading.Thread(target=self.queue.join)
1660 term.setDaemon(True)
1662 while term.isAlive():
1665 # Wait until we have processed all output
1666 self.out_queue.join()
1669 msg = 'Completed: %d total built' % self.count
1670 if self.already_done:
1671 msg += ' (%d previously' % self.already_done
1672 if self.already_done != self.count:
1673 msg += ', %d newly' % (self.count - self.already_done)
1675 duration = datetime.now() - self._start_time
1676 if duration > timedelta(microseconds=1000000):
1677 if duration.microseconds >= 500000:
1678 duration = duration + timedelta(seconds=1)
1679 duration = duration - timedelta(microseconds=duration.microseconds)
1680 msg += ', duration %s' % duration
1683 return (self.fail, self.warned)