diff options
Diffstat (limited to 'meson/mesonbuild/mesonmain.py')
-rw-r--r-- | meson/mesonbuild/mesonmain.py | 329 |
1 files changed, 329 insertions, 0 deletions
diff --git a/meson/mesonbuild/mesonmain.py b/meson/mesonbuild/mesonmain.py new file mode 100644 index 000000000..8b7c9c176 --- /dev/null +++ b/meson/mesonbuild/mesonmain.py @@ -0,0 +1,329 @@ +# Copyright 2012-2021 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Work around some pathlib bugs... +from . import _pathlib +import sys +sys.modules['pathlib'] = _pathlib + +import os.path +import importlib +import traceback +import argparse +import codecs +import shutil + +from . import mesonlib +from . import mlog +from . import mconf, mdist, minit, minstall, mintro, msetup, mtest, rewriter, msubprojects, munstable_coredata, mcompile, mdevenv +from .mesonlib import MesonException +from .environment import detect_msys2_arch +from .wrap import wraptool + +need_setup_vsenv = False + +bat_template = '''@ECHO OFF + +call "{}" + +ECHO {} +SET +''' + +# If on Windows and VS is installed but not set up in the environment, +# set it to be runnable. In this way Meson can be directly invoked +# from any shell, VS Code etc. +def setup_vsenv() -> None: + import subprocess, json, pathlib + if not mesonlib.is_windows(): + return + bat_placeholder = 'nananananananananananananananana' + # If an existing build tool chain exists in PATH -> do nothing. + if shutil.which('cc'): + return + if shutil.which('gcc'): + return + if shutil.which('clang'): + return + if shutil.which('clang-cl'): + return + if os.environ.get('OSTYPE', bat_placeholder) == 'cygwin': + return + if 'Visual Studio' in os.environ['PATH']: + return + # VSINSTALL is set when running setvars from a Visual Studio installation + # Tested with Visual Studio 2012 and 2017 + if 'VSINSTALLDIR' in os.environ: + return + # Check explicitly for cl when on Windows + if shutil.which('cl.exe'): + return + + root = os.environ.get("ProgramFiles(x86)") or os.environ.get("ProgramFiles") + bat_locator_bin = pathlib.Path(root, 'Microsoft Visual Studio/Installer/vswhere.exe') + if not bat_locator_bin.exists(): + return + bat_json = subprocess.check_output( + [ + str(bat_locator_bin), + '-latest', + '-prerelease', + '-requiresAny', + '-requires', 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', + '-products', '*', + '-utf8', + '-format', + 'json' + ] + ) + bat_info = json.loads(bat_json) + if not bat_info: + # VS installer instelled but not VS itself maybe? + return + print('Activating VS', bat_info[0]['catalog']['productDisplayVersion']) + bat_root = pathlib.Path(bat_info[0]['installationPath']) + bat_path = bat_root / 'VC/Auxiliary/Build/vcvars64.bat' + if not bat_path.exists(): + return + + bat_file = pathlib.Path.home() / 'vsdetect.bat' + + bat_separator = '---SPLIT---' + bat_contents = bat_template.format(bat_path, bat_separator) + bat_file.write_text(bat_contents, encoding='utf-8') + try: + bat_output = subprocess.check_output(str(bat_file), universal_newlines=True) + finally: + bat_file.unlink() + bat_lines = bat_output.split('\n') + bat_separator_seen = False + for bat_line in bat_lines: + if bat_line == bat_separator: + bat_separator_seen = True + continue + if not bat_separator_seen: + continue + if not bat_line: + continue + k, v = bat_line.split('=', 1) + os.environ[k] = v + global need_setup_vsenv + need_setup_vsenv = True + + +# Note: when adding arguments, please also add them to the completion +# scripts in $MESONSRC/data/shell-completions/ +class CommandLineParser: + def __init__(self): + 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 = {} + self.hidden_commands = [] + 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('help', self.add_help_arguments, self.run_help_command, + help_msg='Print help of a subcommand') + 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') + + # 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, add_arguments_func, run_func, help_msg, aliases=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): + parser.add_argument('-c', action='store_true', dest='eval_arg', default=False) + parser.add_argument('script_file') + parser.add_argument('script_args', nargs=argparse.REMAINDER) + + def run_runpython_command(self, options): + import runpy + if options.eval_arg: + exec(options.script_file) + else: + sys.argv[1:] = options.script_args + 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): + parser.add_argument('command', nargs='?') + + def run_help_command(self, options): + if options.command: + self.commands[options.command].print_help() + else: + self.parser.print_help() + return 0 + + def run(self, args): + # 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: + 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 + + args = mesonlib.expand_arguments(args) + options = parser.parse_args(args) + + try: + return options.run_func(options) + except MesonException as e: + 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 + return 1 + except Exception: + if os.environ.get('MESON_FORCE_BACKTRACE'): + raise + traceback.print_exc() + return 2 + finally: + mlog.shutdown() + +def run_script_command(script_name, script_args): + # 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(): + if sys.stdout.encoding and not sys.stdout.encoding.upper().startswith('UTF-'): + if sys.version_info >= (3, 7): + sys.stdout.reconfigure(errors='surrogateescape') + else: + sys.stdout = codecs.getwriter('utf-8')(sys.stdout.detach(), + errors='surrogateescape') + sys.stdout.encoding = 'UTF-8' + if not hasattr(sys.stdout, 'buffer'): + sys.stdout.buffer = sys.stdout.raw if hasattr(sys.stdout, 'raw') else sys.stdout + +def run(original_args, mainfile): + if sys.version_info < (3, 6): + print('Meson works correctly only with python 3.6+.') + print(f'You have python {sys.version}.') + print('Please update your environment') + return 1 + + # 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.lower() == 'msys': + mlog.error('This python3 seems to be msys/python on MSYS2 Windows, which is known to have path semantics incompatible with Meson') + msys2_arch = detect_msys2_arch() + if msys2_arch: + mlog.error('Please install and use mingw-w64-i686-python3 and/or mingw-w64-x86_64-python3 with Pacman') + else: + mlog.error('Please download and use Python as detailed at: https://mesonbuild.com/Getting-meson.html') + return 2 + + # Set the meson command that will be used to run scripts and so on + mesonlib.set_meson_command(mainfile) + + 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': + # Rewrite "meson --internal regenerate" command line to + # "meson --reconfigure" + args = ['--reconfigure'] + args[2:] + else: + return run_script_command(args[1], args[2:]) + + return CommandLineParser().run(args) + +def main(): + setup_vsenv() + # 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.realpath(sys.argv[0]) + return run(sys.argv[1:], launcher) + +if __name__ == '__main__': + sys.exit(main()) |