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