| # SPDX-License-Identifier: Apache-2.0 |
| # Copyright 2012-2021 The Meson development team |
| |
| from __future__ import annotations |
| |
| # Work around some pathlib bugs... |
| |
| from . import _pathlib |
| import sys |
| sys.modules['pathlib'] = _pathlib |
| |
| # This file is an entry point for all commands, including scripts. Include the |
| # strict minimum python modules for performance reasons. |
| import os.path |
| import platform |
| import importlib |
| import argparse |
| import typing as T |
| |
| from .utils.core import MesonException, MesonBugException |
| from . import mlog |
| |
| def errorhandler(e: Exception, command: str) -> int: |
| import traceback |
| if isinstance(e, MesonException): |
| mlog.exception(e) |
| logfile = mlog.shutdown() |
| if logfile is not None: |
| mlog.log("\nA full log can be found at", mlog.bold(logfile)) |
| if os.environ.get('MESON_FORCE_BACKTRACE'): |
| raise e |
| return 1 |
| else: |
| # We assume many types of traceback are Meson logic bugs, but most |
| # particularly anything coming from the interpreter during `setup`. |
| # Some things definitely aren't: |
| # - PermissionError is always a problem in the user environment |
| # - runpython doesn't run Meson's own code, even though it is |
| # dispatched by our run() |
| if os.environ.get('MESON_FORCE_BACKTRACE'): |
| raise e |
| traceback.print_exc() |
| |
| if command == 'runpython': |
| return 2 |
| elif isinstance(e, OSError): |
| mlog.exception(Exception("Unhandled python OSError. This is probably not a Meson bug, " |
| "but an issue with your build environment.")) |
| return e.errno |
| else: # Exception |
| msg = 'Unhandled python exception' |
| if all(getattr(e, a, None) is not None for a in ['file', 'lineno', 'colno']): |
| e = MesonBugException(msg, e.file, e.lineno, e.colno) # type: ignore |
| else: |
| e = MesonBugException(msg) |
| mlog.exception(e) |
| return 2 |
| |
| # Note: when adding arguments, please also add them to the completion |
| # scripts in $MESONSRC/data/shell-completions/ |
| class CommandLineParser: |
| def __init__(self) -> None: |
| # only import these once we do full argparse processing |
| from . import mconf, mdist, minit, minstall, mintro, msetup, mtest, rewriter, msubprojects, munstable_coredata, mcompile, mdevenv, mformat |
| from .scripts import env2mfile |
| from .wrap import wraptool |
| import shutil |
| |
| self.term_width = shutil.get_terminal_size().columns |
| self.formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=int(self.term_width / 2), width=self.term_width) |
| |
| self.commands: T.Dict[str, argparse.ArgumentParser] = {} |
| self.hidden_commands: T.List[str] = [] |
| self.parser = argparse.ArgumentParser(prog='meson', formatter_class=self.formatter) |
| self.subparsers = self.parser.add_subparsers(title='Commands', dest='command', |
| description='If no command is specified it defaults to setup command.') |
| self.add_command('setup', msetup.add_arguments, msetup.run, |
| help_msg='Configure the project') |
| self.add_command('configure', mconf.add_arguments, mconf.run, |
| help_msg='Change project options',) |
| self.add_command('dist', mdist.add_arguments, mdist.run, |
| help_msg='Generate release archive',) |
| self.add_command('install', minstall.add_arguments, minstall.run, |
| help_msg='Install the project') |
| self.add_command('introspect', mintro.add_arguments, mintro.run, |
| help_msg='Introspect project') |
| self.add_command('init', minit.add_arguments, minit.run, |
| help_msg='Create a new project') |
| self.add_command('test', mtest.add_arguments, mtest.run, |
| help_msg='Run tests') |
| self.add_command('wrap', wraptool.add_arguments, wraptool.run, |
| help_msg='Wrap tools') |
| self.add_command('subprojects', msubprojects.add_arguments, msubprojects.run, |
| help_msg='Manage subprojects') |
| self.add_command('rewrite', lambda parser: rewriter.add_arguments(parser, self.formatter), rewriter.run, |
| help_msg='Modify the project definition') |
| self.add_command('compile', mcompile.add_arguments, mcompile.run, |
| help_msg='Build the project') |
| self.add_command('devenv', mdevenv.add_arguments, mdevenv.run, |
| help_msg='Run commands in developer environment') |
| self.add_command('env2mfile', env2mfile.add_arguments, env2mfile.run, |
| help_msg='Convert current environment to a cross or native file') |
| self.add_command('format', mformat.add_arguments, mformat.run, aliases=['fmt'], |
| help_msg='Format meson source file') |
| # Add new commands above this line to list them in help command |
| self.add_command('help', self.add_help_arguments, self.run_help_command, |
| help_msg='Print help of a subcommand') |
| |
| # Hidden commands |
| self.add_command('runpython', self.add_runpython_arguments, self.run_runpython_command, |
| help_msg=argparse.SUPPRESS) |
| self.add_command('unstable-coredata', munstable_coredata.add_arguments, munstable_coredata.run, |
| help_msg=argparse.SUPPRESS) |
| |
| def add_command(self, name: str, add_arguments_func: T.Callable[[argparse.ArgumentParser], None], |
| run_func: T.Callable[[argparse.Namespace], int], help_msg: str, aliases: T.List[str] = None) -> None: |
| aliases = aliases or [] |
| # FIXME: Cannot have hidden subparser: |
| # https://bugs.python.org/issue22848 |
| if help_msg == argparse.SUPPRESS: |
| p = argparse.ArgumentParser(prog='meson ' + name, formatter_class=self.formatter) |
| self.hidden_commands.append(name) |
| else: |
| p = self.subparsers.add_parser(name, help=help_msg, aliases=aliases, formatter_class=self.formatter) |
| add_arguments_func(p) |
| p.set_defaults(run_func=run_func) |
| for i in [name] + aliases: |
| self.commands[i] = p |
| |
| def add_runpython_arguments(self, parser: argparse.ArgumentParser) -> None: |
| parser.add_argument('-c', action='store_true', dest='eval_arg', default=False) |
| parser.add_argument('--version', action='version', version=platform.python_version()) |
| parser.add_argument('script_file') |
| parser.add_argument('script_args', nargs=argparse.REMAINDER) |
| |
| def run_runpython_command(self, options: argparse.Namespace) -> int: |
| sys.argv[1:] = options.script_args |
| if options.eval_arg: |
| exec(options.script_file) |
| else: |
| import runpy |
| sys.path.insert(0, os.path.dirname(options.script_file)) |
| runpy.run_path(options.script_file, run_name='__main__') |
| return 0 |
| |
| def add_help_arguments(self, parser: argparse.ArgumentParser) -> None: |
| parser.add_argument('command', nargs='?', choices=list(self.commands.keys())) |
| |
| def run_help_command(self, options: argparse.Namespace) -> int: |
| if options.command: |
| self.commands[options.command].print_help() |
| else: |
| self.parser.print_help() |
| return 0 |
| |
| def run(self, args: T.List[str]) -> int: |
| implicit_setup_command_notice = False |
| # If first arg is not a known command, assume user wants to run the setup |
| # command. |
| known_commands = list(self.commands.keys()) + ['-h', '--help'] |
| if not args or args[0] not in known_commands: |
| implicit_setup_command_notice = True |
| args = ['setup'] + args |
| |
| # Hidden commands have their own parser instead of using the global one |
| if args[0] in self.hidden_commands: |
| command = args[0] |
| parser = self.commands[command] |
| args = args[1:] |
| else: |
| parser = self.parser |
| command = None |
| |
| from . import mesonlib |
| args = mesonlib.expand_arguments(args) |
| options = parser.parse_args(args) |
| |
| if command is None: |
| command = options.command |
| |
| # Bump the version here in order to add a pre-exit warning that we are phasing out |
| # support for old python. If this is already the oldest supported version, then |
| # this can never be true and does nothing. |
| pending_python_deprecation_notice = \ |
| command in {'setup', 'compile', 'test', 'install'} and sys.version_info < (3, 7) |
| |
| try: |
| return options.run_func(options) |
| except Exception as e: |
| return errorhandler(e, command) |
| finally: |
| if implicit_setup_command_notice: |
| mlog.warning('Running the setup command as `meson [options]` instead of ' |
| '`meson setup [options]` is ambiguous and deprecated.', fatal=False) |
| if pending_python_deprecation_notice: |
| mlog.notice('You are using Python 3.6 which is EOL. Starting with v0.62.0, ' |
| 'Meson will require Python 3.7 or newer', fatal=False) |
| mlog.shutdown() |
| |
| def run_script_command(script_name: str, script_args: T.List[str]) -> int: |
| # Map script name to module name for those that doesn't match |
| script_map = {'exe': 'meson_exe', |
| 'install': 'meson_install', |
| 'delsuffix': 'delwithsuffix', |
| 'gtkdoc': 'gtkdochelper', |
| 'hotdoc': 'hotdochelper', |
| 'regencheck': 'regen_checker'} |
| module_name = script_map.get(script_name, script_name) |
| |
| try: |
| module = importlib.import_module('mesonbuild.scripts.' + module_name) |
| except ModuleNotFoundError as e: |
| mlog.exception(e) |
| return 1 |
| |
| try: |
| return module.run(script_args) |
| except MesonException as e: |
| mlog.error(f'Error in {script_name} helper script:') |
| mlog.exception(e) |
| return 1 |
| |
| def ensure_stdout_accepts_unicode() -> None: |
| if sys.stdout.encoding and not sys.stdout.encoding.upper().startswith('UTF-'): |
| sys.stdout.reconfigure(errors='surrogateescape') # type: ignore[attr-defined] |
| |
| def set_meson_command(mainfile: str) -> None: |
| # Set the meson command that will be used to run scripts and so on |
| from . import mesonlib |
| mesonlib.set_meson_command(mainfile) |
| |
| def run(original_args: T.List[str], mainfile: str) -> int: |
| if os.environ.get('MESON_SHOW_DEPRECATIONS'): |
| # workaround for https://bugs.python.org/issue34624 |
| import warnings |
| for typ in [DeprecationWarning, SyntaxWarning, FutureWarning, PendingDeprecationWarning]: |
| warnings.filterwarnings('error', category=typ, module='mesonbuild') |
| warnings.filterwarnings('ignore', message=".*importlib-resources.*") |
| |
| if sys.version_info >= (3, 10) and os.environ.get('MESON_RUNNING_IN_PROJECT_TESTS'): |
| # workaround for https://bugs.python.org/issue34624 |
| import warnings |
| warnings.filterwarnings('error', category=EncodingWarning, module='mesonbuild') |
| # python 3.11 adds a warning that in 3.15, UTF-8 mode will be default. |
| # This is fantastic news, we'd love that. Less fantastic: this warning is silly, |
| # we *want* these checks to be affected. Plus, the recommended alternative API |
| # would (in addition to warning people when UTF-8 mode removed the problem) also |
| # require using a minimum python version of 3.11 (in which the warning was added) |
| # or add verbose if/else soup. |
| warnings.filterwarnings('ignore', message="UTF-8 Mode affects .*getpreferredencoding", category=EncodingWarning) |
| |
| # Meson gets confused if stdout can't output Unicode, if the |
| # locale isn't Unicode, just force stdout to accept it. This tries |
| # to emulate enough of PEP 540 to work elsewhere. |
| ensure_stdout_accepts_unicode() |
| |
| # https://github.com/mesonbuild/meson/issues/3653 |
| if sys.platform == 'cygwin' and os.environ.get('MSYSTEM', '') not in ['MSYS', '']: |
| mlog.error('This python3 seems to be msys/python on MSYS2 Windows, but you are in a MinGW environment') |
| mlog.error('Please install and use mingw-w64-x86_64-python3 and/or mingw-w64-x86_64-meson with Pacman') |
| return 2 |
| |
| args = original_args[:] |
| |
| # Special handling of internal commands called from backends, they don't |
| # need to go through argparse. |
| if len(args) >= 2 and args[0] == '--internal': |
| if args[1] == 'regenerate': |
| set_meson_command(mainfile) |
| from . import msetup |
| try: |
| return msetup.run(['--reconfigure'] + args[2:]) |
| except Exception as e: |
| return errorhandler(e, 'setup') |
| else: |
| return run_script_command(args[1], args[2:]) |
| |
| set_meson_command(mainfile) |
| return CommandLineParser().run(args) |
| |
| def main() -> int: |
| # Always resolve the command path so Ninja can find it for regen, tests, etc. |
| if 'meson.exe' in sys.executable: |
| assert os.path.isabs(sys.executable) |
| launcher = sys.executable |
| else: |
| launcher = os.path.abspath(sys.argv[0]) |
| return run(sys.argv[1:], launcher) |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |