def ProcessResult(self, result): """Process the result of a build, showing progress information Args: result: A CommandResult object, which indicates the result for a single build """ col = terminal.Color() if result: target = result.brd.target if result.return_code < 0: self.active = False command.StopAll() return self.upto += 1 if result.return_code != 0: self.fail += 1 elif result.stderr: self.warned += 1 if result.already_done: self.already_done += 1 if self._verbose: Print('\r', newline=False) self.ClearLine(0) boards_selected = {target: result.brd} self.ResetResultSummary(boards_selected) self.ProduceResultSummary(result.commit_upto, self.commits, boards_selected) else: target = '(starting)' # Display separate counts for ok, warned and fail ok = self.upto - self.warned - self.fail line = '\r' + self.col.Color(self.col.GREEN, '%5d' % ok) line += self.col.Color(self.col.YELLOW, '%5d' % self.warned) line += self.col.Color(self.col.RED, '%5d' % self.fail) name = ' /%-5d ' % self.count # Add our current completion time estimate self._AddTimestamp() if self._complete_delay: name += '%s : ' % self._complete_delay # When building all boards for a commit, we can print a commit # progress message. if result and result.commit_upto is None: name += 'commit %2d/%-3d' % (self.commit_upto + 1, self.commit_count) name += target Print(line + name, newline=False) length = 14 + len(name) self.ClearLine(length)
def ClearLine(self, length): """Clear any characters on the current line Make way for a new line of length 'length', by outputting enough spaces to clear out the old line. Then remember the new length for next time. Args: length: Length of new line, in characters """ if length < self.last_line_len: Print(' ' * (self.last_line_len - length), newline=False) Print('\r', newline=False) self.last_line_len = length sys.stdout.flush()
def PrintFuncSizeDetail(self, fname, old, new): grow, shrink, add, remove, up, down = 0, 0, 0, 0, 0, 0 delta, common = [], {} for a in old: if a in new: common[a] = 1 for name in old: if name not in common: remove += 1 down += old[name] delta.append([-old[name], name]) for name in new: if name not in common: add += 1 up += new[name] delta.append([new[name], name]) for name in common: diff = new.get(name, 0) - old.get(name, 0) if diff > 0: grow, up = grow + 1, up + diff elif diff < 0: shrink, down = shrink + 1, down - diff delta.append([diff, name]) delta.sort() delta.reverse() args = [add, -remove, grow, -shrink, up, -down, up - down] if max(args) == 0: return args = [self.ColourNum(x) for x in args] indent = ' ' * 15 Print('%s%s: add: %s/%s, grow: %s/%s bytes: %s/%s (%s)' % tuple([indent, self.col.Color(self.col.YELLOW, fname)] + args)) Print('%s %-38s %7s %7s %+7s' % (indent, 'function', 'old', 'new', 'delta')) for diff, name in delta: if diff: color = self.col.RED if diff > 0 else self.col.GREEN msg = '%s %-38s %7s %7s %+7d' % ( indent, name, old.get(name, '-'), new.get(name, '-'), diff) Print(msg, colour=color)
def ProduceResultSummary(self, commit_upto, commits, board_selected): (board_dict, err_lines, err_line_boards, warn_lines, warn_line_boards) = self.GetResultSummary( board_selected, commit_upto, read_func_sizes=self._show_bloat) if commits: msg = '%02d: %s' % (commit_upto + 1, commits[commit_upto].subject) Print(msg, colour=self.col.BLUE) self.PrintResultSummary(board_selected, board_dict, err_lines if self._show_errors else [], err_line_boards, warn_lines if self._show_errors else [], warn_line_boards, self._show_sizes, self._show_detail, self._show_bloat)
def PrintSizeDetail(self, target_list, show_bloat): """Show details size information for each board Args: target_list: List of targets, each a dict containing: 'target': Target name 'total_diff': Total difference in bytes across all areas <part_name>: Difference for that part show_bloat: Show detail for each function """ targets_by_diff = sorted(target_list, reverse=True, key=lambda x: x['_total_diff']) for result in targets_by_diff: printed_target = False for name in sorted(result): diff = result[name] if name.startswith('_'): continue if diff != 0: color = self.col.RED if diff > 0 else self.col.GREEN msg = ' %s %+d' % (name, diff) if not printed_target: Print('%10s %-15s:' % ('', result['_target']), newline=False) printed_target = True Print(msg, colour=color, newline=False) if printed_target: Print() if show_bloat: target = result['_target'] outcome = result['_outcome'] base_outcome = self._base_board_dict[target] for fname in outcome.func_sizes: self.PrintFuncSizeDetail( fname, base_outcome.func_sizes[fname], outcome.func_sizes[fname])
def BuildBoards(self, commits, board_selected, keep_outputs, verbose): """Build all commits for a list of boards Args: commits: List of commits to be build, each a Commit object boards_selected: Dict of selected boards, key is target name, value is Board object keep_outputs: True to save build output files verbose: Display build results as they are completed Returns: Tuple containing: - number of boards that failed to build - number of boards that issued warnings """ self.commit_count = len(commits) if commits else 1 self.commits = commits self._verbose = verbose self.ResetResultSummary(board_selected) builderthread.Mkdir(self.base_dir, parents=True) self._PrepareWorkingSpace(min(self.num_threads, len(board_selected)), commits is not None) self._PrepareOutputSpace() self.SetupBuild(board_selected, commits) self.ProcessResult(None) # Create jobs to build all commits for each board for brd in board_selected.itervalues(): job = builderthread.BuilderJob() job.board = brd job.commits = commits job.keep_outputs = keep_outputs job.step = self._step self.queue.put(job) # Wait until all jobs are started self.queue.join() # Wait until we have processed all output self.out_queue.join() Print() self.ClearLine(0) return (self.fail, self.warned)
def ShowSummary(self, commits, board_selected): """Show a build summary for U-Boot for a given board list. Reset the result summary, then repeatedly call GetResultSummary on each commit's results, then display the differences we see. Args: commit: Commit objects to summarise board_selected: Dict containing boards to summarise """ self.commit_count = len(commits) if commits else 1 self.commits = commits self.ResetResultSummary(board_selected) self._error_lines = 0 for commit_upto in range(0, self.commit_count, self._step): self.ProduceResultSummary(commit_upto, commits, board_selected) if not self._error_lines: Print('(no errors to report)', colour=self.col.GREEN)
def CheckOutputDir(output_dir): """Make sure that the output directory is not within the current directory If we try to use an output directory which is within the current directory (which is assumed to hold the U-Boot source) we may end up deleting the U-Boot source code. Detect this and print an error in this case. Args: output_dir: Output directory path to check """ path = os.path.realpath(output_dir) cwd_path = os.path.realpath('.') while True: if os.path.realpath(path) == cwd_path: Print("Cannot use output directory '%s' since it is within the current directory '%s'" % (path, cwd_path)) sys.exit(1) parent = os.path.dirname(path) if parent == path: break path = parent
def _PrepareThread(self, thread_num, setup_git): """Prepare the working directory for a thread. This clones or fetches the repo into the thread's work directory. Args: thread_num: Thread number (0, 1, ...) setup_git: True to set up a git repo clone """ thread_dir = self.GetThreadDir(thread_num) builderthread.Mkdir(thread_dir) git_dir = os.path.join(thread_dir, '.git') # Clone the repo if it doesn't already exist # TODO(sjg@chromium): Perhaps some git hackery to symlink instead, so # we have a private index but uses the origin repo's contents? if setup_git and self.git_dir: src_dir = os.path.abspath(self.git_dir) if os.path.exists(git_dir): gitutil.Fetch(git_dir, thread_dir) else: Print('Cloning repo for thread %d' % thread_num) gitutil.Clone(src_dir, thread_dir)
def ReadFuncSizes(self, fname, fd): """Read function sizes from the output of 'nm' Args: fd: File containing data to read fname: Filename we are reading from (just for errors) Returns: Dictionary containing size of each function in bytes, indexed by function name. """ sym = {} for line in fd.readlines(): try: size, type, name = line[:-1].split() except: Print("Invalid line in file '%s': '%s'" % (fname, line[:-1])) continue if type in 'tTdDbB': # function names begin with '.' on 64-bit powerpc if '.' in name[1:]: name = 'static.' + name.split('.')[0] sym[name] = sym.get(name, 0) + int(size, 16) return sym
def PrintResultSummary(self, board_selected, board_dict, err_lines, err_line_boards, warn_lines, warn_line_boards, show_sizes, show_detail, show_bloat): """Compare results with the base results and display delta. Only boards mentioned in board_selected will be considered. This function is intended to be called repeatedly with the results of each commit. It therefore shows a 'diff' between what it saw in the last call and what it sees now. Args: board_selected: Dict containing boards to summarise, keyed by board.target board_dict: Dict containing boards for which we built this commit, keyed by board.target. The value is an Outcome object. err_lines: A list of errors for this commit, or [] if there is none, or we don't want to print errors err_line_boards: Dict keyed by error line, containing a list of the Board objects with that error warn_lines: A list of warnings for this commit, or [] if there is none, or we don't want to print errors warn_line_boards: Dict keyed by warning line, containing a list of the Board objects with that warning show_sizes: Show image size deltas show_detail: Show detail for each board show_bloat: Show detail for each function """ def _BoardList(line, line_boards): """Helper function to get a line of boards containing a line Args: line: Error line to search for Return: String containing a list of boards with that error line, or '' if the user has not requested such a list """ if self._list_error_boards: names = [] for board in line_boards[line]: if not board.target in names: names.append(board.target) names_str = '(%s) ' % ','.join(names) else: names_str = '' return names_str def _CalcErrorDelta(base_lines, base_line_boards, lines, line_boards, char): better_lines = [] worse_lines = [] for line in lines: if line not in base_lines: worse_lines.append(char + '+' + _BoardList(line, line_boards) + line) for line in base_lines: if line not in lines: better_lines.append(char + '-' + _BoardList(line, base_line_boards) + line) return better_lines, worse_lines better = [] # List of boards fixed since last commit worse = [] # List of new broken boards since last commit new = [] # List of boards that didn't exist last time unknown = [] # List of boards that were not built for target in board_dict: if target not in board_selected: continue # If the board was built last time, add its outcome to a list if target in self._base_board_dict: base_outcome = self._base_board_dict[target].rc outcome = board_dict[target] if outcome.rc == OUTCOME_UNKNOWN: unknown.append(target) elif outcome.rc < base_outcome: better.append(target) elif outcome.rc > base_outcome: worse.append(target) else: new.append(target) # Get a list of errors that have appeared, and disappeared better_err, worse_err = _CalcErrorDelta(self._base_err_lines, self._base_err_line_boards, err_lines, err_line_boards, '') better_warn, worse_warn = _CalcErrorDelta(self._base_warn_lines, self._base_warn_line_boards, warn_lines, warn_line_boards, 'w') # Display results by arch if (better or worse or unknown or new or worse_err or better_err or worse_warn or better_warn): arch_list = {} self.AddOutcome(board_selected, arch_list, better, '', self.col.GREEN) self.AddOutcome(board_selected, arch_list, worse, '+', self.col.RED) self.AddOutcome(board_selected, arch_list, new, '*', self.col.BLUE) if self._show_unknown: self.AddOutcome(board_selected, arch_list, unknown, '?', self.col.MAGENTA) for arch, target_list in arch_list.iteritems(): Print('%10s: %s' % (arch, target_list)) self._error_lines += 1 if better_err: Print('\n'.join(better_err), colour=self.col.GREEN) self._error_lines += 1 if worse_err: Print('\n'.join(worse_err), colour=self.col.RED) self._error_lines += 1 if better_warn: Print('\n'.join(better_warn), colour=self.col.CYAN) self._error_lines += 1 if worse_warn: Print('\n'.join(worse_warn), colour=self.col.MAGENTA) self._error_lines += 1 if show_sizes: self.PrintSizeSummary(board_selected, board_dict, show_detail, show_bloat) # Save our updated information for the next call to this function self._base_board_dict = board_dict self._base_err_lines = err_lines self._base_warn_lines = warn_lines self._base_err_line_boards = err_line_boards self._base_warn_line_boards = warn_line_boards # Get a list of boards that did not get built, if needed not_built = [] for board in board_selected: if not board in board_dict: not_built.append(board) if not_built: Print("Boards not built (%d): %s" % (len(not_built), ', '.join(not_built)))
def PrintSizeSummary(self, board_selected, board_dict, show_detail, show_bloat): """Print a summary of image sizes broken down by section. The summary takes the form of one line per architecture. The line contains deltas for each of the sections (+ means the section got bigger, - means smaller). The nunmbers are the average number of bytes that a board in this section increased by. For example: powerpc: (622 boards) text -0.0 arm: (285 boards) text -0.0 nds32: (3 boards) text -8.0 Args: board_selected: Dict containing boards to summarise, keyed by board.target board_dict: Dict containing boards for which we built this commit, keyed by board.target. The value is an Outcome object. show_detail: Show detail for each board show_bloat: Show detail for each function """ arch_list = {} arch_count = {} # Calculate changes in size for different image parts # The previous sizes are in Board.sizes, for each board for target in board_dict: if target not in board_selected: continue base_sizes = self._base_board_dict[target].sizes outcome = board_dict[target] sizes = outcome.sizes # Loop through the list of images, creating a dict of size # changes for each image/part. We end up with something like # {'target' : 'snapper9g45, 'data' : 5, 'u-boot-spl:text' : -4} # which means that U-Boot data increased by 5 bytes and SPL # text decreased by 4. err = {'_target': target} for image in sizes: if image in base_sizes: base_image = base_sizes[image] # Loop through the text, data, bss parts for part in sorted(sizes[image]): diff = sizes[image][part] - base_image[part] col = None if diff: if image == 'u-boot': name = part else: name = image + ':' + part err[name] = diff arch = board_selected[target].arch if not arch in arch_count: arch_count[arch] = 1 else: arch_count[arch] += 1 if not sizes: pass # Only add to our list when we have some stats elif not arch in arch_list: arch_list[arch] = [err] else: arch_list[arch].append(err) # We now have a list of image size changes sorted by arch # Print out a summary of these for arch, target_list in arch_list.iteritems(): # Get total difference for each type totals = {} for result in target_list: total = 0 for name, diff in result.iteritems(): if name.startswith('_'): continue total += diff if name in totals: totals[name] += diff else: totals[name] = diff result['_total_diff'] = total result['_outcome'] = board_dict[result['_target']] count = len(target_list) printed_arch = False for name in sorted(totals): diff = totals[name] if diff: # Display the average difference in this name for this # architecture avg_diff = float(diff) / count color = self.col.RED if avg_diff > 0 else self.col.GREEN msg = ' %s %+1.1f' % (name, avg_diff) if not printed_arch: Print('%10s: (for %d/%d boards)' % (arch, count, arch_count[arch]), newline=False) printed_arch = True Print(msg, colour=color, newline=False) if printed_arch: Print() if show_detail: self.PrintSizeDetail(target_list, show_bloat)
def DoBuildman(options, args, toolchains=None, make_func=None, boards=None, clean_dir=False): """The main control code for buildman Args: options: Command line options object args: Command line arguments (list of strings) toolchains: Toolchains to use - this should be a Toolchains() object. If None, then it will be created and scanned make_func: Make function to use for the builder. This is called to execute 'make'. If this is None, the normal function will be used, which calls the 'make' tool with suitable arguments. This setting is useful for tests. board: Boards() object to use, containing a list of available boards. If this is None it will be created and scanned. """ global builder if options.full_help: pager = os.getenv('PAGER') if not pager: pager = 'more' fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])), 'README') command.Run(pager, fname) return 0 gitutil.Setup() col = terminal.Color() options.git_dir = os.path.join(options.git, '.git') no_toolchains = toolchains is None if no_toolchains: toolchains = toolchain.Toolchains(options.override_toolchain) if options.fetch_arch: if options.fetch_arch == 'list': sorted_list = toolchains.ListArchs() print col.Color( col.BLUE, 'Available architectures: %s\n' % ' '.join(sorted_list)) return 0 else: fetch_arch = options.fetch_arch if fetch_arch == 'all': fetch_arch = ','.join(toolchains.ListArchs()) print col.Color(col.CYAN, '\nDownloading toolchains: %s' % fetch_arch) for arch in fetch_arch.split(','): print ret = toolchains.FetchAndInstall(arch) if ret: return ret return 0 if no_toolchains: toolchains.GetSettings() toolchains.Scan(options.list_tool_chains and options.verbose) if options.list_tool_chains: toolchains.List() print return 0 # Work out how many commits to build. We want to build everything on the # branch. We also build the upstream commit as a control so we can see # problems introduced by the first commit on the branch. count = options.count has_range = options.branch and '..' in options.branch if count == -1: if not options.branch: count = 1 else: if has_range: count, msg = gitutil.CountCommitsInRange( options.git_dir, options.branch) else: count, msg = gitutil.CountCommitsInBranch( options.git_dir, options.branch) if count is None: sys.exit(col.Color(col.RED, msg)) elif count == 0: sys.exit( col.Color(col.RED, "Range '%s' has no commits" % options.branch)) if msg: print col.Color(col.YELLOW, msg) count += 1 # Build upstream commit also if not count: str = ("No commits found to process in branch '%s': " "set branch's upstream or use -c flag" % options.branch) sys.exit(col.Color(col.RED, str)) # Work out what subset of the boards we are building if not boards: board_file = os.path.join(options.output_dir, 'boards.cfg') genboardscfg = os.path.join(options.git, 'tools/genboardscfg.py') status = subprocess.call([genboardscfg, '-o', board_file]) if status != 0: sys.exit("Failed to generate boards.cfg") boards = board.Boards() boards.ReadBoards(board_file) exclude = [] if options.exclude: for arg in options.exclude: exclude += arg.split(',') if options.boards: requested_boards = [] for b in options.boards: requested_boards += b.split(',') else: requested_boards = None why_selected, board_warnings = boards.SelectBoards(args, exclude, requested_boards) selected = boards.GetSelected() if not len(selected): sys.exit(col.Color(col.RED, 'No matching boards found')) # Read the metadata from the commits. First look at the upstream commit, # then the ones in the branch. We would like to do something like # upstream/master~..branch but that isn't possible if upstream/master is # a merge commit (it will list all the commits that form part of the # merge) # Conflicting tags are not a problem for buildman, since it does not use # them. For example, Series-version is not useful for buildman. On the # other hand conflicting tags will cause an error. So allow later tags # to overwrite earlier ones by setting allow_overwrite=True if options.branch: if count == -1: if has_range: range_expr = options.branch else: range_expr = gitutil.GetRangeInBranch(options.git_dir, options.branch) upstream_commit = gitutil.GetUpstream(options.git_dir, options.branch) series = patchstream.GetMetaDataForList(upstream_commit, options.git_dir, 1, series=None, allow_overwrite=True) series = patchstream.GetMetaDataForList(range_expr, options.git_dir, None, series, allow_overwrite=True) else: # Honour the count series = patchstream.GetMetaDataForList(options.branch, options.git_dir, count, series=None, allow_overwrite=True) else: series = None if not options.dry_run: options.verbose = True if not options.summary: options.show_errors = True # By default we have one thread per CPU. But if there are not enough jobs # we can have fewer threads and use a high '-j' value for make. if not options.threads: options.threads = min(multiprocessing.cpu_count(), len(selected)) if not options.jobs: options.jobs = max(1, (multiprocessing.cpu_count() + len(selected) - 1) / len(selected)) if not options.step: options.step = len(series.commits) - 1 gnu_make = command.Output(os.path.join(options.git, 'scripts/show-gnu-make'), raise_on_error=False).rstrip() if not gnu_make: sys.exit('GNU Make not found') # Create a new builder with the selected options. output_dir = options.output_dir if options.branch: dirname = options.branch.replace('/', '_') # As a special case allow the board directory to be placed in the # output directory itself rather than any subdirectory. if not options.no_subdirs: output_dir = os.path.join(options.output_dir, dirname) if clean_dir and os.path.exists(output_dir): shutil.rmtree(output_dir) CheckOutputDir(output_dir) builder = Builder(toolchains, output_dir, options.git_dir, options.threads, options.jobs, gnu_make=gnu_make, checkout=True, show_unknown=options.show_unknown, step=options.step, no_subdirs=options.no_subdirs, full_path=options.full_path, verbose_build=options.verbose_build, incremental=options.incremental, per_board_out_dir=options.per_board_out_dir, config_only=options.config_only, squash_config_y=not options.preserve_config_y, warnings_as_errors=options.warnings_as_errors) builder.force_config_on_failure = not options.quick if make_func: builder.do_make = make_func # For a dry run, just show our actions as a sanity check if options.dry_run: ShowActions(series, why_selected, selected, builder, options, board_warnings) else: builder.force_build = options.force_build builder.force_build_failures = options.force_build_failures builder.force_reconfig = options.force_reconfig builder.in_tree = options.in_tree # Work out which boards to build board_selected = boards.GetSelectedDict() if series: commits = series.commits # Number the commits for test purposes for commit in range(len(commits)): commits[commit].sequence = commit else: commits = None Print( GetActionSummary(options.summary, commits, board_selected, options)) # We can't show function sizes without board details at present if options.show_bloat: options.show_detail = True builder.SetDisplayOptions(options.show_errors, options.show_sizes, options.show_detail, options.show_bloat, options.list_error_boards, options.show_config, options.show_environment) if options.summary: builder.ShowSummary(commits, board_selected) else: fail, warned = builder.BuildBoards(commits, board_selected, options.keep_outputs, options.verbose) if fail: return 128 elif warned: return 129 return 0