| # SPDX-License-Identifier: Apache-2.0 |
| # Copyright 2013-2025 The Meson development team |
| |
| from __future__ import annotations |
| |
| import argparse |
| import ast |
| import configparser |
| import os |
| import shlex |
| import typing as T |
| from itertools import chain |
| |
| from . import options |
| from .mesonlib import MesonException |
| from .options import OptionKey |
| |
| if T.TYPE_CHECKING: |
| from typing_extensions import Protocol |
| |
| # typeshed |
| StrOrBytesPath = T.Union[str, bytes, os.PathLike[str], os.PathLike[bytes]] |
| |
| class SharedCMDOptions(Protocol): |
| """Representation of command line options from Meson setup, configure, |
| and dist. |
| |
| :param cmd_line_options: command line options parsed into an OptionKey: |
| str mapping |
| """ |
| |
| cmd_line_options: T.Dict[OptionKey, T.Optional[str]] |
| cross_file: T.List[str] |
| native_file: T.List[str] |
| |
| |
| class CmdLineFileParser(configparser.ConfigParser): |
| def __init__(self) -> None: |
| # We don't want ':' as key delimiter, otherwise it would break when |
| # storing subproject options like "subproject:option=value" |
| super().__init__(delimiters=['='], interpolation=None) |
| |
| def read(self, filenames: T.Union['StrOrBytesPath', T.Iterable['StrOrBytesPath']], encoding: T.Optional[str] = 'utf-8') -> T.List[str]: |
| return super().read(filenames, encoding) |
| |
| def optionxform(self, optionstr: str) -> str: |
| # Don't call str.lower() on keys |
| return optionstr |
| |
| |
| def get_cmd_line_file(build_dir: str) -> str: |
| return os.path.join(build_dir, 'meson-private', 'cmd_line.txt') |
| |
| def read_cmd_line_file(build_dir: str, options: SharedCMDOptions) -> None: |
| filename = get_cmd_line_file(build_dir) |
| if not os.path.isfile(filename): |
| return |
| |
| config = CmdLineFileParser() |
| config.read(filename) |
| |
| # Do a copy because config is not really a dict. options.cmd_line_options |
| # overrides values from the file. |
| d = {OptionKey.from_string(k): v for k, v in config['options'].items()} |
| d.update(options.cmd_line_options) |
| options.cmd_line_options = d |
| |
| properties = config['properties'] |
| if not options.cross_file: |
| options.cross_file = ast.literal_eval(properties.get('cross_file', '[]')) |
| if not options.native_file: |
| # This will be a string in the form: "['first', 'second', ...]", use |
| # literal_eval to get it into the list of strings. |
| options.native_file = ast.literal_eval(properties.get('native_file', '[]')) |
| |
| def write_cmd_line_file(build_dir: str, options: SharedCMDOptions) -> None: |
| filename = get_cmd_line_file(build_dir) |
| config = CmdLineFileParser() |
| |
| properties: T.Dict[str, T.List[str]] = {} |
| if options.cross_file: |
| properties['cross_file'] = options.cross_file |
| if options.native_file: |
| properties['native_file'] = options.native_file |
| |
| config['options'] = {str(k): str(v) for k, v in options.cmd_line_options.items()} |
| config['properties'] = {k: repr(v) for k, v in properties.items()} |
| with open(filename, 'w', encoding='utf-8') as f: |
| config.write(f) |
| |
| def update_cmd_line_file(build_dir: str, options: SharedCMDOptions) -> None: |
| filename = get_cmd_line_file(build_dir) |
| config = CmdLineFileParser() |
| config.read(filename) |
| for k, v in options.cmd_line_options.items(): |
| keystr = str(k) |
| if v is not None: |
| config['options'][keystr] = str(v) |
| elif keystr in config['options']: |
| del config['options'][keystr] |
| |
| with open(filename, 'w', encoding='utf-8') as f: |
| config.write(f) |
| |
| def format_cmd_line_options(options: SharedCMDOptions) -> str: |
| cmdline = ['-D{}={}'.format(str(k), v) for k, v in options.cmd_line_options.items()] |
| if options.cross_file: |
| cmdline += [f'--cross-file={f}' for f in options.cross_file] |
| if options.native_file: |
| cmdline += [f'--native-file={f}' for f in options.native_file] |
| return ' '.join([shlex.quote(x) for x in cmdline]) |
| |
| |
| class KeyNoneAction(argparse.Action): |
| """ |
| Custom argparse Action that stores values in a dictionary as keys with value None. |
| """ |
| |
| def __init__(self, option_strings: str, dest: str, nargs: T.Optional[T.Union[int, str]] = None, **kwargs: T.Any) -> None: |
| assert nargs is None or nargs == 1 |
| super().__init__(option_strings, dest, nargs=1, **kwargs) |
| |
| def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, |
| arg: T.List[str], option_string: str = None) -> None: # type: ignore[override] |
| current_dict = getattr(namespace, self.dest) |
| if current_dict is None: |
| current_dict = {} |
| setattr(namespace, self.dest, current_dict) |
| |
| key = OptionKey.from_string(arg[0]) |
| current_dict[key] = None |
| |
| |
| class KeyValueAction(argparse.Action): |
| """ |
| Custom argparse Action that parses KEY=VAL arguments and stores them in a dictionary. |
| """ |
| |
| def __init__(self, option_strings: str, dest: str, nargs: T.Optional[T.Union[int, str]] = None, **kwargs: T.Any) -> None: |
| assert nargs is None or nargs == 1 |
| super().__init__(option_strings, dest, nargs=1, **kwargs) |
| |
| def __call__(self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, |
| arg: T.List[str], option_string: str = None) -> None: # type: ignore[override] |
| current_dict = getattr(namespace, self.dest) |
| if current_dict is None: |
| current_dict = {} |
| setattr(namespace, self.dest, current_dict) |
| |
| try: |
| keystr, value = arg[0].split('=', 1) |
| key = OptionKey.from_string(keystr) |
| current_dict[key] = value |
| except ValueError: |
| parser.error(f'The argument for option {option_string!r} must be in OPTION=VALUE format.') |
| |
| |
| def register_builtin_arguments(parser: argparse.ArgumentParser) -> None: |
| for n, b in options.BUILTIN_OPTIONS.items(): |
| options.option_to_argparse(b, n, parser, '') |
| for n, b in options.BUILTIN_OPTIONS_PER_MACHINE.items(): |
| options.option_to_argparse(b, n, parser, ' (just for host machine)') |
| options.option_to_argparse(b, n.as_build(), parser, ' (just for build machine)') |
| parser.add_argument('-D', action=KeyValueAction, dest='cmd_line_options', default={}, metavar="option=value", |
| help='Set the value of an option, can be used several times to set multiple options.') |
| |
| def parse_cmd_line_options(args: SharedCMDOptions) -> None: |
| # Merge builtin options set with --option into the dict. |
| for key in chain( |
| options.BUILTIN_OPTIONS.keys(), |
| (k.as_build() for k in options.BUILTIN_OPTIONS_PER_MACHINE.keys()), |
| options.BUILTIN_OPTIONS_PER_MACHINE.keys(), |
| ): |
| name = str(key) |
| value = getattr(args, name, None) |
| if value is not None: |
| if key in args.cmd_line_options: |
| cmdline_name = options.argparse_name_to_arg(name) |
| raise MesonException( |
| f'Got argument {name} as both -D{name} and {cmdline_name}. Pick one.') |
| args.cmd_line_options[key] = value |
| delattr(args, name) |