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