Home | History | Annotate | Download | only in buildman
      1 # SPDX-License-Identifier: GPL-2.0+
      2 # Copyright (c) 2013 The Chromium OS Authors.
      3 #
      4 
      5 import multiprocessing
      6 import os
      7 import shutil
      8 import sys
      9 
     10 import board
     11 import bsettings
     12 from builder import Builder
     13 import gitutil
     14 import patchstream
     15 import terminal
     16 from terminal import Print
     17 import toolchain
     18 import command
     19 import subprocess
     20 
     21 def GetPlural(count):
     22     """Returns a plural 's' if count is not 1"""
     23     return 's' if count != 1 else ''
     24 
     25 def GetActionSummary(is_summary, commits, selected, options):
     26     """Return a string summarising the intended action.
     27 
     28     Returns:
     29         Summary string.
     30     """
     31     if commits:
     32         count = len(commits)
     33         count = (count + options.step - 1) / options.step
     34         commit_str = '%d commit%s' % (count, GetPlural(count))
     35     else:
     36         commit_str = 'current source'
     37     str = '%s %s for %d boards' % (
     38         'Summary of' if is_summary else 'Building', commit_str,
     39         len(selected))
     40     str += ' (%d thread%s, %d job%s per thread)' % (options.threads,
     41             GetPlural(options.threads), options.jobs, GetPlural(options.jobs))
     42     return str
     43 
     44 def ShowActions(series, why_selected, boards_selected, builder, options):
     45     """Display a list of actions that we would take, if not a dry run.
     46 
     47     Args:
     48         series: Series object
     49         why_selected: Dictionary where each key is a buildman argument
     50                 provided by the user, and the value is the list of boards
     51                 brought in by that argument. For example, 'arm' might bring
     52                 in 400 boards, so in this case the key would be 'arm' and
     53                 the value would be a list of board names.
     54         boards_selected: Dict of selected boards, key is target name,
     55                 value is Board object
     56         builder: The builder that will be used to build the commits
     57         options: Command line options object
     58     """
     59     col = terminal.Color()
     60     print 'Dry run, so not doing much. But I would do this:'
     61     print
     62     if series:
     63         commits = series.commits
     64     else:
     65         commits = None
     66     print GetActionSummary(False, commits, boards_selected,
     67             options)
     68     print 'Build directory: %s' % builder.base_dir
     69     if commits:
     70         for upto in range(0, len(series.commits), options.step):
     71             commit = series.commits[upto]
     72             print '   ', col.Color(col.YELLOW, commit.hash[:8], bright=False),
     73             print commit.subject
     74     print
     75     for arg in why_selected:
     76         if arg != 'all':
     77             print arg, ': %d boards' % len(why_selected[arg])
     78             if options.verbose:
     79                 print '   %s' % ' '.join(why_selected[arg])
     80     print ('Total boards to build for each commit: %d\n' %
     81             len(why_selected['all']))
     82 
     83 def CheckOutputDir(output_dir):
     84     """Make sure that the output directory is not within the current directory
     85 
     86     If we try to use an output directory which is within the current directory
     87     (which is assumed to hold the U-Boot source) we may end up deleting the
     88     U-Boot source code. Detect this and print an error in this case.
     89 
     90     Args:
     91         output_dir: Output directory path to check
     92     """
     93     path = os.path.realpath(output_dir)
     94     cwd_path = os.path.realpath('.')
     95     while True:
     96         if os.path.realpath(path) == cwd_path:
     97             Print("Cannot use output directory '%s' since it is within the current directtory '%s'" %
     98                   (path, cwd_path))
     99             sys.exit(1)
    100         parent = os.path.dirname(path)
    101         if parent == path:
    102             break
    103         path = parent
    104 
    105 def DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
    106                clean_dir=False):
    107     """The main control code for buildman
    108 
    109     Args:
    110         options: Command line options object
    111         args: Command line arguments (list of strings)
    112         toolchains: Toolchains to use - this should be a Toolchains()
    113                 object. If None, then it will be created and scanned
    114         make_func: Make function to use for the builder. This is called
    115                 to execute 'make'. If this is None, the normal function
    116                 will be used, which calls the 'make' tool with suitable
    117                 arguments. This setting is useful for tests.
    118         board: Boards() object to use, containing a list of available
    119                 boards. If this is None it will be created and scanned.
    120     """
    121     global builder
    122 
    123     if options.full_help:
    124         pager = os.getenv('PAGER')
    125         if not pager:
    126             pager = 'more'
    127         fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
    128                              'README')
    129         command.Run(pager, fname)
    130         return 0
    131 
    132     gitutil.Setup()
    133     col = terminal.Color()
    134 
    135     options.git_dir = os.path.join(options.git, '.git')
    136 
    137     no_toolchains = toolchains is None
    138     if no_toolchains:
    139         toolchains = toolchain.Toolchains()
    140 
    141     if options.fetch_arch:
    142         if options.fetch_arch == 'list':
    143             sorted_list = toolchains.ListArchs()
    144             print col.Color(col.BLUE, 'Available architectures: %s\n' %
    145                             ' '.join(sorted_list))
    146             return 0
    147         else:
    148             fetch_arch = options.fetch_arch
    149             if fetch_arch == 'all':
    150                 fetch_arch = ','.join(toolchains.ListArchs())
    151                 print col.Color(col.CYAN, '\nDownloading toolchains: %s' %
    152                                 fetch_arch)
    153             for arch in fetch_arch.split(','):
    154                 print
    155                 ret = toolchains.FetchAndInstall(arch)
    156                 if ret:
    157                     return ret
    158             return 0
    159 
    160     if no_toolchains:
    161         toolchains.GetSettings()
    162         toolchains.Scan(options.list_tool_chains)
    163     if options.list_tool_chains:
    164         toolchains.List()
    165         print
    166         return 0
    167 
    168     # Work out how many commits to build. We want to build everything on the
    169     # branch. We also build the upstream commit as a control so we can see
    170     # problems introduced by the first commit on the branch.
    171     count = options.count
    172     has_range = options.branch and '..' in options.branch
    173     if count == -1:
    174         if not options.branch:
    175             count = 1
    176         else:
    177             if has_range:
    178                 count, msg = gitutil.CountCommitsInRange(options.git_dir,
    179                                                          options.branch)
    180             else:
    181                 count, msg = gitutil.CountCommitsInBranch(options.git_dir,
    182                                                           options.branch)
    183             if count is None:
    184                 sys.exit(col.Color(col.RED, msg))
    185             elif count == 0:
    186                 sys.exit(col.Color(col.RED, "Range '%s' has no commits" %
    187                                    options.branch))
    188             if msg:
    189                 print col.Color(col.YELLOW, msg)
    190             count += 1   # Build upstream commit also
    191 
    192     if not count:
    193         str = ("No commits found to process in branch '%s': "
    194                "set branch's upstream or use -c flag" % options.branch)
    195         sys.exit(col.Color(col.RED, str))
    196 
    197     # Work out what subset of the boards we are building
    198     if not boards:
    199         board_file = os.path.join(options.git, 'boards.cfg')
    200         status = subprocess.call([os.path.join(options.git,
    201                                                 'tools/genboardscfg.py')])
    202         if status != 0:
    203                 sys.exit("Failed to generate boards.cfg")
    204 
    205         boards = board.Boards()
    206         boards.ReadBoards(os.path.join(options.git, 'boards.cfg'))
    207 
    208     exclude = []
    209     if options.exclude:
    210         for arg in options.exclude:
    211             exclude += arg.split(',')
    212 
    213     why_selected = boards.SelectBoards(args, exclude)
    214     selected = boards.GetSelected()
    215     if not len(selected):
    216         sys.exit(col.Color(col.RED, 'No matching boards found'))
    217 
    218     # Read the metadata from the commits. First look at the upstream commit,
    219     # then the ones in the branch. We would like to do something like
    220     # upstream/master~..branch but that isn't possible if upstream/master is
    221     # a merge commit (it will list all the commits that form part of the
    222     # merge)
    223     # Conflicting tags are not a problem for buildman, since it does not use
    224     # them. For example, Series-version is not useful for buildman. On the
    225     # other hand conflicting tags will cause an error. So allow later tags
    226     # to overwrite earlier ones by setting allow_overwrite=True
    227     if options.branch:
    228         if count == -1:
    229             if has_range:
    230                 range_expr = options.branch
    231             else:
    232                 range_expr = gitutil.GetRangeInBranch(options.git_dir,
    233                                                       options.branch)
    234             upstream_commit = gitutil.GetUpstream(options.git_dir,
    235                                                   options.branch)
    236             series = patchstream.GetMetaDataForList(upstream_commit,
    237                 options.git_dir, 1, series=None, allow_overwrite=True)
    238 
    239             series = patchstream.GetMetaDataForList(range_expr,
    240                     options.git_dir, None, series, allow_overwrite=True)
    241         else:
    242             # Honour the count
    243             series = patchstream.GetMetaDataForList(options.branch,
    244                     options.git_dir, count, series=None, allow_overwrite=True)
    245     else:
    246         series = None
    247         if not options.dry_run:
    248             options.verbose = True
    249             if not options.summary:
    250                 options.show_errors = True
    251 
    252     # By default we have one thread per CPU. But if there are not enough jobs
    253     # we can have fewer threads and use a high '-j' value for make.
    254     if not options.threads:
    255         options.threads = min(multiprocessing.cpu_count(), len(selected))
    256     if not options.jobs:
    257         options.jobs = max(1, (multiprocessing.cpu_count() +
    258                 len(selected) - 1) / len(selected))
    259 
    260     if not options.step:
    261         options.step = len(series.commits) - 1
    262 
    263     gnu_make = command.Output(os.path.join(options.git,
    264             'scripts/show-gnu-make'), raise_on_error=False).rstrip()
    265     if not gnu_make:
    266         sys.exit('GNU Make not found')
    267 
    268     # Create a new builder with the selected options.
    269     output_dir = options.output_dir
    270     if options.branch:
    271         dirname = options.branch.replace('/', '_')
    272         # As a special case allow the board directory to be placed in the
    273         # output directory itself rather than any subdirectory.
    274         if not options.no_subdirs:
    275             output_dir = os.path.join(options.output_dir, dirname)
    276         if clean_dir and os.path.exists(output_dir):
    277             shutil.rmtree(output_dir)
    278     CheckOutputDir(output_dir)
    279     builder = Builder(toolchains, output_dir, options.git_dir,
    280             options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
    281             show_unknown=options.show_unknown, step=options.step,
    282             no_subdirs=options.no_subdirs, full_path=options.full_path,
    283             verbose_build=options.verbose_build,
    284             incremental=options.incremental,
    285             per_board_out_dir=options.per_board_out_dir,
    286             config_only=options.config_only,
    287             squash_config_y=not options.preserve_config_y,
    288             warnings_as_errors=options.warnings_as_errors)
    289     builder.force_config_on_failure = not options.quick
    290     if make_func:
    291         builder.do_make = make_func
    292 
    293     # For a dry run, just show our actions as a sanity check
    294     if options.dry_run:
    295         ShowActions(series, why_selected, selected, builder, options)
    296     else:
    297         builder.force_build = options.force_build
    298         builder.force_build_failures = options.force_build_failures
    299         builder.force_reconfig = options.force_reconfig
    300         builder.in_tree = options.in_tree
    301 
    302         # Work out which boards to build
    303         board_selected = boards.GetSelectedDict()
    304 
    305         if series:
    306             commits = series.commits
    307             # Number the commits for test purposes
    308             for commit in range(len(commits)):
    309                 commits[commit].sequence = commit
    310         else:
    311             commits = None
    312 
    313         Print(GetActionSummary(options.summary, commits, board_selected,
    314                                 options))
    315 
    316         # We can't show function sizes without board details at present
    317         if options.show_bloat:
    318             options.show_detail = True
    319         builder.SetDisplayOptions(options.show_errors, options.show_sizes,
    320                                   options.show_detail, options.show_bloat,
    321                                   options.list_error_boards,
    322                                   options.show_config,
    323                                   options.show_environment)
    324         if options.summary:
    325             builder.ShowSummary(commits, board_selected)
    326         else:
    327             fail, warned = builder.BuildBoards(commits, board_selected,
    328                                 options.keep_outputs, options.verbose)
    329             if fail:
    330                 return 128
    331             elif warned:
    332                 return 129
    333     return 0
    334