808 lines
33 KiB
Python
Executable File
808 lines
33 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Copyright 2018 Open Source Foundries Limited.
|
|
# Copyright 2019 Foundries.io Limited.
|
|
# Copyright (c) 2019, Nordic Semiconductor ASA
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
'''Zephyr RTOS meta-tool (west) main module
|
|
|
|
Nothing in here is public API.
|
|
'''
|
|
|
|
import argparse
|
|
from collections import OrderedDict
|
|
import colorama
|
|
from io import StringIO
|
|
import logging
|
|
import os
|
|
from pathlib import Path, PurePath
|
|
import shutil
|
|
import sys
|
|
from subprocess import CalledProcessError
|
|
import tempfile
|
|
import textwrap
|
|
import traceback
|
|
|
|
from west import log
|
|
from west import configuration as config
|
|
from west.commands import WestCommand, extension_commands, \
|
|
CommandError, ExtensionCommandError
|
|
from west.app.project import List, ManifestCommand, Diff, Status, \
|
|
SelfUpdate, ForAll, Init, Update, Topdir
|
|
from west.app.config import Config
|
|
from west.manifest import Manifest, MalformedConfig, MalformedManifest, \
|
|
ManifestVersionError, ManifestImportFailed, _ManifestImportDepth, \
|
|
ManifestProject, MANIFEST_REV_BRANCH
|
|
from west.util import quote_sh_list, west_topdir, WestNotFound
|
|
from west.version import __version__
|
|
|
|
class WestApp:
|
|
# The west 'application' object.
|
|
#
|
|
# There's enough state to keep track of when building the final
|
|
# WestCommand we want to run that it's convenient to have an
|
|
# object to stash it all in.
|
|
#
|
|
# We could use globals, but that would make it harder to white-box
|
|
# test multiple main() invocations from the same Python process,
|
|
# which is a goal. See #149.
|
|
|
|
def __init__(self):
|
|
self.topdir = None # west_topdir()
|
|
self.manifest = None # west.manifest.Manifest
|
|
self.mle = None # saved exception if load_manifest() fails
|
|
self.builtins = {} # command name -> WestCommand instance
|
|
self.extensions = {} # extension command name -> spec
|
|
self.builtin_groups = OrderedDict() # group name -> WestCommand list
|
|
self.extension_groups = OrderedDict() # project path -> ext spec list
|
|
self.west_parser = None # a WestArgumentParser
|
|
self.subparser_gen = None # an add_subparsers() return value
|
|
|
|
for group, classes in BUILTIN_COMMAND_GROUPS.items():
|
|
lst = [cls() for cls in classes]
|
|
self.builtins.update({command.name: command for command in lst})
|
|
self.builtin_groups[group] = lst
|
|
|
|
# Give the help instance a back-pointer up here.
|
|
#
|
|
# A dirty layering violation, but it does need this data:
|
|
#
|
|
# - 'west help <command>' needs to call into <command>'s
|
|
# parser's print_help()
|
|
# - 'west help' needs self.west_parser, which
|
|
# the argparse API does not give us a future-proof way
|
|
# to access from the Help object's parser attribute,
|
|
# which comes from subparser_gen.
|
|
self.builtins['help'].app = self
|
|
|
|
def run(self, argv):
|
|
# Run the command-line application with argument list 'argv'.
|
|
|
|
# See if we're in a workspace. It's fine if we're not.
|
|
# Note that this falls back on searching from ZEPHYR_BASE
|
|
# if the current directory isn't inside a west workspace.
|
|
try:
|
|
self.topdir = west_topdir()
|
|
except WestNotFound:
|
|
pass
|
|
|
|
# Read the configuration files. We need this to get
|
|
# manifest.path to parse the manifest, etc.
|
|
#
|
|
# TODO: re-work to avoid global state (#149).
|
|
config.read_config(topdir=self.topdir)
|
|
|
|
# Set self.manifest and self.extensions.
|
|
self.load_manifest()
|
|
self.load_extension_specs()
|
|
|
|
# Set up initial argument parsers. This requires knowing
|
|
# self.extensions, so it can't happen before now.
|
|
self.setup_parsers()
|
|
|
|
# OK, we are all set. Run the command.
|
|
self.run_command(argv)
|
|
|
|
def load_manifest(self):
|
|
# Try to parse the manifest. We'll save it if that works, so
|
|
# it doesn't have to be re-parsed.
|
|
|
|
if not self.topdir:
|
|
return
|
|
|
|
try:
|
|
self.manifest = Manifest.from_file(topdir=self.topdir)
|
|
except (ManifestVersionError, MalformedManifest, MalformedConfig,
|
|
FileNotFoundError, ManifestImportFailed) as e:
|
|
# Defer exception handling to WestCommand.run(), which uses
|
|
# handle_builtin_manifest_load_err() to decide what to do.
|
|
#
|
|
# Make sure to update that function if you change the
|
|
# exceptions caught here. Unexpected exceptions should
|
|
# propagate up and fail fast.
|
|
#
|
|
# This might be OK, e.g. if we're running 'west config
|
|
# manifest.path foo' to fix the MalformedConfig error, but
|
|
# there's no way to know until we've parsed the command
|
|
# line arguments.
|
|
if isinstance(e, _ManifestImportDepth):
|
|
log.wrn('recursion depth exceeded during manifest resolution; '
|
|
'your manifest likely contains an import loop. '
|
|
'Run "west -v manifest --resolve" to debug.')
|
|
self.mle = e
|
|
|
|
def handle_builtin_manifest_load_err(self, args):
|
|
# Deferred handling for expected load_manifest() exceptions.
|
|
# Called before attempting to run a built-in command. (No
|
|
# extension commands can be run, because we learn about them
|
|
# from the manifest itself, which we have failed to load.)
|
|
|
|
# A few commands are always safe to run without a manifest.
|
|
# The update command is sometimes safe and sometimes not, but
|
|
# we need to include it in this list because it's the only way
|
|
# to fix a manifest-rev revision in a project which is being
|
|
# imported to point from a bogus manifest to a non-bogus one.
|
|
no_manifest_ok = ['help', 'config', 'topdir', 'init', 'manifest',
|
|
'update']
|
|
|
|
# Handle ManifestVersionError is a special case.
|
|
if isinstance(self.mle, ManifestVersionError):
|
|
if args.command == 'help':
|
|
log.wrn(mve_msg(self.mle, suggest_upgrade=False) +
|
|
'\n Cannot get extension command help, ' +
|
|
"and most commands won't run." +
|
|
'\n To silence this warning, upgrade west.')
|
|
return
|
|
elif args.command in ['config', 'topdir']:
|
|
# config and topdir are safe to run, but let's
|
|
# warn the user that most other commands won't be.
|
|
log.wrn(mve_msg(self.mle, suggest_upgrade=False) +
|
|
"\n This should work, but most commands won't." +
|
|
'\n To silence this warning, upgrade west.')
|
|
return
|
|
elif args.command == 'init':
|
|
# init is fine to run -- it will print its own error,
|
|
# with context about where the workspace was found,
|
|
# and what the user's choices are.
|
|
return
|
|
else:
|
|
assert args.command not in no_manifest_ok
|
|
log.die(mve_msg(self.mle))
|
|
|
|
# Other errors generally just fall back on no_manifest_ok.
|
|
def isinst(*args):
|
|
return any(isinstance(self.mle, t) for t in args)
|
|
|
|
if args.command not in no_manifest_ok:
|
|
if isinst(MalformedManifest, MalformedConfig):
|
|
log.die('\n '.join(["can't load west manifest"] +
|
|
list(self.mle.args)))
|
|
elif isinst(FileNotFoundError):
|
|
# This should ordinarily only happen when the top
|
|
# level manifest is not found.
|
|
log.die(f"file not found: {self.mle.filename}")
|
|
elif isinst(_ManifestImportDepth):
|
|
log.die('failed, likely due to manifest import loop')
|
|
elif isinst(ManifestImportFailed):
|
|
if args.command == 'update':
|
|
return # that's fine
|
|
|
|
p, f = self.mle.project, self.mle.filename
|
|
ctxt = f' Missing file: "{f}"'
|
|
if not isinstance(p, ManifestProject):
|
|
# Try to be more helpful by explaining exactly
|
|
# what west.manifest needs to happen before we can
|
|
# resolve the missing import.
|
|
rev = p.revision
|
|
|
|
ctxt += f' from revision "{rev}"\n'
|
|
ctxt += ' Hint: for this to work:\n'
|
|
ctxt += f' - {p.name} must be cloned\n'
|
|
ctxt += (f' - its {MANIFEST_REV_BRANCH} ref '
|
|
'must point to a commit with the missing file\n')
|
|
ctxt += ' To fix, run:\n'
|
|
ctxt += ' west update'
|
|
|
|
log.die(f'failed manifest import in {p.name_and_path}\n' +
|
|
ctxt)
|
|
else:
|
|
log.die('internal error:',
|
|
f'unhandled manifest load exception: {self.mle}')
|
|
|
|
def load_extension_specs(self):
|
|
if self.manifest is None:
|
|
# "None" means "extensions could not be determined".
|
|
# Leaving this an empty dict would mean "there are no
|
|
# extensions", which is different.
|
|
self.extensions = None
|
|
return
|
|
|
|
path_specs = extension_commands(manifest=self.manifest)
|
|
extension_names = set()
|
|
|
|
for path, specs in path_specs.items():
|
|
# Filter out attempts to shadow built-in commands as well as
|
|
# command names which are already used.
|
|
|
|
filtered = []
|
|
for spec in specs:
|
|
if spec.name in self.builtins:
|
|
log.wrn(f'ignoring project {spec.project.name} '
|
|
f'extension command "{spec.name}"; '
|
|
'this is a built in command')
|
|
continue
|
|
if spec.name in extension_names:
|
|
log.wrn(f'ignoring project {spec.project.name} '
|
|
f'extension command "{spec.name}"; '
|
|
f'command "{spec.name}" is '
|
|
'already defined as extension command')
|
|
continue
|
|
|
|
filtered.append(spec)
|
|
extension_names.add(spec.name)
|
|
self.extensions[spec.name] = spec
|
|
|
|
self.extension_groups[path] = filtered
|
|
|
|
def setup_parsers(self):
|
|
# Set up and install command-line argument parsers.
|
|
|
|
west_parser, subparser_gen = self.make_parsers()
|
|
|
|
# Add sub-parsers for the built-in commands.
|
|
for command in self.builtins.values():
|
|
command.add_parser(subparser_gen)
|
|
|
|
# Add stub parsers for extensions.
|
|
#
|
|
# These just reserve the names of each extension. The real parser
|
|
# for each extension can't be added until we import the
|
|
# extension's code, which we won't do unless parse_known_args()
|
|
# says to run that extension.
|
|
if self.extensions:
|
|
for path, specs in self.extension_groups.items():
|
|
for spec in specs:
|
|
subparser_gen.add_parser(spec.name, add_help=False)
|
|
|
|
# Save the instance state.
|
|
self.west_parser = west_parser
|
|
self.subparser_gen = subparser_gen
|
|
|
|
def make_parsers(self):
|
|
# Make a fresh instance of the top level argument parser
|
|
# and subparser generator, and return them in that order.
|
|
|
|
# The prog='west' override avoids the absolute path of the
|
|
# main.py script showing up when West is run via the wrapper
|
|
parser = WestArgumentParser(
|
|
prog='west', description='The Zephyr RTOS meta-tool.',
|
|
epilog='''Run "west help <command>" for help on each <command>.''',
|
|
add_help=False, west_app=self)
|
|
|
|
# Remember to update zephyr's west-completion.bash if you add or
|
|
# remove flags. This is currently the only place where shell
|
|
# completion is available.
|
|
|
|
parser.add_argument('-h', '--help', action=WestHelpAction, nargs=0,
|
|
help='get help for west or a command')
|
|
|
|
parser.add_argument('-z', '--zephyr-base', default=None,
|
|
help='''Override the Zephyr base directory. The
|
|
default is the manifest project with path
|
|
"zephyr".''')
|
|
|
|
parser.add_argument('-v', '--verbose', default=0, action='count',
|
|
help='''Display verbose output. May be given
|
|
multiple times to increase verbosity.''')
|
|
|
|
parser.add_argument('-V', '--version', action='version',
|
|
version=f'West version: v{__version__}',
|
|
help='print the program version and exit')
|
|
|
|
subparser_gen = parser.add_subparsers(metavar='<command>',
|
|
dest='command')
|
|
|
|
return parser, subparser_gen
|
|
|
|
def run_command(self, argv):
|
|
# Parse command line arguments and run the WestCommand.
|
|
# If we're running an extension, instantiate it from its
|
|
# spec and re-parse arguments before running.
|
|
|
|
args, unknown = self.west_parser.parse_known_args(args=argv)
|
|
|
|
# Set up logging verbosity before running the command, so e.g.
|
|
# verbose messages related to argument handling errors work
|
|
# properly.
|
|
log.set_verbosity(args.verbose)
|
|
log.dbg('args namespace:', args, level=log.VERBOSE_EXTREME)
|
|
|
|
# If we were run as 'west -h ...' or 'west --help ...',
|
|
# monkeypatch the args namespace so we end up running Help. The
|
|
# user might have also provided a command. If so, print help about
|
|
# that command.
|
|
if args.help or args.command is None:
|
|
args.command_name = args.command
|
|
args.command = 'help'
|
|
|
|
# Finally, run the command.
|
|
try:
|
|
if args.command in self.builtins:
|
|
if self.mle:
|
|
self.handle_builtin_manifest_load_err(args)
|
|
|
|
cmd = self.builtins.get(args.command, self.builtins['help'])
|
|
cmd.run(args, unknown, self.topdir, manifest=self.manifest)
|
|
else:
|
|
self.run_extension(args.command, argv)
|
|
except KeyboardInterrupt:
|
|
sys.exit(0)
|
|
except BrokenPipeError:
|
|
sys.exit(0)
|
|
except CalledProcessError as cpe:
|
|
log.err(f'command exited with status {cpe.returncode}: '
|
|
f'{quote_sh_list(cpe.cmd)}', fatal=True)
|
|
if args.verbose >= log.VERBOSE_EXTREME:
|
|
log.banner('Traceback (enabled by -vvv):')
|
|
traceback.print_exc()
|
|
sys.exit(cpe.returncode)
|
|
except ExtensionCommandError as ece:
|
|
msg = f"extension command \"{args.command}\" couldn't be run"
|
|
if ece.hint:
|
|
msg += '\n Hint: ' + ece.hint
|
|
|
|
if args.verbose >= log.VERBOSE_EXTREME:
|
|
log.err(msg, fatal=True)
|
|
log.banner('Traceback (enabled by -vvv):')
|
|
traceback.print_exc()
|
|
else:
|
|
tb_file = dump_traceback()
|
|
msg += f'\n See {tb_file} for a traceback.'
|
|
log.err(msg, fatal=True)
|
|
sys.exit(ece.returncode)
|
|
except CommandError as ce:
|
|
# No need to dump_traceback() here. The command is responsible
|
|
# for logging its own errors.
|
|
sys.exit(ce.returncode)
|
|
except MalformedManifest as mm:
|
|
# We can get here because 'west update' is allowed to run
|
|
# even when an invalid manifest was detected, as a way to
|
|
# try to fix a previous update that left 'manifest-rev'
|
|
# branches pointing at revisions with invalid manifest
|
|
# data in projects that get imported.
|
|
log.die('\n '.join(str(arg) for arg in mm.args))
|
|
|
|
def run_extension(self, name, argv):
|
|
# Check a program invariant. We should never get here
|
|
# unless we were able to parse the manifest. That's where
|
|
# information about extensions is loaded from.
|
|
assert self.manifest is not None and self.mle is None, \
|
|
f'internal error: running extension "{name}" ' \
|
|
f'but got {self.mle}'
|
|
|
|
command = self.extensions[name].factory()
|
|
|
|
# Our original top level parser and subparser generator have some
|
|
# garbage state that prevents us from registering the 'real'
|
|
# command subparser. Just make new ones.
|
|
west_parser, subparser_gen = self.make_parsers()
|
|
command.add_parser(subparser_gen)
|
|
|
|
# Parse arguments again.
|
|
args, unknown = west_parser.parse_known_args(argv)
|
|
|
|
# HACK: try to set ZEPHYR_BASE.
|
|
#
|
|
# Currently required by zephyr extensions like "west build".
|
|
#
|
|
# TODO: get rid of this. Instead:
|
|
#
|
|
# - support a WEST_DIR environment variable to specify the
|
|
# workspace if we're not running under a .west directory
|
|
# (controversial)
|
|
# - make zephyr extensions that need ZEPHYR_BASE just set it
|
|
# themselves (easy if above is OK, unnecessary if it isn't)
|
|
set_zephyr_base(args, self.manifest, self.topdir)
|
|
|
|
command.run(args, unknown, self.topdir, manifest=self.manifest)
|
|
|
|
class Help(WestCommand):
|
|
# west help <command> implementation.
|
|
|
|
def __init__(self):
|
|
super().__init__('help', 'get help for west or a command',
|
|
textwrap.dedent('''\
|
|
With an argument, prints help for that command.
|
|
Without one, prints top-level help for west.'''),
|
|
requires_workspace=False)
|
|
|
|
def do_add_parser(self, parser_adder):
|
|
parser = parser_adder.add_parser(
|
|
self.name, help=self.help, description=self.description,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument('command_name', nargs='?', default=None,
|
|
help='name of command to get help for')
|
|
return parser
|
|
|
|
def do_run(self, args, ignored):
|
|
assert self.app, "Help has no WestApp and can't do its job"
|
|
app = self.app
|
|
name = args.command_name
|
|
|
|
if not name:
|
|
app.west_parser.print_help(top_level=True)
|
|
elif name == 'help':
|
|
self.parser.print_help()
|
|
elif name in app.builtins:
|
|
app.builtins[name].parser.print_help()
|
|
elif app.extensions is not None and name in app.extensions:
|
|
# It's fine that we don't handle any errors here. The
|
|
# exception handling block in app.run_command is in a
|
|
# parent stack frame.
|
|
app.run_extension(name, [name, '--help'])
|
|
else:
|
|
log.wrn(f'unknown command "{name}"')
|
|
app.west_parser.print_help(top_level=True)
|
|
if app.mle:
|
|
log.wrn('your manifest could not be loaded, '
|
|
'which may be causing this issue.\n'
|
|
' Try running "west update" or fixing the manifest.')
|
|
|
|
class WestHelpAction(argparse.Action):
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
# Just mark that help was requested.
|
|
namespace.help = True
|
|
|
|
class WestArgumentParser(argparse.ArgumentParser):
|
|
# The argparse module is infuriatingly coy about its parser and
|
|
# help formatting APIs, marking almost everything you need to
|
|
# customize help output an "implementation detail". Even accessing
|
|
# the parser's description and epilog attributes as we do here is
|
|
# technically breaking the rules.
|
|
#
|
|
# Even though the implementation details have been pretty stable
|
|
# since the module was first introduced in Python 3.2, let's avoid
|
|
# possible headaches by overriding some "proper" argparse APIs
|
|
# here instead of monkey-patching the module or breaking
|
|
# abstraction barriers. This is duplicative but more future-proof.
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
# The super constructor calls add_argument(), so this has to
|
|
# come first as our override of that method relies on it.
|
|
self.west_optionals = []
|
|
self.west_app = kwargs.pop('west_app', None)
|
|
super(WestArgumentParser, self).__init__(*args, **kwargs)
|
|
|
|
def print_help(self, file=None, top_level=False):
|
|
print(self.format_help(top_level=top_level), end='',
|
|
file=file or sys.stdout)
|
|
|
|
def format_help(self, top_level=False):
|
|
# When top_level is True, we override the parent method to
|
|
# produce more readable output, which separates commands into
|
|
# logical groups. In order to print optionals, we rely on the
|
|
# data available in our add_argument() override below.
|
|
#
|
|
# If top_level is False, it's because we're being called from
|
|
# one of the subcommand parsers, and we delegate to super.
|
|
|
|
if not top_level:
|
|
return super(WestArgumentParser, self).format_help()
|
|
|
|
# Format the help to be at most 75 columns wide, the maximum
|
|
# generally recommended by typographers for readability.
|
|
#
|
|
# If the terminal width (COLUMNS) is less than 75, use width
|
|
# (COLUMNS - 2) instead, unless that is less than 30 columns
|
|
# wide, which we treat as a hard minimum.
|
|
width = min(75, max(shutil.get_terminal_size().columns - 2, 30))
|
|
|
|
with StringIO() as sio:
|
|
|
|
def append(*strings):
|
|
for s in strings:
|
|
print(s, file=sio)
|
|
|
|
append(self.format_usage(),
|
|
self.description,
|
|
'')
|
|
|
|
append('optional arguments:')
|
|
for wo in self.west_optionals:
|
|
self.format_west_optional(append, wo, width)
|
|
|
|
append('')
|
|
for group, commands in self.west_app.builtin_groups.items():
|
|
if group is None:
|
|
# Skip hidden commands.
|
|
continue
|
|
|
|
append(group + ':')
|
|
for command in commands:
|
|
self.format_command(append, command, width)
|
|
append('')
|
|
|
|
if self.west_app.extensions is None:
|
|
if not self.west_app.mle:
|
|
# This only happens when there is an error.
|
|
# If there are simply no extensions, it's an empty dict.
|
|
# If the user has already been warned about the error
|
|
# because it's due to a ManifestVersionError, don't
|
|
# warn them again.
|
|
append('Cannot load extension commands; '
|
|
'help for them is not available.')
|
|
append('(To debug, try: "west manifest --validate".)')
|
|
append('')
|
|
else:
|
|
# TODO we may want to be more aggressive about loading
|
|
# command modules by default: the current implementation
|
|
# prevents us from formatting one-line help here.
|
|
#
|
|
# Perhaps a commands.extension_paranoid that if set, uses
|
|
# thunks, and otherwise just loads the modules and
|
|
# provides help for each command.
|
|
#
|
|
# This has its own wrinkle: we can't let a failed
|
|
# import break the built-in commands.
|
|
for path, specs in self.west_app.extension_groups.items():
|
|
# This may occur in case a project defines commands already
|
|
# defined, in which case it has been filtered out.
|
|
if not specs:
|
|
continue
|
|
|
|
project = specs[0].project # they're all from this project
|
|
append('extension commands from project '
|
|
f'{project.name} (path: {project.path}):')
|
|
|
|
for spec in specs:
|
|
self.format_extension_spec(append, spec, width)
|
|
append('')
|
|
|
|
if self.epilog:
|
|
append(self.epilog)
|
|
|
|
return sio.getvalue()
|
|
|
|
def format_west_optional(self, append, wo, width):
|
|
metavar = wo['metavar']
|
|
options = wo['options']
|
|
help = wo.get('help')
|
|
|
|
# Join the various options together as a comma-separated list,
|
|
# with the metavar if there is one. That's our "thing".
|
|
if metavar is not None:
|
|
opt_str = ' ' + ', '.join(f'{o} {metavar}' for o in options)
|
|
else:
|
|
opt_str = ' ' + ', '.join(options)
|
|
|
|
# Delegate to the generic formatter.
|
|
self.format_thing_and_help(append, opt_str, help, width)
|
|
|
|
def format_command(self, append, command, width):
|
|
thing = f' {command.name}:'
|
|
self.format_thing_and_help(append, thing, command.help, width)
|
|
|
|
def format_extension_spec(self, append, spec, width):
|
|
self.format_thing_and_help(append, ' ' + spec.name + ':',
|
|
spec.help, width)
|
|
|
|
def format_thing_and_help(self, append, thing, help, width):
|
|
# Format help for some "thing" (arbitrary text) and its
|
|
# corresponding help text an argparse-like way.
|
|
help_offset = min(max(10, width - 20), 24)
|
|
help_indent = ' ' * help_offset
|
|
|
|
thinglen = len(thing)
|
|
|
|
if help is None:
|
|
# If there's no help string, just print the thing.
|
|
append(thing)
|
|
else:
|
|
# Reflow the lines in help to the desired with, using
|
|
# the help_offset as an initial indent.
|
|
help = ' '.join(help.split())
|
|
help_lines = textwrap.wrap(help, width=width,
|
|
initial_indent=help_indent,
|
|
subsequent_indent=help_indent)
|
|
|
|
if thinglen > help_offset - 1:
|
|
# If the "thing" (plus room for a space) is longer
|
|
# than the initial help offset, print it on its own
|
|
# line, followed by the help on subsequent lines.
|
|
append(thing)
|
|
append(*help_lines)
|
|
else:
|
|
# The "thing" is short enough that we can start
|
|
# printing help on the same line without overflowing
|
|
# the help offset, so combine the "thing" with the
|
|
# first line of help.
|
|
help_lines[0] = thing + help_lines[0][thinglen:]
|
|
append(*help_lines)
|
|
|
|
def add_argument(self, *args, **kwargs):
|
|
# Track information we want for formatting help. The argparse
|
|
# module calls kwargs.pop(), so can't call super first without
|
|
# losing data.
|
|
optional = {'options': [], 'metavar': kwargs.get('metavar', None)}
|
|
need_metavar = (optional['metavar'] is None and
|
|
kwargs.get('action') in (None, 'store'))
|
|
for arg in args:
|
|
if not arg.startswith('-'):
|
|
break
|
|
optional['options'].append(arg)
|
|
# If no metavar was given, the last option name is
|
|
# used. By convention, long options go last, so this
|
|
# matches the default argparse behavior.
|
|
if need_metavar:
|
|
optional['metavar'] = arg.lstrip('-').translate(
|
|
{ord('-'): '_'}).upper()
|
|
optional['help'] = kwargs.get('help')
|
|
self.west_optionals.append(optional)
|
|
|
|
# Let argparse handle the actual argument.
|
|
super().add_argument(*args, **kwargs)
|
|
|
|
def error(self, message):
|
|
if self.west_app and self.west_app.mle and \
|
|
isinstance(self.west_app.mle, ManifestVersionError):
|
|
log.die(mve_msg(self.west_app.mle))
|
|
super().error(message=message)
|
|
|
|
def mve_msg(mve, suggest_upgrade=True):
|
|
return '\n '.join(
|
|
[f'west v{mve.version} or later is required by the manifest',
|
|
f'West version: v{__version__}'] +
|
|
([f'Manifest file: {mve.file}'] if mve.file else []) +
|
|
(['Please upgrade west and retry.'] if suggest_upgrade else []))
|
|
|
|
def set_zephyr_base(args, manifest, topdir):
|
|
'''Ensure ZEPHYR_BASE is set
|
|
Order of precedence:
|
|
1) Value given as command line argument
|
|
2) Value from environment setting: ZEPHYR_BASE
|
|
3) Value of zephyr.base setting in west config file
|
|
4) Project in the manifest with name, or path, "zephyr" (will
|
|
be persisted as zephyr.base in the local config if found)
|
|
|
|
Order of precedence between 2) and 3) can be changed with the setting
|
|
zephyr.base-prefer.
|
|
zephyr.base-prefer takes the values 'env' and 'configfile'
|
|
|
|
If 2) and 3) have different values and zephyr.base-prefer is unset,
|
|
a warning is printed.'''
|
|
|
|
if args.zephyr_base:
|
|
# The command line --zephyr-base takes precedence over
|
|
# everything else.
|
|
zb = os.path.abspath(args.zephyr_base)
|
|
zb_origin = 'command line'
|
|
else:
|
|
# If the user doesn't specify it concretely, then use ZEPHYR_BASE
|
|
# from the environment or zephyr.base from west.configuration.
|
|
#
|
|
# (We will configure zephyr.base to the project that has path
|
|
# 'zephyr' as a last resort here.)
|
|
#
|
|
# At some point, we need a more flexible way to set environment
|
|
# variables based on manifest contents, but this is good enough
|
|
# to get started with and to ask for wider testing.
|
|
zb_env = os.environ.get('ZEPHYR_BASE')
|
|
zb_prefer = config.config.get('zephyr', 'base-prefer',
|
|
fallback=None)
|
|
rel_zb_config = config.config.get('zephyr', 'base', fallback=None)
|
|
if rel_zb_config is None:
|
|
projects = None
|
|
try:
|
|
projects = manifest.get_projects(['zephyr'])
|
|
except ValueError:
|
|
pass
|
|
if projects:
|
|
zephyr = projects[0]
|
|
config.update_config('zephyr', 'base', zephyr.path)
|
|
rel_zb_config = zephyr.path
|
|
if rel_zb_config is not None:
|
|
zb_config = Path(topdir) / rel_zb_config
|
|
else:
|
|
zb_config = None
|
|
|
|
if zb_prefer == 'env' and zb_env is not None:
|
|
zb = zb_env
|
|
zb_origin = 'env'
|
|
elif zb_prefer == 'configfile' and zb_config is not None:
|
|
zb = str(zb_config)
|
|
zb_origin = 'configfile'
|
|
elif zb_env is not None:
|
|
zb = zb_env
|
|
zb_origin = 'env'
|
|
try:
|
|
different = (zb_config and not zb_config.samefile(zb_env))
|
|
except FileNotFoundError:
|
|
different = (zb_config and
|
|
(PurePath(zb_config)) != PurePath(zb_env))
|
|
if different:
|
|
# The environment ZEPHYR_BASE takes precedence over the config
|
|
# setting, but is different than the zephyr.base config value.
|
|
#
|
|
# Therefore, issue a warning as the user might have
|
|
# run zephyr-env.sh/cmd in some other zephyr
|
|
# workspace and forgotten about it.
|
|
log.wrn(f'ZEPHYR_BASE={zb_env} '
|
|
f'in the calling environment will be used,\n'
|
|
f'but the zephyr.base config option in {topdir} '
|
|
f'is "{rel_zb_config}"\n'
|
|
f'which implies a different ZEPHYR_BASE={zb_config}\n'
|
|
f'To disable this warning in the future, execute '
|
|
f"'west config --global zephyr.base-prefer env'")
|
|
elif zb_config:
|
|
zb = str(zb_config)
|
|
zb_origin = 'configfile'
|
|
else:
|
|
zb = None
|
|
zb_origin = None
|
|
# No --zephyr-base, no ZEPHYR_BASE, and no zephyr.base.
|
|
log.wrn("can't find the zephyr repository\n"
|
|
' - no --zephyr-base given\n'
|
|
' - ZEPHYR_BASE is unset\n'
|
|
' - west config contains no zephyr.base setting\n'
|
|
' - no manifest project has name or path "zephyr"\n'
|
|
'\n'
|
|
" If this isn't a Zephyr workspace, you can "
|
|
" silence this warning with something like this:\n"
|
|
' west config zephyr.base not-using-zephyr')
|
|
|
|
if zb is not None:
|
|
os.environ['ZEPHYR_BASE'] = zb
|
|
log.dbg(f'ZEPHYR_BASE={zb} (origin: {zb_origin})')
|
|
|
|
def dump_traceback():
|
|
# Save the current exception to a file and return its path.
|
|
fd, name = tempfile.mkstemp(prefix='west-exc-', suffix='.txt')
|
|
os.close(fd) # traceback has no use for the fd
|
|
with open(name, 'w') as f:
|
|
traceback.print_exc(file=f)
|
|
return name
|
|
|
|
def main(argv=None):
|
|
# Silence validation errors from pykwalify, which are logged at
|
|
# logging.ERROR level. We want to handle those ourselves as
|
|
# needed.
|
|
logging.getLogger('pykwalify').setLevel(logging.CRITICAL)
|
|
|
|
# Makes ANSI color escapes work on Windows, and strips them when
|
|
# stdout/stderr isn't a terminal
|
|
colorama.init()
|
|
|
|
# Create the WestApp instance and let it run.
|
|
app = WestApp()
|
|
app.run(argv or sys.argv[1:])
|
|
|
|
# If you add a command here, make sure to think about how it should be
|
|
# handled in case of ManifestVersionError or other reason the manifest
|
|
# might fail to load (import error, configuration file error, etc.)
|
|
BUILTIN_COMMAND_GROUPS = {
|
|
'built-in commands for managing git repositories': [
|
|
Init,
|
|
Update,
|
|
List,
|
|
ManifestCommand,
|
|
Diff,
|
|
Status,
|
|
ForAll,
|
|
],
|
|
|
|
'other built-in commands': [
|
|
Help,
|
|
Config,
|
|
Topdir,
|
|
],
|
|
|
|
# None is for hidden commands we don't want to show to the user.
|
|
None: [SelfUpdate]
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
main()
|