xref: /openbmc/u-boot/tools/buildman/control.py (revision 8ee59472)
1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2013 The Chromium OS Authors.
3#
4
5import multiprocessing
6import os
7import shutil
8import sys
9
10import board
11import bsettings
12from builder import Builder
13import gitutil
14import patchstream
15import terminal
16from terminal import Print
17import toolchain
18import command
19import subprocess
20
21def GetPlural(count):
22    """Returns a plural 's' if count is not 1"""
23    return 's' if count != 1 else ''
24
25def 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
44def 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
83def 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
105def 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        if options.summary:
324            builder.ShowSummary(commits, board_selected)
325        else:
326            fail, warned = builder.BuildBoards(commits, board_selected,
327                                options.keep_outputs, options.verbose)
328            if fail:
329                return 128
330            elif warned:
331                return 129
332    return 0
333