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