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