1 # SPDX-License-Identifier: GPL-2.0+
2 # Copyright (c) 2013 The Chromium OS Authors.
8 from datetime import datetime, timedelta
20 from buildman import builderthread
21 from buildman import toolchain
22 from patman import command
23 from patman import gitutil
24 from patman import terminal
25 from patman.terminal import Print
30 Please see README for user documentation, and you should be familiar with
31 that before trying to make sense of this.
33 Buildman works by keeping the machine as busy as possible, building different
34 commits for different boards on multiple CPUs at once.
36 The source repo (self.git_dir) contains all the commits to be built. Each
37 thread works on a single board at a time. It checks out the first commit,
38 configures it for that board, then builds it. Then it checks out the next
39 commit and builds it (typically without re-configuring). When it runs out
40 of commits, it gets another job from the builder and starts again with that
43 Clearly the builder threads could work either way - they could check out a
44 commit and then built it for all boards. Using separate directories for each
45 commit/board pair they could leave their build product around afterwards
48 The intent behind building a single board for multiple commits, is to make
49 use of incremental builds. Since each commit is built incrementally from
50 the previous one, builds are faster. Reconfiguring for a different board
51 removes all intermediate object files.
53 Many threads can be working at once, but each has its own working directory.
54 When a thread finishes a build, it puts the output files into a result
57 The base directory used by buildman is normally '../<branch>', i.e.
58 a directory higher than the source repository and named after the branch
61 Within the base directory, we have one subdirectory for each commit. Within
62 that is one subdirectory for each board. Within that is the build output for
63 that commit/board combination.
65 Buildman also create working directories for each thread, in a .bm-work/
66 subdirectory in the base dir.
68 As an example, say we are building branch 'us-net' for boards 'sandbox' and
69 'seaboard', and say that us-net has two commits. We will have directories
72 us-net/ base directory
73 01_g4ed4ebc_net--Add-tftp-speed-/
78 02_g4ed4ebc_net--Check-tftp-comp/
84 00/ working directory for thread 0 (contains source checkout)
86 01/ working directory for thread 1
89 u-boot/ source directory
93 """Holds information about a particular error line we are outputing
95 char: Character representation: '+': error, '-': fixed error, 'w+': warning,
97 boards: List of Board objects which have line in the error/warning output
98 errline: The text of the error line
100 ErrLine = collections.namedtuple('ErrLine', 'char,boards,errline')
102 # Possible build outcomes
103 OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4))
105 # Translate a commit subject into a valid filename (and handle unicode)
106 trans_valid_chars = str.maketrans('/: ', '---')
108 BASE_CONFIG_FILENAMES = [
109 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg'
112 EXTRA_CONFIG_FILENAMES = [
113 '.config', '.config-spl', '.config-tpl',
114 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk',
115 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h',
119 """Holds information about configuration settings for a board."""
120 def __init__(self, config_filename, target):
123 for fname in config_filename:
124 self.config[fname] = {}
126 def Add(self, fname, key, value):
127 self.config[fname][key] = value
131 for fname in self.config:
132 for key, value in self.config[fname].items():
134 val = val ^ hash(key) & hash(value)
138 """Holds information about environment variables for a board."""
139 def __init__(self, target):
141 self.environment = {}
143 def Add(self, key, value):
144 self.environment[key] = value
147 """Class for building U-Boot for a particular commit.
149 Public members: (many should ->private)
150 already_done: Number of builds already completed
151 base_dir: Base directory to use for builder
152 checkout: True to check out source, False to skip that step.
153 This is used for testing.
154 col: terminal.Color() object
155 count: Number of commits to build
156 do_make: Method to call to invoke Make
157 fail: Number of builds that failed due to error
158 force_build: Force building even if a build already exists
159 force_config_on_failure: If a commit fails for a board, disable
160 incremental building for the next commit we build for that
161 board, so that we will see all warnings/errors again.
162 force_build_failures: If a previously-built build (i.e. built on
163 a previous run of buildman) is marked as failed, rebuild it.
164 git_dir: Git directory containing source repository
165 num_jobs: Number of jobs to run at once (passed to make as -j)
166 num_threads: Number of builder threads to run
167 out_queue: Queue of results to process
168 re_make_err: Compiled regular expression for ignore_lines
169 queue: Queue of jobs to run
170 threads: List of active threads
171 toolchains: Toolchains object to use for building
172 upto: Current commit number we are building (0.count-1)
173 warned: Number of builds that produced at least one warning
174 force_reconfig: Reconfigure U-Boot on each comiit. This disables
175 incremental building, where buildman reconfigures on the first
176 commit for a baord, and then just does an incremental build for
177 the following commits. In fact buildman will reconfigure and
178 retry for any failing commits, so generally the only effect of
179 this option is to slow things down.
180 in_tree: Build U-Boot in-tree instead of specifying an output
181 directory separate from the source code. This option is really
182 only useful for testing in-tree builds.
183 work_in_output: Use the output directory as the work directory and
184 don't write to a separate output directory.
185 thread_exceptions: List of exceptions raised by thread jobs
188 _base_board_dict: Last-summarised Dict of boards
189 _base_err_lines: Last-summarised list of errors
190 _base_warn_lines: Last-summarised list of warnings
191 _build_period_us: Time taken for a single build (float object).
192 _complete_delay: Expected delay until completion (timedelta)
193 _next_delay_update: Next time we plan to display a progress update
195 _show_unknown: Show unknown boards (those not built) in summary
196 _start_time: Start time for the build
197 _timestamps: List of timestamps for the completion of the last
198 last _timestamp_count builds. Each is a datetime object.
199 _timestamp_count: Number of timestamps to keep in our list.
200 _working_dir: Base working directory containing all threads
201 _single_builder: BuilderThread object for the singer builder, if
202 threading is not being used
205 """Records a build outcome for a single make invocation
208 rc: Outcome value (OUTCOME_...)
209 err_lines: List of error lines or [] if none
210 sizes: Dictionary of image size information, keyed by filename
211 - Each value is itself a dictionary containing
212 values for 'text', 'data' and 'bss', being the integer
213 size in bytes of each section.
214 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each
215 value is itself a dictionary:
217 value: Size of function in bytes
218 config: Dictionary keyed by filename - e.g. '.config'. Each
219 value is itself a dictionary:
222 environment: Dictionary keyed by environment variable, Each
223 value is the value of environment variable.
225 def __init__(self, rc, err_lines, sizes, func_sizes, config,
228 self.err_lines = err_lines
230 self.func_sizes = func_sizes
232 self.environment = environment
234 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs,
235 gnu_make='make', checkout=True, show_unknown=True, step=1,
236 no_subdirs=False, full_path=False, verbose_build=False,
237 mrproper=False, per_board_out_dir=False,
238 config_only=False, squash_config_y=False,
239 warnings_as_errors=False, work_in_output=False,
240 test_thread_exceptions=False):
241 """Create a new Builder object
244 toolchains: Toolchains object to use for building
245 base_dir: Base directory to use for builder
246 git_dir: Git directory containing source repository
247 num_threads: Number of builder threads to run
248 num_jobs: Number of jobs to run at once (passed to make as -j)
249 gnu_make: the command name of GNU Make.
250 checkout: True to check out source, False to skip that step.
251 This is used for testing.
252 show_unknown: Show unknown boards (those not built) in summary
253 step: 1 to process every commit, n to process every nth commit
254 no_subdirs: Don't create subdirectories when building current
255 source for a single board
256 full_path: Return the full path in CROSS_COMPILE and don't set
258 verbose_build: Run build with V=1 and don't use 'make -s'
259 mrproper: Always run 'make mrproper' when configuring
260 per_board_out_dir: Build in a separate persistent directory per
261 board rather than a thread-specific directory
262 config_only: Only configure each build, don't build it
263 squash_config_y: Convert CONFIG options with the value 'y' to '1'
264 warnings_as_errors: Treat all compiler warnings as errors
265 work_in_output: Use the output directory as the work directory and
266 don't write to a separate output directory.
267 test_thread_exceptions: Uses for tests only, True to make the
268 threads raise an exception instead of reporting their result.
269 This simulates a failure in the code somewhere
271 self.toolchains = toolchains
272 self.base_dir = base_dir
274 self._working_dir = base_dir
276 self._working_dir = os.path.join(base_dir, '.bm-work')
278 self.do_make = self.Make
279 self.gnu_make = gnu_make
280 self.checkout = checkout
281 self.num_threads = num_threads
282 self.num_jobs = num_jobs
283 self.already_done = 0
284 self.force_build = False
285 self.git_dir = git_dir
286 self._show_unknown = show_unknown
287 self._timestamp_count = 10
288 self._build_period_us = None
289 self._complete_delay = None
290 self._next_delay_update = datetime.now()
291 self._start_time = datetime.now()
292 self.force_config_on_failure = True
293 self.force_build_failures = False
294 self.force_reconfig = False
297 self._error_lines = 0
298 self.no_subdirs = no_subdirs
299 self.full_path = full_path
300 self.verbose_build = verbose_build
301 self.config_only = config_only
302 self.squash_config_y = squash_config_y
303 self.config_filenames = BASE_CONFIG_FILENAMES
304 self.work_in_output = work_in_output
305 if not self.squash_config_y:
306 self.config_filenames += EXTRA_CONFIG_FILENAMES
308 self.warnings_as_errors = warnings_as_errors
309 self.col = terminal.Color()
311 self._re_function = re.compile('(.*): In function.*')
312 self._re_files = re.compile('In file included from.*')
313 self._re_warning = re.compile('(.*):(\d*):(\d*): warning: .*')
314 self._re_dtb_warning = re.compile('(.*): Warning .*')
315 self._re_note = re.compile('(.*):(\d*):(\d*): note: this is the location of the previous.*')
316 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n',
317 re.MULTILINE | re.DOTALL)
319 self.thread_exceptions = []
320 self.test_thread_exceptions = test_thread_exceptions
322 self._single_builder = None
323 self.queue = queue.Queue()
324 self.out_queue = queue.Queue()
325 for i in range(self.num_threads):
326 t = builderthread.BuilderThread(
327 self, i, mrproper, per_board_out_dir,
328 test_exception=test_thread_exceptions)
331 self.threads.append(t)
333 t = builderthread.ResultThread(self)
336 self.threads.append(t)
338 self._single_builder = builderthread.BuilderThread(
339 self, -1, mrproper, per_board_out_dir)
341 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)']
342 self.re_make_err = re.compile('|'.join(ignore_lines))
344 # Handle existing graceful with SIGINT / Ctrl-C
345 signal.signal(signal.SIGINT, self.signal_handler)
348 """Get rid of all threads created by the builder"""
349 for t in self.threads:
352 def signal_handler(self, signal, frame):
355 def SetDisplayOptions(self, show_errors=False, show_sizes=False,
356 show_detail=False, show_bloat=False,
357 list_error_boards=False, show_config=False,
358 show_environment=False, filter_dtb_warnings=False,
359 filter_migration_warnings=False):
360 """Setup display options for the builder.
363 show_errors: True to show summarised error/warning info
364 show_sizes: Show size deltas
365 show_detail: Show size delta detail for each board if show_sizes
366 show_bloat: Show detail for each function
367 list_error_boards: Show the boards which caused each error/warning
368 show_config: Show config deltas
369 show_environment: Show environment deltas
370 filter_dtb_warnings: Filter out any warnings from the device-tree
372 filter_migration_warnings: Filter out any warnings about migrating
373 a board to driver model
375 self._show_errors = show_errors
376 self._show_sizes = show_sizes
377 self._show_detail = show_detail
378 self._show_bloat = show_bloat
379 self._list_error_boards = list_error_boards
380 self._show_config = show_config
381 self._show_environment = show_environment
382 self._filter_dtb_warnings = filter_dtb_warnings
383 self._filter_migration_warnings = filter_migration_warnings
385 def _AddTimestamp(self):
386 """Add a new timestamp to the list and record the build period.
388 The build period is the length of time taken to perform a single
389 build (one board, one commit).
392 self._timestamps.append(now)
393 count = len(self._timestamps)
394 delta = self._timestamps[-1] - self._timestamps[0]
395 seconds = delta.total_seconds()
397 # If we have enough data, estimate build period (time taken for a
398 # single build) and therefore completion time.
399 if count > 1 and self._next_delay_update < now:
400 self._next_delay_update = now + timedelta(seconds=2)
402 self._build_period = float(seconds) / count
403 todo = self.count - self.upto
404 self._complete_delay = timedelta(microseconds=
405 self._build_period * todo * 1000000)
407 self._complete_delay -= timedelta(
408 microseconds=self._complete_delay.microseconds)
411 self._timestamps.popleft()
414 def SelectCommit(self, commit, checkout=True):
415 """Checkout the selected commit for this build
418 if checkout and self.checkout:
419 gitutil.Checkout(commit.hash)
421 def Make(self, commit, brd, stage, cwd, *args, **kwargs):
425 commit: Commit object that is being built
426 brd: Board object that is being built
427 stage: Stage that we are at (mrproper, config, build)
428 cwd: Directory where make should be run
429 args: Arguments to pass to make
430 kwargs: Arguments to pass to command.RunPipe()
432 cmd = [self.gnu_make] + list(args)
433 result = command.RunPipe([cmd], capture=True, capture_stderr=True,
434 cwd=cwd, raise_on_error=False, infile='/dev/null', **kwargs)
435 if self.verbose_build:
436 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout
437 result.combined = '%s\n' % (' '.join(cmd)) + result.combined
440 def ProcessResult(self, result):
441 """Process the result of a build, showing progress information
444 result: A CommandResult object, which indicates the result for
447 col = terminal.Color()
449 target = result.brd.target
452 if result.return_code != 0:
456 if result.already_done:
457 self.already_done += 1
459 terminal.PrintClear()
460 boards_selected = {target : result.brd}
461 self.ResetResultSummary(boards_selected)
462 self.ProduceResultSummary(result.commit_upto, self.commits,
465 target = '(starting)'
467 # Display separate counts for ok, warned and fail
468 ok = self.upto - self.warned - self.fail
469 line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok)
470 line += self.col.Color(self.col.YELLOW, '%5d' % self.warned)
471 line += self.col.Color(self.col.RED, '%5d' % self.fail)
473 line += ' /%-5d ' % self.count
474 remaining = self.count - self.upto
476 line += self.col.Color(self.col.MAGENTA, ' -%-5d ' % remaining)
480 # Add our current completion time estimate
482 if self._complete_delay:
483 line += '%s : ' % self._complete_delay
486 terminal.PrintClear()
487 Print(line, newline=False, limit_to_line=True)
489 def _GetOutputDir(self, commit_upto):
490 """Get the name of the output directory for a commit number
492 The output directory is typically .../<branch>/<commit>.
495 commit_upto: Commit number to use (0..self.count-1)
497 if self.work_in_output:
498 return self._working_dir
502 commit = self.commits[commit_upto]
503 subject = commit.subject.translate(trans_valid_chars)
504 # See _GetOutputSpaceRemovals() which parses this name
505 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1,
506 commit.hash, subject[:20]))
507 elif not self.no_subdirs:
508 commit_dir = 'current'
511 return os.path.join(self.base_dir, commit_dir)
513 def GetBuildDir(self, commit_upto, target):
514 """Get the name of the build directory for a commit number
516 The build directory is typically .../<branch>/<commit>/<target>.
519 commit_upto: Commit number to use (0..self.count-1)
522 output_dir = self._GetOutputDir(commit_upto)
523 if self.work_in_output:
525 return os.path.join(output_dir, target)
527 def GetDoneFile(self, commit_upto, target):
528 """Get the name of the done file for a commit number
531 commit_upto: Commit number to use (0..self.count-1)
534 return os.path.join(self.GetBuildDir(commit_upto, target), 'done')
536 def GetSizesFile(self, commit_upto, target):
537 """Get the name of the sizes file for a commit number
540 commit_upto: Commit number to use (0..self.count-1)
543 return os.path.join(self.GetBuildDir(commit_upto, target), 'sizes')
545 def GetFuncSizesFile(self, commit_upto, target, elf_fname):
546 """Get the name of the funcsizes file for a commit number and ELF file
549 commit_upto: Commit number to use (0..self.count-1)
551 elf_fname: Filename of elf image
553 return os.path.join(self.GetBuildDir(commit_upto, target),
554 '%s.sizes' % elf_fname.replace('/', '-'))
556 def GetObjdumpFile(self, commit_upto, target, elf_fname):
557 """Get the name of the objdump file for a commit number and ELF file
560 commit_upto: Commit number to use (0..self.count-1)
562 elf_fname: Filename of elf image
564 return os.path.join(self.GetBuildDir(commit_upto, target),
565 '%s.objdump' % elf_fname.replace('/', '-'))
567 def GetErrFile(self, commit_upto, target):
568 """Get the name of the err file for a commit number
571 commit_upto: Commit number to use (0..self.count-1)
574 output_dir = self.GetBuildDir(commit_upto, target)
575 return os.path.join(output_dir, 'err')
577 def FilterErrors(self, lines):
578 """Filter out errors in which we have no interest
580 We should probably use map().
583 lines: List of error lines, each a string
585 New list with only interesting lines included
588 if self._filter_migration_warnings:
589 text = '\n'.join(lines)
590 text = self._re_migration_warning.sub('', text)
591 lines = text.splitlines()
593 if self.re_make_err.search(line):
595 if self._filter_dtb_warnings and self._re_dtb_warning.search(line):
597 out_lines.append(line)
600 def ReadFuncSizes(self, fname, fd):
601 """Read function sizes from the output of 'nm'
604 fd: File containing data to read
605 fname: Filename we are reading from (just for errors)
608 Dictionary containing size of each function in bytes, indexed by
612 for line in fd.readlines():
615 size, type, name = line[:-1].split()
617 Print("Invalid line in file '%s': '%s'" % (fname, line[:-1]))
620 # function names begin with '.' on 64-bit powerpc
622 name = 'static.' + name.split('.')[0]
623 sym[name] = sym.get(name, 0) + int(size, 16)
626 def _ProcessConfig(self, fname):
627 """Read in a .config, autoconf.mk or autoconf.h file
629 This function handles all config file types. It ignores comments and
630 any #defines which don't start with CONFIG_.
633 fname: Filename to read
637 key: Config name (e.g. CONFIG_DM)
638 value: Config value (e.g. 1)
641 if os.path.exists(fname):
642 with open(fname) as fd:
645 if line.startswith('#define'):
646 values = line[8:].split(' ', 1)
651 value = '1' if self.squash_config_y else ''
652 if not key.startswith('CONFIG_'):
654 elif not line or line[0] in ['#', '*', '/']:
657 key, value = line.split('=', 1)
658 if self.squash_config_y and value == 'y':
663 def _ProcessEnvironment(self, fname):
664 """Read in a uboot.env file
666 This function reads in environment variables from a file.
669 fname: Filename to read
673 key: environment variable (e.g. bootlimit)
674 value: value of environment variable (e.g. 1)
677 if os.path.exists(fname):
678 with open(fname) as fd:
679 for line in fd.read().split('\0'):
681 key, value = line.split('=', 1)
682 environment[key] = value
684 # ignore lines we can't parse
688 def GetBuildOutcome(self, commit_upto, target, read_func_sizes,
689 read_config, read_environment):
690 """Work out the outcome of a build.
693 commit_upto: Commit number to check (0..n-1)
694 target: Target board to check
695 read_func_sizes: True to read function size information
696 read_config: True to read .config and autoconf.h files
697 read_environment: True to read uboot.env files
702 done_file = self.GetDoneFile(commit_upto, target)
703 sizes_file = self.GetSizesFile(commit_upto, target)
708 if os.path.exists(done_file):
709 with open(done_file, 'r') as fd:
711 return_code = int(fd.readline())
713 # The file may be empty due to running out of disk space.
717 err_file = self.GetErrFile(commit_upto, target)
718 if os.path.exists(err_file):
719 with open(err_file, 'r') as fd:
720 err_lines = self.FilterErrors(fd.readlines())
722 # Decide whether the build was ok, failed or created warnings
730 # Convert size information to our simple format
731 if os.path.exists(sizes_file):
732 with open(sizes_file, 'r') as fd:
733 for line in fd.readlines():
734 values = line.split()
737 rodata = int(values[6], 16)
739 'all' : int(values[0]) + int(values[1]) +
741 'text' : int(values[0]) - rodata,
742 'data' : int(values[1]),
743 'bss' : int(values[2]),
746 sizes[values[5]] = size_dict
749 pattern = self.GetFuncSizesFile(commit_upto, target, '*')
750 for fname in glob.glob(pattern):
751 with open(fname, 'r') as fd:
752 dict_name = os.path.basename(fname).replace('.sizes',
754 func_sizes[dict_name] = self.ReadFuncSizes(fname, fd)
757 output_dir = self.GetBuildDir(commit_upto, target)
758 for name in self.config_filenames:
759 fname = os.path.join(output_dir, name)
760 config[name] = self._ProcessConfig(fname)
763 output_dir = self.GetBuildDir(commit_upto, target)
764 fname = os.path.join(output_dir, 'uboot.env')
765 environment = self._ProcessEnvironment(fname)
767 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config,
770 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {})
772 def GetResultSummary(self, boards_selected, commit_upto, read_func_sizes,
773 read_config, read_environment):
774 """Calculate a summary of the results of building a commit.
777 board_selected: Dict containing boards to summarise
778 commit_upto: Commit number to summarize (0..self.count-1)
779 read_func_sizes: True to read function size information
780 read_config: True to read .config and autoconf.h files
781 read_environment: True to read uboot.env files
785 Dict containing boards which passed building this commit.
786 keyed by board.target
787 List containing a summary of error lines
788 Dict keyed by error line, containing a list of the Board
789 objects with that error
790 List containing a summary of warning lines
791 Dict keyed by error line, containing a list of the Board
792 objects with that warning
793 Dictionary keyed by board.target. Each value is a dictionary:
794 key: filename - e.g. '.config'
795 value is itself a dictionary:
798 Dictionary keyed by board.target. Each value is a dictionary:
799 key: environment variable
800 value: value of environment variable
802 def AddLine(lines_summary, lines_boards, line, board):
804 if line in lines_boards:
805 lines_boards[line].append(board)
807 lines_boards[line] = [board]
808 lines_summary.append(line)
811 err_lines_summary = []
812 err_lines_boards = {}
813 warn_lines_summary = []
814 warn_lines_boards = {}
818 for board in boards_selected.values():
819 outcome = self.GetBuildOutcome(commit_upto, board.target,
820 read_func_sizes, read_config,
822 board_dict[board.target] = outcome
824 last_was_warning = False
825 for line in outcome.err_lines:
827 if (self._re_function.match(line) or
828 self._re_files.match(line)):
831 is_warning = (self._re_warning.match(line) or
832 self._re_dtb_warning.match(line))
833 is_note = self._re_note.match(line)
834 if is_warning or (last_was_warning and is_note):
836 AddLine(warn_lines_summary, warn_lines_boards,
838 AddLine(warn_lines_summary, warn_lines_boards,
842 AddLine(err_lines_summary, err_lines_boards,
844 AddLine(err_lines_summary, err_lines_boards,
846 last_was_warning = is_warning
848 tconfig = Config(self.config_filenames, board.target)
849 for fname in self.config_filenames:
851 for key, value in outcome.config[fname].items():
852 tconfig.Add(fname, key, value)
853 config[board.target] = tconfig
855 tenvironment = Environment(board.target)
856 if outcome.environment:
857 for key, value in outcome.environment.items():
858 tenvironment.Add(key, value)
859 environment[board.target] = tenvironment
861 return (board_dict, err_lines_summary, err_lines_boards,
862 warn_lines_summary, warn_lines_boards, config, environment)
864 def AddOutcome(self, board_dict, arch_list, changes, char, color):
865 """Add an output to our list of outcomes for each architecture
867 This simple function adds failing boards (changes) to the
868 relevant architecture string, so we can print the results out
869 sorted by architecture.
872 board_dict: Dict containing all boards
873 arch_list: Dict keyed by arch name. Value is a string containing
874 a list of board names which failed for that arch.
875 changes: List of boards to add to arch_list
876 color: terminal.Colour object
879 for target in changes:
880 if target in board_dict:
881 arch = board_dict[target].arch
884 str = self.col.Color(color, ' ' + target)
885 if not arch in done_arch:
886 str = ' %s %s' % (self.col.Color(color, char), str)
887 done_arch[arch] = True
888 if not arch in arch_list:
889 arch_list[arch] = str
891 arch_list[arch] += str
894 def ColourNum(self, num):
895 color = self.col.RED if num > 0 else self.col.GREEN
898 return self.col.Color(color, str(num))
900 def ResetResultSummary(self, board_selected):
901 """Reset the results summary ready for use.
903 Set up the base board list to be all those selected, and set the
904 error lines to empty.
906 Following this, calls to PrintResultSummary() will use this
907 information to work out what has changed.
910 board_selected: Dict containing boards to summarise, keyed by
913 self._base_board_dict = {}
914 for board in board_selected:
915 self._base_board_dict[board] = Builder.Outcome(0, [], [], {}, {},
917 self._base_err_lines = []
918 self._base_warn_lines = []
919 self._base_err_line_boards = {}
920 self._base_warn_line_boards = {}
921 self._base_config = None
922 self._base_environment = None
924 def PrintFuncSizeDetail(self, fname, old, new):
925 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0
926 delta, common = [], {}
933 if name not in common:
936 delta.append([-old[name], name])
939 if name not in common:
942 delta.append([new[name], name])
945 diff = new.get(name, 0) - old.get(name, 0)
947 grow, up = grow + 1, up + diff
949 shrink, down = shrink + 1, down - diff
950 delta.append([diff, name])
955 args = [add, -remove, grow, -shrink, up, -down, up - down]
956 if max(args) == 0 and min(args) == 0:
958 args = [self.ColourNum(x) for x in args]
960 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' %
961 tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args))
962 Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new',
964 for diff, name in delta:
966 color = self.col.RED if diff > 0 else self.col.GREEN
967 msg = '%s %-38s %7s %7s %+7d' % (indent, name,
968 old.get(name, '-'), new.get(name,'-'), diff)
969 Print(msg, colour=color)
972 def PrintSizeDetail(self, target_list, show_bloat):
973 """Show details size information for each board
976 target_list: List of targets, each a dict containing:
977 'target': Target name
978 'total_diff': Total difference in bytes across all areas
979 <part_name>: Difference for that part
980 show_bloat: Show detail for each function
982 targets_by_diff = sorted(target_list, reverse=True,
983 key=lambda x: x['_total_diff'])
984 for result in targets_by_diff:
985 printed_target = False
986 for name in sorted(result):
988 if name.startswith('_'):
991 color = self.col.RED if diff > 0 else self.col.GREEN
992 msg = ' %s %+d' % (name, diff)
993 if not printed_target:
994 Print('%10s %-15s:' % ('', result['_target']),
996 printed_target = True
997 Print(msg, colour=color, newline=False)
1001 target = result['_target']
1002 outcome = result['_outcome']
1003 base_outcome = self._base_board_dict[target]
1004 for fname in outcome.func_sizes:
1005 self.PrintFuncSizeDetail(fname,
1006 base_outcome.func_sizes[fname],
1007 outcome.func_sizes[fname])
1010 def PrintSizeSummary(self, board_selected, board_dict, show_detail,
1012 """Print a summary of image sizes broken down by section.
1014 The summary takes the form of one line per architecture. The
1015 line contains deltas for each of the sections (+ means the section
1016 got bigger, - means smaller). The numbers are the average number
1017 of bytes that a board in this section increased by.
1020 powerpc: (622 boards) text -0.0
1021 arm: (285 boards) text -0.0
1022 nds32: (3 boards) text -8.0
1025 board_selected: Dict containing boards to summarise, keyed by
1027 board_dict: Dict containing boards for which we built this
1028 commit, keyed by board.target. The value is an Outcome object.
1029 show_detail: Show size delta detail for each board
1030 show_bloat: Show detail for each function
1035 # Calculate changes in size for different image parts
1036 # The previous sizes are in Board.sizes, for each board
1037 for target in board_dict:
1038 if target not in board_selected:
1040 base_sizes = self._base_board_dict[target].sizes
1041 outcome = board_dict[target]
1042 sizes = outcome.sizes
1044 # Loop through the list of images, creating a dict of size
1045 # changes for each image/part. We end up with something like
1046 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4}
1047 # which means that U-Boot data increased by 5 bytes and SPL
1048 # text decreased by 4.
1049 err = {'_target' : target}
1051 if image in base_sizes:
1052 base_image = base_sizes[image]
1053 # Loop through the text, data, bss parts
1054 for part in sorted(sizes[image]):
1055 diff = sizes[image][part] - base_image[part]
1058 if image == 'u-boot':
1061 name = image + ':' + part
1063 arch = board_selected[target].arch
1064 if not arch in arch_count:
1065 arch_count[arch] = 1
1067 arch_count[arch] += 1
1069 pass # Only add to our list when we have some stats
1070 elif not arch in arch_list:
1071 arch_list[arch] = [err]
1073 arch_list[arch].append(err)
1075 # We now have a list of image size changes sorted by arch
1076 # Print out a summary of these
1077 for arch, target_list in arch_list.items():
1078 # Get total difference for each type
1080 for result in target_list:
1082 for name, diff in result.items():
1083 if name.startswith('_'):
1087 totals[name] += diff
1090 result['_total_diff'] = total
1091 result['_outcome'] = board_dict[result['_target']]
1093 count = len(target_list)
1094 printed_arch = False
1095 for name in sorted(totals):
1098 # Display the average difference in this name for this
1100 avg_diff = float(diff) / count
1101 color = self.col.RED if avg_diff > 0 else self.col.GREEN
1102 msg = ' %s %+1.1f' % (name, avg_diff)
1103 if not printed_arch:
1104 Print('%10s: (for %d/%d boards)' % (arch, count,
1105 arch_count[arch]), newline=False)
1107 Print(msg, colour=color, newline=False)
1112 self.PrintSizeDetail(target_list, show_bloat)
1115 def PrintResultSummary(self, board_selected, board_dict, err_lines,
1116 err_line_boards, warn_lines, warn_line_boards,
1117 config, environment, show_sizes, show_detail,
1118 show_bloat, show_config, show_environment):
1119 """Compare results with the base results and display delta.
1121 Only boards mentioned in board_selected will be considered. This
1122 function is intended to be called repeatedly with the results of
1123 each commit. It therefore shows a 'diff' between what it saw in
1124 the last call and what it sees now.
1127 board_selected: Dict containing boards to summarise, keyed by
1129 board_dict: Dict containing boards for which we built this
1130 commit, keyed by board.target. The value is an Outcome object.
1131 err_lines: A list of errors for this commit, or [] if there is
1132 none, or we don't want to print errors
1133 err_line_boards: Dict keyed by error line, containing a list of
1134 the Board objects with that error
1135 warn_lines: A list of warnings for this commit, or [] if there is
1136 none, or we don't want to print errors
1137 warn_line_boards: Dict keyed by warning line, containing a list of
1138 the Board objects with that warning
1139 config: Dictionary keyed by filename - e.g. '.config'. Each
1140 value is itself a dictionary:
1143 environment: Dictionary keyed by environment variable, Each
1144 value is the value of environment variable.
1145 show_sizes: Show image size deltas
1146 show_detail: Show size delta detail for each board if show_sizes
1147 show_bloat: Show detail for each function
1148 show_config: Show config changes
1149 show_environment: Show environment changes
1151 def _BoardList(line, line_boards):
1152 """Helper function to get a line of boards containing a line
1155 line: Error line to search for
1156 line_boards: boards to search, each a Board
1158 List of boards with that error line, or [] if the user has not
1159 requested such a list
1163 if self._list_error_boards:
1164 for board in line_boards[line]:
1165 if not board in board_set:
1166 boards.append(board)
1167 board_set.add(board)
1170 def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards,
1172 """Calculate the required output based on changes in errors
1175 base_lines: List of errors/warnings for previous commit
1176 base_line_boards: Dict keyed by error line, containing a list
1177 of the Board objects with that error in the previous commit
1178 lines: List of errors/warning for this commit, each a str
1179 line_boards: Dict keyed by error line, containing a list
1180 of the Board objects with that error in this commit
1181 char: Character representing error ('') or warning ('w'). The
1182 broken ('+') or fixed ('-') characters are added in this
1187 List of ErrLine objects for 'better' lines
1188 List of ErrLine objects for 'worse' lines
1193 if line not in base_lines:
1194 errline = ErrLine(char + '+', _BoardList(line, line_boards),
1196 worse_lines.append(errline)
1197 for line in base_lines:
1198 if line not in lines:
1199 errline = ErrLine(char + '-',
1200 _BoardList(line, base_line_boards), line)
1201 better_lines.append(errline)
1202 return better_lines, worse_lines
1204 def _CalcConfig(delta, name, config):
1205 """Calculate configuration changes
1208 delta: Type of the delta, e.g. '+'
1209 name: name of the file which changed (e.g. .config)
1210 config: configuration change dictionary
1214 String containing the configuration changes which can be
1218 for key in sorted(config.keys()):
1219 out += '%s=%s ' % (key, config[key])
1220 return '%s %s: %s' % (delta, name, out)
1222 def _AddConfig(lines, name, config_plus, config_minus, config_change):
1223 """Add changes in configuration to a list
1226 lines: list to add to
1227 name: config file name
1228 config_plus: configurations added, dictionary
1231 config_minus: configurations removed, dictionary
1234 config_change: configurations changed, dictionary
1239 lines.append(_CalcConfig('+', name, config_plus))
1241 lines.append(_CalcConfig('-', name, config_minus))
1243 lines.append(_CalcConfig('c', name, config_change))
1245 def _OutputConfigInfo(lines):
1250 col = self.col.GREEN
1251 elif line[0] == '-':
1253 elif line[0] == 'c':
1254 col = self.col.YELLOW
1255 Print(' ' + line, newline=True, colour=col)
1257 def _OutputErrLines(err_lines, colour):
1258 """Output the line of error/warning lines, if not empty
1260 Also increments self._error_lines if err_lines not empty
1263 err_lines: List of ErrLine objects, each an error or warning
1264 line, possibly including a list of boards with that
1266 colour: Colour to use for output
1270 for line in err_lines:
1272 names = [board.target for board in line.boards]
1273 board_str = ' '.join(names) if names else ''
1275 out = self.col.Color(colour, line.char + '(')
1276 out += self.col.Color(self.col.MAGENTA, board_str,
1278 out += self.col.Color(colour, ') %s' % line.errline)
1280 out = self.col.Color(colour, line.char + line.errline)
1281 out_list.append(out)
1282 Print('\n'.join(out_list))
1283 self._error_lines += 1
1286 ok_boards = [] # List of boards fixed since last commit
1287 warn_boards = [] # List of boards with warnings since last commit
1288 err_boards = [] # List of new broken boards since last commit
1289 new_boards = [] # List of boards that didn't exist last time
1290 unknown_boards = [] # List of boards that were not built
1292 for target in board_dict:
1293 if target not in board_selected:
1296 # If the board was built last time, add its outcome to a list
1297 if target in self._base_board_dict:
1298 base_outcome = self._base_board_dict[target].rc
1299 outcome = board_dict[target]
1300 if outcome.rc == OUTCOME_UNKNOWN:
1301 unknown_boards.append(target)
1302 elif outcome.rc < base_outcome:
1303 if outcome.rc == OUTCOME_WARNING:
1304 warn_boards.append(target)
1306 ok_boards.append(target)
1307 elif outcome.rc > base_outcome:
1308 if outcome.rc == OUTCOME_WARNING:
1309 warn_boards.append(target)
1311 err_boards.append(target)
1313 new_boards.append(target)
1315 # Get a list of errors and warnings that have appeared, and disappeared
1316 better_err, worse_err = _CalcErrorDelta(self._base_err_lines,
1317 self._base_err_line_boards, err_lines, err_line_boards, '')
1318 better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines,
1319 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w')
1321 # Display results by arch
1322 if any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards,
1323 worse_err, better_err, worse_warn, better_warn)):
1325 self.AddOutcome(board_selected, arch_list, ok_boards, '',
1327 self.AddOutcome(board_selected, arch_list, warn_boards, 'w+',
1329 self.AddOutcome(board_selected, arch_list, err_boards, '+',
1331 self.AddOutcome(board_selected, arch_list, new_boards, '*', self.col.BLUE)
1332 if self._show_unknown:
1333 self.AddOutcome(board_selected, arch_list, unknown_boards, '?',
1335 for arch, target_list in arch_list.items():
1336 Print('%10s: %s' % (arch, target_list))
1337 self._error_lines += 1
1338 _OutputErrLines(better_err, colour=self.col.GREEN)
1339 _OutputErrLines(worse_err, colour=self.col.RED)
1340 _OutputErrLines(better_warn, colour=self.col.CYAN)
1341 _OutputErrLines(worse_warn, colour=self.col.YELLOW)
1344 self.PrintSizeSummary(board_selected, board_dict, show_detail,
1347 if show_environment and self._base_environment:
1350 for target in board_dict:
1351 if target not in board_selected:
1354 tbase = self._base_environment[target]
1355 tenvironment = environment[target]
1356 environment_plus = {}
1357 environment_minus = {}
1358 environment_change = {}
1359 base = tbase.environment
1360 for key, value in tenvironment.environment.items():
1362 environment_plus[key] = value
1363 for key, value in base.items():
1364 if key not in tenvironment.environment:
1365 environment_minus[key] = value
1366 for key, value in base.items():
1367 new_value = tenvironment.environment.get(key)
1368 if new_value and value != new_value:
1369 desc = '%s -> %s' % (value, new_value)
1370 environment_change[key] = desc
1372 _AddConfig(lines, target, environment_plus, environment_minus,
1375 _OutputConfigInfo(lines)
1377 if show_config and self._base_config:
1379 arch_config_plus = {}
1380 arch_config_minus = {}
1381 arch_config_change = {}
1384 for target in board_dict:
1385 if target not in board_selected:
1387 arch = board_selected[target].arch
1388 if arch not in arch_list:
1389 arch_list.append(arch)
1391 for arch in arch_list:
1392 arch_config_plus[arch] = {}
1393 arch_config_minus[arch] = {}
1394 arch_config_change[arch] = {}
1395 for name in self.config_filenames:
1396 arch_config_plus[arch][name] = {}
1397 arch_config_minus[arch][name] = {}
1398 arch_config_change[arch][name] = {}
1400 for target in board_dict:
1401 if target not in board_selected:
1404 arch = board_selected[target].arch
1406 all_config_plus = {}
1407 all_config_minus = {}
1408 all_config_change = {}
1409 tbase = self._base_config[target]
1410 tconfig = config[target]
1412 for name in self.config_filenames:
1413 if not tconfig.config[name]:
1418 base = tbase.config[name]
1419 for key, value in tconfig.config[name].items():
1421 config_plus[key] = value
1422 all_config_plus[key] = value
1423 for key, value in base.items():
1424 if key not in tconfig.config[name]:
1425 config_minus[key] = value
1426 all_config_minus[key] = value
1427 for key, value in base.items():
1428 new_value = tconfig.config.get(key)
1429 if new_value and value != new_value:
1430 desc = '%s -> %s' % (value, new_value)
1431 config_change[key] = desc
1432 all_config_change[key] = desc
1434 arch_config_plus[arch][name].update(config_plus)
1435 arch_config_minus[arch][name].update(config_minus)
1436 arch_config_change[arch][name].update(config_change)
1438 _AddConfig(lines, name, config_plus, config_minus,
1440 _AddConfig(lines, 'all', all_config_plus, all_config_minus,
1442 summary[target] = '\n'.join(lines)
1444 lines_by_target = {}
1445 for target, lines in summary.items():
1446 if lines in lines_by_target:
1447 lines_by_target[lines].append(target)
1449 lines_by_target[lines] = [target]
1451 for arch in arch_list:
1456 for name in self.config_filenames:
1457 all_plus.update(arch_config_plus[arch][name])
1458 all_minus.update(arch_config_minus[arch][name])
1459 all_change.update(arch_config_change[arch][name])
1460 _AddConfig(lines, name, arch_config_plus[arch][name],
1461 arch_config_minus[arch][name],
1462 arch_config_change[arch][name])
1463 _AddConfig(lines, 'all', all_plus, all_minus, all_change)
1464 #arch_summary[target] = '\n'.join(lines)
1467 _OutputConfigInfo(lines)
1469 for lines, targets in lines_by_target.items():
1472 Print('%s :' % ' '.join(sorted(targets)))
1473 _OutputConfigInfo(lines.split('\n'))
1476 # Save our updated information for the next call to this function
1477 self._base_board_dict = board_dict
1478 self._base_err_lines = err_lines
1479 self._base_warn_lines = warn_lines
1480 self._base_err_line_boards = err_line_boards
1481 self._base_warn_line_boards = warn_line_boards
1482 self._base_config = config
1483 self._base_environment = environment
1485 # Get a list of boards that did not get built, if needed
1487 for board in board_selected:
1488 if not board in board_dict:
1489 not_built.append(board)
1491 Print("Boards not built (%d): %s" % (len(not_built),
1492 ', '.join(not_built)))
1494 def ProduceResultSummary(self, commit_upto, commits, board_selected):
1495 (board_dict, err_lines, err_line_boards, warn_lines,
1496 warn_line_boards, config, environment) = self.GetResultSummary(
1497 board_selected, commit_upto,
1498 read_func_sizes=self._show_bloat,
1499 read_config=self._show_config,
1500 read_environment=self._show_environment)
1502 msg = '%02d: %s' % (commit_upto + 1,
1503 commits[commit_upto].subject)
1504 Print(msg, colour=self.col.BLUE)
1505 self.PrintResultSummary(board_selected, board_dict,
1506 err_lines if self._show_errors else [], err_line_boards,
1507 warn_lines if self._show_errors else [], warn_line_boards,
1508 config, environment, self._show_sizes, self._show_detail,
1509 self._show_bloat, self._show_config, self._show_environment)
1511 def ShowSummary(self, commits, board_selected):
1512 """Show a build summary for U-Boot for a given board list.
1514 Reset the result summary, then repeatedly call GetResultSummary on
1515 each commit's results, then display the differences we see.
1518 commit: Commit objects to summarise
1519 board_selected: Dict containing boards to summarise
1521 self.commit_count = len(commits) if commits else 1
1522 self.commits = commits
1523 self.ResetResultSummary(board_selected)
1524 self._error_lines = 0
1526 for commit_upto in range(0, self.commit_count, self._step):
1527 self.ProduceResultSummary(commit_upto, commits, board_selected)
1528 if not self._error_lines:
1529 Print('(no errors to report)', colour=self.col.GREEN)
1532 def SetupBuild(self, board_selected, commits):
1533 """Set up ready to start a build.
1536 board_selected: Selected boards to build
1537 commits: Selected commits to build
1539 # First work out how many commits we will build
1540 count = (self.commit_count + self._step - 1) // self._step
1541 self.count = len(board_selected) * count
1542 self.upto = self.warned = self.fail = 0
1543 self._timestamps = collections.deque()
1545 def GetThreadDir(self, thread_num):
1546 """Get the directory path to the working dir for a thread.
1549 thread_num: Number of thread to check (-1 for main process, which
1552 if self.work_in_output:
1553 return self._working_dir
1554 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0))
1556 def _PrepareThread(self, thread_num, setup_git):
1557 """Prepare the working directory for a thread.
1559 This clones or fetches the repo into the thread's work directory.
1560 Optionally, it can create a linked working tree of the repo in the
1561 thread's work directory instead.
1564 thread_num: Thread number (0, 1, ...)
1566 'clone' to set up a git clone
1567 'worktree' to set up a git worktree
1569 thread_dir = self.GetThreadDir(thread_num)
1570 builderthread.Mkdir(thread_dir)
1571 git_dir = os.path.join(thread_dir, '.git')
1573 # Create a worktree or a git repo clone for this thread if it
1574 # doesn't already exist
1575 if setup_git and self.git_dir:
1576 src_dir = os.path.abspath(self.git_dir)
1577 if os.path.isdir(git_dir):
1578 # This is a clone of the src_dir repo, we can keep using
1579 # it but need to fetch from src_dir.
1580 Print('\rFetching repo for thread %d' % thread_num,
1582 gitutil.Fetch(git_dir, thread_dir)
1583 terminal.PrintClear()
1584 elif os.path.isfile(git_dir):
1585 # This is a worktree of the src_dir repo, we don't need to
1586 # create it again or update it in any way.
1588 elif os.path.exists(git_dir):
1589 # Don't know what could trigger this, but we probably
1590 # can't create a git worktree/clone here.
1591 raise ValueError('Git dir %s exists, but is not a file '
1592 'or a directory.' % git_dir)
1593 elif setup_git == 'worktree':
1594 Print('\rChecking out worktree for thread %d' % thread_num,
1596 gitutil.AddWorktree(src_dir, thread_dir)
1597 terminal.PrintClear()
1598 elif setup_git == 'clone' or setup_git == True:
1599 Print('\rCloning repo for thread %d' % thread_num,
1601 gitutil.Clone(src_dir, thread_dir)
1602 terminal.PrintClear()
1604 raise ValueError("Can't setup git repo with %s." % setup_git)
1606 def _PrepareWorkingSpace(self, max_threads, setup_git):
1607 """Prepare the working directory for use.
1609 Set up the git repo for each thread. Creates a linked working tree
1610 if git-worktree is available, or clones the repo if it isn't.
1613 max_threads: Maximum number of threads we expect to need. If 0 then
1614 1 is set up, since the main process still needs somewhere to
1616 setup_git: True to set up a git worktree or a git clone
1618 builderthread.Mkdir(self._working_dir)
1619 if setup_git and self.git_dir:
1620 src_dir = os.path.abspath(self.git_dir)
1621 if gitutil.CheckWorktreeIsAvailable(src_dir):
1622 setup_git = 'worktree'
1623 # If we previously added a worktree but the directory for it
1624 # got deleted, we need to prune its files from the repo so
1625 # that we can check out another in its place.
1626 gitutil.PruneWorktrees(src_dir)
1630 # Always do at least one thread
1631 for thread in range(max(max_threads, 1)):
1632 self._PrepareThread(thread, setup_git)
1634 def _GetOutputSpaceRemovals(self):
1635 """Get the output directories ready to receive files.
1637 Figure out what needs to be deleted in the output directory before it
1638 can be used. We only delete old buildman directories which have the
1639 expected name pattern. See _GetOutputDir().
1642 List of full paths of directories to remove
1644 if not self.commits:
1647 for commit_upto in range(self.commit_count):
1648 dir_list.append(self._GetOutputDir(commit_upto))
1651 for dirname in glob.glob(os.path.join(self.base_dir, '*')):
1652 if dirname not in dir_list:
1653 leaf = dirname[len(self.base_dir) + 1:]
1654 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf)
1656 to_remove.append(dirname)
1659 def _PrepareOutputSpace(self):
1660 """Get the output directories ready to receive files.
1662 We delete any output directories which look like ones we need to
1663 create. Having left over directories is confusing when the user wants
1664 to check the output manually.
1666 to_remove = self._GetOutputSpaceRemovals()
1668 Print('Removing %d old build directories...' % len(to_remove),
1670 for dirname in to_remove:
1671 shutil.rmtree(dirname)
1672 terminal.PrintClear()
1674 def BuildBoards(self, commits, board_selected, keep_outputs, verbose):
1675 """Build all commits for a list of boards
1678 commits: List of commits to be build, each a Commit object
1679 boards_selected: Dict of selected boards, key is target name,
1680 value is Board object
1681 keep_outputs: True to save build output files
1682 verbose: Display build results as they are completed
1685 - number of boards that failed to build
1686 - number of boards that issued warnings
1687 - list of thread exceptions raised
1689 self.commit_count = len(commits) if commits else 1
1690 self.commits = commits
1691 self._verbose = verbose
1693 self.ResetResultSummary(board_selected)
1694 builderthread.Mkdir(self.base_dir, parents = True)
1695 self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)),
1696 commits is not None)
1697 self._PrepareOutputSpace()
1698 Print('\rStarting build...', newline=False)
1699 self.SetupBuild(board_selected, commits)
1700 self.ProcessResult(None)
1701 self.thread_exceptions = []
1702 # Create jobs to build all commits for each board
1703 for brd in board_selected.values():
1704 job = builderthread.BuilderJob()
1706 job.commits = commits
1707 job.keep_outputs = keep_outputs
1708 job.work_in_output = self.work_in_output
1709 job.step = self._step
1710 if self.num_threads:
1713 results = self._single_builder.RunJob(job)
1715 if self.num_threads:
1716 term = threading.Thread(target=self.queue.join)
1717 term.setDaemon(True)
1719 while term.is_alive():
1722 # Wait until we have processed all output
1723 self.out_queue.join()
1726 msg = 'Completed: %d total built' % self.count
1727 if self.already_done:
1728 msg += ' (%d previously' % self.already_done
1729 if self.already_done != self.count:
1730 msg += ', %d newly' % (self.count - self.already_done)
1732 duration = datetime.now() - self._start_time
1733 if duration > timedelta(microseconds=1000000):
1734 if duration.microseconds >= 500000:
1735 duration = duration + timedelta(seconds=1)
1736 duration = duration - timedelta(microseconds=duration.microseconds)
1737 rate = float(self.count) / duration.total_seconds()
1738 msg += ', duration %s, rate %1.2f' % (duration, rate)
1740 if self.thread_exceptions:
1741 Print('Failed: %d thread exceptions' % len(self.thread_exceptions),
1742 colour=self.col.RED)
1744 return (self.fail, self.warned, self.thread_exceptions)