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 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: Number of commits to build
167 do_make: Method to call to invoke Make
168 fail: Number of builds that failed due to error
169 force_build: Force building even if a build already exists
170 force_config_on_failure: If a commit fails for a board, disable
171 incremental building for the next commit we build for that
172 board, so that we will see all warnings/errors again.
173 force_build_failures: If a previously-built build (i.e. built on
174 a previous run of buildman) is marked as failed, rebuild it.
175 git_dir: Git directory containing source repository
176 num_jobs: Number of jobs to run at once (passed to make as -j)
177 num_threads: Number of builder threads to run
178 out_queue: Queue of results to process
179 re_make_err: Compiled regular expression for ignore_lines
180 queue: Queue of jobs to run
181 threads: List of active threads
182 toolchains: Toolchains object to use for building
183 upto: Current commit number we are building (0.count-1)
184 warned: Number of builds that produced at least one warning
185 force_reconfig: Reconfigure U-Boot on each comiit. This disables
186 incremental building, where buildman reconfigures on the first
187 commit for a baord, and then just does an incremental build for
188 the following commits. In fact buildman will reconfigure and
189 retry for any failing commits, so generally the only effect of
190 this option is to slow things down.
191 in_tree: Build U-Boot in-tree instead of specifying an output
192 directory separate from the source code. This option is really
193 only useful for testing in-tree builds.
194 work_in_output: Use the output directory as the work directory and
195 don't write to a separate output directory.
196 thread_exceptions: List of exceptions raised by thread jobs
197 no_lto (bool): True to set the NO_LTO flag when building
198 reproducible_builds (bool): True to set SOURCE_DATE_EPOCH=0 for builds
201 _base_board_dict: Last-summarised Dict of boards
202 _base_err_lines: Last-summarised list of errors
203 _base_warn_lines: Last-summarised list of warnings
204 _build_period_us: Time taken for a single build (float object).
205 _complete_delay: Expected delay until completion (timedelta)
206 _next_delay_update: Next time we plan to display a progress update
208 _show_unknown: Show unknown boards (those not built) in summary
209 _start_time: Start time for the build
210 _timestamps: List of timestamps for the completion of the last
211 last _timestamp_count builds. Each is a datetime object.
212 _timestamp_count: Number of timestamps to keep in our list.
213 _working_dir: Base working directory containing all threads
214 _single_builder: BuilderThread object for the singer builder, if
215 threading is not being used
216 _terminated: Thread was terminated due to an error
217 _restarting_config: True if 'Restart config' is detected in output
218 _ide: Produce output suitable for an Integrated Development Environment,
219 i.e. dont emit progress information and put errors/warnings on stderr
222 """Records a build outcome for a single make invocation
225 rc: Outcome value (OUTCOME_...)
226 err_lines: List of error lines or [] if none
227 sizes: Dictionary of image size information, keyed by filename
228 - Each value is itself a dictionary containing
229 values for 'text', 'data' and 'bss', being the integer
230 size in bytes of each section.
231 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
232 value is itself a dictionary:
234 value: Size of function in bytes
235 config: Dictionary keyed by filename - e.g. '.config'. Each
236 value is itself a dictionary:
239 environment: Dictionary keyed by environment variable, Each
240 value is the value of environment variable.
242 def __init__(self, rc, err_lines, sizes, func_sizes, config,
245 self.err_lines = err_lines
247 self.func_sizes = func_sizes
249 self.environment = environment
251 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
252 gnu_make='make', checkout=True, show_unknown=True, step=1,
253 no_subdirs=False, full_path=False, verbose_build=False,
254 mrproper=False, per_board_out_dir=False,
255 config_only=False, squash_config_y=False,
256 warnings_as_errors=False, work_in_output=False,
257 test_thread_exceptions=False, adjust_cfg=None,
258 allow_missing=False, no_lto=False, reproducible_builds=False):
259 """Create a new Builder object
262 toolchains: Toolchains object to use for building
263 base_dir: Base directory to use for builder
264 git_dir: Git directory containing source repository
265 num_threads: Number of builder threads to run
266 num_jobs: Number of jobs to run at once (passed to make as -j)
267 gnu_make: the command name of GNU Make.
268 checkout: True to check out source, False to skip that step.
269 This is used for testing.
270 show_unknown: Show unknown boards (those not built) in summary
271 step: 1 to process every commit, n to process every nth commit
272 no_subdirs: Don't create subdirectories when building current
273 source for a single board
274 full_path: Return the full path in CROSS_COMPILE and don't set
276 verbose_build: Run build with V=1 and don't use 'make -s'
277 mrproper: Always run 'make mrproper' when configuring
278 per_board_out_dir: Build in a separate persistent directory per
279 board rather than a thread-specific directory
280 config_only: Only configure each build, don't build it
281 squash_config_y: Convert CONFIG options with the value 'y' to '1'
282 warnings_as_errors: Treat all compiler warnings as errors
283 work_in_output: Use the output directory as the work directory and
284 don't write to a separate output directory.
285 test_thread_exceptions: Uses for tests only, True to make the
286 threads raise an exception instead of reporting their result.
287 This simulates a failure in the code somewhere
288 adjust_cfg_list (list of str): List of changes to make to .config
289 file before building. Each is one of (where C is the config
290 option with or without the CONFIG_ prefix)
294 C=val to set the value of C (val must have quotes if C is
296 allow_missing: Run build with BINMAN_ALLOW_MISSING=1
297 no_lto (bool): True to set the NO_LTO flag when building
300 self.toolchains = toolchains
301 self.base_dir = base_dir
303 self._working_dir = base_dir
305 self._working_dir = os.path.join(base_dir, '.bm-work')
307 self.do_make = self.Make
308 self.gnu_make = gnu_make
309 self.checkout = checkout
310 self.num_threads = num_threads
311 self.num_jobs = num_jobs
312 self.already_done = 0
313 self.force_build = False
314 self.git_dir = git_dir
315 self._show_unknown = show_unknown
316 self._timestamp_count = 10
317 self._build_period_us = None
318 self._complete_delay = None
319 self._next_delay_update = datetime.now()
320 self._start_time = datetime.now()
321 self.force_config_on_failure = True
322 self.force_build_failures = False
323 self.force_reconfig = False
326 self._error_lines = 0
327 self.no_subdirs = no_subdirs
328 self.full_path = full_path
329 self.verbose_build = verbose_build
330 self.config_only = config_only
331 self.squash_config_y = squash_config_y
332 self.config_filenames = BASE_CONFIG_FILENAMES
333 self.work_in_output = work_in_output
334 self.adjust_cfg = adjust_cfg
335 self.allow_missing = allow_missing
338 self.reproducible_builds = reproducible_builds
340 if not self.squash_config_y:
341 self.config_filenames += EXTRA_CONFIG_FILENAMES
342 self._terminated = False
343 self._restarting_config = False
345 self.warnings_as_errors = warnings_as_errors
346 self.col = terminal.Color()
348 self._re_function = re.compile('(.*): In function.*')
349 self._re_files = re.compile('In file included from.*')
350 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
351 self._re_dtb_warning = re.compile('(.*): Warning .*')
352 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
353 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
354 re.MULTILINE | re.DOTALL)
356 self.thread_exceptions = []
357 self.test_thread_exceptions = test_thread_exceptions
359 self._single_builder = None
360 self.queue = queue.Queue()
361 self.out_queue = queue.Queue()
362 for i in range(self.num_threads):
363 t = builderthread.BuilderThread(
364 self, i, mrproper, per_board_out_dir,
365 test_exception=test_thread_exceptions)
368 self.threads.append(t)
370 t = builderthread.ResultThread(self)
373 self.threads.append(t)
375 self._single_builder = builderthread.BuilderThread(
376 self, -1, mrproper, per_board_out_dir)
378 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
379 self.re_make_err = re.compile('|'.join(ignore_lines))
381 # Handle existing graceful with SIGINT / Ctrl-C
382 signal.signal(signal.SIGINT, self.signal_handler)
385 """Get rid of all threads created by the builder"""
386 for t in self.threads:
389 def signal_handler(self, signal, frame):
392 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
393 show_detail=False, show_bloat=False,
394 list_error_boards=False, show_config=False,
395 show_environment=False, filter_dtb_warnings=False,
396 filter_migration_warnings=False, ide=False):
397 """Setup display options for the builder.
400 show_errors: True to show summarised error/warning info
401 show_sizes: Show size deltas
402 show_detail: Show size delta detail for each board if show_sizes
403 show_bloat: Show detail for each function
404 list_error_boards: Show the boards which caused each error/warning
405 show_config: Show config deltas
406 show_environment: Show environment deltas
407 filter_dtb_warnings: Filter out any warnings from the device-tree
409 filter_migration_warnings: Filter out any warnings about migrating
410 a board to driver model
411 ide: Create output that can be parsed by an IDE. There is no '+' prefix on
412 error lines and output on stderr stays on stderr.
414 self._show_errors = show_errors
415 self._show_sizes = show_sizes
416 self._show_detail = show_detail
417 self._show_bloat = show_bloat
418 self._list_error_boards = list_error_boards
419 self._show_config = show_config
420 self._show_environment = show_environment
421 self._filter_dtb_warnings = filter_dtb_warnings
422 self._filter_migration_warnings = filter_migration_warnings
425 def _AddTimestamp(self):
426 """Add a new timestamp to the list and record the build period.
428 The build period is the length of time taken to perform a single
429 build (one board, one commit).
432 self._timestamps.append(now)
433 count = len(self._timestamps)
434 delta = self._timestamps[-1] - self._timestamps[0]
435 seconds = delta.total_seconds()
437 # If we have enough data, estimate build period (time taken for a
438 # single build) and therefore completion time.
439 if count > 1 and self._next_delay_update < now:
440 self._next_delay_update = now + timedelta(seconds=2)
442 self._build_period = float(seconds) / count
443 todo = self.count - self.upto
444 self._complete_delay = timedelta(microseconds=
445 self._build_period * todo * 1000000)
447 self._complete_delay -= timedelta(
448 microseconds=self._complete_delay.microseconds)
451 self._timestamps.popleft()
454 def SelectCommit(self, commit, checkout=True):
455 """Checkout the selected commit for this build
458 if checkout and self.checkout:
459 gitutil.checkout(commit.hash)
461 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
465 commit: Commit object that is being built
466 brd: Board object that is being built
467 stage: Stage that we are at (mrproper, config, build)
468 cwd: Directory where make should be run
469 args: Arguments to pass to make
470 kwargs: Arguments to pass to command.run_pipe()
473 def check_output(stream, data):
474 if b'Restart config' in data:
475 self._restarting_config = True
477 # If we see 'Restart config' following by multiple errors
478 if self._restarting_config:
479 m = RE_NO_DEFAULT.findall(data)
481 # Number of occurences of each Kconfig item
482 multiple = [m.count(val) for val in set(m)]
484 # If any of them occur more than once, we have a loop
485 if [val for val in multiple if val > 1]:
486 self._terminated = True
490 self._restarting_config = False
491 self._terminated = False
492 cmd = [self.gnu_make] + list(args)
493 result = command.run_pipe([cmd], capture=True, capture_stderr=True,
494 cwd=cwd, raise_on_error=False, infile='/dev/null',
495 output_func=check_output, **kwargs)
499 result.stderr += '(** did you define an int/hex Kconfig with no default? **)'
501 if self.verbose_build:
502 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
503 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
506 def ProcessResult(self, result):
507 """Process the result of a build, showing progress information
510 result: A CommandResult object, which indicates the result for
513 col = terminal.Color()
515 target = result.brd.target
518 if result.return_code != 0:
522 if result.already_done:
523 self.already_done += 1
525 terminal.print_clear()
526 boards_selected = {target : result.brd}
527 self.ResetResultSummary(boards_selected)
528 self.ProduceResultSummary(result.commit_upto, self.commits,
531 target = '(starting)'
533 # Display separate counts for ok, warned and fail
534 ok = self.upto - self.warned - self.fail
535 line = '\r' + self.col.build(self.col.GREEN, '%5d' % ok)
536 line += self.col.build(self.col.YELLOW, '%5d' % self.warned)
537 line += self.col.build(self.col.RED, '%5d' % self.fail)
539 line += ' /%-5d ' % self.count
540 remaining = self.count - self.upto
542 line += self.col.build(self.col.MAGENTA, ' -%-5d ' % remaining)
546 # Add our current completion time estimate
548 if self._complete_delay:
549 line += '%s : ' % self._complete_delay
553 terminal.print_clear()
554 tprint(line, newline=False, limit_to_line=True)
556 def _GetOutputDir(self, commit_upto):
557 """Get the name of the output directory for a commit number
559 The output directory is typically .../<branch>/<commit>.
562 commit_upto: Commit number to use (0..self.count-1)
564 if self.work_in_output:
565 return self._working_dir
569 commit = self.commits[commit_upto]
570 subject = commit.subject.translate(trans_valid_chars)
571 # See _GetOutputSpaceRemovals() which parses this name
572 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
573 commit.hash, subject[:20]))
574 elif not self.no_subdirs:
575 commit_dir = 'current'
578 return os.path.join(self.base_dir, commit_dir)
580 def GetBuildDir(self, commit_upto, target):
581 """Get the name of the build directory for a commit number
583 The build directory is typically .../<branch>/<commit>/<target>.
586 commit_upto: Commit number to use (0..self.count-1)
589 output_dir = self._GetOutputDir(commit_upto)
590 if self.work_in_output:
592 return os.path.join(output_dir, target)
594 def GetDoneFile(self, commit_upto, target):
595 """Get the name of the done file for a commit number
598 commit_upto: Commit number to use (0..self.count-1)
601 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
603 def GetSizesFile(self, commit_upto, target):
604 """Get the name of the sizes file for a commit number
607 commit_upto: Commit number to use (0..self.count-1)
610 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
612 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
613 """Get the name of the funcsizes file for a commit number and ELF file
616 commit_upto: Commit number to use (0..self.count-1)
618 elf_fname: Filename of elf image
620 return os.path.join(self.GetBuildDir(commit_upto, target),
621 '%s.sizes' % elf_fname.replace('/', '-'))
623 def GetObjdumpFile(self, commit_upto, target, elf_fname):
624 """Get the name of the objdump file for a commit number and ELF file
627 commit_upto: Commit number to use (0..self.count-1)
629 elf_fname: Filename of elf image
631 return os.path.join(self.GetBuildDir(commit_upto, target),
632 '%s.objdump' % elf_fname.replace('/', '-'))
634 def GetErrFile(self, commit_upto, target):
635 """Get the name of the err file for a commit number
638 commit_upto: Commit number to use (0..self.count-1)
641 output_dir = self.GetBuildDir(commit_upto, target)
642 return os.path.join(output_dir, 'err')
644 def FilterErrors(self, lines):
645 """Filter out errors in which we have no interest
647 We should probably use map().
650 lines: List of error lines, each a string
652 New list with only interesting lines included
655 if self._filter_migration_warnings:
656 text = '\n'.join(lines)
657 text = self._re_migration_warning.sub('', text)
658 lines = text.splitlines()
660 if self.re_make_err.search(line):
662 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
664 out_lines.append(line)
667 def ReadFuncSizes(self, fname, fd):
668 """Read function sizes from the output of 'nm'
671 fd: File containing data to read
672 fname: Filename we are reading from (just for errors)
675 Dictionary containing size of each function in bytes, indexed by
679 for line in fd.readlines():
682 if line and len(parts) == 3:
683 size, type, name = line.split()
685 # function names begin with '.' on 64-bit powerpc
687 name = 'static.' + name.split('.')[0]
688 sym[name] = sym.get(name, 0) + int(size, 16)
691 def _ProcessConfig(self, fname):
692 """Read in a .config, autoconf.mk or autoconf.h file
694 This function handles all config file types. It ignores comments and
695 any #defines which don't start with CONFIG_.
698 fname: Filename to read
702 key: Config name (e.g. CONFIG_DM)
703 value: Config value (e.g. 1)
706 if os.path.exists(fname):
707 with open(fname) as fd:
710 if line.startswith('#define'):
711 values = line[8:].split(' ', 1)
716 value = '1' if self.squash_config_y else ''
717 if not key.startswith('CONFIG_'):
719 elif not line or line[0] in ['#', '*', '/']:
722 key, value = line.split('=', 1)
723 if self.squash_config_y and value == 'y':
728 def _ProcessEnvironment(self, fname):
729 """Read in a uboot.env file
731 This function reads in environment variables from a file.
734 fname: Filename to read
738 key: environment variable (e.g. bootlimit)
739 value: value of environment variable (e.g. 1)
742 if os.path.exists(fname):
743 with open(fname) as fd:
744 for line in fd.read().split('\0'):
746 key, value = line.split('=', 1)
747 environment[key] = value
749 # ignore lines we can't parse
753 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
754 read_config, read_environment):
755 """Work out the outcome of a build.
758 commit_upto: Commit number to check (0..n-1)
759 target: Target board to check
760 read_func_sizes: True to read function size information
761 read_config: True to read .config and autoconf.h files
762 read_environment: True to read uboot.env files
767 done_file = self.GetDoneFile(commit_upto, target)
768 sizes_file = self.GetSizesFile(commit_upto, target)
773 if os.path.exists(done_file):
774 with open(done_file, 'r') as fd:
776 return_code = int(fd.readline())
778 # The file may be empty due to running out of disk space.
782 err_file = self.GetErrFile(commit_upto, target)
783 if os.path.exists(err_file):
784 with open(err_file, 'r') as fd:
785 err_lines = self.FilterErrors(fd.readlines())
787 # Decide whether the build was ok, failed or created warnings
795 # Convert size information to our simple format
796 if os.path.exists(sizes_file):
797 with open(sizes_file, 'r') as fd:
798 for line in fd.readlines():
799 values = line.split()
802 rodata = int(values[6], 16)
804 'all' : int(values[0]) + int(values[1]) +
806 'text' : int(values[0]) - rodata,
807 'data' : int(values[1]),
808 'bss' : int(values[2]),
811 sizes[values[5]] = size_dict
814 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
815 for fname in glob.glob(pattern):
816 with open(fname, 'r') as fd:
817 dict_name = os.path.basename(fname).replace('.sizes',
819 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
822 output_dir = self.GetBuildDir(commit_upto, target)
823 for name in self.config_filenames:
824 fname = os.path.join(output_dir, name)
825 config[name] = self._ProcessConfig(fname)
828 output_dir = self.GetBuildDir(commit_upto, target)
829 fname = os.path.join(output_dir, 'uboot.env')
830 environment = self._ProcessEnvironment(fname)
832 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
835 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
837 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
838 read_config, read_environment):
839 """Calculate a summary of the results of building a commit.
842 board_selected: Dict containing boards to summarise
843 commit_upto: Commit number to summarize (0..self.count-1)
844 read_func_sizes: True to read function size information
845 read_config: True to read .config and autoconf.h files
846 read_environment: True to read uboot.env files
850 Dict containing boards which built this commit:
852 value: Builder.Outcome object
853 List containing a summary of error lines
854 Dict keyed by error line, containing a list of the Board
855 objects with that error
856 List containing a summary of warning lines
857 Dict keyed by error line, containing a list of the Board
858 objects with that warning
859 Dictionary keyed by board.target. Each value is a dictionary:
860 key: filename - e.g. '.config'
861 value is itself a dictionary:
864 Dictionary keyed by board.target. Each value is a dictionary:
865 key: environment variable
866 value: value of environment variable
868 def AddLine(lines_summary, lines_boards, line, board):
870 if line in lines_boards:
871 lines_boards[line].append(board)
873 lines_boards[line] = [board]
874 lines_summary.append(line)
877 err_lines_summary = []
878 err_lines_boards = {}
879 warn_lines_summary = []
880 warn_lines_boards = {}
884 for brd in boards_selected.values():
885 outcome = self.GetBuildOutcome(commit_upto, brd.target,
886 read_func_sizes, read_config,
888 board_dict[brd.target] = outcome
890 last_was_warning = False
891 for line in outcome.err_lines:
893 if (self._re_function.match(line) or
894 self._re_files.match(line)):
897 is_warning = (self._re_warning.match(line) or
898 self._re_dtb_warning.match(line))
899 is_note = self._re_note.match(line)
900 if is_warning or (last_was_warning and is_note):
902 AddLine(warn_lines_summary, warn_lines_boards,
904 AddLine(warn_lines_summary, warn_lines_boards,
908 AddLine(err_lines_summary, err_lines_boards,
910 AddLine(err_lines_summary, err_lines_boards,
912 last_was_warning = is_warning
914 tconfig = Config(self.config_filenames, brd.target)
915 for fname in self.config_filenames:
917 for key, value in outcome.config[fname].items():
918 tconfig.Add(fname, key, value)
919 config[brd.target] = tconfig
921 tenvironment = Environment(brd.target)
922 if outcome.environment:
923 for key, value in outcome.environment.items():
924 tenvironment.Add(key, value)
925 environment[brd.target] = tenvironment
927 return (board_dict, err_lines_summary, err_lines_boards,
928 warn_lines_summary, warn_lines_boards, config, environment)
930 def AddOutcome(self, board_dict, arch_list, changes, char, color):
931 """Add an output to our list of outcomes for each architecture
933 This simple function adds failing boards (changes) to the
934 relevant architecture string, so we can print the results out
935 sorted by architecture.
938 board_dict: Dict containing all boards
939 arch_list: Dict keyed by arch name. Value is a string containing
940 a list of board names which failed for that arch.
941 changes: List of boards to add to arch_list
942 color: terminal.Colour object
945 for target in changes:
946 if target in board_dict:
947 arch = board_dict[target].arch
950 str = self.col.build(color, ' ' + target)
951 if not arch in done_arch:
952 str = ' %s %s' % (self.col.build(color, char), str)
953 done_arch[arch] = True
954 if not arch in arch_list:
955 arch_list[arch] = str
957 arch_list[arch] += str
960 def ColourNum(self, num):
961 color = self.col.RED if num > 0 else self.col.GREEN
964 return self.col.build(color, str(num))
966 def ResetResultSummary(self, board_selected):
967 """Reset the results summary ready for use.
969 Set up the base board list to be all those selected, and set the
970 error lines to empty.
972 Following this, calls to PrintResultSummary() will use this
973 information to work out what has changed.
976 board_selected: Dict containing boards to summarise, keyed by
979 self._base_board_dict = {}
980 for brd in board_selected:
981 self._base_board_dict[brd] = Builder.Outcome(0, [], [], {}, {}, {})
982 self._base_err_lines = []
983 self._base_warn_lines = []
984 self._base_err_line_boards = {}
985 self._base_warn_line_boards = {}
986 self._base_config = None
987 self._base_environment = None
989 def PrintFuncSizeDetail(self, fname, old, new):
990 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
991 delta, common = [], {}
998 if name not in common:
1001 delta.append([-old[name], name])
1004 if name not in common:
1007 delta.append([new[name], name])
1010 diff = new.get(name, 0) - old.get(name, 0)
1012 grow, up = grow + 1, up + diff
1014 shrink, down = shrink + 1, down - diff
1015 delta.append([diff, name])
1020 args = [add, -remove, grow, -shrink, up, -down, up - down]
1021 if max(args) == 0 and min(args) == 0:
1023 args = [self.ColourNum(x) for x in args]
1025 tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
1026 tuple([indent, self.col.build(self.col.YELLOW, fname)] + args))
1027 tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
1029 for diff, name in delta:
1031 color = self.col.RED if diff > 0 else self.col.GREEN
1032 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
1033 old.get(name, '-'), new.get(name,'-'), diff)
1034 tprint(msg, colour=color)
1037 def PrintSizeDetail(self, target_list, show_bloat):
1038 """Show details size information for each board
1041 target_list: List of targets, each a dict containing:
1042 'target': Target name
1043 'total_diff': Total difference in bytes across all areas
1044 <part_name>: Difference for that part
1045 show_bloat: Show detail for each function
1047 targets_by_diff = sorted(target_list, reverse=True,
1048 key=lambda x: x['_total_diff'])
1049 for result in targets_by_diff:
1050 printed_target = False
1051 for name in sorted(result):
1053 if name.startswith('_'):
1056 color = self.col.RED if diff > 0 else self.col.GREEN
1057 msg = ' %s %+d' % (name, diff)
1058 if not printed_target:
1059 tprint('%10s %-15s:' % ('', result['_target']),
1061 printed_target = True
1062 tprint(msg, colour=color, newline=False)
1066 target = result['_target']
1067 outcome = result['_outcome']
1068 base_outcome = self._base_board_dict[target]
1069 for fname in outcome.func_sizes:
1070 self.PrintFuncSizeDetail(fname,
1071 base_outcome.func_sizes[fname],
1072 outcome.func_sizes[fname])
1075 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
1077 """Print a summary of image sizes broken down by section.
1079 The summary takes the form of one line per architecture. The
1080 line contains deltas for each of the sections (+ means the section
1081 got bigger, - means smaller). The numbers are the average number
1082 of bytes that a board in this section increased by.
1085 powerpc: (622 boards) text -0.0
1086 arm: (285 boards) text -0.0
1089 board_selected: Dict containing boards to summarise, keyed by
1091 board_dict: Dict containing boards for which we built this
1092 commit, keyed by board.target. The value is an Outcome object.
1093 show_detail: Show size delta detail for each board
1094 show_bloat: Show detail for each function
1099 # Calculate changes in size for different image parts
1100 # The previous sizes are in Board.sizes, for each board
1101 for target in board_dict:
1102 if target not in board_selected:
1104 base_sizes = self._base_board_dict[target].sizes
1105 outcome = board_dict[target]
1106 sizes = outcome.sizes
1108 # Loop through the list of images, creating a dict of size
1109 # changes for each image/part. We end up with something like
1110 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1111 # which means that U-Boot data increased by 5 bytes and SPL
1112 # text decreased by 4.
1113 err = {'_target' : target}
1115 if image in base_sizes:
1116 base_image = base_sizes[image]
1117 # Loop through the text, data, bss parts
1118 for part in sorted(sizes[image]):
1119 diff = sizes[image][part] - base_image[part]
1122 if image == 'u-boot':
1125 name = image + ':' + part
1127 arch = board_selected[target].arch
1128 if not arch in arch_count:
1129 arch_count[arch] = 1
1131 arch_count[arch] += 1
1133 pass # Only add to our list when we have some stats
1134 elif not arch in arch_list:
1135 arch_list[arch] = [err]
1137 arch_list[arch].append(err)
1139 # We now have a list of image size changes sorted by arch
1140 # Print out a summary of these
1141 for arch, target_list in arch_list.items():
1142 # Get total difference for each type
1144 for result in target_list:
1146 for name, diff in result.items():
1147 if name.startswith('_'):
1151 totals[name] += diff
1154 result['_total_diff'] = total
1155 result['_outcome'] = board_dict[result['_target']]
1157 count = len(target_list)
1158 printed_arch = False
1159 for name in sorted(totals):
1162 # Display the average difference in this name for this
1164 avg_diff = float(diff) / count
1165 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1166 msg = ' %s %+1.1f' % (name, avg_diff)
1167 if not printed_arch:
1168 tprint('%10s: (for %d/%d boards)' % (arch, count,
1169 arch_count[arch]), newline=False)
1171 tprint(msg, colour=color, newline=False)
1176 self.PrintSizeDetail(target_list, show_bloat)
1179 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1180 err_line_boards, warn_lines, warn_line_boards,
1181 config, environment, show_sizes, show_detail,
1182 show_bloat, show_config, show_environment):
1183 """Compare results with the base results and display delta.
1185 Only boards mentioned in board_selected will be considered. This
1186 function is intended to be called repeatedly with the results of
1187 each commit. It therefore shows a 'diff' between what it saw in
1188 the last call and what it sees now.
1191 board_selected: Dict containing boards to summarise, keyed by
1193 board_dict: Dict containing boards for which we built this
1194 commit, keyed by board.target. The value is an Outcome object.
1195 err_lines: A list of errors for this commit, or [] if there is
1196 none, or we don't want to print errors
1197 err_line_boards: Dict keyed by error line, containing a list of
1198 the Board objects with that error
1199 warn_lines: A list of warnings for this commit, or [] if there is
1200 none, or we don't want to print errors
1201 warn_line_boards: Dict keyed by warning line, containing a list of
1202 the Board objects with that warning
1203 config: Dictionary keyed by filename - e.g. '.config'. Each
1204 value is itself a dictionary:
1207 environment: Dictionary keyed by environment variable, Each
1208 value is the value of environment variable.
1209 show_sizes: Show image size deltas
1210 show_detail: Show size delta detail for each board if show_sizes
1211 show_bloat: Show detail for each function
1212 show_config: Show config changes
1213 show_environment: Show environment changes
1215 def _BoardList(line, line_boards):
1216 """Helper function to get a line of boards containing a line
1219 line: Error line to search for
1220 line_boards: boards to search, each a Board
1222 List of boards with that error line, or [] if the user has not
1223 requested such a list
1227 if self._list_error_boards:
1228 for brd in line_boards[line]:
1229 if not brd in board_set:
1234 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1236 """Calculate the required output based on changes in errors
1239 base_lines: List of errors/warnings for previous commit
1240 base_line_boards: Dict keyed by error line, containing a list
1241 of the Board objects with that error in the previous commit
1242 lines: List of errors/warning for this commit, each a str
1243 line_boards: Dict keyed by error line, containing a list
1244 of the Board objects with that error in this commit
1245 char: Character representing error ('') or warning ('w'). The
1246 broken ('+') or fixed ('-') characters are added in this
1251 List of ErrLine objects for 'better' lines
1252 List of ErrLine objects for 'worse' lines
1257 if line not in base_lines:
1258 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1260 worse_lines.append(errline)
1261 for line in base_lines:
1262 if line not in lines:
1263 errline = ErrLine(char + '-',
1264 _BoardList(line, base_line_boards), line)
1265 better_lines.append(errline)
1266 return better_lines, worse_lines
1268 def _CalcConfig(delta, name, config):
1269 """Calculate configuration changes
1272 delta: Type of the delta, e.g. '+'
1273 name: name of the file which changed (e.g. .config)
1274 config: configuration change dictionary
1278 String containing the configuration changes which can be
1282 for key in sorted(config.keys()):
1283 out += '%s=%s ' % (key, config[key])
1284 return '%s %s: %s' % (delta, name, out)
1286 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1287 """Add changes in configuration to a list
1290 lines: list to add to
1291 name: config file name
1292 config_plus: configurations added, dictionary
1295 config_minus: configurations removed, dictionary
1298 config_change: configurations changed, dictionary
1303 lines.append(_CalcConfig('+', name, config_plus))
1305 lines.append(_CalcConfig('-', name, config_minus))
1307 lines.append(_CalcConfig('c', name, config_change))
1309 def _OutputConfigInfo(lines):
1314 col = self.col.GREEN
1315 elif line[0] == '-':
1317 elif line[0] == 'c':
1318 col = self.col.YELLOW
1319 tprint(' ' + line, newline=True, colour=col)
1321 def _OutputErrLines(err_lines, colour):
1322 """Output the line of error/warning lines, if not empty
1324 Also increments self._error_lines if err_lines not empty
1327 err_lines: List of ErrLine objects, each an error or warning
1328 line, possibly including a list of boards with that
1330 colour: Colour to use for output
1334 for line in err_lines:
1335 names = [brd.target for brd in line.brds]
1336 board_str = ' '.join(names) if names else ''
1338 out = self.col.build(colour, line.char + '(')
1339 out += self.col.build(self.col.MAGENTA, board_str,
1341 out += self.col.build(colour, ') %s' % line.errline)
1343 out = self.col.build(colour, line.char + line.errline)
1344 out_list.append(out)
1345 tprint('\n'.join(out_list))
1346 self._error_lines += 1
1349 ok_boards = [] # List of boards fixed since last commit
1350 warn_boards = [] # List of boards with warnings since last commit
1351 err_boards = [] # List of new broken boards since last commit
1352 new_boards = [] # List of boards that didn't exist last time
1353 unknown_boards = [] # List of boards that were not built
1355 for target in board_dict:
1356 if target not in board_selected:
1359 # If the board was built last time, add its outcome to a list
1360 if target in self._base_board_dict:
1361 base_outcome = self._base_board_dict[target].rc
1362 outcome = board_dict[target]
1363 if outcome.rc == OUTCOME_UNKNOWN:
1364 unknown_boards.append(target)
1365 elif outcome.rc < base_outcome:
1366 if outcome.rc == OUTCOME_WARNING:
1367 warn_boards.append(target)
1369 ok_boards.append(target)
1370 elif outcome.rc > base_outcome:
1371 if outcome.rc == OUTCOME_WARNING:
1372 warn_boards.append(target)
1374 err_boards.append(target)
1376 new_boards.append(target)
1378 # Get a list of errors and warnings that have appeared, and disappeared
1379 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1380 self._base_err_line_boards, err_lines, err_line_boards, '')
1381 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1382 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1384 # For the IDE mode, print out all the output
1386 outcome = board_dict[target]
1387 for line in outcome.err_lines:
1388 sys.stderr.write(line)
1390 # Display results by arch
1391 elif any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1392 worse_err, better_err, worse_warn, better_warn)):
1394 self.AddOutcome(board_selected, arch_list, ok_boards, '',
1396 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1398 self.AddOutcome(board_selected, arch_list, err_boards, '+',
1400 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1401 if self._show_unknown:
1402 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
1404 for arch, target_list in arch_list.items():
1405 tprint('%10s: %s' % (arch, target_list))
1406 self._error_lines += 1
1407 _OutputErrLines(better_err, colour=self.col.GREEN)
1408 _OutputErrLines(worse_err, colour=self.col.RED)
1409 _OutputErrLines(better_warn, colour=self.col.CYAN)
1410 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
1413 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1416 if show_environment and self._base_environment:
1419 for target in board_dict:
1420 if target not in board_selected:
1423 tbase = self._base_environment[target]
1424 tenvironment = environment[target]
1425 environment_plus = {}
1426 environment_minus = {}
1427 environment_change = {}
1428 base = tbase.environment
1429 for key, value in tenvironment.environment.items():
1431 environment_plus[key] = value
1432 for key, value in base.items():
1433 if key not in tenvironment.environment:
1434 environment_minus[key] = value
1435 for key, value in base.items():
1436 new_value = tenvironment.environment.get(key)
1437 if new_value and value != new_value:
1438 desc = '%s -> %s' % (value, new_value)
1439 environment_change[key] = desc
1441 _AddConfig(lines, target, environment_plus, environment_minus,
1444 _OutputConfigInfo(lines)
1446 if show_config and self._base_config:
1448 arch_config_plus = {}
1449 arch_config_minus = {}
1450 arch_config_change = {}
1453 for target in board_dict:
1454 if target not in board_selected:
1456 arch = board_selected[target].arch
1457 if arch not in arch_list:
1458 arch_list.append(arch)
1460 for arch in arch_list:
1461 arch_config_plus[arch] = {}
1462 arch_config_minus[arch] = {}
1463 arch_config_change[arch] = {}
1464 for name in self.config_filenames:
1465 arch_config_plus[arch][name] = {}
1466 arch_config_minus[arch][name] = {}
1467 arch_config_change[arch][name] = {}
1469 for target in board_dict:
1470 if target not in board_selected:
1473 arch = board_selected[target].arch
1475 all_config_plus = {}
1476 all_config_minus = {}
1477 all_config_change = {}
1478 tbase = self._base_config[target]
1479 tconfig = config[target]
1481 for name in self.config_filenames:
1482 if not tconfig.config[name]:
1487 base = tbase.config[name]
1488 for key, value in tconfig.config[name].items():
1490 config_plus[key] = value
1491 all_config_plus[key] = value
1492 for key, value in base.items():
1493 if key not in tconfig.config[name]:
1494 config_minus[key] = value
1495 all_config_minus[key] = value
1496 for key, value in base.items():
1497 new_value = tconfig.config.get(key)
1498 if new_value and value != new_value:
1499 desc = '%s -> %s' % (value, new_value)
1500 config_change[key] = desc
1501 all_config_change[key] = desc
1503 arch_config_plus[arch][name].update(config_plus)
1504 arch_config_minus[arch][name].update(config_minus)
1505 arch_config_change[arch][name].update(config_change)
1507 _AddConfig(lines, name, config_plus, config_minus,
1509 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1511 summary[target] = '\n'.join(lines)
1513 lines_by_target = {}
1514 for target, lines in summary.items():
1515 if lines in lines_by_target:
1516 lines_by_target[lines].append(target)
1518 lines_by_target[lines] = [target]
1520 for arch in arch_list:
1525 for name in self.config_filenames:
1526 all_plus.update(arch_config_plus[arch][name])
1527 all_minus.update(arch_config_minus[arch][name])
1528 all_change.update(arch_config_change[arch][name])
1529 _AddConfig(lines, name, arch_config_plus[arch][name],
1530 arch_config_minus[arch][name],
1531 arch_config_change[arch][name])
1532 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1533 #arch_summary[target] = '\n'.join(lines)
1535 tprint('%s:' % arch)
1536 _OutputConfigInfo(lines)
1538 for lines, targets in lines_by_target.items():
1541 tprint('%s :' % ' '.join(sorted(targets)))
1542 _OutputConfigInfo(lines.split('\n'))
1545 # Save our updated information for the next call to this function
1546 self._base_board_dict = board_dict
1547 self._base_err_lines = err_lines
1548 self._base_warn_lines = warn_lines
1549 self._base_err_line_boards = err_line_boards
1550 self._base_warn_line_boards = warn_line_boards
1551 self._base_config = config
1552 self._base_environment = environment
1554 # Get a list of boards that did not get built, if needed
1556 for brd in board_selected:
1557 if not brd in board_dict:
1558 not_built.append(brd)
1560 tprint("Boards not built (%d): %s" % (len(not_built),
1561 ', '.join(not_built)))
1563 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1564 (board_dict, err_lines, err_line_boards, warn_lines,
1565 warn_line_boards, config, environment) = self.GetResultSummary(
1566 board_selected, commit_upto,
1567 read_func_sizes=self._show_bloat,
1568 read_config=self._show_config,
1569 read_environment=self._show_environment)
1571 msg = '%02d: %s' % (commit_upto + 1,
1572 commits[commit_upto].subject)
1573 tprint(msg, colour=self.col.BLUE)
1574 self.PrintResultSummary(board_selected, board_dict,
1575 err_lines if self._show_errors else [], err_line_boards,
1576 warn_lines if self._show_errors else [], warn_line_boards,
1577 config, environment, self._show_sizes, self._show_detail,
1578 self._show_bloat, self._show_config, self._show_environment)
1580 def ShowSummary(self, commits, board_selected):
1581 """Show a build summary for U-Boot for a given board list.
1583 Reset the result summary, then repeatedly call GetResultSummary on
1584 each commit's results, then display the differences we see.
1587 commit: Commit objects to summarise
1588 board_selected: Dict containing boards to summarise
1590 self.commit_count = len(commits) if commits else 1
1591 self.commits = commits
1592 self.ResetResultSummary(board_selected)
1593 self._error_lines = 0
1595 for commit_upto in range(0, self.commit_count, self._step):
1596 self.ProduceResultSummary(commit_upto, commits, board_selected)
1597 if not self._error_lines:
1598 tprint('(no errors to report)', colour=self.col.GREEN)
1601 def SetupBuild(self, board_selected, commits):
1602 """Set up ready to start a build.
1605 board_selected: Selected boards to build
1606 commits: Selected commits to build
1608 # First work out how many commits we will build
1609 count = (self.commit_count + self._step - 1) // self._step
1610 self.count = len(board_selected) * count
1611 self.upto = self.warned = self.fail = 0
1612 self._timestamps = collections.deque()
1614 def GetThreadDir(self, thread_num):
1615 """Get the directory path to the working dir for a thread.
1618 thread_num: Number of thread to check (-1 for main process, which
1621 if self.work_in_output:
1622 return self._working_dir
1623 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
1625 def _PrepareThread(self, thread_num, setup_git):
1626 """Prepare the working directory for a thread.
1628 This clones or fetches the repo into the thread's work directory.
1629 Optionally, it can create a linked working tree of the repo in the
1630 thread's work directory instead.
1633 thread_num: Thread number (0, 1, ...)
1635 'clone' to set up a git clone
1636 'worktree' to set up a git worktree
1638 thread_dir = self.GetThreadDir(thread_num)
1639 builderthread.Mkdir(thread_dir)
1640 git_dir = os.path.join(thread_dir, '.git')
1642 # Create a worktree or a git repo clone for this thread if it
1643 # doesn't already exist
1644 if setup_git and self.git_dir:
1645 src_dir = os.path.abspath(self.git_dir)
1646 if os.path.isdir(git_dir):
1647 # This is a clone of the src_dir repo, we can keep using
1648 # it but need to fetch from src_dir.
1649 tprint('\rFetching repo for thread %d' % thread_num,
1651 gitutil.fetch(git_dir, thread_dir)
1652 terminal.print_clear()
1653 elif os.path.isfile(git_dir):
1654 # This is a worktree of the src_dir repo, we don't need to
1655 # create it again or update it in any way.
1657 elif os.path.exists(git_dir):
1658 # Don't know what could trigger this, but we probably
1659 # can't create a git worktree/clone here.
1660 raise ValueError('Git dir %s exists, but is not a file '
1661 'or a directory.' % git_dir)
1662 elif setup_git == 'worktree':
1663 tprint('\rChecking out worktree for thread %d' % thread_num,
1665 gitutil.add_worktree(src_dir, thread_dir)
1666 terminal.print_clear()
1667 elif setup_git == 'clone' or setup_git == True:
1668 tprint('\rCloning repo for thread %d' % thread_num,
1670 gitutil.clone(src_dir, thread_dir)
1671 terminal.print_clear()
1673 raise ValueError("Can't setup git repo with %s." % setup_git)
1675 def _PrepareWorkingSpace(self, max_threads, setup_git):
1676 """Prepare the working directory for use.
1678 Set up the git repo for each thread. Creates a linked working tree
1679 if git-worktree is available, or clones the repo if it isn't.
1682 max_threads: Maximum number of threads we expect to need. If 0 then
1683 1 is set up, since the main process still needs somewhere to
1685 setup_git: True to set up a git worktree or a git clone
1687 builderthread.Mkdir(self._working_dir)
1688 if setup_git and self.git_dir:
1689 src_dir = os.path.abspath(self.git_dir)
1690 if gitutil.check_worktree_is_available(src_dir):
1691 setup_git = 'worktree'
1692 # If we previously added a worktree but the directory for it
1693 # got deleted, we need to prune its files from the repo so
1694 # that we can check out another in its place.
1695 gitutil.prune_worktrees(src_dir)
1699 # Always do at least one thread
1700 for thread in range(max(max_threads, 1)):
1701 self._PrepareThread(thread, setup_git)
1703 def _GetOutputSpaceRemovals(self):
1704 """Get the output directories ready to receive files.
1706 Figure out what needs to be deleted in the output directory before it
1707 can be used. We only delete old buildman directories which have the
1708 expected name pattern. See _GetOutputDir().
1711 List of full paths of directories to remove
1713 if not self.commits:
1716 for commit_upto in range(self.commit_count):
1717 dir_list.append(self._GetOutputDir(commit_upto))
1720 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1721 if dirname not in dir_list:
1722 leaf = dirname[len(self.base_dir) + 1:]
1723 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
1725 to_remove.append(dirname)
1728 def _PrepareOutputSpace(self):
1729 """Get the output directories ready to receive files.
1731 We delete any output directories which look like ones we need to
1732 create. Having left over directories is confusing when the user wants
1733 to check the output manually.
1735 to_remove = self._GetOutputSpaceRemovals()
1737 tprint('Removing %d old build directories...' % len(to_remove),
1739 for dirname in to_remove:
1740 shutil.rmtree(dirname)
1741 terminal.print_clear()
1743 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1744 """Build all commits for a list of boards
1747 commits: List of commits to be build, each a Commit object
1748 boards_selected: Dict of selected boards, key is target name,
1749 value is Board object
1750 keep_outputs: True to save build output files
1751 verbose: Display build results as they are completed
1754 - number of boards that failed to build
1755 - number of boards that issued warnings
1756 - list of thread exceptions raised
1758 self.commit_count = len(commits) if commits else 1
1759 self.commits = commits
1760 self._verbose = verbose
1762 self.ResetResultSummary(board_selected)
1763 builderthread.Mkdir(self.base_dir, parents = True)
1764 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1765 commits is not None)
1766 self._PrepareOutputSpace()
1768 tprint('\rStarting build...', newline=False)
1769 self.SetupBuild(board_selected, commits)
1770 self.ProcessResult(None)
1771 self.thread_exceptions = []
1772 # Create jobs to build all commits for each board
1773 for brd in board_selected.values():
1774 job = builderthread.BuilderJob()
1776 job.commits = commits
1777 job.keep_outputs = keep_outputs
1778 job.work_in_output = self.work_in_output
1779 job.adjust_cfg = self.adjust_cfg
1780 job.step = self._step
1781 if self.num_threads:
1784 self._single_builder.RunJob(job)
1786 if self.num_threads:
1787 term = threading.Thread(target=self.queue.join)
1788 term.setDaemon(True)
1790 while term.is_alive():
1793 # Wait until we have processed all output
1794 self.out_queue.join()
1798 msg = 'Completed: %d total built' % self.count
1799 if self.already_done:
1800 msg += ' (%d previously' % self.already_done
1801 if self.already_done != self.count:
1802 msg += ', %d newly' % (self.count - self.already_done)
1804 duration = datetime.now() - self._start_time
1805 if duration > timedelta(microseconds=1000000):
1806 if duration.microseconds >= 500000:
1807 duration = duration + timedelta(seconds=1)
1808 duration = duration - timedelta(microseconds=duration.microseconds)
1809 rate = float(self.count) / duration.total_seconds()
1810 msg += ', duration %s, rate %1.2f' % (duration, rate)
1812 if self.thread_exceptions:
1813 tprint('Failed: %d thread exceptions' % len(self.thread_exceptions),
1814 colour=self.col.RED)
1816 return (self.fail, self.warned, self.thread_exceptions)