1# Copyright 2014-2016 The Meson development team
2
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6
7#     http://www.apache.org/licenses/LICENSE-2.0
8
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import itertools
16import shutil
17import os
18import textwrap
19import typing as T
20
21from . import build
22from . import coredata
23from . import environment
24from . import mesonlib
25from . import mintro
26from . import mlog
27from .ast import AstIDGenerator
28from .mesonlib import MachineChoice, OptionKey
29
30if T.TYPE_CHECKING:
31    import argparse
32    from .coredata import UserOption
33
34def add_arguments(parser: 'argparse.ArgumentParser') -> None:
35    coredata.register_builtin_arguments(parser)
36    parser.add_argument('builddir', nargs='?', default='.')
37    parser.add_argument('--clearcache', action='store_true', default=False,
38                        help='Clear cached state (e.g. found dependencies)')
39
40def make_lower_case(val: T.Any) -> T.Union[str, T.List[T.Any]]:  # T.Any because of recursion...
41    if isinstance(val, bool):
42        return str(val).lower()
43    elif isinstance(val, list):
44        return [make_lower_case(i) for i in val]
45    else:
46        return str(val)
47
48
49class ConfException(mesonlib.MesonException):
50    pass
51
52
53class Conf:
54    def __init__(self, build_dir):
55        self.build_dir = os.path.abspath(os.path.realpath(build_dir))
56        if 'meson.build' in [os.path.basename(self.build_dir), self.build_dir]:
57            self.build_dir = os.path.dirname(self.build_dir)
58        self.build = None
59        self.max_choices_line_length = 60
60        self.name_col = []
61        self.value_col = []
62        self.choices_col = []
63        self.descr_col = []
64        # XXX: is there a case where this can actually remain false?
65        self.has_choices = False
66        self.all_subprojects: T.Set[str] = set()
67        self.yielding_options: T.Set[OptionKey] = set()
68
69        if os.path.isdir(os.path.join(self.build_dir, 'meson-private')):
70            self.build = build.load(self.build_dir)
71            self.source_dir = self.build.environment.get_source_dir()
72            self.coredata = coredata.load(self.build_dir)
73            self.default_values_only = False
74        elif os.path.isfile(os.path.join(self.build_dir, environment.build_filename)):
75            # Make sure that log entries in other parts of meson don't interfere with the JSON output
76            mlog.disable()
77            self.source_dir = os.path.abspath(os.path.realpath(self.build_dir))
78            intr = mintro.IntrospectionInterpreter(self.source_dir, '', 'ninja', visitors = [AstIDGenerator()])
79            intr.analyze()
80            # Re-enable logging just in case
81            mlog.enable()
82            self.coredata = intr.coredata
83            self.default_values_only = True
84        else:
85            raise ConfException(f'Directory {build_dir} is neither a Meson build directory nor a project source directory.')
86
87    def clear_cache(self):
88        self.coredata.clear_deps_cache()
89
90    def set_options(self, options):
91        self.coredata.set_options(options)
92
93    def save(self):
94        # Do nothing when using introspection
95        if self.default_values_only:
96            return
97        # Only called if something has changed so overwrite unconditionally.
98        coredata.save(self.coredata, self.build_dir)
99        # We don't write the build file because any changes to it
100        # are erased when Meson is executed the next time, i.e. when
101        # Ninja is run.
102
103    def print_aligned(self) -> None:
104        """Do the actual printing.
105
106        This prints the generated output in an aligned, pretty form. it aims
107        for a total width of 160 characters, but will use whatever the tty
108        reports it's value to be. Though this is much wider than the standard
109        80 characters of terminals, and even than the newer 120, compressing
110        it to those lengths makes the output hard to read.
111
112        Each column will have a specific width, and will be line wrapped.
113        """
114        total_width = shutil.get_terminal_size(fallback=(160, 0))[0]
115        _col = max(total_width // 5, 20)
116        four_column = (_col, _col, _col, total_width - (3 * _col))
117        # In this case we don't have the choices field, so we can redistribute
118        # the extra 40 characters to val and desc
119        three_column = (_col, _col * 2, total_width // 2)
120
121        for line in zip(self.name_col, self.value_col, self.choices_col, self.descr_col):
122            if not any(line):
123                print('')
124                continue
125
126            # This is a header, like `Subproject foo:`,
127            # We just want to print that and get on with it
128            if line[0] and not any(line[1:]):
129                print(line[0])
130                continue
131
132            # wrap will take a long string, and create a list of strings no
133            # longer than the size given. Then that list can be zipped into, to
134            # print each line of the output, such the that columns are printed
135            # to the right width, row by row.
136            if self.has_choices:
137                name = textwrap.wrap(line[0], four_column[0])
138                val = textwrap.wrap(line[1], four_column[1])
139                choice = textwrap.wrap(line[2], four_column[2])
140                desc = textwrap.wrap(line[3], four_column[3])
141                for l in itertools.zip_longest(name, val, choice, desc, fillvalue=''):
142                    # We must use the length modifier here to get even rows, as
143                    # `textwrap.wrap` will only shorten, not lengthen each item
144                    print('{:{widths[0]}} {:{widths[1]}} {:{widths[2]}} {}'.format(*l, widths=four_column))
145            else:
146                name = textwrap.wrap(line[0], three_column[0])
147                val = textwrap.wrap(line[1], three_column[1])
148                desc = textwrap.wrap(line[3], three_column[2])
149                for l in itertools.zip_longest(name, val, desc, fillvalue=''):
150                    print('{:{widths[0]}} {:{widths[1]}} {}'.format(*l, widths=three_column))
151
152    def split_options_per_subproject(self, options: 'coredata.KeyedOptionDictType') -> T.Dict[str, T.Dict[str, 'UserOption']]:
153        result: T.Dict[str, T.Dict[str, 'UserOption']] = {}
154        for k, o in options.items():
155            subproject = k.subproject
156            if k.subproject:
157                k = k.as_root()
158                if o.yielding and k in options:
159                    self.yielding_options.add(k)
160                self.all_subprojects.add(subproject)
161            result.setdefault(subproject, {})[str(k)] = o
162        return result
163
164    def _add_line(self, name: OptionKey, value, choices, descr) -> None:
165        self.name_col.append(' ' * self.print_margin + str(name))
166        self.value_col.append(value)
167        self.choices_col.append(choices)
168        self.descr_col.append(descr)
169
170    def add_option(self, name, descr, value, choices):
171        if isinstance(value, list):
172            value = '[{}]'.format(', '.join(make_lower_case(value)))
173        else:
174            value = make_lower_case(value)
175
176        if choices:
177            self.has_choices = True
178            if isinstance(choices, list):
179                choices_list = make_lower_case(choices)
180                current = '['
181                while choices_list:
182                    i = choices_list.pop(0)
183                    if len(current) + len(i) >= self.max_choices_line_length:
184                        self._add_line(name, value, current + ',', descr)
185                        name = ''
186                        value = ''
187                        descr = ''
188                        current = ' '
189                    if len(current) > 1:
190                        current += ', '
191                    current += i
192                choices = current + ']'
193            else:
194                choices = make_lower_case(choices)
195        else:
196            choices = ''
197
198        self._add_line(name, value, choices, descr)
199
200    def add_title(self, title):
201        titles = {'descr': 'Description', 'value': 'Current Value', 'choices': 'Possible Values'}
202        if self.default_values_only:
203            titles['value'] = 'Default Value'
204        self._add_line('', '', '', '')
205        self._add_line(title, titles['value'], titles['choices'], titles['descr'])
206        self._add_line('-' * len(title), '-' * len(titles['value']), '-' * len(titles['choices']), '-' * len(titles['descr']))
207
208    def add_section(self, section):
209        self.print_margin = 0
210        self._add_line('', '', '', '')
211        self._add_line(section + ':', '', '', '')
212        self.print_margin = 2
213
214    def print_options(self, title: str, options: 'coredata.KeyedOptionDictType') -> None:
215        if not options:
216            return
217        if title:
218            self.add_title(title)
219        for k, o in sorted(options.items()):
220            printable_value = o.printable_value()
221            if k in self.yielding_options:
222                printable_value = '<inherited from main project>'
223            self.add_option(k, o.description, printable_value, o.choices)
224
225    def print_conf(self):
226        def print_default_values_warning():
227            mlog.warning('The source directory instead of the build directory was specified.')
228            mlog.warning('Only the default values for the project are printed, and all command line parameters are ignored.')
229
230        if self.default_values_only:
231            print_default_values_warning()
232            print('')
233
234        print('Core properties:')
235        print('  Source dir', self.source_dir)
236        if not self.default_values_only:
237            print('  Build dir ', self.build_dir)
238
239        dir_option_names = set(coredata.BUILTIN_DIR_OPTIONS)
240        test_option_names = {OptionKey('errorlogs'),
241                            OptionKey('stdsplit')}
242
243        dir_options: 'coredata.KeyedOptionDictType' = {}
244        test_options: 'coredata.KeyedOptionDictType' = {}
245        core_options: 'coredata.KeyedOptionDictType' = {}
246        for k, v in self.coredata.options.items():
247            if k in dir_option_names:
248                dir_options[k] = v
249            elif k in test_option_names:
250                test_options[k] = v
251            elif k.is_builtin():
252                core_options[k] = v
253
254        host_core_options = self.split_options_per_subproject({k: v for k, v in core_options.items() if k.machine is MachineChoice.HOST})
255        build_core_options = self.split_options_per_subproject({k: v for k, v in core_options.items() if k.machine is MachineChoice.BUILD})
256        host_compiler_options = self.split_options_per_subproject({k: v for k, v in self.coredata.options.items() if k.is_compiler() and k.machine is MachineChoice.HOST})
257        build_compiler_options = self.split_options_per_subproject({k: v for k, v in self.coredata.options.items() if k.is_compiler() and k.machine is MachineChoice.BUILD})
258        project_options = self.split_options_per_subproject({k: v for k, v in self.coredata.options.items() if k.is_project()})
259        show_build_options = self.default_values_only or self.build.environment.is_cross_build()
260
261        self.add_section('Main project options')
262        self.print_options('Core options', host_core_options[''])
263        if show_build_options:
264            self.print_options('', build_core_options[''])
265        self.print_options('Backend options', {str(k): v for k, v in self.coredata.options.items() if k.is_backend()})
266        self.print_options('Base options', {str(k): v for k, v in self.coredata.options.items() if k.is_base()})
267        self.print_options('Compiler options', host_compiler_options.get('', {}))
268        if show_build_options:
269            self.print_options('', build_compiler_options.get('', {}))
270        self.print_options('Directories', dir_options)
271        self.print_options('Testing options', test_options)
272        self.print_options('Project options', project_options.get('', {}))
273        for subproject in sorted(self.all_subprojects):
274            if subproject == '':
275                continue
276            self.add_section('Subproject ' + subproject)
277            if subproject in host_core_options:
278                self.print_options('Core options', host_core_options[subproject])
279            if subproject in build_core_options and show_build_options:
280                self.print_options('', build_core_options[subproject])
281            if subproject in host_compiler_options:
282                self.print_options('Compiler options', host_compiler_options[subproject])
283            if subproject in build_compiler_options and show_build_options:
284                self.print_options('', build_compiler_options[subproject])
285            if subproject in project_options:
286                self.print_options('Project options', project_options[subproject])
287        self.print_aligned()
288
289        # Print the warning twice so that the user shouldn't be able to miss it
290        if self.default_values_only:
291            print('')
292            print_default_values_warning()
293
294        self.print_nondefault_buildtype_options()
295
296    def print_nondefault_buildtype_options(self):
297        mismatching = self.coredata.get_nondefault_buildtype_args()
298        if not mismatching:
299            return
300        print("\nThe following option(s) have a different value than the build type default\n")
301        print(f'               current   default')
302        for m in mismatching:
303            print(f'{m[0]:21}{m[1]:10}{m[2]:10}')
304
305def run(options):
306    coredata.parse_cmd_line_options(options)
307    builddir = os.path.abspath(os.path.realpath(options.builddir))
308    c = None
309    try:
310        c = Conf(builddir)
311        if c.default_values_only:
312            c.print_conf()
313            return 0
314
315        save = False
316        if options.cmd_line_options:
317            c.set_options(options.cmd_line_options)
318            coredata.update_cmd_line_file(builddir, options)
319            save = True
320        elif options.clearcache:
321            c.clear_cache()
322            save = True
323        else:
324            c.print_conf()
325        if save:
326            c.save()
327            mintro.update_build_options(c.coredata, c.build.environment.info_dir)
328            mintro.write_meson_info_file(c.build, [])
329    except ConfException as e:
330        print('Meson configurator encountered an error:')
331        if c is not None and c.build is not None:
332            mintro.write_meson_info_file(c.build, [e])
333        raise e
334    return 0
335