diff --git a/scripts/meta/west/__init__.py b/scripts/meta/west/__init__.py new file mode 100644 index 00000000000..22e6ddd8a44 --- /dev/null +++ b/scripts/meta/west/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2018 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +# Nothing here for now. diff --git a/scripts/meta/west/__main__.py b/scripts/meta/west/__main__.py new file mode 100644 index 00000000000..a4a964a9db0 --- /dev/null +++ b/scripts/meta/west/__main__.py @@ -0,0 +1,12 @@ +# Copyright 2018 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Zephyr RTOS meta-tool (west) +''' + +from .main import main +import sys + +if __name__ == '__main__': + main(sys.argv[1:]) diff --git a/scripts/meta/west/cmd/__init__.py b/scripts/meta/west/cmd/__init__.py new file mode 100644 index 00000000000..769757140d2 --- /dev/null +++ b/scripts/meta/west/cmd/__init__.py @@ -0,0 +1,74 @@ +# Copyright 2018 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''West's commands subpackage. + +All commands should be implemented within modules in this package. +''' + +from abc import ABC, abstractmethod + +__all__ = ['CommandContextError', 'WestCommand'] + + +class CommandContextError(RuntimeError): + '''Indicates that a context-dependent command could not be run.''' + + +class WestCommand(ABC): + '''Abstract superclass for a west command. + + All top-level commands supported by west implement this interface.''' + + def __init__(self, name, description, accepts_unknown_args=False): + '''Create a command instance. + + `name`: the command's name, as entered by the user. + `description`: one-line command description to show to the user. + + `accepts_unknown_args`: if true, the command can handle + arbitrary unknown command line arguments in its run() + method. Otherwise, passing unknown arguments will cause + UnknownArgumentsError to be raised. + ''' + self.name = name + self.description = description + self._accept_unknown = accepts_unknown_args + + def run(self, args, unknown): + '''Run the command. + + `args`: known arguments parsed via `register_arguments()` + `unknown`: unknown arguments present on the command line + ''' + if unknown and not self._accept_unknown: + self.parser.error('unexpected arguments: {}'.format(unknown)) + self.do_run(args, unknown) + + def add_parser(self, parser_adder): + '''Registers a parser for this command, and returns it. + ''' + self.parser = self.do_add_parser(parser_adder) + return self.parser + + # + # Mandatory subclass hooks + # + + @abstractmethod + def do_add_parser(self, parser_adder): + '''Subclass method for registering command line arguments. + + `parser_adder` is an argparse argument subparsers adder.''' + + @abstractmethod + def do_run(self, args, unknown): + '''Subclasses must implement; called when the command is run. + + `args` is the namespace of parsed known arguments. + + If `accepts_unknown_args` was False when constructing this + object, `unknown` will be empty. Otherwise, it is an iterable + containing all unknown arguments present on the command line. + ''' diff --git a/scripts/meta/west/log.py b/scripts/meta/west/log.py new file mode 100644 index 00000000000..38bbf2ecbf1 --- /dev/null +++ b/scripts/meta/west/log.py @@ -0,0 +1,66 @@ +# Copyright 2018 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Logging module for west + +Provides common methods for logging messages to display to the user.''' + +import sys + +VERBOSE_NONE = 0 +'''Base verbosity level (zero), no verbose messages printed.''' + +VERBOSE_NORMAL = 1 +'''Base verbosity level, some verbose messages printed.''' + +VERBOSE_VERY = 2 +'''Very verbose output messages will be printed.''' + +VERBOSE_EXTREME = 3 +'''Extremely verbose output messages will be printed.''' + +VERBOSE = VERBOSE_NONE +'''Global verbosity level. VERBOSE_NONE is the default.''' + + +def set_verbosity(value): + '''Set the logging verbosity level.''' + global VERBOSE + VERBOSE = int(value) + + +def dbg(*args, level=VERBOSE_NORMAL): + '''Print a verbose debug logging message. + + The message is only printed if level is at least the current + verbosity level.''' + if level > VERBOSE: + return + print(*args) + + +def inf(*args): + '''Print an informational message.''' + print(*args) + + +def wrn(*args): + '''Print a warning.''' + print('warning:', end=' ', file=sys.stderr, flush=False) + print(*args, file=sys.stderr) + + +def err(*args, fatal=False): + '''Print an error.''' + if fatal: + print('fatal', end=' ', file=sys.stderr, flush=False) + print('error:', end=' ', file=sys.stderr, flush=False) + print(*args, file=sys.stderr) + + +def die(*args, exit_code=1): + '''Print a fatal error, and abort with the given exit code.''' + print('fatal error:', end=' ', file=sys.stderr, flush=False) + print(*args, file=sys.stderr) + sys.exit(exit_code) diff --git a/scripts/meta/west/main.py b/scripts/meta/west/main.py new file mode 100644 index 00000000000..d29b7092070 --- /dev/null +++ b/scripts/meta/west/main.py @@ -0,0 +1,109 @@ +# Copyright 2018 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Zephyr RTOS meta-tool (west) main module +''' + + +import argparse +from functools import partial +import os +import sys +from subprocess import CalledProcessError + +from . import log +from .cmd import CommandContextError +from .util import quote_sh_list + + +COMMANDS = () +'''Supported top-level commands.''' + + +class InvalidWestContext(RuntimeError): + pass + + +def command_handler(command, known_args, unknown_args): + command.run(known_args, unknown_args) + + +def validate_context(args, unknown): + '''Validate the run-time context expected by west.''' + if args.zephyr_base: + os.environ['ZEPHYR_BASE'] = args.zephyr_base + else: + if 'ZEPHYR_BASE' not in os.environ: + raise InvalidWestContext( + '--zephyr-base missing and no ZEPHYR_BASE ' + 'in the environment') + else: + args.zephyr_base = os.environ['ZEPHYR_BASE'] + + +def parse_args(argv): + west_parser = argparse.ArgumentParser( + prog='west', description='The Zephyr RTOS meta-tool.', + epilog='Run "west -h" for help on each command.') + west_parser.add_argument('-z', '--zephyr-base', default=None, + help='''Path to the Zephyr base directory. If not + given, ZEPHYR_BASE must be defined in the + environment, and will be used instead.''') + west_parser.add_argument('-v', '--verbose', default=0, action='count', + help='''Display verbose output. May be given + multiple times to increase verbosity.''') + subparser_gen = west_parser.add_subparsers(title='commands', + dest='command') + + for command in COMMANDS: + parser = command.add_parser(subparser_gen) + parser.set_defaults(handler=partial(command_handler, command)) + + args, unknown = west_parser.parse_known_args(args=argv) + + # Set up logging verbosity before doing anything else, so + # e.g. verbose messages related to argument handling errors + # work properly. + log.set_verbosity(args.verbose) + + try: + validate_context(args, unknown) + except InvalidWestContext as iwc: + log.err(*iwc.args, fatal=True) + west_parser.print_usage(file=sys.stderr) + sys.exit(1) + + if 'handler' not in args: + log.err('you must specify a command', fatal=True) + west_parser.print_usage(file=sys.stderr) + sys.exit(1) + + return args, unknown + + +def main(argv): + args, unknown = parse_args(argv) + + for_stack_trace = 'run as "west -v ... {} ..." for a stack trace'.format( + args.command) + try: + args.handler(args, unknown) + except KeyboardInterrupt: + sys.exit(0) + except CalledProcessError as cpe: + log.err('command exited with status {}: {}'.format( + cpe.args[0], quote_sh_list(cpe.args[1]))) + if args.verbose: + raise + else: + log.inf(for_stack_trace) + except CommandContextError as cce: + log.die('command', args.command, 'cannot be run in this context:', + *cce.args) + except Exception as exc: + log.err(*exc.args, fatal=True) + if args.verbose: + raise + else: + log.inf(for_stack_trace) diff --git a/scripts/meta/west/util.py b/scripts/meta/west/util.py new file mode 100644 index 00000000000..a9f8180d1e9 --- /dev/null +++ b/scripts/meta/west/util.py @@ -0,0 +1,22 @@ +# Copyright 2018 Open Source Foundries Limited. +# +# SPDX-License-Identifier: Apache-2.0 + +'''Miscellaneous utilities used by west +''' + +import shlex +import textwrap + + +def quote_sh_list(cmd): + '''Transform a command from list into shell string form.''' + fmt = ' '.join('{}' for _ in cmd) + args = [shlex.quote(s) for s in cmd] + return fmt.format(*args) + + +def wrap(text, indent): + '''Convenience routine for wrapping text to a consistent indent.''' + return textwrap.wrap(text, initial_indent=indent, + subsequent_indent=indent) diff --git a/scripts/west b/scripts/west new file mode 100755 index 00000000000..964699a0036 --- /dev/null +++ b/scripts/west @@ -0,0 +1,5 @@ +#!/bin/sh + +# UNIX operating system entry point to the west tool. +export "PYTHONPATH=${PYTHONPATH:+${PYTHONPATH}:}$ZEPHYR_BASE/scripts/meta" +python3 -m west $@ diff --git a/scripts/west-win.py b/scripts/west-win.py new file mode 100644 index 00000000000..4f0adc397f9 --- /dev/null +++ b/scripts/west-win.py @@ -0,0 +1,11 @@ +# Windows-specific launcher alias for west (west wind?). + +import os +import sys + +zephyr_base = os.environ['ZEPHYR_BASE'] +sys.path.append(os.path.join(zephyr_base, 'scripts', 'meta')) + +from west.main import main # noqa E402 (silence flake8 warning) + +main(sys.argv[1:]) diff --git a/zephyr-env.cmd b/zephyr-env.cmd index 6c79799e88d..89e6e44bf67 100644 --- a/zephyr-env.cmd +++ b/zephyr-env.cmd @@ -4,3 +4,6 @@ set ZEPHYR_BASE=%~dp0 if exist "%userprofile%\zephyrrc.cmd" ( call "%userprofile%\zephyrrc.cmd" ) + +rem Zephyr meta-tool (west) launcher alias +doskey west=py -3 %ZEPHYR_BASE%\scripts\west-win.py $*