From 37067a53c4b3b99982ef8e1f431ba0c9302b66e8 Mon Sep 17 00:00:00 2001 From: Xavier Claessens Date: Sun, 13 May 2018 10:36:58 -0400 Subject: [PATCH] Use a single ArgumentParser for all subcommands This has the adventage that "meson --help" shows a list of all commands, making them discoverable. This also reduce the manual parsing of arguments to the strict minimum needed for backward compatibility. --- mesonbuild/mconf.py | 10 +-- mesonbuild/mesonmain.py | 139 +++++++++++++++++++++++------------- mesonbuild/minit.py | 8 +-- mesonbuild/minstall.py | 12 +--- mesonbuild/mintro.py | 8 +-- mesonbuild/msetup.py | 38 ++-------- mesonbuild/mtest.py | 14 ++-- mesonbuild/rewriter.py | 9 +-- mesonbuild/wrap/wraptool.py | 6 +- run_project_tests.py | 4 +- run_tests.py | 2 +- 11 files changed, 116 insertions(+), 134 deletions(-) diff --git a/mesonbuild/mconf.py b/mesonbuild/mconf.py index 2fd69b01c..576c574a2 100644 --- a/mesonbuild/mconf.py +++ b/mesonbuild/mconf.py @@ -13,17 +13,13 @@ # limitations under the License. import os -import argparse from . import (coredata, mesonlib, build) -def buildparser(): - parser = argparse.ArgumentParser(prog='meson configure') +def add_arguments(parser): coredata.register_builtin_arguments(parser) - parser.add_argument('builddir', nargs='?', default='.') parser.add_argument('--clearcache', action='store_true', default=False, help='Clear cached state (e.g. found dependencies)') - return parser class ConfException(mesonlib.MesonException): @@ -149,9 +145,7 @@ class Conf: self.print_options('Testing options', test_options) -def run(args): - args = mesonlib.expand_arguments(args) - options = buildparser().parse_args(args) +def run(options): coredata.parse_cmd_line_options(options) builddir = os.path.abspath(os.path.realpath(options.builddir)) try: diff --git a/mesonbuild/mesonmain.py b/mesonbuild/mesonmain.py index 2174e14db..684926c73 100644 --- a/mesonbuild/mesonmain.py +++ b/mesonbuild/mesonmain.py @@ -15,11 +15,97 @@ import sys import os.path import importlib +import traceback +import argparse from . import mesonlib from . import mlog +from . import mconf, minit, minstall, mintro, msetup, mtest, rewriter from .mesonlib import MesonException from .environment import detect_msys2_arch +from .wrap import wraptool + + +class CommandLineParser: + def __init__(self): + self.commands = {} + self.parser = argparse.ArgumentParser(prog='meson') + self.subparsers = self.parser.add_subparsers(title='Commands', + description='If no command is specified it defaults to setup command.') + self.add_command('setup', msetup.add_arguments, msetup.run, + help='Configure the project') + self.add_command('configure', mconf.add_arguments, mconf.run, + help='Change project options',) + self.add_command('install', minstall.add_arguments, minstall.run, + help='Install the project') + self.add_command('introspect', mintro.add_arguments, mintro.run, + help='Introspect project') + self.add_command('init', minit.add_arguments, minit.run, + help='Create a new project') + self.add_command('test', mtest.add_arguments, mtest.run, + help='Run tests') + self.add_command('rewrite', rewriter.add_arguments, rewriter.run, + help='Edit project files') + self.add_command('wrap', wraptool.add_arguments, wraptool.run, + help='Wrap tools') + self.add_command('runpython', self.add_runpython_arguments, self.run_runpython_command, + help='Run a python script') + self.add_command('help', self.add_help_arguments, self.run_help_command, + help='Print help of a subcommand') + + def add_command(self, name, add_arguments_func, run_func, help): + p = self.subparsers.add_parser(name, help=help) + add_arguments_func(p) + p.set_defaults(run_func=run_func) + self.commands[name] = p + + def add_runpython_arguments(self, parser): + parser.add_argument('script_file') + parser.add_argument('script_args', nargs=argparse.REMAINDER) + + def run_runpython_command(self, options): + import runpy + sys.argv[1:] = options.script_args + 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 len(args) == 0 or args[0] not in known_commands: + args = ['setup'] + args + + args = mesonlib.expand_arguments(args) + options = self.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 as e: + 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 @@ -50,6 +136,7 @@ def run(original_args, mainfile): print('You have python %s.' % sys.version) print('Please update your environment') return 1 + # 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') @@ -75,57 +162,7 @@ def run(original_args, mainfile): else: return run_script_command(args[1], args[2:]) - if len(args) > 0: - # First check if we want to run a subcommand. - cmd_name = args[0] - remaining_args = args[1:] - # "help" is a special case: Since printing of the help may be - # delegated to a subcommand, we edit cmd_name before executing - # the rest of the logic here. - if cmd_name == 'help': - remaining_args += ['--help'] - args = remaining_args - cmd_name = args[0] - if cmd_name == 'test': - from . import mtest - return mtest.run(remaining_args) - elif cmd_name == 'install': - from . import minstall - return minstall.run(remaining_args) - elif cmd_name == 'introspect': - from . import mintro - return mintro.run(remaining_args) - elif cmd_name == 'rewrite': - from . import rewriter - return rewriter.run(remaining_args) - elif cmd_name == 'configure': - try: - from . import mconf - return mconf.run(remaining_args) - except MesonException as e: - mlog.exception(e) - sys.exit(1) - elif cmd_name == 'wrap': - from .wrap import wraptool - return wraptool.run(remaining_args) - elif cmd_name == 'init': - from . import minit - return minit.run(remaining_args) - elif cmd_name == 'runpython': - import runpy - script_file = remaining_args[0] - sys.argv[1:] = remaining_args[1:] - runpy.run_path(script_file, run_name='__main__') - sys.exit(0) - else: - # If cmd_name is not a known command, assume user wants to run the - # setup command. - from . import msetup - if cmd_name != 'setup': - remaining_args = args - return msetup.run(remaining_args) - - return 0 + return CommandLineParser().run(args) def main(): # Always resolve the command path so Ninja can find it for regen, tests, etc. diff --git a/mesonbuild/minit.py b/mesonbuild/minit.py index a66361f8e..394fe40db 100644 --- a/mesonbuild/minit.py +++ b/mesonbuild/minit.py @@ -14,7 +14,7 @@ """Code that creates simple startup projects.""" -import os, sys, argparse, re, shutil, subprocess +import os, sys, re, shutil, subprocess from glob import glob from mesonbuild import mesonlib from mesonbuild.environment import detect_ninja @@ -425,8 +425,7 @@ def create_meson_build(options): open('meson.build', 'w').write(content) print('Generated meson.build file:\n\n' + content) -def run(args): - parser = argparse.ArgumentParser(prog='meson') +def add_arguments(parser): parser.add_argument("srcfiles", metavar="sourcefile", nargs="*", help="source files. default: all recognized files in current directory") parser.add_argument("-n", "--name", help="project name. default: name of current directory") @@ -441,7 +440,8 @@ def run(args): parser.add_argument('--type', default='executable', choices=['executable', 'library']) parser.add_argument('--version', default='0.1') - options = parser.parse_args(args) + +def run(options): if len(glob('*')) == 0: autodetect_options(options, sample=True) if not options.language: diff --git a/mesonbuild/minstall.py b/mesonbuild/minstall.py index 1d721795d..b65abe058 100644 --- a/mesonbuild/minstall.py +++ b/mesonbuild/minstall.py @@ -14,7 +14,6 @@ import sys, pickle, os, shutil, subprocess, gzip, errno import shlex -import argparse from glob import glob from .scripts import depfixer from .scripts import destdir_join @@ -33,15 +32,13 @@ build definitions so that it will not break when the change happens.''' selinux_updates = [] -def buildparser(): - parser = argparse.ArgumentParser(prog='meson install') +def add_arguments(parser): parser.add_argument('-C', default='.', dest='wd', help='directory to cd into before running') parser.add_argument('--no-rebuild', default=False, action='store_true', help='Do not rebuild before installing.') parser.add_argument('--only-changed', default=False, action='store_true', help='Only overwrite files that are older than the copied file.') - return parser class DirMaker: def __init__(self, lf): @@ -501,9 +498,7 @@ class Installer: else: raise -def run(args): - parser = buildparser() - opts = parser.parse_args(args) +def run(opts): datafilename = 'meson-private/install.dat' private_dir = os.path.dirname(datafilename) log_dir = os.path.join(private_dir, '../meson-logs') @@ -520,6 +515,3 @@ def run(args): append_to_log(lf, '# Does not contain files installed by custom scripts.') installer.do_install(datafilename) return 0 - -if __name__ == '__main__': - sys.exit(run(sys.argv[1:])) diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index 188459acd..b15a608fe 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -23,12 +23,10 @@ import json from . import build, mtest, coredata as cdata from . import mesonlib from .backend import ninjabackend -import argparse import sys, os import pathlib -def buildparser(): - parser = argparse.ArgumentParser(prog='meson introspect') +def add_arguments(parser): parser.add_argument('--targets', action='store_true', dest='list_targets', default=False, help='List top level targets.') parser.add_argument('--installed', action='store_true', dest='list_installed', default=False, @@ -48,7 +46,6 @@ def buildparser(): parser.add_argument('--projectinfo', action='store_true', dest='projectinfo', default=False, help='Information about projects.') parser.add_argument('builddir', nargs='?', default='.', help='The build directory') - return parser def determine_installed_path(target, installdata): install_target = None @@ -206,9 +203,8 @@ def list_projinfo(builddata): result['subprojects'] = subprojects print(json.dumps(result)) -def run(args): +def run(options): datadir = 'meson-private' - options = buildparser().parse_args(args) if options.builddir is not None: datadir = os.path.join(options.builddir, datadir) if not os.path.isdir(datadir): diff --git a/mesonbuild/msetup.py b/mesonbuild/msetup.py index b07e7c4de..157655657 100644 --- a/mesonbuild/msetup.py +++ b/mesonbuild/msetup.py @@ -19,7 +19,6 @@ import os.path import platform import cProfile as profile import argparse -import traceback from . import environment, interpreter, mesonlib from . import build @@ -27,8 +26,7 @@ from . import mlog, coredata from .mesonlib import MesonException from .wrap import WrapMode -def buildparser(): - parser = argparse.ArgumentParser(prog='meson') +def add_arguments(parser): coredata.register_builtin_arguments(parser) parser.add_argument('--cross-file', default=None, help='File describing cross compilation environment.') @@ -48,7 +46,6 @@ def buildparser(): 'is not working.') parser.add_argument('builddir', nargs='?', default=None) parser.add_argument('sourcedir', nargs='?', default=None) - return parser def wrapmodetype(string): try: @@ -193,35 +190,8 @@ class MesonApp: os.unlink(cdf) raise -def run(args): - args = mesonlib.expand_arguments(args) - options = buildparser().parse_args(args) +def run(options): coredata.parse_cmd_line_options(options) - try: - app = MesonApp(options) - except Exception as e: - # Log directory does not exist, so just print - # to stdout. - print('Error during basic setup:\n') - print(e) - return 1 - try: - app.generate() - except Exception as e: - if isinstance(e, MesonException): - mlog.exception(e) - # Path to log file - mlog.shutdown() - logfile = os.path.join(app.build_dir, environment.Environment.log_dir, mlog.log_fname) - mlog.log("\nA full log can be found at", mlog.bold(logfile)) - if os.environ.get('MESON_FORCE_BACKTRACE'): - raise - return 1 - else: - if os.environ.get('MESON_FORCE_BACKTRACE'): - raise - traceback.print_exc() - return 2 - finally: - mlog.shutdown() + app = MesonApp(options) + app.generate() return 0 diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index 8d9a58531..78f22529f 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -60,8 +60,7 @@ def determine_worker_count(): num_workers = 1 return num_workers -def buildparser(): - parser = argparse.ArgumentParser(prog='meson test') +def add_arguments(parser): parser.add_argument('--repeat', default=1, dest='repeat', type=int, help='Number of times to run the tests.') parser.add_argument('--no-rebuild', default=False, action='store_true', @@ -102,7 +101,6 @@ def buildparser(): help='Arguments to pass to the specified test(s) or all tests') parser.add_argument('args', nargs='*', help='Optional list of tests to run') - return parser def returncode_to_status(retcode): @@ -737,9 +735,7 @@ def rebuild_all(wd): return True -def run(args): - options = buildparser().parse_args(args) - +def run(options): if options.benchmark: options.num_processes = 1 @@ -784,3 +780,9 @@ def run(args): else: print(e) return 1 + +def run_with_args(args): + parser = argparse.ArgumentParser(prog='meson test') + add_arguments(parser) + options = parser.parse_args(args) + return run(options) diff --git a/mesonbuild/rewriter.py b/mesonbuild/rewriter.py index 11272887d..5da8c8903 100644 --- a/mesonbuild/rewriter.py +++ b/mesonbuild/rewriter.py @@ -27,11 +27,8 @@ import mesonbuild.astinterpreter from mesonbuild.mesonlib import MesonException from mesonbuild import mlog import sys, traceback -import argparse - -def buildparser(): - parser = argparse.ArgumentParser(prog='meson rewrite') +def add_arguments(parser): parser.add_argument('--sourcedir', default='.', help='Path to source directory.') parser.add_argument('--target', default=None, @@ -39,10 +36,8 @@ def buildparser(): parser.add_argument('--filename', default=None, help='Name of source file to add or remove to target.') parser.add_argument('commands', nargs='+') - return parser -def run(args): - options = buildparser().parse_args(args) +def run(options): if options.target is None or options.filename is None: sys.exit("Must specify both target and filename.") print('This tool is highly experimental, use with care.') diff --git a/mesonbuild/wrap/wraptool.py b/mesonbuild/wrap/wraptool.py index 364452d5e..bb64b5b24 100644 --- a/mesonbuild/wrap/wraptool.py +++ b/mesonbuild/wrap/wraptool.py @@ -16,7 +16,6 @@ import json import sys, os import configparser import shutil -import argparse from glob import glob @@ -208,9 +207,6 @@ def status(options): else: print('', name, 'not up to date. Have %s %d, but %s %d is available.' % (current_branch, current_revision, latest_branch, latest_revision)) -def run(args): - parser = argparse.ArgumentParser(prog='wraptool') - add_arguments(parser) - options = parser.parse_args(args) +def run(options): options.wrap_func(options) return 0 diff --git a/run_project_tests.py b/run_project_tests.py index ba7b5e088..876d1359a 100755 --- a/run_project_tests.py +++ b/run_project_tests.py @@ -247,12 +247,12 @@ def run_test_inprocess(testdir): os.chdir(testdir) test_log_fname = Path('meson-logs', 'testlog.txt') try: - returncode_test = mtest.run(['--no-rebuild']) + returncode_test = mtest.run_with_args(['--no-rebuild']) if test_log_fname.exists(): test_log = test_log_fname.open(errors='ignore').read() else: test_log = '' - returncode_benchmark = mtest.run(['--no-rebuild', '--benchmark', '--logbase', 'benchmarklog']) + returncode_benchmark = mtest.run_with_args(['--no-rebuild', '--benchmark', '--logbase', 'benchmarklog']) finally: sys.stdout = old_stdout sys.stderr = old_stderr diff --git a/run_tests.py b/run_tests.py index 2a078ef96..af926eab3 100755 --- a/run_tests.py +++ b/run_tests.py @@ -181,7 +181,7 @@ def run_mtest_inprocess(commandlist): old_stderr = sys.stderr sys.stderr = mystderr = StringIO() try: - returncode = mtest.run(commandlist) + returncode = mtest.run_with_args(commandlist) finally: sys.stdout = old_stdout sys.stderr = old_stderr