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 gitutil
23 from u_boot_pylib import command
24 from u_boot_pylib import terminal
25 from u_boot_pylib.terminal import tprint
27 # This indicates an new int or hex Kconfig property with no default
28 # It hangs the build since the 'conf' tool cannot proceed without valid input.
30 # We get a repeat sequence of something like this:
32 # Break things (BREAK_ME) [] (NEW)
33 # Error in reading or end of file.
35 # which indicates that BREAK_ME has an empty default
36 RE_NO_DEFAULT = re.compile(b'\((\w+)\) \[] \(NEW\)')
41 Please see README for user documentation, and you should be familiar with
42 that before trying to make sense of this.
44 Buildman works by keeping the machine as busy as possible, building different
45 commits for different boards on multiple CPUs at once.
47 The source repo (self.git_dir) contains all the commits to be built. Each
48 thread works on a single board at a time. It checks out the first commit,
49 configures it for that board, then builds it. Then it checks out the next
50 commit and builds it (typically without re-configuring). When it runs out
51 of commits, it gets another job from the builder and starts again with that
54 Clearly the builder threads could work either way - they could check out a
55 commit and then built it for all boards. Using separate directories for each
56 commit/board pair they could leave their build product around afterwards
59 The intent behind building a single board for multiple commits, is to make
60 use of incremental builds. Since each commit is built incrementally from
61 the previous one, builds are faster. Reconfiguring for a different board
62 removes all intermediate object files.
64 Many threads can be working at once, but each has its own working directory.
65 When a thread finishes a build, it puts the output files into a result
68 The base directory used by buildman is normally '../<branch>', i.e.
69 a directory higher than the source repository and named after the branch
72 Within the base directory, we have one subdirectory for each commit. Within
73 that is one subdirectory for each board. Within that is the build output for
74 that commit/board combination.
76 Buildman also create working directories for each thread, in a .bm-work/
77 subdirectory in the base dir.
79 As an example, say we are building branch 'us-net' for boards 'sandbox' and
80 'seaboard', and say that us-net has two commits. We will have directories
83 us-net/ base directory
84 01_g4ed4ebc_net--Add-tftp-speed-/
89 02_g4ed4ebc_net--Check-tftp-comp/
95 00/ working directory for thread 0 (contains source checkout)
97 01/ working directory for thread 1
100 u-boot/ source directory
104 """Holds information about a particular error line we are outputing
106 char: Character representation: '+': error, '-': fixed error, 'w+': warning,
108 boards: List of Board objects which have line in the error/warning output
109 errline: The text of the error line
111 ErrLine = collections.namedtuple('ErrLine', 'char,brds,errline')
113 # Possible build outcomes
114 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4))
116 # Translate a commit subject into a valid filename (and handle unicode)
117 trans_valid_chars = str.maketrans('/: ', '---')
119 BASE_CONFIG_FILENAMES = [
120 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
123 EXTRA_CONFIG_FILENAMES = [
124 '.config', '.config-spl', '.config-tpl',
125 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
126 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
130 """Holds information about configuration settings for a board."""
131 def __init__(self, config_filename, target):
134 for fname in config_filename:
135 self.config[fname] = {}
137 def add(self, fname, key, value):
138 self.config[fname][key] = value
142 for fname in self.config:
143 for key, value in self.config[fname].items():
145 val = val ^ hash(key) & hash(value)
149 """Holds information about environment variables for a board."""
150 def __init__(self, target):
152 self.environment = {}
154 def add(self, key, value):
155 self.environment[key] = value
158 """Class for building U-Boot for a particular commit.
160 Public members: (many should ->private)
161 already_done: Number of builds already completed
162 base_dir: Base directory to use for builder
163 checkout: True to check out source, False to skip that step.
164 This is used for testing.
165 col: terminal.Color() object
166 count: Total number of commits to build, which is the number of commits
167 multiplied by the number of boards
168 do_make: Method to call to invoke Make
169 fail: Number of builds that failed due to error
170 force_build: Force building even if a build already exists
171 force_config_on_failure: If a commit fails for a board, disable
172 incremental building for the next commit we build for that
173 board, so that we will see all warnings/errors again.
174 force_build_failures: If a previously-built build (i.e. built on
175 a previous run of buildman) is marked as failed, rebuild it.
176 git_dir: Git directory containing source repository
177 num_jobs: Number of jobs to run at once (passed to make as -j)
178 num_threads: Number of builder threads to run
179 out_queue: Queue of results to process
180 re_make_err: Compiled regular expression for ignore_lines
181 queue: Queue of jobs to run
182 threads: List of active threads
183 toolchains: Toolchains object to use for building
184 upto: Current commit number we are building (0.count-1)
185 warned: Number of builds that produced at least one warning
186 force_reconfig: Reconfigure U-Boot on each comiit. This disables
187 incremental building, where buildman reconfigures on the first
188 commit for a baord, and then just does an incremental build for
189 the following commits. In fact buildman will reconfigure and
190 retry for any failing commits, so generally the only effect of
191 this option is to slow things down.
192 in_tree: Build U-Boot in-tree instead of specifying an output
193 directory separate from the source code. This option is really
194 only useful for testing in-tree builds.
195 work_in_output: Use the output directory as the work directory and
196 don't write to a separate output directory.
197 thread_exceptions: List of exceptions raised by thread jobs
198 no_lto (bool): True to set the NO_LTO flag when building
199 reproducible_builds (bool): True to set SOURCE_DATE_EPOCH=0 for builds
202 _base_board_dict: Last-summarised Dict of boards
203 _base_err_lines: Last-summarised list of errors
204 _base_warn_lines: Last-summarised list of warnings
205 _build_period_us: Time taken for a single build (float object).
206 _complete_delay: Expected delay until completion (timedelta)
207 _next_delay_update: Next time we plan to display a progress update
209 _show_unknown: Show unknown boards (those not built) in summary
210 _start_time: Start time for the build
211 _timestamps: List of timestamps for the completion of the last
212 last _timestamp_count builds. Each is a datetime object.
213 _timestamp_count: Number of timestamps to keep in our list.
214 _working_dir: Base working directory containing all threads
215 _single_builder: BuilderThread object for the singer builder, if
216 threading is not being used
217 _terminated: Thread was terminated due to an error
218 _restarting_config: True if 'Restart config' is detected in output
219 _ide: Produce output suitable for an Integrated Development Environment,
220 i.e. dont emit progress information and put errors/warnings on stderr
223 """Records a build outcome for a single make invocation
226 rc: Outcome value (OUTCOME_...)
227 err_lines: List of error lines or [] if none
228 sizes: Dictionary of image size information, keyed by filename
229 - Each value is itself a dictionary containing
230 values for 'text', 'data' and 'bss', being the integer
231 size in bytes of each section.
232 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
233 value is itself a dictionary:
235 value: Size of function in bytes
236 config: Dictionary keyed by filename - e.g. '.config'. Each
237 value is itself a dictionary:
240 environment: Dictionary keyed by environment variable, Each
241 value is the value of environment variable.
243 def __init__(self, rc, err_lines, sizes, func_sizes, config,
246 self.err_lines = err_lines
248 self.func_sizes = func_sizes
250 self.environment = environment
252 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
253 gnu_make='make', checkout=True, show_unknown=True, step=1,
254 no_subdirs=False, full_path=False, verbose_build=False,
255 mrproper=False, per_board_out_dir=False,
256 config_only=False, squash_config_y=False,
257 warnings_as_errors=False, work_in_output=False,
258 test_thread_exceptions=False, adjust_cfg=None,
259 allow_missing=False, no_lto=False, reproducible_builds=False,
260 force_build=False, force_build_failures=False,
261 force_reconfig=False, in_tree=False,
262 force_config_on_failure=False, make_func=None):
263 """Create a new Builder object
266 toolchains: Toolchains object to use for building
267 base_dir: Base directory to use for builder
268 git_dir: Git directory containing source repository
269 num_threads: Number of builder threads to run
270 num_jobs: Number of jobs to run at once (passed to make as -j)
271 gnu_make: the command name of GNU Make.
272 checkout: True to check out source, False to skip that step.
273 This is used for testing.
274 show_unknown: Show unknown boards (those not built) in summary
275 step: 1 to process every commit, n to process every nth commit
276 no_subdirs: Don't create subdirectories when building current
277 source for a single board
278 full_path: Return the full path in CROSS_COMPILE and don't set
280 verbose_build: Run build with V=1 and don't use 'make -s'
281 mrproper: Always run 'make mrproper' when configuring
282 per_board_out_dir: Build in a separate persistent directory per
283 board rather than a thread-specific directory
284 config_only: Only configure each build, don't build it
285 squash_config_y: Convert CONFIG options with the value 'y' to '1'
286 warnings_as_errors: Treat all compiler warnings as errors
287 work_in_output: Use the output directory as the work directory and
288 don't write to a separate output directory.
289 test_thread_exceptions: Uses for tests only, True to make the
290 threads raise an exception instead of reporting their result.
291 This simulates a failure in the code somewhere
292 adjust_cfg_list (list of str): List of changes to make to .config
293 file before building. Each is one of (where C is the config
294 option with or without the CONFIG_ prefix)
298 C=val to set the value of C (val must have quotes if C is
300 allow_missing: Run build with BINMAN_ALLOW_MISSING=1
301 no_lto (bool): True to set the NO_LTO flag when building
302 force_build (bool): Rebuild even commits that are already built
303 force_build_failures (bool): Rebuild commits that have not been
304 built, or failed to build
305 force_reconfig (bool): Reconfigure on each commit
306 in_tree (bool): Bulid in tree instead of out-of-tree
307 force_config_on_failure (bool): Reconfigure the build before
308 retrying a failed build
309 make_func (function): Function to call to run 'make'
311 self.toolchains = toolchains
312 self.base_dir = base_dir
314 self._working_dir = base_dir
316 self._working_dir = os.path.join(base_dir, '.bm-work')
318 self.do_make = make_func or self.make
319 self.gnu_make = gnu_make
320 self.checkout = checkout
321 self.num_threads = num_threads
322 self.num_jobs = num_jobs
323 self.already_done = 0
324 self.force_build = False
325 self.git_dir = git_dir
326 self._show_unknown = show_unknown
327 self._timestamp_count = 10
328 self._build_period_us = None
329 self._complete_delay = None
330 self._next_delay_update = datetime.now()
331 self._start_time = datetime.now()
333 self._error_lines = 0
334 self.no_subdirs = no_subdirs
335 self.full_path = full_path
336 self.verbose_build = verbose_build
337 self.config_only = config_only
338 self.squash_config_y = squash_config_y
339 self.config_filenames = BASE_CONFIG_FILENAMES
340 self.work_in_output = work_in_output
341 self.adjust_cfg = adjust_cfg
342 self.allow_missing = allow_missing
345 self.reproducible_builds = reproducible_builds
346 self.force_build = force_build
347 self.force_build_failures = force_build_failures
348 self.force_reconfig = force_reconfig
349 self.in_tree = in_tree
350 self.force_config_on_failure = force_config_on_failure
352 if not self.squash_config_y:
353 self.config_filenames += EXTRA_CONFIG_FILENAMES
354 self._terminated = False
355 self._restarting_config = False
357 self.warnings_as_errors = warnings_as_errors
358 self.col = terminal.Color()
360 self._re_function = re.compile('(.*): In function.*')
361 self._re_files = re.compile('In file included from.*')
362 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
363 self._re_dtb_warning = re.compile('(.*): Warning .*')
364 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
365 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
366 re.MULTILINE | re.DOTALL)
368 self.thread_exceptions = []
369 self.test_thread_exceptions = test_thread_exceptions
371 self._single_builder = None
372 self.queue = queue.Queue()
373 self.out_queue = queue.Queue()
374 for i in range(self.num_threads):
375 t = builderthread.BuilderThread(
376 self, i, mrproper, per_board_out_dir,
377 test_exception=test_thread_exceptions)
380 self.threads.append(t)
382 t = builderthread.ResultThread(self)
385 self.threads.append(t)
387 self._single_builder = builderthread.BuilderThread(
388 self, -1, mrproper, per_board_out_dir)
390 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
391 self.re_make_err = re.compile('|'.join(ignore_lines))
393 # Handle existing graceful with SIGINT / Ctrl-C
394 signal.signal(signal.SIGINT, self.signal_handler)
397 """Get rid of all threads created by the builder"""
398 for t in self.threads:
401 def signal_handler(self, signal, frame):
404 def set_display_options(self, show_errors=False, show_sizes=False,
405 show_detail=False, show_bloat=False,
406 list_error_boards=False, show_config=False,
407 show_environment=False, filter_dtb_warnings=False,
408 filter_migration_warnings=False, ide=False):
409 """Setup display options for the builder.
412 show_errors: True to show summarised error/warning info
413 show_sizes: Show size deltas
414 show_detail: Show size delta detail for each board if show_sizes
415 show_bloat: Show detail for each function
416 list_error_boards: Show the boards which caused each error/warning
417 show_config: Show config deltas
418 show_environment: Show environment deltas
419 filter_dtb_warnings: Filter out any warnings from the device-tree
421 filter_migration_warnings: Filter out any warnings about migrating
422 a board to driver model
423 ide: Create output that can be parsed by an IDE. There is no '+' prefix on
424 error lines and output on stderr stays on stderr.
426 self._show_errors = show_errors
427 self._show_sizes = show_sizes
428 self._show_detail = show_detail
429 self._show_bloat = show_bloat
430 self._list_error_boards = list_error_boards
431 self._show_config = show_config
432 self._show_environment = show_environment
433 self._filter_dtb_warnings = filter_dtb_warnings
434 self._filter_migration_warnings = filter_migration_warnings
437 def _add_timestamp(self):
438 """Add a new timestamp to the list and record the build period.
440 The build period is the length of time taken to perform a single
441 build (one board, one commit).
444 self._timestamps.append(now)
445 count = len(self._timestamps)
446 delta = self._timestamps[-1] - self._timestamps[0]
447 seconds = delta.total_seconds()
449 # If we have enough data, estimate build period (time taken for a
450 # single build) and therefore completion time.
451 if count > 1 and self._next_delay_update < now:
452 self._next_delay_update = now + timedelta(seconds=2)
454 self._build_period = float(seconds) / count
455 todo = self.count - self.upto
456 self._complete_delay = timedelta(microseconds=
457 self._build_period * todo * 1000000)
459 self._complete_delay -= timedelta(
460 microseconds=self._complete_delay.microseconds)
463 self._timestamps.popleft()
466 def select_commit(self, commit, checkout=True):
467 """Checkout the selected commit for this build
470 if checkout and self.checkout:
471 gitutil.checkout(commit.hash)
473 def make(self, commit, brd, stage, cwd, *args, **kwargs):
477 commit: Commit object that is being built
478 brd: Board object that is being built
479 stage: Stage that we are at (mrproper, config, build)
480 cwd: Directory where make should be run
481 args: Arguments to pass to make
482 kwargs: Arguments to pass to command.run_pipe()
485 def check_output(stream, data):
486 if b'Restart config' in data:
487 self._restarting_config = True
489 # If we see 'Restart config' following by multiple errors
490 if self._restarting_config:
491 m = RE_NO_DEFAULT.findall(data)
493 # Number of occurences of each Kconfig item
494 multiple = [m.count(val) for val in set(m)]
496 # If any of them occur more than once, we have a loop
497 if [val for val in multiple if val > 1]:
498 self._terminated = True
502 self._restarting_config = False
503 self._terminated = False
504 cmd = [self.gnu_make] + list(args)
505 result = command.run_pipe([cmd], capture=True, capture_stderr=True,
506 cwd=cwd, raise_on_error=False, infile='/dev/null',
507 output_func=check_output, **kwargs)
511 result.stderr += '(** did you define an int/hex Kconfig with no default? **)'
513 if self.verbose_build:
514 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
515 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
518 def process_result(self, result):
519 """Process the result of a build, showing progress information
522 result: A CommandResult object, which indicates the result for
525 col = terminal.Color()
527 target = result.brd.target
530 if result.return_code != 0:
534 if result.already_done:
535 self.already_done += 1
537 terminal.print_clear()
538 boards_selected = {target : result.brd}
539 self.reset_result_summary(boards_selected)
540 self.produce_result_summary(result.commit_upto, self.commits,
543 target = '(starting)'
545 # Display separate counts for ok, warned and fail
546 ok = self.upto - self.warned - self.fail
547 line = '\r' + self.col.build(self.col.GREEN, '%5d' % ok)
548 line += self.col.build(self.col.YELLOW, '%5d' % self.warned)
549 line += self.col.build(self.col.RED, '%5d' % self.fail)
551 line += ' /%-5d ' % self.count
552 remaining = self.count - self.upto
554 line += self.col.build(self.col.MAGENTA, ' -%-5d ' % remaining)
558 # Add our current completion time estimate
559 self._add_timestamp()
560 if self._complete_delay:
561 line += '%s : ' % self._complete_delay
565 terminal.print_clear()
566 tprint(line, newline=False, limit_to_line=True)
568 def get_output_dir(self, commit_upto):
569 """Get the name of the output directory for a commit number
571 The output directory is typically .../<branch>/<commit>.
574 commit_upto: Commit number to use (0..self.count-1)
576 if self.work_in_output:
577 return self._working_dir
581 commit = self.commits[commit_upto]
582 subject = commit.subject.translate(trans_valid_chars)
583 # See _get_output_space_removals() which parses this name
584 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
585 commit.hash, subject[:20]))
586 elif not self.no_subdirs:
587 commit_dir = 'current'
590 return os.path.join(self.base_dir, commit_dir)
592 def get_build_dir(self, commit_upto, target):
593 """Get the name of the build directory for a commit number
595 The build directory is typically .../<branch>/<commit>/<target>.
598 commit_upto: Commit number to use (0..self.count-1)
601 output_dir = self.get_output_dir(commit_upto)
602 if self.work_in_output:
604 return os.path.join(output_dir, target)
606 def get_done_file(self, commit_upto, target):
607 """Get the name of the done file for a commit number
610 commit_upto: Commit number to use (0..self.count-1)
613 return os.path.join(self.get_build_dir(commit_upto, target), 'done')
615 def get_sizes_file(self, commit_upto, target):
616 """Get the name of the sizes file for a commit number
619 commit_upto: Commit number to use (0..self.count-1)
622 return os.path.join(self.get_build_dir(commit_upto, target), 'sizes')
624 def get_func_sizes_file(self, commit_upto, target, elf_fname):
625 """Get the name of the funcsizes file for a commit number and ELF file
628 commit_upto: Commit number to use (0..self.count-1)
630 elf_fname: Filename of elf image
632 return os.path.join(self.get_build_dir(commit_upto, target),
633 '%s.sizes' % elf_fname.replace('/', '-'))
635 def get_objdump_file(self, commit_upto, target, elf_fname):
636 """Get the name of the objdump file for a commit number and ELF file
639 commit_upto: Commit number to use (0..self.count-1)
641 elf_fname: Filename of elf image
643 return os.path.join(self.get_build_dir(commit_upto, target),
644 '%s.objdump' % elf_fname.replace('/', '-'))
646 def get_err_file(self, commit_upto, target):
647 """Get the name of the err file for a commit number
650 commit_upto: Commit number to use (0..self.count-1)
653 output_dir = self.get_build_dir(commit_upto, target)
654 return os.path.join(output_dir, 'err')
656 def filter_errors(self, lines):
657 """Filter out errors in which we have no interest
659 We should probably use map().
662 lines: List of error lines, each a string
664 New list with only interesting lines included
667 if self._filter_migration_warnings:
668 text = '\n'.join(lines)
669 text = self._re_migration_warning.sub('', text)
670 lines = text.splitlines()
672 if self.re_make_err.search(line):
674 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
676 out_lines.append(line)
679 def read_func_sizes(self, fname, fd):
680 """Read function sizes from the output of 'nm'
683 fd: File containing data to read
684 fname: Filename we are reading from (just for errors)
687 Dictionary containing size of each function in bytes, indexed by
691 for line in fd.readlines():
694 if line and len(parts) == 3:
695 size, type, name = line.split()
697 # function names begin with '.' on 64-bit powerpc
699 name = 'static.' + name.split('.')[0]
700 sym[name] = sym.get(name, 0) + int(size, 16)
703 def _process_config(self, fname):
704 """Read in a .config, autoconf.mk or autoconf.h file
706 This function handles all config file types. It ignores comments and
707 any #defines which don't start with CONFIG_.
710 fname: Filename to read
714 key: Config name (e.g. CONFIG_DM)
715 value: Config value (e.g. 1)
718 if os.path.exists(fname):
719 with open(fname) as fd:
722 if line.startswith('#define'):
723 values = line[8:].split(' ', 1)
728 value = '1' if self.squash_config_y else ''
729 if not key.startswith('CONFIG_'):
731 elif not line or line[0] in ['#', '*', '/']:
734 key, value = line.split('=', 1)
735 if self.squash_config_y and value == 'y':
740 def _process_environment(self, fname):
741 """Read in a uboot.env file
743 This function reads in environment variables from a file.
746 fname: Filename to read
750 key: environment variable (e.g. bootlimit)
751 value: value of environment variable (e.g. 1)
754 if os.path.exists(fname):
755 with open(fname) as fd:
756 for line in fd.read().split('\0'):
758 key, value = line.split('=', 1)
759 environment[key] = value
761 # ignore lines we can't parse
765 def get_build_outcome(self, commit_upto, target, read_func_sizes,
766 read_config, read_environment):
767 """Work out the outcome of a build.
770 commit_upto: Commit number to check (0..n-1)
771 target: Target board to check
772 read_func_sizes: True to read function size information
773 read_config: True to read .config and autoconf.h files
774 read_environment: True to read uboot.env files
779 done_file = self.get_done_file(commit_upto, target)
780 sizes_file = self.get_sizes_file(commit_upto, target)
785 if os.path.exists(done_file):
786 with open(done_file, 'r') as fd:
788 return_code = int(fd.readline())
790 # The file may be empty due to running out of disk space.
794 err_file = self.get_err_file(commit_upto, target)
795 if os.path.exists(err_file):
796 with open(err_file, 'r') as fd:
797 err_lines = self.filter_errors(fd.readlines())
799 # Decide whether the build was ok, failed or created warnings
807 # Convert size information to our simple format
808 if os.path.exists(sizes_file):
809 with open(sizes_file, 'r') as fd:
810 for line in fd.readlines():
811 values = line.split()
814 rodata = int(values[6], 16)
816 'all' : int(values[0]) + int(values[1]) +
818 'text' : int(values[0]) - rodata,
819 'data' : int(values[1]),
820 'bss' : int(values[2]),
823 sizes[values[5]] = size_dict
826 pattern = self.get_func_sizes_file(commit_upto, target, '*')
827 for fname in glob.glob(pattern):
828 with open(fname, 'r') as fd:
829 dict_name = os.path.basename(fname).replace('.sizes',
831 func_sizes[dict_name] = self.read_func_sizes(fname, fd)
834 output_dir = self.get_build_dir(commit_upto, target)
835 for name in self.config_filenames:
836 fname = os.path.join(output_dir, name)
837 config[name] = self._process_config(fname)
840 output_dir = self.get_build_dir(commit_upto, target)
841 fname = os.path.join(output_dir, 'uboot.env')
842 environment = self._process_environment(fname)
844 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
847 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
849 def get_result_summary(self, boards_selected, commit_upto, read_func_sizes,
850 read_config, read_environment):
851 """Calculate a summary of the results of building a commit.
854 board_selected: Dict containing boards to summarise
855 commit_upto: Commit number to summarize (0..self.count-1)
856 read_func_sizes: True to read function size information
857 read_config: True to read .config and autoconf.h files
858 read_environment: True to read uboot.env files
862 Dict containing boards which built this commit:
864 value: Builder.Outcome object
865 List containing a summary of error lines
866 Dict keyed by error line, containing a list of the Board
867 objects with that error
868 List containing a summary of warning lines
869 Dict keyed by error line, containing a list of the Board
870 objects with that warning
871 Dictionary keyed by board.target. Each value is a dictionary:
872 key: filename - e.g. '.config'
873 value is itself a dictionary:
876 Dictionary keyed by board.target. Each value is a dictionary:
877 key: environment variable
878 value: value of environment variable
880 def add_line(lines_summary, lines_boards, line, board):
882 if line in lines_boards:
883 lines_boards[line].append(board)
885 lines_boards[line] = [board]
886 lines_summary.append(line)
889 err_lines_summary = []
890 err_lines_boards = {}
891 warn_lines_summary = []
892 warn_lines_boards = {}
896 for brd in boards_selected.values():
897 outcome = self.get_build_outcome(commit_upto, brd.target,
898 read_func_sizes, read_config,
900 board_dict[brd.target] = outcome
902 last_was_warning = False
903 for line in outcome.err_lines:
905 if (self._re_function.match(line) or
906 self._re_files.match(line)):
909 is_warning = (self._re_warning.match(line) or
910 self._re_dtb_warning.match(line))
911 is_note = self._re_note.match(line)
912 if is_warning or (last_was_warning and is_note):
914 add_line(warn_lines_summary, warn_lines_boards,
916 add_line(warn_lines_summary, warn_lines_boards,
920 add_line(err_lines_summary, err_lines_boards,
922 add_line(err_lines_summary, err_lines_boards,
924 last_was_warning = is_warning
926 tconfig = Config(self.config_filenames, brd.target)
927 for fname in self.config_filenames:
929 for key, value in outcome.config[fname].items():
930 tconfig.add(fname, key, value)
931 config[brd.target] = tconfig
933 tenvironment = Environment(brd.target)
934 if outcome.environment:
935 for key, value in outcome.environment.items():
936 tenvironment.add(key, value)
937 environment[brd.target] = tenvironment
939 return (board_dict, err_lines_summary, err_lines_boards,
940 warn_lines_summary, warn_lines_boards, config, environment)
942 def add_outcome(self, board_dict, arch_list, changes, char, color):
943 """Add an output to our list of outcomes for each architecture
945 This simple function adds failing boards (changes) to the
946 relevant architecture string, so we can print the results out
947 sorted by architecture.
950 board_dict: Dict containing all boards
951 arch_list: Dict keyed by arch name. Value is a string containing
952 a list of board names which failed for that arch.
953 changes: List of boards to add to arch_list
954 color: terminal.Colour object
957 for target in changes:
958 if target in board_dict:
959 arch = board_dict[target].arch
962 str = self.col.build(color, ' ' + target)
963 if not arch in done_arch:
964 str = ' %s %s' % (self.col.build(color, char), str)
965 done_arch[arch] = True
966 if not arch in arch_list:
967 arch_list[arch] = str
969 arch_list[arch] += str
972 def colour_num(self, num):
973 color = self.col.RED if num > 0 else self.col.GREEN
976 return self.col.build(color, str(num))
978 def reset_result_summary(self, board_selected):
979 """Reset the results summary ready for use.
981 Set up the base board list to be all those selected, and set the
982 error lines to empty.
984 Following this, calls to print_result_summary() will use this
985 information to work out what has changed.
988 board_selected: Dict containing boards to summarise, keyed by
991 self._base_board_dict = {}
992 for brd in board_selected:
993 self._base_board_dict[brd] = Builder.Outcome(0, [], [], {}, {}, {})
994 self._base_err_lines = []
995 self._base_warn_lines = []
996 self._base_err_line_boards = {}
997 self._base_warn_line_boards = {}
998 self._base_config = None
999 self._base_environment = None
1001 def print_func_size_detail(self, fname, old, new):
1002 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
1003 delta, common = [], {}
1010 if name not in common:
1013 delta.append([-old[name], name])
1016 if name not in common:
1019 delta.append([new[name], name])
1022 diff = new.get(name, 0) - old.get(name, 0)
1024 grow, up = grow + 1, up + diff
1026 shrink, down = shrink + 1, down - diff
1027 delta.append([diff, name])
1032 args = [add, -remove, grow, -shrink, up, -down, up - down]
1033 if max(args) == 0 and min(args) == 0:
1035 args = [self.colour_num(x) for x in args]
1037 tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
1038 tuple([indent, self.col.build(self.col.YELLOW, fname)] + args))
1039 tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
1041 for diff, name in delta:
1043 color = self.col.RED if diff > 0 else self.col.GREEN
1044 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1045 old.get(name, '-'), new.get(name,'-'), diff)
1046 tprint(msg, colour=color)
1049 def print_size_detail(self, target_list, show_bloat):
1050 """Show details size information for each board
1053 target_list: List of targets, each a dict containing:
1054 'target': Target name
1055 'total_diff': Total difference in bytes across all areas
1056 <part_name>: Difference for that part
1057 show_bloat: Show detail for each function
1059 targets_by_diff = sorted(target_list, reverse=True,
1060 key=lambda x: x['_total_diff'])
1061 for result in targets_by_diff:
1062 printed_target = False
1063 for name in sorted(result):
1065 if name.startswith('_'):
1068 color = self.col.RED if diff > 0 else self.col.GREEN
1069 msg = ' %s %+d' % (name, diff)
1070 if not printed_target:
1071 tprint('%10s %-15s:' % ('', result['_target']),
1073 printed_target = True
1074 tprint(msg, colour=color, newline=False)
1078 target = result['_target']
1079 outcome = result['_outcome']
1080 base_outcome = self._base_board_dict[target]
1081 for fname in outcome.func_sizes:
1082 self.print_func_size_detail(fname,
1083 base_outcome.func_sizes[fname],
1084 outcome.func_sizes[fname])
1087 def print_size_summary(self, board_selected, board_dict, show_detail,
1089 """Print a summary of image sizes broken down by section.
1091 The summary takes the form of one line per architecture. The
1092 line contains deltas for each of the sections (+ means the section
1093 got bigger, - means smaller). The numbers are the average number
1094 of bytes that a board in this section increased by.
1097 powerpc: (622 boards) text -0.0
1098 arm: (285 boards) text -0.0
1101 board_selected: Dict containing boards to summarise, keyed by
1103 board_dict: Dict containing boards for which we built this
1104 commit, keyed by board.target. The value is an Outcome object.
1105 show_detail: Show size delta detail for each board
1106 show_bloat: Show detail for each function
1111 # Calculate changes in size for different image parts
1112 # The previous sizes are in Board.sizes, for each board
1113 for target in board_dict:
1114 if target not in board_selected:
1116 base_sizes = self._base_board_dict[target].sizes
1117 outcome = board_dict[target]
1118 sizes = outcome.sizes
1120 # Loop through the list of images, creating a dict of size
1121 # changes for each image/part. We end up with something like
1122 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1123 # which means that U-Boot data increased by 5 bytes and SPL
1124 # text decreased by 4.
1125 err = {'_target' : target}
1127 if image in base_sizes:
1128 base_image = base_sizes[image]
1129 # Loop through the text, data, bss parts
1130 for part in sorted(sizes[image]):
1131 diff = sizes[image][part] - base_image[part]
1134 if image == 'u-boot':
1137 name = image + ':' + part
1139 arch = board_selected[target].arch
1140 if not arch in arch_count:
1141 arch_count[arch] = 1
1143 arch_count[arch] += 1
1145 pass # Only add to our list when we have some stats
1146 elif not arch in arch_list:
1147 arch_list[arch] = [err]
1149 arch_list[arch].append(err)
1151 # We now have a list of image size changes sorted by arch
1152 # Print out a summary of these
1153 for arch, target_list in arch_list.items():
1154 # Get total difference for each type
1156 for result in target_list:
1158 for name, diff in result.items():
1159 if name.startswith('_'):
1163 totals[name] += diff
1166 result['_total_diff'] = total
1167 result['_outcome'] = board_dict[result['_target']]
1169 count = len(target_list)
1170 printed_arch = False
1171 for name in sorted(totals):
1174 # Display the average difference in this name for this
1176 avg_diff = float(diff) / count
1177 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1178 msg = ' %s %+1.1f' % (name, avg_diff)
1179 if not printed_arch:
1180 tprint('%10s: (for %d/%d boards)' % (arch, count,
1181 arch_count[arch]), newline=False)
1183 tprint(msg, colour=color, newline=False)
1188 self.print_size_detail(target_list, show_bloat)
1191 def print_result_summary(self, board_selected, board_dict, err_lines,
1192 err_line_boards, warn_lines, warn_line_boards,
1193 config, environment, show_sizes, show_detail,
1194 show_bloat, show_config, show_environment):
1195 """Compare results with the base results and display delta.
1197 Only boards mentioned in board_selected will be considered. This
1198 function is intended to be called repeatedly with the results of
1199 each commit. It therefore shows a 'diff' between what it saw in
1200 the last call and what it sees now.
1203 board_selected: Dict containing boards to summarise, keyed by
1205 board_dict: Dict containing boards for which we built this
1206 commit, keyed by board.target. The value is an Outcome object.
1207 err_lines: A list of errors for this commit, or [] if there is
1208 none, or we don't want to print errors
1209 err_line_boards: Dict keyed by error line, containing a list of
1210 the Board objects with that error
1211 warn_lines: A list of warnings for this commit, or [] if there is
1212 none, or we don't want to print errors
1213 warn_line_boards: Dict keyed by warning line, containing a list of
1214 the Board objects with that warning
1215 config: Dictionary keyed by filename - e.g. '.config'. Each
1216 value is itself a dictionary:
1219 environment: Dictionary keyed by environment variable, Each
1220 value is the value of environment variable.
1221 show_sizes: Show image size deltas
1222 show_detail: Show size delta detail for each board if show_sizes
1223 show_bloat: Show detail for each function
1224 show_config: Show config changes
1225 show_environment: Show environment changes
1227 def _board_list(line, line_boards):
1228 """Helper function to get a line of boards containing a line
1231 line: Error line to search for
1232 line_boards: boards to search, each a Board
1234 List of boards with that error line, or [] if the user has not
1235 requested such a list
1239 if self._list_error_boards:
1240 for brd in line_boards[line]:
1241 if not brd in board_set:
1246 def _calc_error_delta(base_lines, base_line_boards, lines, line_boards,
1248 """Calculate the required output based on changes in errors
1251 base_lines: List of errors/warnings for previous commit
1252 base_line_boards: Dict keyed by error line, containing a list
1253 of the Board objects with that error in the previous commit
1254 lines: List of errors/warning for this commit, each a str
1255 line_boards: Dict keyed by error line, containing a list
1256 of the Board objects with that error in this commit
1257 char: Character representing error ('') or warning ('w'). The
1258 broken ('+') or fixed ('-') characters are added in this
1263 List of ErrLine objects for 'better' lines
1264 List of ErrLine objects for 'worse' lines
1269 if line not in base_lines:
1270 errline = ErrLine(char + '+', _board_list(line, line_boards),
1272 worse_lines.append(errline)
1273 for line in base_lines:
1274 if line not in lines:
1275 errline = ErrLine(char + '-',
1276 _board_list(line, base_line_boards), line)
1277 better_lines.append(errline)
1278 return better_lines, worse_lines
1280 def _calc_config(delta, name, config):
1281 """Calculate configuration changes
1284 delta: Type of the delta, e.g. '+'
1285 name: name of the file which changed (e.g. .config)
1286 config: configuration change dictionary
1290 String containing the configuration changes which can be
1294 for key in sorted(config.keys()):
1295 out += '%s=%s ' % (key, config[key])
1296 return '%s %s: %s' % (delta, name, out)
1298 def _add_config(lines, name, config_plus, config_minus, config_change):
1299 """Add changes in configuration to a list
1302 lines: list to add to
1303 name: config file name
1304 config_plus: configurations added, dictionary
1307 config_minus: configurations removed, dictionary
1310 config_change: configurations changed, dictionary
1315 lines.append(_calc_config('+', name, config_plus))
1317 lines.append(_calc_config('-', name, config_minus))
1319 lines.append(_calc_config('c', name, config_change))
1321 def _output_config_info(lines):
1326 col = self.col.GREEN
1327 elif line[0] == '-':
1329 elif line[0] == 'c':
1330 col = self.col.YELLOW
1331 tprint(' ' + line, newline=True, colour=col)
1333 def _output_err_lines(err_lines, colour):
1334 """Output the line of error/warning lines, if not empty
1336 Also increments self._error_lines if err_lines not empty
1339 err_lines: List of ErrLine objects, each an error or warning
1340 line, possibly including a list of boards with that
1342 colour: Colour to use for output
1346 for line in err_lines:
1347 names = [brd.target for brd in line.brds]
1348 board_str = ' '.join(names) if names else ''
1350 out = self.col.build(colour, line.char + '(')
1351 out += self.col.build(self.col.MAGENTA, board_str,
1353 out += self.col.build(colour, ') %s' % line.errline)
1355 out = self.col.build(colour, line.char + line.errline)
1356 out_list.append(out)
1357 tprint('\n'.join(out_list))
1358 self._error_lines += 1
1361 ok_boards = [] # List of boards fixed since last commit
1362 warn_boards = [] # List of boards with warnings since last commit
1363 err_boards = [] # List of new broken boards since last commit
1364 new_boards = [] # List of boards that didn't exist last time
1365 unknown_boards = [] # List of boards that were not built
1367 for target in board_dict:
1368 if target not in board_selected:
1371 # If the board was built last time, add its outcome to a list
1372 if target in self._base_board_dict:
1373 base_outcome = self._base_board_dict[target].rc
1374 outcome = board_dict[target]
1375 if outcome.rc == OUTCOME_UNKNOWN:
1376 unknown_boards.append(target)
1377 elif outcome.rc < base_outcome:
1378 if outcome.rc == OUTCOME_WARNING:
1379 warn_boards.append(target)
1381 ok_boards.append(target)
1382 elif outcome.rc > base_outcome:
1383 if outcome.rc == OUTCOME_WARNING:
1384 warn_boards.append(target)
1386 err_boards.append(target)
1388 new_boards.append(target)
1390 # Get a list of errors and warnings that have appeared, and disappeared
1391 better_err, worse_err = _calc_error_delta(self._base_err_lines,
1392 self._base_err_line_boards, err_lines, err_line_boards, '')
1393 better_warn, worse_warn = _calc_error_delta(self._base_warn_lines,
1394 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1396 # For the IDE mode, print out all the output
1398 outcome = board_dict[target]
1399 for line in outcome.err_lines:
1400 sys.stderr.write(line)
1402 # Display results by arch
1403 elif any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1404 worse_err, better_err, worse_warn, better_warn)):
1406 self.add_outcome(board_selected, arch_list, ok_boards, '',
1408 self.add_outcome(board_selected, arch_list, warn_boards, 'w+',
1410 self.add_outcome(board_selected, arch_list, err_boards, '+',
1412 self.add_outcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1413 if self._show_unknown:
1414 self.add_outcome(board_selected, arch_list, unknown_boards, '?',
1416 for arch, target_list in arch_list.items():
1417 tprint('%10s: %s' % (arch, target_list))
1418 self._error_lines += 1
1419 _output_err_lines(better_err, colour=self.col.GREEN)
1420 _output_err_lines(worse_err, colour=self.col.RED)
1421 _output_err_lines(better_warn, colour=self.col.CYAN)
1422 _output_err_lines(worse_warn, colour=self.col.YELLOW)
1425 self.print_size_summary(board_selected, board_dict, show_detail,
1428 if show_environment and self._base_environment:
1431 for target in board_dict:
1432 if target not in board_selected:
1435 tbase = self._base_environment[target]
1436 tenvironment = environment[target]
1437 environment_plus = {}
1438 environment_minus = {}
1439 environment_change = {}
1440 base = tbase.environment
1441 for key, value in tenvironment.environment.items():
1443 environment_plus[key] = value
1444 for key, value in base.items():
1445 if key not in tenvironment.environment:
1446 environment_minus[key] = value
1447 for key, value in base.items():
1448 new_value = tenvironment.environment.get(key)
1449 if new_value and value != new_value:
1450 desc = '%s -> %s' % (value, new_value)
1451 environment_change[key] = desc
1453 _add_config(lines, target, environment_plus, environment_minus,
1456 _output_config_info(lines)
1458 if show_config and self._base_config:
1460 arch_config_plus = {}
1461 arch_config_minus = {}
1462 arch_config_change = {}
1465 for target in board_dict:
1466 if target not in board_selected:
1468 arch = board_selected[target].arch
1469 if arch not in arch_list:
1470 arch_list.append(arch)
1472 for arch in arch_list:
1473 arch_config_plus[arch] = {}
1474 arch_config_minus[arch] = {}
1475 arch_config_change[arch] = {}
1476 for name in self.config_filenames:
1477 arch_config_plus[arch][name] = {}
1478 arch_config_minus[arch][name] = {}
1479 arch_config_change[arch][name] = {}
1481 for target in board_dict:
1482 if target not in board_selected:
1485 arch = board_selected[target].arch
1487 all_config_plus = {}
1488 all_config_minus = {}
1489 all_config_change = {}
1490 tbase = self._base_config[target]
1491 tconfig = config[target]
1493 for name in self.config_filenames:
1494 if not tconfig.config[name]:
1499 base = tbase.config[name]
1500 for key, value in tconfig.config[name].items():
1502 config_plus[key] = value
1503 all_config_plus[key] = value
1504 for key, value in base.items():
1505 if key not in tconfig.config[name]:
1506 config_minus[key] = value
1507 all_config_minus[key] = value
1508 for key, value in base.items():
1509 new_value = tconfig.config.get(key)
1510 if new_value and value != new_value:
1511 desc = '%s -> %s' % (value, new_value)
1512 config_change[key] = desc
1513 all_config_change[key] = desc
1515 arch_config_plus[arch][name].update(config_plus)
1516 arch_config_minus[arch][name].update(config_minus)
1517 arch_config_change[arch][name].update(config_change)
1519 _add_config(lines, name, config_plus, config_minus,
1521 _add_config(lines, 'all', all_config_plus, all_config_minus,
1523 summary[target] = '\n'.join(lines)
1525 lines_by_target = {}
1526 for target, lines in summary.items():
1527 if lines in lines_by_target:
1528 lines_by_target[lines].append(target)
1530 lines_by_target[lines] = [target]
1532 for arch in arch_list:
1537 for name in self.config_filenames:
1538 all_plus.update(arch_config_plus[arch][name])
1539 all_minus.update(arch_config_minus[arch][name])
1540 all_change.update(arch_config_change[arch][name])
1541 _add_config(lines, name, arch_config_plus[arch][name],
1542 arch_config_minus[arch][name],
1543 arch_config_change[arch][name])
1544 _add_config(lines, 'all', all_plus, all_minus, all_change)
1545 #arch_summary[target] = '\n'.join(lines)
1547 tprint('%s:' % arch)
1548 _output_config_info(lines)
1550 for lines, targets in lines_by_target.items():
1553 tprint('%s :' % ' '.join(sorted(targets)))
1554 _output_config_info(lines.split('\n'))
1557 # Save our updated information for the next call to this function
1558 self._base_board_dict = board_dict
1559 self._base_err_lines = err_lines
1560 self._base_warn_lines = warn_lines
1561 self._base_err_line_boards = err_line_boards
1562 self._base_warn_line_boards = warn_line_boards
1563 self._base_config = config
1564 self._base_environment = environment
1566 # Get a list of boards that did not get built, if needed
1568 for brd in board_selected:
1569 if not brd in board_dict:
1570 not_built.append(brd)
1572 tprint("Boards not built (%d): %s" % (len(not_built),
1573 ', '.join(not_built)))
1575 def produce_result_summary(self, commit_upto, commits, board_selected):
1576 (board_dict, err_lines, err_line_boards, warn_lines,
1577 warn_line_boards, config, environment) = self.get_result_summary(
1578 board_selected, commit_upto,
1579 read_func_sizes=self._show_bloat,
1580 read_config=self._show_config,
1581 read_environment=self._show_environment)
1583 msg = '%02d: %s' % (commit_upto + 1,
1584 commits[commit_upto].subject)
1585 tprint(msg, colour=self.col.BLUE)
1586 self.print_result_summary(board_selected, board_dict,
1587 err_lines if self._show_errors else [], err_line_boards,
1588 warn_lines if self._show_errors else [], warn_line_boards,
1589 config, environment, self._show_sizes, self._show_detail,
1590 self._show_bloat, self._show_config, self._show_environment)
1592 def show_summary(self, commits, board_selected):
1593 """Show a build summary for U-Boot for a given board list.
1595 Reset the result summary, then repeatedly call GetResultSummary on
1596 each commit's results, then display the differences we see.
1599 commit: Commit objects to summarise
1600 board_selected: Dict containing boards to summarise
1602 self.commit_count = len(commits) if commits else 1
1603 self.commits = commits
1604 self.reset_result_summary(board_selected)
1605 self._error_lines = 0
1607 for commit_upto in range(0, self.commit_count, self._step):
1608 self.produce_result_summary(commit_upto, commits, board_selected)
1609 if not self._error_lines:
1610 tprint('(no errors to report)', colour=self.col.GREEN)
1613 def setup_build(self, board_selected, commits):
1614 """Set up ready to start a build.
1617 board_selected: Selected boards to build
1618 commits: Selected commits to build
1620 # First work out how many commits we will build
1621 count = (self.commit_count + self._step - 1) // self._step
1622 self.count = len(board_selected) * count
1623 self.upto = self.warned = self.fail = 0
1624 self._timestamps = collections.deque()
1626 def get_thread_dir(self, thread_num):
1627 """Get the directory path to the working dir for a thread.
1630 thread_num: Number of thread to check (-1 for main process, which
1633 if self.work_in_output:
1634 return self._working_dir
1635 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
1637 def _prepare_thread(self, thread_num, setup_git):
1638 """Prepare the working directory for a thread.
1640 This clones or fetches the repo into the thread's work directory.
1641 Optionally, it can create a linked working tree of the repo in the
1642 thread's work directory instead.
1645 thread_num: Thread number (0, 1, ...)
1647 'clone' to set up a git clone
1648 'worktree' to set up a git worktree
1650 thread_dir = self.get_thread_dir(thread_num)
1651 builderthread.mkdir(thread_dir)
1652 git_dir = os.path.join(thread_dir, '.git')
1654 # Create a worktree or a git repo clone for this thread if it
1655 # doesn't already exist
1656 if setup_git and self.git_dir:
1657 src_dir = os.path.abspath(self.git_dir)
1658 if os.path.isdir(git_dir):
1659 # This is a clone of the src_dir repo, we can keep using
1660 # it but need to fetch from src_dir.
1661 tprint('\rFetching repo for thread %d' % thread_num,
1663 gitutil.fetch(git_dir, thread_dir)
1664 terminal.print_clear()
1665 elif os.path.isfile(git_dir):
1666 # This is a worktree of the src_dir repo, we don't need to
1667 # create it again or update it in any way.
1669 elif os.path.exists(git_dir):
1670 # Don't know what could trigger this, but we probably
1671 # can't create a git worktree/clone here.
1672 raise ValueError('Git dir %s exists, but is not a file '
1673 'or a directory.' % git_dir)
1674 elif setup_git == 'worktree':
1675 tprint('\rChecking out worktree for thread %d' % thread_num,
1677 gitutil.add_worktree(src_dir, thread_dir)
1678 terminal.print_clear()
1679 elif setup_git == 'clone' or setup_git == True:
1680 tprint('\rCloning repo for thread %d' % thread_num,
1682 gitutil.clone(src_dir, thread_dir)
1683 terminal.print_clear()
1685 raise ValueError("Can't setup git repo with %s." % setup_git)
1687 def _prepare_working_space(self, max_threads, setup_git):
1688 """Prepare the working directory for use.
1690 Set up the git repo for each thread. Creates a linked working tree
1691 if git-worktree is available, or clones the repo if it isn't.
1694 max_threads: Maximum number of threads we expect to need. If 0 then
1695 1 is set up, since the main process still needs somewhere to
1697 setup_git: True to set up a git worktree or a git clone
1699 builderthread.mkdir(self._working_dir)
1700 if setup_git and self.git_dir:
1701 src_dir = os.path.abspath(self.git_dir)
1702 if gitutil.check_worktree_is_available(src_dir):
1703 setup_git = 'worktree'
1704 # If we previously added a worktree but the directory for it
1705 # got deleted, we need to prune its files from the repo so
1706 # that we can check out another in its place.
1707 gitutil.prune_worktrees(src_dir)
1711 # Always do at least one thread
1712 for thread in range(max(max_threads, 1)):
1713 self._prepare_thread(thread, setup_git)
1715 def _get_output_space_removals(self):
1716 """Get the output directories ready to receive files.
1718 Figure out what needs to be deleted in the output directory before it
1719 can be used. We only delete old buildman directories which have the
1720 expected name pattern. See get_output_dir().
1723 List of full paths of directories to remove
1725 if not self.commits:
1728 for commit_upto in range(self.commit_count):
1729 dir_list.append(self.get_output_dir(commit_upto))
1732 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1733 if dirname not in dir_list:
1734 leaf = dirname[len(self.base_dir) + 1:]
1735 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
1737 to_remove.append(dirname)
1740 def _prepare_output_space(self):
1741 """Get the output directories ready to receive files.
1743 We delete any output directories which look like ones we need to
1744 create. Having left over directories is confusing when the user wants
1745 to check the output manually.
1747 to_remove = self._get_output_space_removals()
1749 tprint('Removing %d old build directories...' % len(to_remove),
1751 for dirname in to_remove:
1752 shutil.rmtree(dirname)
1753 terminal.print_clear()
1755 def build_boards(self, commits, board_selected, keep_outputs, verbose):
1756 """Build all commits for a list of boards
1759 commits: List of commits to be build, each a Commit object
1760 boards_selected: Dict of selected boards, key is target name,
1761 value is Board object
1762 keep_outputs: True to save build output files
1763 verbose: Display build results as they are completed
1766 - number of boards that failed to build
1767 - number of boards that issued warnings
1768 - list of thread exceptions raised
1770 self.commit_count = len(commits) if commits else 1
1771 self.commits = commits
1772 self._verbose = verbose
1774 self.reset_result_summary(board_selected)
1775 builderthread.mkdir(self.base_dir, parents = True)
1776 self._prepare_working_space(min(self.num_threads, len(board_selected)),
1777 commits is not None)
1778 self._prepare_output_space()
1780 tprint('\rStarting build...', newline=False)
1781 self.setup_build(board_selected, commits)
1782 self.process_result(None)
1783 self.thread_exceptions = []
1784 # Create jobs to build all commits for each board
1785 for brd in board_selected.values():
1786 job = builderthread.BuilderJob()
1788 job.commits = commits
1789 job.keep_outputs = keep_outputs
1790 job.work_in_output = self.work_in_output
1791 job.adjust_cfg = self.adjust_cfg
1792 job.step = self._step
1793 if self.num_threads:
1796 self._single_builder.run_job(job)
1798 if self.num_threads:
1799 term = threading.Thread(target=self.queue.join)
1800 term.setDaemon(True)
1802 while term.is_alive():
1805 # Wait until we have processed all output
1806 self.out_queue.join()
1810 msg = 'Completed: %d total built' % self.count
1811 if self.already_done:
1812 msg += ' (%d previously' % self.already_done
1813 if self.already_done != self.count:
1814 msg += ', %d newly' % (self.count - self.already_done)
1816 duration = datetime.now() - self._start_time
1817 if duration > timedelta(microseconds=1000000):
1818 if duration.microseconds >= 500000:
1819 duration = duration + timedelta(seconds=1)
1820 duration = duration - timedelta(microseconds=duration.microseconds)
1821 rate = float(self.count) / duration.total_seconds()
1822 msg += ', duration %s, rate %1.2f' % (duration, rate)
1824 if self.thread_exceptions:
1825 tprint('Failed: %d thread exceptions' % len(self.thread_exceptions),
1826 colour=self.col.RED)
1828 return (self.fail, self.warned, self.thread_exceptions)