xref: /openbmc/u-boot/tools/buildman/control.py (revision 409fc029)
11a459660SWolfgang Denk# SPDX-License-Identifier: GPL-2.0+
283d290c5STom Rini# Copyright (c) 2013 The Chromium OS Authors.
3fc3fe1c2SSimon Glass#
4fc3fe1c2SSimon Glass
5fc3fe1c2SSimon Glassimport multiprocessing
6fc3fe1c2SSimon Glassimport os
7883a321aSSimon Glassimport shutil
8fc3fe1c2SSimon Glassimport sys
9fc3fe1c2SSimon Glass
10fc3fe1c2SSimon Glassimport board
11fc3fe1c2SSimon Glassimport bsettings
12fc3fe1c2SSimon Glassfrom builder import Builder
13fc3fe1c2SSimon Glassimport gitutil
14fc3fe1c2SSimon Glassimport patchstream
15fc3fe1c2SSimon Glassimport terminal
16d4144e45SSimon Glassfrom terminal import Print
17fc3fe1c2SSimon Glassimport toolchain
1899796923SMasahiro Yamadaimport command
1973f30b9bSMasahiro Yamadaimport subprocess
20fc3fe1c2SSimon Glass
21fc3fe1c2SSimon Glassdef GetPlural(count):
22fc3fe1c2SSimon Glass    """Returns a plural 's' if count is not 1"""
23fc3fe1c2SSimon Glass    return 's' if count != 1 else ''
24fc3fe1c2SSimon Glass
25fea5858eSSimon Glassdef GetActionSummary(is_summary, commits, selected, options):
26fc3fe1c2SSimon Glass    """Return a string summarising the intended action.
27fc3fe1c2SSimon Glass
28fc3fe1c2SSimon Glass    Returns:
29fc3fe1c2SSimon Glass        Summary string.
30fc3fe1c2SSimon Glass    """
31fea5858eSSimon Glass    if commits:
32fea5858eSSimon Glass        count = len(commits)
33fc3fe1c2SSimon Glass        count = (count + options.step - 1) / options.step
34fea5858eSSimon Glass        commit_str = '%d commit%s' % (count, GetPlural(count))
35fea5858eSSimon Glass    else:
36fea5858eSSimon Glass        commit_str = 'current source'
37fea5858eSSimon Glass    str = '%s %s for %d boards' % (
38fea5858eSSimon Glass        'Summary of' if is_summary else 'Building', commit_str,
39fc3fe1c2SSimon Glass        len(selected))
40fc3fe1c2SSimon Glass    str += ' (%d thread%s, %d job%s per thread)' % (options.threads,
41fc3fe1c2SSimon Glass            GetPlural(options.threads), options.jobs, GetPlural(options.jobs))
42fc3fe1c2SSimon Glass    return str
43fc3fe1c2SSimon Glass
44fc3fe1c2SSimon Glassdef ShowActions(series, why_selected, boards_selected, builder, options):
45fc3fe1c2SSimon Glass    """Display a list of actions that we would take, if not a dry run.
46fc3fe1c2SSimon Glass
47fc3fe1c2SSimon Glass    Args:
48fc3fe1c2SSimon Glass        series: Series object
49fc3fe1c2SSimon Glass        why_selected: Dictionary where each key is a buildman argument
508d7523c5SSimon Glass                provided by the user, and the value is the list of boards
518d7523c5SSimon Glass                brought in by that argument. For example, 'arm' might bring
528d7523c5SSimon Glass                in 400 boards, so in this case the key would be 'arm' and
53fc3fe1c2SSimon Glass                the value would be a list of board names.
54fc3fe1c2SSimon Glass        boards_selected: Dict of selected boards, key is target name,
55fc3fe1c2SSimon Glass                value is Board object
56fc3fe1c2SSimon Glass        builder: The builder that will be used to build the commits
57fc3fe1c2SSimon Glass        options: Command line options object
58fc3fe1c2SSimon Glass    """
59fc3fe1c2SSimon Glass    col = terminal.Color()
60fc3fe1c2SSimon Glass    print 'Dry run, so not doing much. But I would do this:'
61fc3fe1c2SSimon Glass    print
62fea5858eSSimon Glass    if series:
63fea5858eSSimon Glass        commits = series.commits
64fea5858eSSimon Glass    else:
65fea5858eSSimon Glass        commits = None
66fea5858eSSimon Glass    print GetActionSummary(False, commits, boards_selected,
67fc3fe1c2SSimon Glass            options)
68fc3fe1c2SSimon Glass    print 'Build directory: %s' % builder.base_dir
69fea5858eSSimon Glass    if commits:
70fc3fe1c2SSimon Glass        for upto in range(0, len(series.commits), options.step):
71fc3fe1c2SSimon Glass            commit = series.commits[upto]
721ddda1b3SSimon Glass            print '   ', col.Color(col.YELLOW, commit.hash[:8], bright=False),
73fc3fe1c2SSimon Glass            print commit.subject
74fc3fe1c2SSimon Glass    print
75fc3fe1c2SSimon Glass    for arg in why_selected:
76fc3fe1c2SSimon Glass        if arg != 'all':
778d7523c5SSimon Glass            print arg, ': %d boards' % len(why_selected[arg])
788d7523c5SSimon Glass            if options.verbose:
798d7523c5SSimon Glass                print '   %s' % ' '.join(why_selected[arg])
80fc3fe1c2SSimon Glass    print ('Total boards to build for each commit: %d\n' %
818d7523c5SSimon Glass            len(why_selected['all']))
82fc3fe1c2SSimon Glass
83*409fc029SLothar Waßmanndef CheckOutputDir(output_dir):
84*409fc029SLothar Waßmann    """Make sure that the output directory is not within the current directory
85*409fc029SLothar Waßmann
86*409fc029SLothar Waßmann    If we try to use an output directory which is within the current directory
87*409fc029SLothar Waßmann    (which is assumed to hold the U-Boot source) we may end up deleting the
88*409fc029SLothar Waßmann    U-Boot source code. Detect this and print an error in this case.
89*409fc029SLothar Waßmann
90*409fc029SLothar Waßmann    Args:
91*409fc029SLothar Waßmann        output_dir: Output directory path to check
92*409fc029SLothar Waßmann    """
93*409fc029SLothar Waßmann    path = os.path.realpath(output_dir)
94*409fc029SLothar Waßmann    cwd_path = os.path.realpath('.')
95*409fc029SLothar Waßmann    while True:
96*409fc029SLothar Waßmann        if os.path.realpath(path) == cwd_path:
97*409fc029SLothar Waßmann            Print("Cannot use output directory '%s' since it is within the current directtory '%s'" %
98*409fc029SLothar Waßmann                  (path, cwd_path))
99*409fc029SLothar Waßmann            sys.exit(1)
100*409fc029SLothar Waßmann        parent = os.path.dirname(path)
101*409fc029SLothar Waßmann        if parent == path:
102*409fc029SLothar Waßmann            break
103*409fc029SLothar Waßmann        path = parent
104*409fc029SLothar Waßmann
105883a321aSSimon Glassdef DoBuildman(options, args, toolchains=None, make_func=None, boards=None,
106883a321aSSimon Glass               clean_dir=False):
107fc3fe1c2SSimon Glass    """The main control code for buildman
108fc3fe1c2SSimon Glass
109fc3fe1c2SSimon Glass    Args:
110fc3fe1c2SSimon Glass        options: Command line options object
111fc3fe1c2SSimon Glass        args: Command line arguments (list of strings)
112d4144e45SSimon Glass        toolchains: Toolchains to use - this should be a Toolchains()
113d4144e45SSimon Glass                object. If None, then it will be created and scanned
114d4144e45SSimon Glass        make_func: Make function to use for the builder. This is called
115d4144e45SSimon Glass                to execute 'make'. If this is None, the normal function
116d4144e45SSimon Glass                will be used, which calls the 'make' tool with suitable
117d4144e45SSimon Glass                arguments. This setting is useful for tests.
118823e60b6SSimon Glass        board: Boards() object to use, containing a list of available
119823e60b6SSimon Glass                boards. If this is None it will be created and scanned.
120fc3fe1c2SSimon Glass    """
121883a321aSSimon Glass    global builder
122883a321aSSimon Glass
12348ba5856SSimon Glass    if options.full_help:
12448ba5856SSimon Glass        pager = os.getenv('PAGER')
12548ba5856SSimon Glass        if not pager:
12648ba5856SSimon Glass            pager = 'more'
1272bdeade0SSimon Glass        fname = os.path.join(os.path.dirname(os.path.realpath(sys.argv[0])),
1282bdeade0SSimon Glass                             'README')
12948ba5856SSimon Glass        command.Run(pager, fname)
13048ba5856SSimon Glass        return 0
13148ba5856SSimon Glass
132fc3fe1c2SSimon Glass    gitutil.Setup()
133713bea38SSimon Glass    col = terminal.Color()
134fc3fe1c2SSimon Glass
135fc3fe1c2SSimon Glass    options.git_dir = os.path.join(options.git, '.git')
136fc3fe1c2SSimon Glass
1377e92e46eSSimon Glass    no_toolchains = toolchains is None
1387e92e46eSSimon Glass    if no_toolchains:
139fc3fe1c2SSimon Glass        toolchains = toolchain.Toolchains()
140fc3fe1c2SSimon Glass
141827e37b5SSimon Glass    if options.fetch_arch:
142827e37b5SSimon Glass        if options.fetch_arch == 'list':
143827e37b5SSimon Glass            sorted_list = toolchains.ListArchs()
144713bea38SSimon Glass            print col.Color(col.BLUE, 'Available architectures: %s\n' %
145713bea38SSimon Glass                            ' '.join(sorted_list))
146827e37b5SSimon Glass            return 0
147827e37b5SSimon Glass        else:
148827e37b5SSimon Glass            fetch_arch = options.fetch_arch
149827e37b5SSimon Glass            if fetch_arch == 'all':
150827e37b5SSimon Glass                fetch_arch = ','.join(toolchains.ListArchs())
151713bea38SSimon Glass                print col.Color(col.CYAN, '\nDownloading toolchains: %s' %
152713bea38SSimon Glass                                fetch_arch)
153827e37b5SSimon Glass            for arch in fetch_arch.split(','):
154713bea38SSimon Glass                print
155827e37b5SSimon Glass                ret = toolchains.FetchAndInstall(arch)
156827e37b5SSimon Glass                if ret:
157827e37b5SSimon Glass                    return ret
158827e37b5SSimon Glass            return 0
159827e37b5SSimon Glass
1607e92e46eSSimon Glass    if no_toolchains:
1617e92e46eSSimon Glass        toolchains.GetSettings()
1627e92e46eSSimon Glass        toolchains.Scan(options.list_tool_chains)
1637e92e46eSSimon Glass    if options.list_tool_chains:
1647e92e46eSSimon Glass        toolchains.List()
1657e92e46eSSimon Glass        print
1667e92e46eSSimon Glass        return 0
1677e92e46eSSimon Glass
168fc3fe1c2SSimon Glass    # Work out how many commits to build. We want to build everything on the
169fc3fe1c2SSimon Glass    # branch. We also build the upstream commit as a control so we can see
170fc3fe1c2SSimon Glass    # problems introduced by the first commit on the branch.
171fc3fe1c2SSimon Glass    count = options.count
1725abab20dSSimon Glass    has_range = options.branch and '..' in options.branch
173fc3fe1c2SSimon Glass    if count == -1:
174fc3fe1c2SSimon Glass        if not options.branch:
175fea5858eSSimon Glass            count = 1
176fea5858eSSimon Glass        else:
1775abab20dSSimon Glass            if has_range:
1785abab20dSSimon Glass                count, msg = gitutil.CountCommitsInRange(options.git_dir,
1795abab20dSSimon Glass                                                         options.branch)
1805abab20dSSimon Glass            else:
1812a9e2c6aSSimon Glass                count, msg = gitutil.CountCommitsInBranch(options.git_dir,
182fea5858eSSimon Glass                                                          options.branch)
183cce717a9SSimon Glass            if count is None:
1842a9e2c6aSSimon Glass                sys.exit(col.Color(col.RED, msg))
1855abab20dSSimon Glass            elif count == 0:
1865abab20dSSimon Glass                sys.exit(col.Color(col.RED, "Range '%s' has no commits" %
1875abab20dSSimon Glass                                   options.branch))
1882a9e2c6aSSimon Glass            if msg:
1892a9e2c6aSSimon Glass                print col.Color(col.YELLOW, msg)
190fc3fe1c2SSimon Glass            count += 1   # Build upstream commit also
191fc3fe1c2SSimon Glass
192fc3fe1c2SSimon Glass    if not count:
193fc3fe1c2SSimon Glass        str = ("No commits found to process in branch '%s': "
194fc3fe1c2SSimon Glass               "set branch's upstream or use -c flag" % options.branch)
19531e2141dSMasahiro Yamada        sys.exit(col.Color(col.RED, str))
196fc3fe1c2SSimon Glass
197fc3fe1c2SSimon Glass    # Work out what subset of the boards we are building
198823e60b6SSimon Glass    if not boards:
19973f30b9bSMasahiro Yamada        board_file = os.path.join(options.git, 'boards.cfg')
20073f30b9bSMasahiro Yamada        status = subprocess.call([os.path.join(options.git,
20173f30b9bSMasahiro Yamada                                                'tools/genboardscfg.py')])
20273f30b9bSMasahiro Yamada        if status != 0:
20331e2141dSMasahiro Yamada                sys.exit("Failed to generate boards.cfg")
20473f30b9bSMasahiro Yamada
205fc3fe1c2SSimon Glass        boards = board.Boards()
206fc3fe1c2SSimon Glass        boards.ReadBoards(os.path.join(options.git, 'boards.cfg'))
2073cf4ae6fSSimon Glass
2083cf4ae6fSSimon Glass    exclude = []
2093cf4ae6fSSimon Glass    if options.exclude:
2103cf4ae6fSSimon Glass        for arg in options.exclude:
2113cf4ae6fSSimon Glass            exclude += arg.split(',')
2123cf4ae6fSSimon Glass
2133cf4ae6fSSimon Glass    why_selected = boards.SelectBoards(args, exclude)
214fc3fe1c2SSimon Glass    selected = boards.GetSelected()
215fc3fe1c2SSimon Glass    if not len(selected):
21631e2141dSMasahiro Yamada        sys.exit(col.Color(col.RED, 'No matching boards found'))
217fc3fe1c2SSimon Glass
218fc3fe1c2SSimon Glass    # Read the metadata from the commits. First look at the upstream commit,
219fc3fe1c2SSimon Glass    # then the ones in the branch. We would like to do something like
220fc3fe1c2SSimon Glass    # upstream/master~..branch but that isn't possible if upstream/master is
221fc3fe1c2SSimon Glass    # a merge commit (it will list all the commits that form part of the
222fc3fe1c2SSimon Glass    # merge)
223950a2313SSimon Glass    # Conflicting tags are not a problem for buildman, since it does not use
224950a2313SSimon Glass    # them. For example, Series-version is not useful for buildman. On the
225950a2313SSimon Glass    # other hand conflicting tags will cause an error. So allow later tags
226950a2313SSimon Glass    # to overwrite earlier ones by setting allow_overwrite=True
227fea5858eSSimon Glass    if options.branch:
2283b74ba5fSSimon Glass        if count == -1:
2295abab20dSSimon Glass            if has_range:
2305abab20dSSimon Glass                range_expr = options.branch
2315abab20dSSimon Glass            else:
2323b74ba5fSSimon Glass                range_expr = gitutil.GetRangeInBranch(options.git_dir,
2333b74ba5fSSimon Glass                                                      options.branch)
2343b74ba5fSSimon Glass            upstream_commit = gitutil.GetUpstream(options.git_dir,
2353b74ba5fSSimon Glass                                                  options.branch)
236fea5858eSSimon Glass            series = patchstream.GetMetaDataForList(upstream_commit,
237950a2313SSimon Glass                options.git_dir, 1, series=None, allow_overwrite=True)
238fea5858eSSimon Glass
2393b74ba5fSSimon Glass            series = patchstream.GetMetaDataForList(range_expr,
240950a2313SSimon Glass                    options.git_dir, None, series, allow_overwrite=True)
2413b74ba5fSSimon Glass        else:
2423b74ba5fSSimon Glass            # Honour the count
2433b74ba5fSSimon Glass            series = patchstream.GetMetaDataForList(options.branch,
244950a2313SSimon Glass                    options.git_dir, count, series=None, allow_overwrite=True)
245fea5858eSSimon Glass    else:
246fea5858eSSimon Glass        series = None
2478d7523c5SSimon Glass        if not options.dry_run:
248e5a0e5d8SSimon Glass            options.verbose = True
24958d818f1SSimon Glass            if not options.summary:
250e5a0e5d8SSimon Glass                options.show_errors = True
251fc3fe1c2SSimon Glass
252fc3fe1c2SSimon Glass    # By default we have one thread per CPU. But if there are not enough jobs
253fc3fe1c2SSimon Glass    # we can have fewer threads and use a high '-j' value for make.
254fc3fe1c2SSimon Glass    if not options.threads:
255fc3fe1c2SSimon Glass        options.threads = min(multiprocessing.cpu_count(), len(selected))
256fc3fe1c2SSimon Glass    if not options.jobs:
257fc3fe1c2SSimon Glass        options.jobs = max(1, (multiprocessing.cpu_count() +
258fc3fe1c2SSimon Glass                len(selected) - 1) / len(selected))
259fc3fe1c2SSimon Glass
260fc3fe1c2SSimon Glass    if not options.step:
261fc3fe1c2SSimon Glass        options.step = len(series.commits) - 1
262fc3fe1c2SSimon Glass
26399796923SMasahiro Yamada    gnu_make = command.Output(os.path.join(options.git,
264785f1548SSimon Glass            'scripts/show-gnu-make'), raise_on_error=False).rstrip()
26599796923SMasahiro Yamada    if not gnu_make:
26631e2141dSMasahiro Yamada        sys.exit('GNU Make not found')
26799796923SMasahiro Yamada
26805c96b18SSimon Glass    # Create a new builder with the selected options.
26905c96b18SSimon Glass    output_dir = options.output_dir
270fea5858eSSimon Glass    if options.branch:
271f7582ce8SSimon Glass        dirname = options.branch.replace('/', '_')
2725971ab5cSSimon Glass        # As a special case allow the board directory to be placed in the
2735971ab5cSSimon Glass        # output directory itself rather than any subdirectory.
2745971ab5cSSimon Glass        if not options.no_subdirs:
275fea5858eSSimon Glass            output_dir = os.path.join(options.output_dir, dirname)
276*409fc029SLothar Waßmann        if clean_dir and os.path.exists(output_dir):
277883a321aSSimon Glass            shutil.rmtree(output_dir)
278*409fc029SLothar Waßmann    CheckOutputDir(output_dir)
279fc3fe1c2SSimon Glass    builder = Builder(toolchains, output_dir, options.git_dir,
28099796923SMasahiro Yamada            options.threads, options.jobs, gnu_make=gnu_make, checkout=True,
2815971ab5cSSimon Glass            show_unknown=options.show_unknown, step=options.step,
282d2ce658dSSimon Glass            no_subdirs=options.no_subdirs, full_path=options.full_path,
283f79f1e0cSStephen Warren            verbose_build=options.verbose_build,
284f79f1e0cSStephen Warren            incremental=options.incremental,
285b50113f3SSimon Glass            per_board_out_dir=options.per_board_out_dir,
286b464f8e7SSimon Glass            config_only=options.config_only,
2872371d1bcSDaniel Schwierzeck            squash_config_y=not options.preserve_config_y,
2882371d1bcSDaniel Schwierzeck            warnings_as_errors=options.warnings_as_errors)
289fc3fe1c2SSimon Glass    builder.force_config_on_failure = not options.quick
290d4144e45SSimon Glass    if make_func:
291d4144e45SSimon Glass        builder.do_make = make_func
292fc3fe1c2SSimon Glass
293fc3fe1c2SSimon Glass    # For a dry run, just show our actions as a sanity check
294fc3fe1c2SSimon Glass    if options.dry_run:
295fc3fe1c2SSimon Glass        ShowActions(series, why_selected, selected, builder, options)
296fc3fe1c2SSimon Glass    else:
297fc3fe1c2SSimon Glass        builder.force_build = options.force_build
2984266dc28SSimon Glass        builder.force_build_failures = options.force_build_failures
29997e91526SSimon Glass        builder.force_reconfig = options.force_reconfig
300189a4968SSimon Glass        builder.in_tree = options.in_tree
301fc3fe1c2SSimon Glass
302fc3fe1c2SSimon Glass        # Work out which boards to build
303fc3fe1c2SSimon Glass        board_selected = boards.GetSelectedDict()
304fc3fe1c2SSimon Glass
305fea5858eSSimon Glass        if series:
306fea5858eSSimon Glass            commits = series.commits
307883a321aSSimon Glass            # Number the commits for test purposes
308883a321aSSimon Glass            for commit in range(len(commits)):
309883a321aSSimon Glass                commits[commit].sequence = commit
310fea5858eSSimon Glass        else:
311fea5858eSSimon Glass            commits = None
312fea5858eSSimon Glass
313d4144e45SSimon Glass        Print(GetActionSummary(options.summary, commits, board_selected,
314d4144e45SSimon Glass                                options))
315fc3fe1c2SSimon Glass
3167798e228SSimon Glass        # We can't show function sizes without board details at present
3177798e228SSimon Glass        if options.show_bloat:
3187798e228SSimon Glass            options.show_detail = True
319b2ea7ab2SSimon Glass        builder.SetDisplayOptions(options.show_errors, options.show_sizes,
320ed966657SSimon Glass                                  options.show_detail, options.show_bloat,
321843312dcSSimon Glass                                  options.list_error_boards,
322843312dcSSimon Glass                                  options.show_config)
323fc3fe1c2SSimon Glass        if options.summary:
324b2ea7ab2SSimon Glass            builder.ShowSummary(commits, board_selected)
325fc3fe1c2SSimon Glass        else:
3262c3deb97SSimon Glass            fail, warned = builder.BuildBoards(commits, board_selected,
327e5a0e5d8SSimon Glass                                options.keep_outputs, options.verbose)
3282c3deb97SSimon Glass            if fail:
3292c3deb97SSimon Glass                return 128
3302c3deb97SSimon Glass            elif warned:
3312c3deb97SSimon Glass                return 129
3322c3deb97SSimon Glass    return 0
333