1# Development tool - utility commands plugin
2#
3# Copyright (C) 2015-2016 Intel Corporation
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8"""Devtool utility plugins"""
9
10import os
11import sys
12import shutil
13import tempfile
14import logging
15import argparse
16import subprocess
17import scriptutils
18from devtool import exec_build_env_command, setup_tinfoil, check_workspace_recipe, DevtoolError
19from devtool import parse_recipe
20
21logger = logging.getLogger('devtool')
22
23def _find_recipe_path(args, config, basepath, workspace):
24    if args.any_recipe:
25        logger.warning('-a/--any-recipe option is now always active, and thus the option will be removed in a future release')
26    if args.recipename in workspace:
27        recipefile = workspace[args.recipename]['recipefile']
28    else:
29        recipefile = None
30    if not recipefile:
31        tinfoil = setup_tinfoil(config_only=False, basepath=basepath)
32        try:
33            rd = parse_recipe(config, tinfoil, args.recipename, True)
34            if not rd:
35                raise DevtoolError("Failed to find specified recipe")
36            recipefile = rd.getVar('FILE')
37        finally:
38            tinfoil.shutdown()
39    return recipefile
40
41
42def find_recipe(args, config, basepath, workspace):
43    """Entry point for the devtool 'find-recipe' subcommand"""
44    recipefile = _find_recipe_path(args, config, basepath, workspace)
45    print(recipefile)
46    return 0
47
48
49def edit_recipe(args, config, basepath, workspace):
50    """Entry point for the devtool 'edit-recipe' subcommand"""
51    return scriptutils.run_editor(_find_recipe_path(args, config, basepath, workspace), logger)
52
53
54def configure_help(args, config, basepath, workspace):
55    """Entry point for the devtool 'configure-help' subcommand"""
56    import oe.utils
57
58    check_workspace_recipe(workspace, args.recipename)
59    tinfoil = setup_tinfoil(config_only=False, basepath=basepath)
60    try:
61        rd = parse_recipe(config, tinfoil, args.recipename, appends=True, filter_workspace=False)
62        if not rd:
63            return 1
64        b = rd.getVar('B')
65        s = rd.getVar('S')
66        configurescript = os.path.join(s, 'configure')
67        confdisabled = 'noexec' in rd.getVarFlags('do_configure') or 'do_configure' not in (rd.getVar('__BBTASKS', False) or [])
68        configureopts = oe.utils.squashspaces(rd.getVar('CONFIGUREOPTS') or '')
69        extra_oeconf = oe.utils.squashspaces(rd.getVar('EXTRA_OECONF') or '')
70        extra_oecmake = oe.utils.squashspaces(rd.getVar('EXTRA_OECMAKE') or '')
71        do_configure = rd.getVar('do_configure') or ''
72        do_configure_noexpand = rd.getVar('do_configure', False) or ''
73        packageconfig = rd.getVarFlags('PACKAGECONFIG') or []
74        autotools = bb.data.inherits_class('autotools', rd) and ('oe_runconf' in do_configure or 'autotools_do_configure' in do_configure)
75        cmake = bb.data.inherits_class('cmake', rd) and ('cmake_do_configure' in do_configure)
76        cmake_do_configure = rd.getVar('cmake_do_configure')
77        pn = rd.getVar('PN')
78    finally:
79        tinfoil.shutdown()
80
81    if 'doc' in packageconfig:
82        del packageconfig['doc']
83
84    if autotools and not os.path.exists(configurescript):
85        logger.info('Running do_configure to generate configure script')
86        try:
87            stdout, _ = exec_build_env_command(config.init_path, basepath,
88                                               'bitbake -c configure %s' % args.recipename,
89                                               stderr=subprocess.STDOUT)
90        except bb.process.ExecutionError:
91            pass
92
93    if confdisabled or do_configure.strip() in ('', ':'):
94        raise DevtoolError("do_configure task has been disabled for this recipe")
95    elif args.no_pager and not os.path.exists(configurescript):
96        raise DevtoolError("No configure script found and no other information to display")
97    else:
98        configopttext = ''
99        if autotools and configureopts:
100            configopttext = '''
101Arguments currently passed to the configure script:
102
103%s
104
105Some of those are fixed.''' % (configureopts + ' ' + extra_oeconf)
106            if extra_oeconf:
107                configopttext += ''' The ones that are specified through EXTRA_OECONF (which you can change or add to easily):
108
109%s''' % extra_oeconf
110
111        elif cmake:
112            in_cmake = False
113            cmake_cmd = ''
114            for line in cmake_do_configure.splitlines():
115                if in_cmake:
116                    cmake_cmd = cmake_cmd + ' ' + line.strip().rstrip('\\')
117                    if not line.endswith('\\'):
118                        break
119                if line.lstrip().startswith('cmake '):
120                    cmake_cmd = line.strip().rstrip('\\')
121                    if line.endswith('\\'):
122                        in_cmake = True
123                    else:
124                        break
125            if cmake_cmd:
126                configopttext = '''
127The current cmake command line:
128
129%s
130
131Arguments specified through EXTRA_OECMAKE (which you can change or add to easily)
132
133%s''' % (oe.utils.squashspaces(cmake_cmd), extra_oecmake)
134            else:
135                configopttext = '''
136The current implementation of cmake_do_configure:
137
138cmake_do_configure() {
139%s
140}
141
142Arguments specified through EXTRA_OECMAKE (which you can change or add to easily)
143
144%s''' % (cmake_do_configure.rstrip(), extra_oecmake)
145
146        elif do_configure:
147            configopttext = '''
148The current implementation of do_configure:
149
150do_configure() {
151%s
152}''' % do_configure.rstrip()
153            if '${EXTRA_OECONF}' in do_configure_noexpand:
154                configopttext += '''
155
156Arguments specified through EXTRA_OECONF (which you can change or add to easily):
157
158%s''' % extra_oeconf
159
160        if packageconfig:
161            configopttext += '''
162
163Some of these options may be controlled through PACKAGECONFIG; for more details please see the recipe.'''
164
165        if args.arg:
166            helpargs = ' '.join(args.arg)
167        elif cmake:
168            helpargs = '-LH'
169        else:
170            helpargs = '--help'
171
172        msg = '''configure information for %s
173------------------------------------------
174%s''' % (pn, configopttext)
175
176        if cmake:
177            msg += '''
178
179The cmake %s output for %s follows. After "-- Cache values" you should see a list of variables you can add to EXTRA_OECMAKE (prefixed with -D and suffixed with = followed by the desired value, without any spaces).
180------------------------------------------''' % (helpargs, pn)
181        elif os.path.exists(configurescript):
182            msg += '''
183
184The ./configure %s output for %s follows.
185------------------------------------------''' % (helpargs, pn)
186
187        olddir = os.getcwd()
188        tmppath = tempfile.mkdtemp()
189        with tempfile.NamedTemporaryFile('w', delete=False) as tf:
190            if not args.no_header:
191                tf.write(msg + '\n')
192            tf.close()
193            try:
194                try:
195                    cmd = 'cat %s' % tf.name
196                    if cmake:
197                        cmd += '; cmake %s %s 2>&1' % (helpargs, s)
198                        os.chdir(b)
199                    elif os.path.exists(configurescript):
200                        cmd += '; %s %s' % (configurescript, helpargs)
201                    if sys.stdout.isatty() and not args.no_pager:
202                        pager = os.environ.get('PAGER', 'less')
203                        cmd = '(%s) | %s' % (cmd, pager)
204                    subprocess.check_call(cmd, shell=True)
205                except subprocess.CalledProcessError as e:
206                    return e.returncode
207            finally:
208                os.chdir(olddir)
209                shutil.rmtree(tmppath)
210                os.remove(tf.name)
211
212
213def register_commands(subparsers, context):
214    """Register devtool subcommands from this plugin"""
215    parser_edit_recipe = subparsers.add_parser('edit-recipe', help='Edit a recipe file',
216                                         description='Runs the default editor (as specified by the EDITOR variable) on the specified recipe. Note that this will be quicker for recipes in the workspace as the cache does not need to be loaded in that case.',
217                                         group='working')
218    parser_edit_recipe.add_argument('recipename', help='Recipe to edit')
219    # FIXME drop -a at some point in future
220    parser_edit_recipe.add_argument('--any-recipe', '-a', action="store_true", help='Does nothing (exists for backwards-compatibility)')
221    parser_edit_recipe.set_defaults(func=edit_recipe)
222
223    # Find-recipe
224    parser_find_recipe = subparsers.add_parser('find-recipe', help='Find a recipe file',
225                                         description='Finds a recipe file. Note that this will be quicker for recipes in the workspace as the cache does not need to be loaded in that case.',
226                                         group='working')
227    parser_find_recipe.add_argument('recipename', help='Recipe to find')
228    # FIXME drop -a at some point in future
229    parser_find_recipe.add_argument('--any-recipe', '-a', action="store_true", help='Does nothing (exists for backwards-compatibility)')
230    parser_find_recipe.set_defaults(func=find_recipe)
231
232    # NOTE: Needed to override the usage string here since the default
233    # gets the order wrong - recipename must come before --arg
234    parser_configure_help = subparsers.add_parser('configure-help', help='Get help on configure script options',
235                                         usage='devtool configure-help [options] recipename [--arg ...]',
236                                         description='Displays the help for the configure script for the specified recipe (i.e. runs ./configure --help) prefaced by a header describing the current options being specified. Output is piped through less (or whatever PAGER is set to, if set) for easy browsing.',
237                                         group='working')
238    parser_configure_help.add_argument('recipename', help='Recipe to show configure help for')
239    parser_configure_help.add_argument('-p', '--no-pager', help='Disable paged output', action="store_true")
240    parser_configure_help.add_argument('-n', '--no-header', help='Disable explanatory header text', action="store_true")
241    parser_configure_help.add_argument('--arg', help='Pass remaining arguments to the configure script instead of --help (useful if the script has additional help options)', nargs=argparse.REMAINDER)
242    parser_configure_help.set_defaults(func=configure_help)
243