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