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