1# SPDX-License-Identifier: GPL-2.0+ 2# Copyright (c) 2013 The Chromium OS Authors. 3# 4# Bloat-o-meter code used here Copyright 2004 Matt Mackall <mpm@selenic.com> 5# 6 7import collections 8from datetime import datetime, timedelta 9import glob 10import os 11import re 12import queue 13import shutil 14import signal 15import string 16import sys 17import threading 18import time 19 20from buildman import builderthread 21from buildman import toolchain 22from u_boot_pylib import command 23from u_boot_pylib import gitutil 24from u_boot_pylib import terminal 25from u_boot_pylib import tools 26from u_boot_pylib.terminal import tprint 27 28# This indicates an new int or hex Kconfig property with no default 29# It hangs the build since the 'conf' tool cannot proceed without valid input. 30# 31# We get a repeat sequence of something like this: 32# >> 33# Break things (BREAK_ME) [] (NEW) 34# Error in reading or end of file. 35# << 36# which indicates that BREAK_ME has an empty default 37RE_NO_DEFAULT = re.compile(br'\((\w+)\) \[] \(NEW\)') 38 39# Symbol types which appear in the bloat feature (-B). Others are silently 40# dropped when reading in the 'nm' output 41NM_SYMBOL_TYPES = 'tTdDbBr' 42 43""" 44Theory of Operation 45 46Please see README for user documentation, and you should be familiar with 47that before trying to make sense of this. 48 49Buildman works by keeping the machine as busy as possible, building different 50commits for different boards on multiple CPUs at once. 51 52The source repo (self.git_dir) contains all the commits to be built. Each 53thread works on a single board at a time. It checks out the first commit, 54configures it for that board, then builds it. Then it checks out the next 55commit and builds it (typically without re-configuring). When it runs out 56of commits, it gets another job from the builder and starts again with that 57board. 58 59Clearly the builder threads could work either way - they could check out a 60commit and then built it for all boards. Using separate directories for each 61commit/board pair they could leave their build product around afterwards 62also. 63 64The intent behind building a single board for multiple commits, is to make 65use of incremental builds. Since each commit is built incrementally from 66the previous one, builds are faster. Reconfiguring for a different board 67removes all intermediate object files. 68 69Many threads can be working at once, but each has its own working directory. 70When a thread finishes a build, it puts the output files into a result 71directory. 72 73The base directory used by buildman is normally '../<branch>', i.e. 74a directory higher than the source repository and named after the branch 75being built. 76 77Within the base directory, we have one subdirectory for each commit. Within 78that is one subdirectory for each board. Within that is the build output for 79that commit/board combination. 80 81Buildman also create working directories for each thread, in a .bm-work/ 82subdirectory in the base dir. 83 84As an example, say we are building branch 'us-net' for boards 'sandbox' and 85'seaboard', and say that us-net has two commits. We will have directories 86like this: 87 88us-net/ base directory 89 01_g4ed4ebc_net--Add-tftp-speed-/ 90 sandbox/ 91 u-boot.bin 92 seaboard/ 93 u-boot.bin 94 02_g4ed4ebc_net--Check-tftp-comp/ 95 sandbox/ 96 u-boot.bin 97 seaboard/ 98 u-boot.bin 99 .bm-work/ 100 00/ working directory for thread 0 (contains source checkout) 101 build/ build output 102 01/ working directory for thread 1 103 build/ build output 104 ... 105u-boot/ source directory 106 .git/ repository 107""" 108 109"""Holds information about a particular error line we are outputing 110 111 char: Character representation: '+': error, '-': fixed error, 'w+': warning, 112 'w-' = fixed warning 113 boards: List of Board objects which have line in the error/warning output 114 errline: The text of the error line 115""" 116ErrLine = collections.namedtuple('ErrLine', 'char,brds,errline') 117 118# Possible build outcomes 119OUTCOME_OK, OUTCOME_WARNING, OUTCOME_ERROR, OUTCOME_UNKNOWN = list(range(4)) 120 121# Translate a commit subject into a valid filename (and handle unicode) 122trans_valid_chars = str.maketrans('/: ', '---') 123 124BASE_CONFIG_FILENAMES = [ 125 'u-boot.cfg', 'u-boot-spl.cfg', 'u-boot-tpl.cfg' 126] 127 128EXTRA_CONFIG_FILENAMES = [ 129 '.config', '.config-spl', '.config-tpl', 130 'autoconf.mk', 'autoconf-spl.mk', 'autoconf-tpl.mk', 131 'autoconf.h', 'autoconf-spl.h','autoconf-tpl.h', 132] 133 134class Config: 135 """Holds information about configuration settings for a board.""" 136 def __init__(self, config_filename, target): 137 self.target = target 138 self.config = {} 139 for fname in config_filename: 140 self.config[fname] = {} 141 142 def add(self, fname, key, value): 143 self.config[fname][key] = value 144 145 def __hash__(self): 146 val = 0 147 for fname in self.config: 148 for key, value in self.config[fname].items(): 149 print(key, value) 150 val = val ^ hash(key) & hash(value) 151 return val 152 153class Environment: 154 """Holds information about environment variables for a board.""" 155 def __init__(self, target): 156 self.target = target 157 self.environment = {} 158 159 def add(self, key, value): 160 self.environment[key] = value 161 162class Builder: 163 """Class for building U-Boot for a particular commit. 164 165 Public members: (many should ->private) 166 already_done: Number of builds already completed 167 base_dir: Base directory to use for builder 168 checkout: True to check out source, False to skip that step. 169 This is used for testing. 170 col: terminal.Color() object 171 count: Total number of commits to build, which is the number of commits 172 multiplied by the number of boards 173 do_make: Method to call to invoke Make 174 fail: Number of builds that failed due to error 175 force_build: Force building even if a build already exists 176 force_config_on_failure: If a commit fails for a board, disable 177 incremental building for the next commit we build for that 178 board, so that we will see all warnings/errors again. 179 force_build_failures: If a previously-built build (i.e. built on 180 a previous run of buildman) is marked as failed, rebuild it. 181 git_dir: Git directory containing source repository 182 num_jobs: Number of jobs to run at once (passed to make as -j) 183 num_threads: Number of builder threads to run 184 out_queue: Queue of results to process 185 re_make_err: Compiled regular expression for ignore_lines 186 queue: Queue of jobs to run 187 threads: List of active threads 188 toolchains: Toolchains object to use for building 189 upto: Current commit number we are building (0.count-1) 190 warned: Number of builds that produced at least one warning 191 force_reconfig: Reconfigure U-Boot on each comiit. This disables 192 incremental building, where buildman reconfigures on the first 193 commit for a baord, and then just does an incremental build for 194 the following commits. In fact buildman will reconfigure and 195 retry for any failing commits, so generally the only effect of 196 this option is to slow things down. 197 in_tree: Build U-Boot in-tree instead of specifying an output 198 directory separate from the source code. This option is really 199 only useful for testing in-tree builds. 200 work_in_output: Use the output directory as the work directory and 201 don't write to a separate output directory. 202 thread_exceptions: List of exceptions raised by thread jobs 203 no_lto (bool): True to set the NO_LTO flag when building 204 reproducible_builds (bool): True to set SOURCE_DATE_EPOCH=0 for builds 205 206 Private members: 207 _base_board_dict: Last-summarised Dict of boards 208 _base_err_lines: Last-summarised list of errors 209 _base_warn_lines: Last-summarised list of warnings 210 _build_period_us: Time taken for a single build (float object). 211 _complete_delay: Expected delay until completion (timedelta) 212 _next_delay_update: Next time we plan to display a progress update 213 (datatime) 214 _show_unknown: Show unknown boards (those not built) in summary 215 _start_time: Start time for the build 216 _timestamps: List of timestamps for the completion of the last 217 last _timestamp_count builds. Each is a datetime object. 218 _timestamp_count: Number of timestamps to keep in our list. 219 _working_dir: Base working directory containing all threads 220 _single_builder: BuilderThread object for the singer builder, if 221 threading is not being used 222 _terminated: Thread was terminated due to an error 223 _restarting_config: True if 'Restart config' is detected in output 224 _ide: Produce output suitable for an Integrated Development Environment, 225 i.e. dont emit progress information and put errors/warnings on stderr 226 """ 227 class Outcome: 228 """Records a build outcome for a single make invocation 229 230 Public Members: 231 rc: Outcome value (OUTCOME_...) 232 err_lines: List of error lines or [] if none 233 sizes: Dictionary of image size information, keyed by filename 234 - Each value is itself a dictionary containing 235 values for 'text', 'data' and 'bss', being the integer 236 size in bytes of each section. 237 func_sizes: Dictionary keyed by filename - e.g. 'u-boot'. Each 238 value is itself a dictionary: 239 key: function name 240 value: Size of function in bytes 241 config: Dictionary keyed by filename - e.g. '.config'. Each 242 value is itself a dictionary: 243 key: config name 244 value: config value 245 environment: Dictionary keyed by environment variable, Each 246 value is the value of environment variable. 247 """ 248 def __init__(self, rc, err_lines, sizes, func_sizes, config, 249 environment): 250 self.rc = rc 251 self.err_lines = err_lines 252 self.sizes = sizes 253 self.func_sizes = func_sizes 254 self.config = config 255 self.environment = environment 256 257 def __init__(self, toolchains, base_dir, git_dir, num_threads, num_jobs, 258 gnu_make='make', checkout=True, show_unknown=True, step=1, 259 no_subdirs=False, full_path=False, verbose_build=False, 260 mrproper=False, fallback_mrproper=False, 261 per_board_out_dir=False, config_only=False, 262 squash_config_y=False, warnings_as_errors=False, 263 work_in_output=False, test_thread_exceptions=False, 264 adjust_cfg=None, allow_missing=False, no_lto=False, 265 reproducible_builds=False, force_build=False, 266 force_build_failures=False, force_reconfig=False, 267 in_tree=False, force_config_on_failure=False, make_func=None, 268 dtc_skip=False, build_target=None): 269 """Create a new Builder object 270 271 Args: 272 toolchains: Toolchains object to use for building 273 base_dir: Base directory to use for builder 274 git_dir: Git directory containing source repository 275 num_threads: Number of builder threads to run 276 num_jobs: Number of jobs to run at once (passed to make as -j) 277 gnu_make: the command name of GNU Make. 278 checkout: True to check out source, False to skip that step. 279 This is used for testing. 280 show_unknown: Show unknown boards (those not built) in summary 281 step: 1 to process every commit, n to process every nth commit 282 no_subdirs: Don't create subdirectories when building current 283 source for a single board 284 full_path: Return the full path in CROSS_COMPILE and don't set 285 PATH 286 verbose_build: Run build with V=1 and don't use 'make -s' 287 mrproper: Always run 'make mrproper' when configuring 288 fallback_mrproper: Run 'make mrproper' and retry on build failure 289 per_board_out_dir: Build in a separate persistent directory per 290 board rather than a thread-specific directory 291 config_only: Only configure each build, don't build it 292 squash_config_y: Convert CONFIG options with the value 'y' to '1' 293 warnings_as_errors: Treat all compiler warnings as errors 294 work_in_output: Use the output directory as the work directory and 295 don't write to a separate output directory. 296 test_thread_exceptions: Uses for tests only, True to make the 297 threads raise an exception instead of reporting their result. 298 This simulates a failure in the code somewhere 299 adjust_cfg_list (list of str): List of changes to make to .config 300 file before building. Each is one of (where C is the config 301 option with or without the CONFIG_ prefix) 302 303 C to enable C 304 ~C to disable C 305 C=val to set the value of C (val must have quotes if C is 306 a string Kconfig 307 allow_missing: Run build with BINMAN_ALLOW_MISSING=1 308 no_lto (bool): True to set the NO_LTO flag when building 309 force_build (bool): Rebuild even commits that are already built 310 force_build_failures (bool): Rebuild commits that have not been 311 built, or failed to build 312 force_reconfig (bool): Reconfigure on each commit 313 in_tree (bool): Bulid in tree instead of out-of-tree 314 force_config_on_failure (bool): Reconfigure the build before 315 retrying a failed build 316 make_func (function): Function to call to run 'make' 317 dtc_skip (bool): True to skip building dtc and use the system one 318 build_target (str): Build target to use (None to use the default) 319 """ 320 self.toolchains = toolchains 321 self.base_dir = base_dir 322 if work_in_output: 323 self._working_dir = base_dir 324 else: 325 self._working_dir = os.path.join(base_dir, '.bm-work') 326 self.threads = [] 327 self.do_make = make_func or self.make 328 self.gnu_make = gnu_make 329 self.checkout = checkout 330 self.num_threads = num_threads 331 self.num_jobs = num_jobs 332 self.already_done = 0 333 self.force_build = False 334 self.git_dir = git_dir 335 self._show_unknown = show_unknown 336 self._timestamp_count = 10 337 self._build_period_us = None 338 self._complete_delay = None 339 self._next_delay_update = datetime.now() 340 self._start_time = None 341 self._step = step 342 self._error_lines = 0 343 self.no_subdirs = no_subdirs 344 self.full_path = full_path 345 self.verbose_build = verbose_build 346 self.config_only = config_only 347 self.squash_config_y = squash_config_y 348 self.config_filenames = BASE_CONFIG_FILENAMES 349 self.work_in_output = work_in_output 350 self.adjust_cfg = adjust_cfg 351 self.allow_missing = allow_missing 352 self._ide = False 353 self.no_lto = no_lto 354 self.reproducible_builds = reproducible_builds 355 self.force_build = force_build 356 self.force_build_failures = force_build_failures 357 self.force_reconfig = force_reconfig 358 self.in_tree = in_tree 359 self.force_config_on_failure = force_config_on_failure 360 self.fallback_mrproper = fallback_mrproper 361 if dtc_skip: 362 self.dtc = shutil.which('dtc') 363 if not self.dtc: 364 raise ValueError('Cannot find dtc') 365 else: 366 self.dtc = None 367 self.build_target = build_target 368 369 if not self.squash_config_y: 370 self.config_filenames += EXTRA_CONFIG_FILENAMES 371 self._terminated = False 372 self._restarting_config = False 373 374 self.warnings_as_errors = warnings_as_errors 375 self.col = terminal.Color() 376 377 self._re_function = re.compile('(.*): In function.*') 378 self._re_files = re.compile('In file included from.*') 379 self._re_warning = re.compile(r'(.*):(\d*):(\d*): warning: .*') 380 self._re_dtb_warning = re.compile('(.*): Warning .*') 381 self._re_note = re.compile(r'(.*):(\d*):(\d*): note: this is the location of the previous.*') 382 self._re_migration_warning = re.compile(r'^={21} WARNING ={22}\n.*\n=+\n', 383 re.MULTILINE | re.DOTALL) 384 385 self.thread_exceptions = [] 386 self.test_thread_exceptions = test_thread_exceptions 387 if self.num_threads: 388 self._single_builder = None 389 self.queue = queue.Queue() 390 self.out_queue = queue.Queue() 391 for i in range(self.num_threads): 392 t = builderthread.BuilderThread( 393 self, i, mrproper, per_board_out_dir, 394 test_exception=test_thread_exceptions) 395 t.setDaemon(True) 396 t.start() 397 self.threads.append(t) 398 399 t = builderthread.ResultThread(self) 400 t.setDaemon(True) 401 t.start() 402 self.threads.append(t) 403 else: 404 self._single_builder = builderthread.BuilderThread( 405 self, -1, mrproper, per_board_out_dir) 406 407 ignore_lines = ['(make.*Waiting for unfinished)', '(Segmentation fault)'] 408 self.re_make_err = re.compile('|'.join(ignore_lines)) 409 410 # Handle existing graceful with SIGINT / Ctrl-C 411 signal.signal(signal.SIGINT, self.signal_handler) 412 413 def __del__(self): 414 """Get rid of all threads created by the builder""" 415 for t in self.threads: 416 del t 417 418 def signal_handler(self, signal, frame): 419 sys.exit(1) 420 421 def make_environment(self, toolchain): 422 """Create the environment to use for building 423 424 Args: 425 toolchain (Toolchain): Toolchain to use for building 426 427 Returns: 428 dict: 429 key (str): Variable name 430 value (str): Variable value 431 """ 432 env = toolchain.MakeEnvironment(self.full_path) 433 if self.dtc: 434 env[b'DTC'] = tools.to_bytes(self.dtc) 435 return env 436 437 def set_display_options(self, show_errors=False, show_sizes=False, 438 show_detail=False, show_bloat=False, 439 list_error_boards=False, show_config=False, 440 show_environment=False, filter_dtb_warnings=False, 441 filter_migration_warnings=False, ide=False): 442 """Setup display options for the builder. 443 444 Args: 445 show_errors: True to show summarised error/warning info 446 show_sizes: Show size deltas 447 show_detail: Show size delta detail for each board if show_sizes 448 show_bloat: Show detail for each function 449 list_error_boards: Show the boards which caused each error/warning 450 show_config: Show config deltas 451 show_environment: Show environment deltas 452 filter_dtb_warnings: Filter out any warnings from the device-tree 453 compiler 454 filter_migration_warnings: Filter out any warnings about migrating 455 a board to driver model 456 ide: Create output that can be parsed by an IDE. There is no '+' prefix on 457 error lines and output on stderr stays on stderr. 458 """ 459 self._show_errors = show_errors 460 self._show_sizes = show_sizes 461 self._show_detail = show_detail 462 self._show_bloat = show_bloat 463 self._list_error_boards = list_error_boards 464 self._show_config = show_config 465 self._show_environment = show_environment 466 self._filter_dtb_warnings = filter_dtb_warnings 467 self._filter_migration_warnings = filter_migration_warnings 468 self._ide = ide 469 470 def _add_timestamp(self): 471 """Add a new timestamp to the list and record the build period. 472 473 The build period is the length of time taken to perform a single 474 build (one board, one commit). 475 """ 476 now = datetime.now() 477 self._timestamps.append(now) 478 count = len(self._timestamps) 479 delta = self._timestamps[-1] - self._timestamps[0] 480 seconds = delta.total_seconds() 481 482 # If we have enough data, estimate build period (time taken for a 483 # single build) and therefore completion time. 484 if count > 1 and self._next_delay_update < now: 485 self._next_delay_update = now + timedelta(seconds=2) 486 if seconds > 0: 487 self._build_period = float(seconds) / count 488 todo = self.count - self.upto 489 self._complete_delay = timedelta(microseconds= 490 self._build_period * todo * 1000000) 491 # Round it 492 self._complete_delay -= timedelta( 493 microseconds=self._complete_delay.microseconds) 494 495 if seconds > 60: 496 self._timestamps.popleft() 497 count -= 1 498 499 def select_commit(self, commit, checkout=True): 500 """Checkout the selected commit for this build 501 """ 502 self.commit = commit 503 if checkout and self.checkout: 504 gitutil.checkout(commit.hash) 505 506 def make(self, commit, brd, stage, cwd, *args, **kwargs): 507 """Run make 508 509 Args: 510 commit: Commit object that is being built 511 brd: Board object that is being built 512 stage: Stage that we are at (mrproper, config, oldconfig, build) 513 cwd: Directory where make should be run 514 args: Arguments to pass to make 515 kwargs: Arguments to pass to command.run_one() 516 """ 517 518 def check_output(stream, data): 519 if b'Restart config' in data: 520 self._restarting_config = True 521 522 # If we see 'Restart config' following by multiple errors 523 if self._restarting_config: 524 m = RE_NO_DEFAULT.findall(data) 525 526 # Number of occurences of each Kconfig item 527 multiple = [m.count(val) for val in set(m)] 528 529 # If any of them occur more than once, we have a loop 530 if [val for val in multiple if val > 1]: 531 self._terminated = True 532 return True 533 return False 534 535 self._restarting_config = False 536 self._terminated = False 537 cmd = [self.gnu_make] + list(args) 538 result = command.run_one(*cmd, capture=True, capture_stderr=True, 539 cwd=cwd, raise_on_error=False, 540 infile='/dev/null', output_func=check_output, 541 **kwargs) 542 543 if self._terminated: 544 # Try to be helpful 545 result.stderr += '(** did you define an int/hex Kconfig with no default? **)' 546 547 if self.verbose_build: 548 result.stdout = '%s\n' % (' '.join(cmd)) + result.stdout 549 result.combined = '%s\n' % (' '.join(cmd)) + result.combined 550 return result 551 552 def process_result(self, result): 553 """Process the result of a build, showing progress information 554 555 Args: 556 result: A CommandResult object, which indicates the result for 557 a single build 558 """ 559 col = terminal.Color() 560 if result: 561 target = result.brd.target 562 563 self.upto += 1 564 if result.return_code != 0: 565 self.fail += 1 566 elif result.stderr: 567 self.warned += 1 568 if result.already_done: 569 self.already_done += 1 570 if self._verbose: 571 terminal.print_clear() 572 boards_selected = {target : result.brd} 573 self.reset_result_summary(boards_selected) 574 self.produce_result_summary(result.commit_upto, self.commits, 575 boards_selected) 576 else: 577 target = '(starting)' 578 579 # Display separate counts for ok, warned and fail 580 ok = self.upto - self.warned - self.fail 581 line = '\r' + self.col.build(self.col.GREEN, '%5d' % ok) 582 line += self.col.build(self.col.YELLOW, '%5d' % self.warned) 583 line += self.col.build(self.col.RED, '%5d' % self.fail) 584 585 line += ' /%-5d ' % self.count 586 remaining = self.count - self.upto 587 if remaining: 588 line += self.col.build(self.col.MAGENTA, ' -%-5d ' % remaining) 589 else: 590 line += ' ' * 8 591 592 # Add our current completion time estimate 593 self._add_timestamp() 594 if self._complete_delay: 595 line += '%s : ' % self._complete_delay 596 597 line += target 598 if not self._ide: 599 terminal.print_clear() 600 tprint(line, newline=False, limit_to_line=True) 601 602 def get_output_dir(self, commit_upto): 603 """Get the name of the output directory for a commit number 604 605 The output directory is typically .../<branch>/<commit>. 606 607 Args: 608 commit_upto: Commit number to use (0..self.count-1) 609 """ 610 if self.work_in_output: 611 return self._working_dir 612 613 commit_dir = None 614 if self.commits: 615 commit = self.commits[commit_upto] 616 subject = commit.subject.translate(trans_valid_chars) 617 # See _get_output_space_removals() which parses this name 618 commit_dir = ('%02d_g%s_%s' % (commit_upto + 1, 619 commit.hash, subject[:20])) 620 elif not self.no_subdirs: 621 commit_dir = 'current' 622 if not commit_dir: 623 return self.base_dir 624 return os.path.join(self.base_dir, commit_dir) 625 626 def get_build_dir(self, commit_upto, target): 627 """Get the name of the build directory for a commit number 628 629 The build directory is typically .../<branch>/<commit>/<target>. 630 631 Args: 632 commit_upto: Commit number to use (0..self.count-1) 633 target: Target name 634 635 Return: 636 str: Output directory to use, or '' if None 637 """ 638 output_dir = self.get_output_dir(commit_upto) 639 if self.work_in_output: 640 return output_dir or '' 641 return os.path.join(output_dir, target) 642 643 def get_done_file(self, commit_upto, target): 644 """Get the name of the done file for a commit number 645 646 Args: 647 commit_upto: Commit number to use (0..self.count-1) 648 target: Target name 649 """ 650 return os.path.join(self.get_build_dir(commit_upto, target), 'done') 651 652 def get_sizes_file(self, commit_upto, target): 653 """Get the name of the sizes file for a commit number 654 655 Args: 656 commit_upto: Commit number to use (0..self.count-1) 657 target: Target name 658 """ 659 return os.path.join(self.get_build_dir(commit_upto, target), 'sizes') 660 661 def get_func_sizes_file(self, commit_upto, target, elf_fname): 662 """Get the name of the funcsizes file for a commit number and ELF file 663 664 Args: 665 commit_upto: Commit number to use (0..self.count-1) 666 target: Target name 667 elf_fname: Filename of elf image 668 """ 669 return os.path.join(self.get_build_dir(commit_upto, target), 670 '%s.sizes' % elf_fname.replace('/', '-')) 671 672 def get_objdump_file(self, commit_upto, target, elf_fname): 673 """Get the name of the objdump file for a commit number and ELF file 674 675 Args: 676 commit_upto: Commit number to use (0..self.count-1) 677 target: Target name 678 elf_fname: Filename of elf image 679 """ 680 return os.path.join(self.get_build_dir(commit_upto, target), 681 '%s.objdump' % elf_fname.replace('/', '-')) 682 683 def get_err_file(self, commit_upto, target): 684 """Get the name of the err file for a commit number 685 686 Args: 687 commit_upto: Commit number to use (0..self.count-1) 688 target: Target name 689 """ 690 output_dir = self.get_build_dir(commit_upto, target) 691 return os.path.join(output_dir, 'err') 692 693 def filter_errors(self, lines): 694 """Filter out errors in which we have no interest 695 696 We should probably use map(). 697 698 Args: 699 lines: List of error lines, each a string 700 Returns: 701 New list with only interesting lines included 702 """ 703 out_lines = [] 704 if self._filter_migration_warnings: 705 text = '\n'.join(lines) 706 text = self._re_migration_warning.sub('', text) 707 lines = text.splitlines() 708 for line in lines: 709 if self.re_make_err.search(line): 710 continue 711 if self._filter_dtb_warnings and self._re_dtb_warning.search(line): 712 continue 713 out_lines.append(line) 714 return out_lines 715 716 def read_func_sizes(self, fname, fd): 717 """Read function sizes from the output of 'nm' 718 719 Args: 720 fd: File containing data to read 721 fname: Filename we are reading from (just for errors) 722 723 Returns: 724 Dictionary containing size of each function in bytes, indexed by 725 function name. 726 """ 727 sym = {} 728 for line in fd.readlines(): 729 line = line.strip() 730 parts = line.split() 731 if line and len(parts) == 3: 732 size, type, name = line.split() 733 if type in NM_SYMBOL_TYPES: 734 # function names begin with '.' on 64-bit powerpc 735 if '.' in name[1:]: 736 name = 'static.' + name.split('.')[0] 737 sym[name] = sym.get(name, 0) + int(size, 16) 738 return sym 739 740 def _process_config(self, fname): 741 """Read in a .config, autoconf.mk or autoconf.h file 742 743 This function handles all config file types. It ignores comments and 744 any #defines which don't start with CONFIG_. 745 746 Args: 747 fname: Filename to read 748 749 Returns: 750 Dictionary: 751 key: Config name (e.g. CONFIG_DM) 752 value: Config value (e.g. 1) 753 """ 754 config = {} 755 if os.path.exists(fname): 756 with open(fname) as fd: 757 for line in fd: 758 line = line.strip() 759 if line.startswith('#define'): 760 values = line[8:].split(' ', 1) 761 if len(values) > 1: 762 key, value = values 763 else: 764 key = values[0] 765 value = '1' if self.squash_config_y else '' 766 if not key.startswith('CONFIG_'): 767 continue 768 elif not line or line[0] in ['#', '*', '/']: 769 continue 770 else: 771 key, value = line.split('=', 1) 772 if self.squash_config_y and value == 'y': 773 value = '1' 774 config[key] = value 775 return config 776 777 def _process_environment(self, fname): 778 """Read in a uboot.env file 779 780 This function reads in environment variables from a file. 781 782 Args: 783 fname: Filename to read 784 785 Returns: 786 Dictionary: 787 key: environment variable (e.g. bootlimit) 788 value: value of environment variable (e.g. 1) 789 """ 790 environment = {} 791 if os.path.exists(fname): 792 with open(fname) as fd: 793 for line in fd.read().split('\0'): 794 try: 795 key, value = line.split('=', 1) 796 environment[key] = value 797 except ValueError: 798 # ignore lines we can't parse 799 pass 800 return environment 801 802 def get_build_outcome(self, commit_upto, target, read_func_sizes, 803 read_config, read_environment): 804 """Work out the outcome of a build. 805 806 Args: 807 commit_upto: Commit number to check (0..n-1) 808 target: Target board to check 809 read_func_sizes: True to read function size information 810 read_config: True to read .config and autoconf.h files 811 read_environment: True to read uboot.env files 812 813 Returns: 814 Outcome object 815 """ 816 done_file = self.get_done_file(commit_upto, target) 817 sizes_file = self.get_sizes_file(commit_upto, target) 818 sizes = {} 819 func_sizes = {} 820 config = {} 821 environment = {} 822 if os.path.exists(done_file): 823 with open(done_file, 'r') as fd: 824 try: 825 return_code = int(fd.readline()) 826 except ValueError: 827 # The file may be empty due to running out of disk space. 828 # Try a rebuild 829 return_code = 1 830 err_lines = [] 831 err_file = self.get_err_file(commit_upto, target) 832 if os.path.exists(err_file): 833 with open(err_file, 'r') as fd: 834 err_lines = self.filter_errors(fd.readlines()) 835 836 # Decide whether the build was ok, failed or created warnings 837 if return_code: 838 rc = OUTCOME_ERROR 839 elif len(err_lines): 840 rc = OUTCOME_WARNING 841 else: 842 rc = OUTCOME_OK 843 844 # Convert size information to our simple format 845 if os.path.exists(sizes_file): 846 with open(sizes_file, 'r') as fd: 847 for line in fd.readlines(): 848 values = line.split() 849 rodata = 0 850 if len(values) > 6: 851 rodata = int(values[6], 16) 852 size_dict = { 853 'all' : int(values[0]) + int(values[1]) + 854 int(values[2]), 855 'text' : int(values[0]) - rodata, 856 'data' : int(values[1]), 857 'bss' : int(values[2]), 858 'rodata' : rodata, 859 } 860 sizes[values[5]] = size_dict 861 862 if read_func_sizes: 863 pattern = self.get_func_sizes_file(commit_upto, target, '*') 864 for fname in glob.glob(pattern): 865 with open(fname, 'r') as fd: 866 dict_name = os.path.basename(fname).replace('.sizes', 867 '') 868 func_sizes[dict_name] = self.read_func_sizes(fname, fd) 869 870 if read_config: 871 output_dir = self.get_build_dir(commit_upto, target) 872 for name in self.config_filenames: 873 fname = os.path.join(output_dir, name) 874 config[name] = self._process_config(fname) 875 876 if read_environment: 877 output_dir = self.get_build_dir(commit_upto, target) 878 fname = os.path.join(output_dir, 'uboot.env') 879 environment = self._process_environment(fname) 880 881 return Builder.Outcome(rc, err_lines, sizes, func_sizes, config, 882 environment) 883 884 return Builder.Outcome(OUTCOME_UNKNOWN, [], {}, {}, {}, {}) 885 886 def get_result_summary(self, boards_selected, commit_upto, read_func_sizes, 887 read_config, read_environment): 888 """Calculate a summary of the results of building a commit. 889 890 Args: 891 board_selected: Dict containing boards to summarise 892 commit_upto: Commit number to summarize (0..self.count-1) 893 read_func_sizes: True to read function size information 894 read_config: True to read .config and autoconf.h files 895 read_environment: True to read uboot.env files 896 897 Returns: 898 Tuple: 899 Dict containing boards which built this commit: 900 key: board.target 901 value: Builder.Outcome object 902 List containing a summary of error lines 903 Dict keyed by error line, containing a list of the Board 904 objects with that error 905 List containing a summary of warning lines 906 Dict keyed by error line, containing a list of the Board 907 objects with that warning 908 Dictionary keyed by board.target. Each value is a dictionary: 909 key: filename - e.g. '.config' 910 value is itself a dictionary: 911 key: config name 912 value: config value 913 Dictionary keyed by board.target. Each value is a dictionary: 914 key: environment variable 915 value: value of environment variable 916 """ 917 def add_line(lines_summary, lines_boards, line, board): 918 line = line.rstrip() 919 if line in lines_boards: 920 lines_boards[line].append(board) 921 else: 922 lines_boards[line] = [board] 923 lines_summary.append(line) 924 925 board_dict = {} 926 err_lines_summary = [] 927 err_lines_boards = {} 928 warn_lines_summary = [] 929 warn_lines_boards = {} 930 config = {} 931 environment = {} 932 933 for brd in boards_selected.values(): 934 outcome = self.get_build_outcome(commit_upto, brd.target, 935 read_func_sizes, read_config, 936 read_environment) 937 board_dict[brd.target] = outcome 938 last_func = None 939 last_was_warning = False 940 for line in outcome.err_lines: 941 if line: 942 if (self._re_function.match(line) or 943 self._re_files.match(line)): 944 last_func = line 945 else: 946 is_warning = (self._re_warning.match(line) or 947 self._re_dtb_warning.match(line)) 948 is_note = self._re_note.match(line) 949 if is_warning or (last_was_warning and is_note): 950 if last_func: 951 add_line(warn_lines_summary, warn_lines_boards, 952 last_func, brd) 953 add_line(warn_lines_summary, warn_lines_boards, 954 line, brd) 955 else: 956 if last_func: 957 add_line(err_lines_summary, err_lines_boards, 958 last_func, brd) 959 add_line(err_lines_summary, err_lines_boards, 960 line, brd) 961 last_was_warning = is_warning 962 last_func = None 963 tconfig = Config(self.config_filenames, brd.target) 964 for fname in self.config_filenames: 965 if outcome.config: 966 for key, value in outcome.config[fname].items(): 967 tconfig.add(fname, key, value) 968 config[brd.target] = tconfig 969 970 tenvironment = Environment(brd.target) 971 if outcome.environment: 972 for key, value in outcome.environment.items(): 973 tenvironment.add(key, value) 974 environment[brd.target] = tenvironment 975 976 return (board_dict, err_lines_summary, err_lines_boards, 977 warn_lines_summary, warn_lines_boards, config, environment) 978 979 def add_outcome(self, board_dict, arch_list, changes, char, color): 980 """Add an output to our list of outcomes for each architecture 981 982 This simple function adds failing boards (changes) to the 983 relevant architecture string, so we can print the results out 984 sorted by architecture. 985 986 Args: 987 board_dict: Dict containing all boards 988 arch_list: Dict keyed by arch name. Value is a string containing 989 a list of board names which failed for that arch. 990 changes: List of boards to add to arch_list 991 color: terminal.Colour object 992 """ 993 done_arch = {} 994 for target in changes: 995 if target in board_dict: 996 arch = board_dict[target].arch 997 else: 998 arch = 'unknown' 999 str = self.col.build(color, ' ' + target) 1000 if not arch in done_arch: 1001 str = ' %s %s' % (self.col.build(color, char), str) 1002 done_arch[arch] = True 1003 if not arch in arch_list: 1004 arch_list[arch] = str 1005 else: 1006 arch_list[arch] += str 1007 1008 1009 def colour_num(self, num): 1010 color = self.col.RED if num > 0 else self.col.GREEN 1011 if num == 0: 1012 return '0' 1013 return self.col.build(color, str(num)) 1014 1015 def reset_result_summary(self, board_selected): 1016 """Reset the results summary ready for use. 1017 1018 Set up the base board list to be all those selected, and set the 1019 error lines to empty. 1020 1021 Following this, calls to print_result_summary() will use this 1022 information to work out what has changed. 1023 1024 Args: 1025 board_selected: Dict containing boards to summarise, keyed by 1026 board.target 1027 """ 1028 self._base_board_dict = {} 1029 for brd in board_selected: 1030 self._base_board_dict[brd] = Builder.Outcome(0, [], [], {}, {}, {}) 1031 self._base_err_lines = [] 1032 self._base_warn_lines = [] 1033 self._base_err_line_boards = {} 1034 self._base_warn_line_boards = {} 1035 self._base_config = None 1036 self._base_environment = None 1037 1038 def print_func_size_detail(self, fname, old, new): 1039 grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0 1040 delta, common = [], {} 1041 1042 for a in old: 1043 if a in new: 1044 common[a] = 1 1045 1046 for name in old: 1047 if name not in common: 1048 remove += 1 1049 down += old[name] 1050 delta.append([-old[name], name]) 1051 1052 for name in new: 1053 if name not in common: 1054 add += 1 1055 up += new[name] 1056 delta.append([new[name], name]) 1057 1058 for name in common: 1059 diff = new.get(name, 0) - old.get(name, 0) 1060 if diff > 0: 1061 grow, up = grow + 1, up + diff 1062 elif diff < 0: 1063 shrink, down = shrink + 1, down - diff 1064 delta.append([diff, name]) 1065 1066 delta.sort() 1067 delta.reverse() 1068 1069 args = [add, -remove, grow, -shrink, up, -down, up - down] 1070 if max(args) == 0 and min(args) == 0: 1071 return 1072 args = [self.colour_num(x) for x in args] 1073 indent = ' ' * 15 1074 tprint('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' % 1075 tuple([indent, self.col.build(self.col.YELLOW, fname)] + args)) 1076 tprint('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new', 1077 'delta')) 1078 for diff, name in delta: 1079 if diff: 1080 color = self.col.RED if diff > 0 else self.col.GREEN 1081 msg = '%s %-38s %7s %7s %+7d' % (indent, name, 1082 old.get(name, '-'), new.get(name,'-'), diff) 1083 tprint(msg, colour=color) 1084 1085 1086 def print_size_detail(self, target_list, show_bloat): 1087 """Show details size information for each board 1088 1089 Args: 1090 target_list: List of targets, each a dict containing: 1091 'target': Target name 1092 'total_diff': Total difference in bytes across all areas 1093 <part_name>: Difference for that part 1094 show_bloat: Show detail for each function 1095 """ 1096 targets_by_diff = sorted(target_list, reverse=True, 1097 key=lambda x: x['_total_diff']) 1098 for result in targets_by_diff: 1099 printed_target = False 1100 for name in sorted(result): 1101 diff = result[name] 1102 if name.startswith('_'): 1103 continue 1104 colour = self.col.RED if diff > 0 else self.col.GREEN 1105 msg = ' %s %+d' % (name, diff) 1106 if not printed_target: 1107 tprint('%10s %-15s:' % ('', result['_target']), 1108 newline=False) 1109 printed_target = True 1110 tprint(msg, colour=colour, newline=False) 1111 if printed_target: 1112 tprint() 1113 if show_bloat: 1114 target = result['_target'] 1115 outcome = result['_outcome'] 1116 base_outcome = self._base_board_dict[target] 1117 for fname in outcome.func_sizes: 1118 self.print_func_size_detail(fname, 1119 base_outcome.func_sizes[fname], 1120 outcome.func_sizes[fname]) 1121 1122 1123 def print_size_summary(self, board_selected, board_dict, show_detail, 1124 show_bloat): 1125 """Print a summary of image sizes broken down by section. 1126 1127 The summary takes the form of one line per architecture. The 1128 line contains deltas for each of the sections (+ means the section 1129 got bigger, - means smaller). The numbers are the average number 1130 of bytes that a board in this section increased by. 1131 1132 For example: 1133 powerpc: (622 boards) text -0.0 1134 arm: (285 boards) text -0.0 1135 1136 Args: 1137 board_selected: Dict containing boards to summarise, keyed by 1138 board.target 1139 board_dict: Dict containing boards for which we built this 1140 commit, keyed by board.target. The value is an Outcome object. 1141 show_detail: Show size delta detail for each board 1142 show_bloat: Show detail for each function 1143 """ 1144 arch_list = {} 1145 arch_count = {} 1146 1147 # Calculate changes in size for different image parts 1148 # The previous sizes are in Board.sizes, for each board 1149 for target in board_dict: 1150 if target not in board_selected: 1151 continue 1152 base_sizes = self._base_board_dict[target].sizes 1153 outcome = board_dict[target] 1154 sizes = outcome.sizes 1155 1156 # Loop through the list of images, creating a dict of size 1157 # changes for each image/part. We end up with something like 1158 # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4} 1159 # which means that U-Boot data increased by 5 bytes and SPL 1160 # text decreased by 4. 1161 err = {'_target' : target} 1162 for image in sizes: 1163 if image in base_sizes: 1164 base_image = base_sizes[image] 1165 # Loop through the text, data, bss parts 1166 for part in sorted(sizes[image]): 1167 diff = sizes[image][part] - base_image[part] 1168 col = None 1169 if diff: 1170 if image == 'u-boot': 1171 name = part 1172 else: 1173 name = image + ':' + part 1174 err[name] = diff 1175 arch = board_selected[target].arch 1176 if not arch in arch_count: 1177 arch_count[arch] = 1 1178 else: 1179 arch_count[arch] += 1 1180 if not sizes: 1181 pass # Only add to our list when we have some stats 1182 elif not arch in arch_list: 1183 arch_list[arch] = [err] 1184 else: 1185 arch_list[arch].append(err) 1186 1187 # We now have a list of image size changes sorted by arch 1188 # Print out a summary of these 1189 for arch, target_list in arch_list.items(): 1190 # Get total difference for each type 1191 totals = {} 1192 for result in target_list: 1193 total = 0 1194 for name, diff in result.items(): 1195 if name.startswith('_'): 1196 continue 1197 total += diff 1198 if name in totals: 1199 totals[name] += diff 1200 else: 1201 totals[name] = diff 1202 result['_total_diff'] = total 1203 result['_outcome'] = board_dict[result['_target']] 1204 1205 count = len(target_list) 1206 printed_arch = False 1207 for name in sorted(totals): 1208 diff = totals[name] 1209 if diff: 1210 # Display the average difference in this name for this 1211 # architecture 1212 avg_diff = float(diff) / count 1213 color = self.col.RED if avg_diff > 0 else self.col.GREEN 1214 msg = ' %s %+1.1f' % (name, avg_diff) 1215 if not printed_arch: 1216 tprint('%10s: (for %d/%d boards)' % (arch, count, 1217 arch_count[arch]), newline=False) 1218 printed_arch = True 1219 tprint(msg, colour=color, newline=False) 1220 1221 if printed_arch: 1222 tprint() 1223 if show_detail: 1224 self.print_size_detail(target_list, show_bloat) 1225 1226 1227 def print_result_summary(self, board_selected, board_dict, err_lines, 1228 err_line_boards, warn_lines, warn_line_boards, 1229 config, environment, show_sizes, show_detail, 1230 show_bloat, show_config, show_environment): 1231 """Compare results with the base results and display delta. 1232 1233 Only boards mentioned in board_selected will be considered. This 1234 function is intended to be called repeatedly with the results of 1235 each commit. It therefore shows a 'diff' between what it saw in 1236 the last call and what it sees now. 1237 1238 Args: 1239 board_selected: Dict containing boards to summarise, keyed by 1240 board.target 1241 board_dict: Dict containing boards for which we built this 1242 commit, keyed by board.target. The value is an Outcome object. 1243 err_lines: A list of errors for this commit, or [] if there is 1244 none, or we don't want to print errors 1245 err_line_boards: Dict keyed by error line, containing a list of 1246 the Board objects with that error 1247 warn_lines: A list of warnings for this commit, or [] if there is 1248 none, or we don't want to print errors 1249 warn_line_boards: Dict keyed by warning line, containing a list of 1250 the Board objects with that warning 1251 config: Dictionary keyed by filename - e.g. '.config'. Each 1252 value is itself a dictionary: 1253 key: config name 1254 value: config value 1255 environment: Dictionary keyed by environment variable, Each 1256 value is the value of environment variable. 1257 show_sizes: Show image size deltas 1258 show_detail: Show size delta detail for each board if show_sizes 1259 show_bloat: Show detail for each function 1260 show_config: Show config changes 1261 show_environment: Show environment changes 1262 """ 1263 def _board_list(line, line_boards): 1264 """Helper function to get a line of boards containing a line 1265 1266 Args: 1267 line: Error line to search for 1268 line_boards: boards to search, each a Board 1269 Return: 1270 List of boards with that error line, or [] if the user has not 1271 requested such a list 1272 """ 1273 brds = [] 1274 board_set = set() 1275 if self._list_error_boards: 1276 for brd in line_boards[line]: 1277 if not brd in board_set: 1278 brds.append(brd) 1279 board_set.add(brd) 1280 return brds 1281 1282 def _calc_error_delta(base_lines, base_line_boards, lines, line_boards, 1283 char): 1284 """Calculate the required output based on changes in errors 1285 1286 Args: 1287 base_lines: List of errors/warnings for previous commit 1288 base_line_boards: Dict keyed by error line, containing a list 1289 of the Board objects with that error in the previous commit 1290 lines: List of errors/warning for this commit, each a str 1291 line_boards: Dict keyed by error line, containing a list 1292 of the Board objects with that error in this commit 1293 char: Character representing error ('') or warning ('w'). The 1294 broken ('+') or fixed ('-') characters are added in this 1295 function 1296 1297 Returns: 1298 Tuple 1299 List of ErrLine objects for 'better' lines 1300 List of ErrLine objects for 'worse' lines 1301 """ 1302 better_lines = [] 1303 worse_lines = [] 1304 for line in lines: 1305 if line not in base_lines: 1306 errline = ErrLine(char + '+', _board_list(line, line_boards), 1307 line) 1308 worse_lines.append(errline) 1309 for line in base_lines: 1310 if line not in lines: 1311 errline = ErrLine(char + '-', 1312 _board_list(line, base_line_boards), line) 1313 better_lines.append(errline) 1314 return better_lines, worse_lines 1315 1316 def _calc_config(delta, name, config): 1317 """Calculate configuration changes 1318 1319 Args: 1320 delta: Type of the delta, e.g. '+' 1321 name: name of the file which changed (e.g. .config) 1322 config: configuration change dictionary 1323 key: config name 1324 value: config value 1325 Returns: 1326 String containing the configuration changes which can be 1327 printed 1328 """ 1329 out = '' 1330 for key in sorted(config.keys()): 1331 out += '%s=%s ' % (key, config[key]) 1332 return '%s %s: %s' % (delta, name, out) 1333 1334 def _add_config(lines, name, config_plus, config_minus, config_change): 1335 """Add changes in configuration to a list 1336 1337 Args: 1338 lines: list to add to 1339 name: config file name 1340 config_plus: configurations added, dictionary 1341 key: config name 1342 value: config value 1343 config_minus: configurations removed, dictionary 1344 key: config name 1345 value: config value 1346 config_change: configurations changed, dictionary 1347 key: config name 1348 value: config value 1349 """ 1350 if config_plus: 1351 lines.append(_calc_config('+', name, config_plus)) 1352 if config_minus: 1353 lines.append(_calc_config('-', name, config_minus)) 1354 if config_change: 1355 lines.append(_calc_config('c', name, config_change)) 1356 1357 def _output_config_info(lines): 1358 for line in lines: 1359 if not line: 1360 continue 1361 col = None 1362 if line[0] == '+': 1363 col = self.col.GREEN 1364 elif line[0] == '-': 1365 col = self.col.RED 1366 elif line[0] == 'c': 1367 col = self.col.YELLOW 1368 tprint(' ' + line, newline=True, colour=col) 1369 1370 def _output_err_lines(err_lines, colour): 1371 """Output the line of error/warning lines, if not empty 1372 1373 Also increments self._error_lines if err_lines not empty 1374 1375 Args: 1376 err_lines: List of ErrLine objects, each an error or warning 1377 line, possibly including a list of boards with that 1378 error/warning 1379 colour: Colour to use for output 1380 """ 1381 if err_lines: 1382 out_list = [] 1383 for line in err_lines: 1384 names = [brd.target for brd in line.brds] 1385 board_str = ' '.join(names) if names else '' 1386 if board_str: 1387 out = self.col.build(colour, line.char + '(') 1388 out += self.col.build(self.col.MAGENTA, board_str, 1389 bright=False) 1390 out += self.col.build(colour, ') %s' % line.errline) 1391 else: 1392 out = self.col.build(colour, line.char + line.errline) 1393 out_list.append(out) 1394 tprint('\n'.join(out_list)) 1395 self._error_lines += 1 1396 1397 1398 ok_boards = [] # List of boards fixed since last commit 1399 warn_boards = [] # List of boards with warnings since last commit 1400 err_boards = [] # List of new broken boards since last commit 1401 new_boards = [] # List of boards that didn't exist last time 1402 unknown_boards = [] # List of boards that were not built 1403 1404 for target in board_dict: 1405 if target not in board_selected: 1406 continue 1407 1408 # If the board was built last time, add its outcome to a list 1409 if target in self._base_board_dict: 1410 base_outcome = self._base_board_dict[target].rc 1411 outcome = board_dict[target] 1412 if outcome.rc == OUTCOME_UNKNOWN: 1413 unknown_boards.append(target) 1414 elif outcome.rc < base_outcome: 1415 if outcome.rc == OUTCOME_WARNING: 1416 warn_boards.append(target) 1417 else: 1418 ok_boards.append(target) 1419 elif outcome.rc > base_outcome: 1420 if outcome.rc == OUTCOME_WARNING: 1421 warn_boards.append(target) 1422 else: 1423 err_boards.append(target) 1424 else: 1425 new_boards.append(target) 1426 1427 # Get a list of errors and warnings that have appeared, and disappeared 1428 better_err, worse_err = _calc_error_delta(self._base_err_lines, 1429 self._base_err_line_boards, err_lines, err_line_boards, '') 1430 better_warn, worse_warn = _calc_error_delta(self._base_warn_lines, 1431 self._base_warn_line_boards, warn_lines, warn_line_boards, 'w') 1432 1433 # For the IDE mode, print out all the output 1434 if self._ide: 1435 outcome = board_dict[target] 1436 for line in outcome.err_lines: 1437 sys.stderr.write(line) 1438 1439 # Display results by arch 1440 elif any((ok_boards, warn_boards, err_boards, unknown_boards, new_boards, 1441 worse_err, better_err, worse_warn, better_warn)): 1442 arch_list = {} 1443 self.add_outcome(board_selected, arch_list, ok_boards, '', 1444 self.col.GREEN) 1445 self.add_outcome(board_selected, arch_list, warn_boards, 'w+', 1446 self.col.YELLOW) 1447 self.add_outcome(board_selected, arch_list, err_boards, '+', 1448 self.col.RED) 1449 self.add_outcome(board_selected, arch_list, new_boards, '*', self.col.BLUE) 1450 if self._show_unknown: 1451 self.add_outcome(board_selected, arch_list, unknown_boards, '?', 1452 self.col.MAGENTA) 1453 for arch, target_list in arch_list.items(): 1454 tprint('%10s: %s' % (arch, target_list)) 1455 self._error_lines += 1 1456 _output_err_lines(better_err, colour=self.col.GREEN) 1457 _output_err_lines(worse_err, colour=self.col.RED) 1458 _output_err_lines(better_warn, colour=self.col.CYAN) 1459 _output_err_lines(worse_warn, colour=self.col.YELLOW) 1460 1461 if show_sizes: 1462 self.print_size_summary(board_selected, board_dict, show_detail, 1463 show_bloat) 1464 1465 if show_environment and self._base_environment: 1466 lines = [] 1467 1468 for target in board_dict: 1469 if target not in board_selected: 1470 continue 1471 1472 tbase = self._base_environment[target] 1473 tenvironment = environment[target] 1474 environment_plus = {} 1475 environment_minus = {} 1476 environment_change = {} 1477 base = tbase.environment 1478 for key, value in tenvironment.environment.items(): 1479 if key not in base: 1480 environment_plus[key] = value 1481 for key, value in base.items(): 1482 if key not in tenvironment.environment: 1483 environment_minus[key] = value 1484 for key, value in base.items(): 1485 new_value = tenvironment.environment.get(key) 1486 if new_value and value != new_value: 1487 desc = '%s -> %s' % (value, new_value) 1488 environment_change[key] = desc 1489 1490 _add_config(lines, target, environment_plus, environment_minus, 1491 environment_change) 1492 1493 _output_config_info(lines) 1494 1495 if show_config and self._base_config: 1496 summary = {} 1497 arch_config_plus = {} 1498 arch_config_minus = {} 1499 arch_config_change = {} 1500 arch_list = [] 1501 1502 for target in board_dict: 1503 if target not in board_selected: 1504 continue 1505 arch = board_selected[target].arch 1506 if arch not in arch_list: 1507 arch_list.append(arch) 1508 1509 for arch in arch_list: 1510 arch_config_plus[arch] = {} 1511 arch_config_minus[arch] = {} 1512 arch_config_change[arch] = {} 1513 for name in self.config_filenames: 1514 arch_config_plus[arch][name] = {} 1515 arch_config_minus[arch][name] = {} 1516 arch_config_change[arch][name] = {} 1517 1518 for target in board_dict: 1519 if target not in board_selected: 1520 continue 1521 1522 arch = board_selected[target].arch 1523 1524 all_config_plus = {} 1525 all_config_minus = {} 1526 all_config_change = {} 1527 tbase = self._base_config[target] 1528 tconfig = config[target] 1529 lines = [] 1530 for name in self.config_filenames: 1531 if not tconfig.config[name]: 1532 continue 1533 config_plus = {} 1534 config_minus = {} 1535 config_change = {} 1536 base = tbase.config[name] 1537 for key, value in tconfig.config[name].items(): 1538 if key not in base: 1539 config_plus[key] = value 1540 all_config_plus[key] = value 1541 for key, value in base.items(): 1542 if key not in tconfig.config[name]: 1543 config_minus[key] = value 1544 all_config_minus[key] = value 1545 for key, value in base.items(): 1546 new_value = tconfig.config.get(key) 1547 if new_value and value != new_value: 1548 desc = '%s -> %s' % (value, new_value) 1549 config_change[key] = desc 1550 all_config_change[key] = desc 1551 1552 arch_config_plus[arch][name].update(config_plus) 1553 arch_config_minus[arch][name].update(config_minus) 1554 arch_config_change[arch][name].update(config_change) 1555 1556 _add_config(lines, name, config_plus, config_minus, 1557 config_change) 1558 _add_config(lines, 'all', all_config_plus, all_config_minus, 1559 all_config_change) 1560 summary[target] = '\n'.join(lines) 1561 1562 lines_by_target = {} 1563 for target, lines in summary.items(): 1564 if lines in lines_by_target: 1565 lines_by_target[lines].append(target) 1566 else: 1567 lines_by_target[lines] = [target] 1568 1569 for arch in arch_list: 1570 lines = [] 1571 all_plus = {} 1572 all_minus = {} 1573 all_change = {} 1574 for name in self.config_filenames: 1575 all_plus.update(arch_config_plus[arch][name]) 1576 all_minus.update(arch_config_minus[arch][name]) 1577 all_change.update(arch_config_change[arch][name]) 1578 _add_config(lines, name, arch_config_plus[arch][name], 1579 arch_config_minus[arch][name], 1580 arch_config_change[arch][name]) 1581 _add_config(lines, 'all', all_plus, all_minus, all_change) 1582 #arch_summary[target] = '\n'.join(lines) 1583 if lines: 1584 tprint('%s:' % arch) 1585 _output_config_info(lines) 1586 1587 for lines, targets in lines_by_target.items(): 1588 if not lines: 1589 continue 1590 tprint('%s :' % ' '.join(sorted(targets))) 1591 _output_config_info(lines.split('\n')) 1592 1593 1594 # Save our updated information for the next call to this function 1595 self._base_board_dict = board_dict 1596 self._base_err_lines = err_lines 1597 self._base_warn_lines = warn_lines 1598 self._base_err_line_boards = err_line_boards 1599 self._base_warn_line_boards = warn_line_boards 1600 self._base_config = config 1601 self._base_environment = environment 1602 1603 # Get a list of boards that did not get built, if needed 1604 not_built = [] 1605 for brd in board_selected: 1606 if not brd in board_dict: 1607 not_built.append(brd) 1608 if not_built: 1609 tprint("Boards not built (%d): %s" % (len(not_built), 1610 ', '.join(not_built))) 1611 1612 def produce_result_summary(self, commit_upto, commits, board_selected): 1613 (board_dict, err_lines, err_line_boards, warn_lines, 1614 warn_line_boards, config, environment) = self.get_result_summary( 1615 board_selected, commit_upto, 1616 read_func_sizes=self._show_bloat, 1617 read_config=self._show_config, 1618 read_environment=self._show_environment) 1619 if commits: 1620 msg = '%02d: %s' % (commit_upto + 1, 1621 commits[commit_upto].subject) 1622 tprint(msg, colour=self.col.BLUE) 1623 self.print_result_summary(board_selected, board_dict, 1624 err_lines if self._show_errors else [], err_line_boards, 1625 warn_lines if self._show_errors else [], warn_line_boards, 1626 config, environment, self._show_sizes, self._show_detail, 1627 self._show_bloat, self._show_config, self._show_environment) 1628 1629 def show_summary(self, commits, board_selected): 1630 """Show a build summary for U-Boot for a given board list. 1631 1632 Reset the result summary, then repeatedly call GetResultSummary on 1633 each commit's results, then display the differences we see. 1634 1635 Args: 1636 commit: Commit objects to summarise 1637 board_selected: Dict containing boards to summarise 1638 """ 1639 self.commit_count = len(commits) if commits else 1 1640 self.commits = commits 1641 self.reset_result_summary(board_selected) 1642 self._error_lines = 0 1643 1644 for commit_upto in range(0, self.commit_count, self._step): 1645 self.produce_result_summary(commit_upto, commits, board_selected) 1646 if not self._error_lines: 1647 tprint('(no errors to report)', colour=self.col.GREEN) 1648 1649 1650 def setup_build(self, board_selected, commits): 1651 """Set up ready to start a build. 1652 1653 Args: 1654 board_selected: Selected boards to build 1655 commits: Selected commits to build 1656 """ 1657 # First work out how many commits we will build 1658 count = (self.commit_count + self._step - 1) // self._step 1659 self.count = len(board_selected) * count 1660 self.upto = self.warned = self.fail = 0 1661 self._timestamps = collections.deque() 1662 1663 def get_thread_dir(self, thread_num): 1664 """Get the directory path to the working dir for a thread. 1665 1666 Args: 1667 thread_num: Number of thread to check (-1 for main process, which 1668 is treated as 0) 1669 """ 1670 if self.work_in_output: 1671 return self._working_dir 1672 return os.path.join(self._working_dir, '%02d' % max(thread_num, 0)) 1673 1674 def _prepare_thread(self, thread_num, setup_git): 1675 """Prepare the working directory for a thread. 1676 1677 This clones or fetches the repo into the thread's work directory. 1678 Optionally, it can create a linked working tree of the repo in the 1679 thread's work directory instead. 1680 1681 Args: 1682 thread_num: Thread number (0, 1, ...) 1683 setup_git: 1684 'clone' to set up a git clone 1685 'worktree' to set up a git worktree 1686 """ 1687 thread_dir = self.get_thread_dir(thread_num) 1688 builderthread.mkdir(thread_dir) 1689 git_dir = os.path.join(thread_dir, '.git') if thread_dir else None 1690 1691 # Create a worktree or a git repo clone for this thread if it 1692 # doesn't already exist 1693 if setup_git and self.git_dir: 1694 src_dir = os.path.abspath(self.git_dir) 1695 if os.path.isdir(git_dir): 1696 # This is a clone of the src_dir repo, we can keep using 1697 # it but need to fetch from src_dir. 1698 tprint('\rFetching repo for thread %d' % thread_num, 1699 newline=False) 1700 gitutil.fetch(git_dir, thread_dir) 1701 terminal.print_clear() 1702 elif os.path.isfile(git_dir): 1703 # This is a worktree of the src_dir repo, we don't need to 1704 # create it again or update it in any way. 1705 pass 1706 elif os.path.exists(git_dir): 1707 # Don't know what could trigger this, but we probably 1708 # can't create a git worktree/clone here. 1709 raise ValueError('Git dir %s exists, but is not a file ' 1710 'or a directory.' % git_dir) 1711 elif setup_git == 'worktree': 1712 tprint('\rChecking out worktree for thread %d' % thread_num, 1713 newline=False) 1714 gitutil.add_worktree(src_dir, thread_dir) 1715 terminal.print_clear() 1716 elif setup_git == 'clone' or setup_git == True: 1717 tprint('\rCloning repo for thread %d' % thread_num, 1718 newline=False) 1719 gitutil.clone(src_dir, thread_dir) 1720 terminal.print_clear() 1721 else: 1722 raise ValueError("Can't setup git repo with %s." % setup_git) 1723 1724 def _prepare_working_space(self, max_threads, setup_git): 1725 """Prepare the working directory for use. 1726 1727 Set up the git repo for each thread. Creates a linked working tree 1728 if git-worktree is available, or clones the repo if it isn't. 1729 1730 Args: 1731 max_threads: Maximum number of threads we expect to need. If 0 then 1732 1 is set up, since the main process still needs somewhere to 1733 work 1734 setup_git: True to set up a git worktree or a git clone 1735 """ 1736 builderthread.mkdir(self._working_dir) 1737 if setup_git and self.git_dir: 1738 src_dir = os.path.abspath(self.git_dir) 1739 if gitutil.check_worktree_is_available(src_dir): 1740 setup_git = 'worktree' 1741 # If we previously added a worktree but the directory for it 1742 # got deleted, we need to prune its files from the repo so 1743 # that we can check out another in its place. 1744 gitutil.prune_worktrees(src_dir) 1745 else: 1746 setup_git = 'clone' 1747 1748 # Always do at least one thread 1749 for thread in range(max(max_threads, 1)): 1750 self._prepare_thread(thread, setup_git) 1751 1752 def _get_output_space_removals(self): 1753 """Get the output directories ready to receive files. 1754 1755 Figure out what needs to be deleted in the output directory before it 1756 can be used. We only delete old buildman directories which have the 1757 expected name pattern. See get_output_dir(). 1758 1759 Returns: 1760 List of full paths of directories to remove 1761 """ 1762 if not self.commits: 1763 return 1764 dir_list = [] 1765 for commit_upto in range(self.commit_count): 1766 dir_list.append(self.get_output_dir(commit_upto)) 1767 1768 to_remove = [] 1769 for dirname in glob.glob(os.path.join(self.base_dir, '*')): 1770 if dirname not in dir_list: 1771 leaf = dirname[len(self.base_dir) + 1:] 1772 m = re.match('[0-9]+_g[0-9a-f]+_.*', leaf) 1773 if m: 1774 to_remove.append(dirname) 1775 return to_remove 1776 1777 def _prepare_output_space(self): 1778 """Get the output directories ready to receive files. 1779 1780 We delete any output directories which look like ones we need to 1781 create. Having left over directories is confusing when the user wants 1782 to check the output manually. 1783 """ 1784 to_remove = self._get_output_space_removals() 1785 if to_remove: 1786 tprint('Removing %d old build directories...' % len(to_remove), 1787 newline=False) 1788 for dirname in to_remove: 1789 shutil.rmtree(dirname) 1790 terminal.print_clear() 1791 1792 def build_boards(self, commits, board_selected, keep_outputs, verbose): 1793 """Build all commits for a list of boards 1794 1795 Args: 1796 commits: List of commits to be build, each a Commit object 1797 boards_selected: Dict of selected boards, key is target name, 1798 value is Board object 1799 keep_outputs: True to save build output files 1800 verbose: Display build results as they are completed 1801 Returns: 1802 Tuple containing: 1803 - number of boards that failed to build 1804 - number of boards that issued warnings 1805 - list of thread exceptions raised 1806 """ 1807 self.commit_count = len(commits) if commits else 1 1808 self.commits = commits 1809 self._verbose = verbose 1810 1811 self.reset_result_summary(board_selected) 1812 builderthread.mkdir(self.base_dir, parents = True) 1813 self._prepare_working_space(min(self.num_threads, len(board_selected)), 1814 commits is not None) 1815 self._prepare_output_space() 1816 if not self._ide: 1817 tprint('\rStarting build...', newline=False) 1818 self._start_time = datetime.now() 1819 self.setup_build(board_selected, commits) 1820 self.process_result(None) 1821 self.thread_exceptions = [] 1822 # Create jobs to build all commits for each board 1823 for brd in board_selected.values(): 1824 job = builderthread.BuilderJob() 1825 job.brd = brd 1826 job.commits = commits 1827 job.keep_outputs = keep_outputs 1828 job.work_in_output = self.work_in_output 1829 job.adjust_cfg = self.adjust_cfg 1830 job.step = self._step 1831 if self.num_threads: 1832 self.queue.put(job) 1833 else: 1834 self._single_builder.run_job(job) 1835 1836 if self.num_threads: 1837 term = threading.Thread(target=self.queue.join) 1838 term.setDaemon(True) 1839 term.start() 1840 while term.is_alive(): 1841 term.join(100) 1842 1843 # Wait until we have processed all output 1844 self.out_queue.join() 1845 if not self._ide: 1846 tprint() 1847 1848 msg = 'Completed: %d total built' % self.count 1849 if self.already_done: 1850 msg += ' (%d previously' % self.already_done 1851 if self.already_done != self.count: 1852 msg += ', %d newly' % (self.count - self.already_done) 1853 msg += ')' 1854 duration = datetime.now() - self._start_time 1855 if duration > timedelta(microseconds=1000000): 1856 if duration.microseconds >= 500000: 1857 duration = duration + timedelta(seconds=1) 1858 duration = duration - timedelta(microseconds=duration.microseconds) 1859 rate = float(self.count) / duration.total_seconds() 1860 msg += ', duration %s, rate %1.2f' % (duration, rate) 1861 tprint(msg) 1862 if self.thread_exceptions: 1863 tprint('Failed: %d thread exceptions' % len(self.thread_exceptions), 1864 colour=self.col.RED) 1865 1866 return (self.fail, self.warned, self.thread_exceptions) 1867