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