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