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