440 lines
14 KiB
Python
440 lines
14 KiB
Python
# Copyright 2018 Open Source Foundries Limited.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
'''West's bootstrap/wrapper script.
|
|
'''
|
|
|
|
import argparse
|
|
import configparser
|
|
import os
|
|
import platform
|
|
import pykwalify.core
|
|
import subprocess
|
|
import sys
|
|
import yaml
|
|
|
|
import west._bootstrap.version as version
|
|
|
|
if sys.version_info < (3,):
|
|
sys.exit('fatal error: you are running Python 2')
|
|
|
|
|
|
#
|
|
# Special files and directories in the west installation.
|
|
#
|
|
# These are given variable names for clarity, but they can't be
|
|
# changed without propagating the changes into west itself.
|
|
#
|
|
|
|
# Top-level west directory, containing west itself and the manifest.
|
|
WEST_DIR = 'west'
|
|
# Subdirectory to check out the west source repository into.
|
|
WEST = 'west'
|
|
# Default west repository URL.
|
|
WEST_URL_DEFAULT = 'https://github.com/zephyrproject-rtos/west'
|
|
# Default revision to check out of the west repository.
|
|
WEST_REV_DEFAULT = 'master'
|
|
# File inside of WEST_DIR which marks it as the top level of the
|
|
# Zephyr project installation.
|
|
#
|
|
# (The WEST_DIR name is not distinct enough to use when searching for
|
|
# the top level; other directories named "west" may exist elsewhere,
|
|
# e.g. zephyr/doc/west.)
|
|
WEST_MARKER = '.west_topdir'
|
|
|
|
# Manifest repository directory under WEST_DIR.
|
|
MANIFEST = 'manifest'
|
|
# Default manifest repository URL.
|
|
MANIFEST_URL_DEFAULT = 'https://github.com/zephyrproject-rtos/manifest'
|
|
# Default revision to check out of the manifest repository.
|
|
MANIFEST_REV_DEFAULT = 'master'
|
|
|
|
_SCHEMA_PATH = os.path.join(os.path.dirname(__file__), "west-schema.yml")
|
|
|
|
#
|
|
# Helpers shared between init and wrapper mode
|
|
#
|
|
|
|
|
|
class WestError(RuntimeError):
|
|
pass
|
|
|
|
|
|
class WestNotFound(WestError):
|
|
'''Neither the current directory nor any parent has a West installation.'''
|
|
|
|
|
|
def west_dir(start=None):
|
|
'''
|
|
Returns the path to the west/ directory, searching ``start`` and its
|
|
parents.
|
|
|
|
Raises WestNotFound if no west directory is found.
|
|
'''
|
|
return os.path.join(west_topdir(start), WEST_DIR)
|
|
|
|
|
|
def manifest_dir(start=None):
|
|
'''
|
|
Returns the path to the manifest/ directory, searching ``start`` and its
|
|
parents.
|
|
|
|
Raises WestNotFound if no west directory is found.
|
|
'''
|
|
return os.path.join(west_topdir(start), MANIFEST)
|
|
|
|
|
|
def west_topdir(start=None):
|
|
'''
|
|
Like west_dir(), but returns the path to the parent directory of the west/
|
|
directory instead, where project repositories are stored
|
|
'''
|
|
# If you change this function, make sure to update west.util.west_topdir().
|
|
|
|
cur_dir = start or os.getcwd()
|
|
|
|
while True:
|
|
if os.path.isfile(os.path.join(cur_dir, WEST_DIR, WEST_MARKER)):
|
|
return cur_dir
|
|
|
|
parent_dir = os.path.dirname(cur_dir)
|
|
if cur_dir == parent_dir:
|
|
# At the root
|
|
raise WestNotFound('Could not find a West installation '
|
|
'in this or any parent directory')
|
|
cur_dir = parent_dir
|
|
|
|
|
|
def clone(desc, url, rev, dest):
|
|
if os.path.exists(dest):
|
|
raise WestError('refusing to clone into existing location ' + dest)
|
|
|
|
print('=== Cloning {} from {}, rev. {} ==='.format(desc, url, rev))
|
|
subprocess.check_call(('git', 'clone', '-b', rev, '--', url, dest))
|
|
|
|
|
|
#
|
|
# west init
|
|
#
|
|
|
|
|
|
def init(argv):
|
|
'''Command line handler for ``west init`` invocations.
|
|
|
|
This exits the program with a nonzero exit code if fatal errors occur.'''
|
|
|
|
# Remember to update scripts/west-completion.bash if you add or remove
|
|
# flags
|
|
|
|
init_parser = argparse.ArgumentParser(
|
|
prog='west init',
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
description='''
|
|
Initializes a Zephyr installation. Use "west clone" afterwards to fetch the
|
|
sources.
|
|
|
|
In more detail, does the following:
|
|
|
|
1. Clones the manifest repository to west/manifest, and the west repository
|
|
to west/west
|
|
|
|
2. Creates a marker file west/{}
|
|
|
|
3. Creates an initial configuration file west/config
|
|
|
|
As an alternative to manually editing west/config, 'west init' can be rerun on
|
|
an already initialized West instance to update configuration settings. Only
|
|
explicitly passed configuration values (e.g. --mr MANIFEST_REVISION) are
|
|
updated.
|
|
|
|
Updating the manifest URL or revision via 'west init' automatically runs 'west
|
|
update --reset-manifest --reset-projects' afterwards to reset the manifest to
|
|
the new revision, and all projects to their new manifest revisions.
|
|
|
|
Updating the west URL or revision also runs 'west update --reset-west'.
|
|
|
|
To suppress the reset of the manifest, west, and projects, pass --no-reset.
|
|
With --no-reset, only the configuration file will be updated, and you will have
|
|
to handle any resetting yourself.
|
|
'''.format(WEST_MARKER))
|
|
|
|
init_parser.add_argument(
|
|
'-m', '--manifest-url',
|
|
help='Manifest repository URL (default: {})'
|
|
.format(MANIFEST_URL_DEFAULT))
|
|
|
|
init_parser.add_argument(
|
|
'--mr', '--manifest-rev', dest='manifest_rev',
|
|
help='Manifest revision to fetch (default: {})'
|
|
.format(MANIFEST_REV_DEFAULT))
|
|
|
|
init_parser.add_argument(
|
|
'--nr', '--no-reset', dest='reset', action='store_false',
|
|
help='''Suppress the automatic reset of the manifest, west, and project
|
|
repositories when re-running 'west init' in an existing
|
|
installation to update the manifest or west URL/revision''')
|
|
|
|
init_parser.add_argument(
|
|
'directory', nargs='?', default=None,
|
|
help='''Directory to initialize West in. Missing directories will be
|
|
created automatically. (default: current directory)''')
|
|
|
|
args = init_parser.parse_args(args=argv)
|
|
|
|
try:
|
|
reinit(os.path.join(west_dir(args.directory), 'config'), args)
|
|
except WestNotFound:
|
|
bootstrap(args)
|
|
|
|
|
|
def bootstrap(args):
|
|
'''Bootstrap a new manifest + West installation.'''
|
|
|
|
west_url = WEST_URL_DEFAULT
|
|
manifest_url = args.manifest_url or MANIFEST_URL_DEFAULT
|
|
|
|
west_rev = WEST_REV_DEFAULT
|
|
manifest_rev = args.manifest_rev or MANIFEST_REV_DEFAULT
|
|
|
|
directory = args.directory or os.getcwd()
|
|
|
|
if not os.path.isdir(directory):
|
|
try:
|
|
print('Initializing in new directory', directory)
|
|
os.makedirs(directory, exist_ok=False)
|
|
except PermissionError:
|
|
sys.exit('Cannot initialize in {}: permission denied'.format(
|
|
directory))
|
|
except FileExistsError:
|
|
sys.exit('Something else created {} concurrently; quitting'.format(
|
|
directory))
|
|
except Exception as e:
|
|
sys.exit("Can't create directory {}: {}".format(
|
|
directory, e.args))
|
|
else:
|
|
print('Initializing in', directory)
|
|
|
|
# Clone the west source code and the manifest into west/. Git will create
|
|
# the west/ directory if it does not exist.
|
|
|
|
clone('manifest repository', manifest_url, manifest_rev,
|
|
os.path.join(directory, WEST_DIR, MANIFEST))
|
|
|
|
# Parse the manifest and look for a section named "west"
|
|
manifest_file = os.path.join(directory, WEST_DIR, MANIFEST, 'default.yml')
|
|
with open(manifest_file, 'r') as f:
|
|
data = yaml.safe_load(f.read())
|
|
|
|
if 'west' in data:
|
|
wdata = data['west']
|
|
try:
|
|
pykwalify.core.Core(
|
|
source_data=wdata,
|
|
schema_files=[_SCHEMA_PATH]
|
|
).validate()
|
|
except pykwalify.errors.SchemaError as e:
|
|
sys.exit("Error: Failed to parse manifest file '{}': {}"
|
|
.format(manifest_file, e))
|
|
|
|
if 'url' in wdata:
|
|
west_url = wdata['url']
|
|
if 'revision' in wdata:
|
|
west_rev = wdata['revision']
|
|
|
|
print("cloning {} at revision {}".format(west_url, west_rev))
|
|
clone('west repository', west_url, west_rev,
|
|
os.path.join(directory, WEST_DIR, WEST))
|
|
|
|
# Create an initial configuration file
|
|
|
|
config_path = os.path.join(directory, WEST_DIR, 'config')
|
|
update_conf(config_path, manifest_url, manifest_rev)
|
|
print('=== Initial configuration written to {} ==='.format(config_path))
|
|
|
|
# Create a dotfile to mark the installation. Hide it on Windows.
|
|
|
|
with open(os.path.join(directory, WEST_DIR, WEST_MARKER), 'w') as f:
|
|
hide_file(f.name)
|
|
|
|
print('=== West initialized. Now run "west clone" in {}. ==='.
|
|
format(directory))
|
|
|
|
|
|
def reinit(config_path, args):
|
|
'''
|
|
Reinitialize an existing installation.
|
|
|
|
This updates the west/config configuration file, and optionally resets the
|
|
manifest, west, and project repositories to the new revision.
|
|
'''
|
|
manifest_url = args.manifest_url
|
|
|
|
if not (manifest_url or args.manifest_rev):
|
|
sys.exit('West already initialized. Please pass any settings you '
|
|
'want to change.')
|
|
|
|
update_conf(config_path, manifest_url, args.manifest_rev)
|
|
|
|
print('=== Updated configuration written to {} ==='.format(config_path))
|
|
|
|
if args.reset:
|
|
cmd = ['update', '--reset-manifest', '--reset-projects',
|
|
'--reset-west']
|
|
print("=== Running 'west {}' to update repositories ==="
|
|
.format(' '.join(cmd)))
|
|
wrap(cmd)
|
|
|
|
|
|
def update_conf(config_path, manifest_url, manifest_rev):
|
|
'''
|
|
Creates or updates the configuration file at 'config_path' with the
|
|
specified values. Values that are None/empty are ignored.
|
|
'''
|
|
config = configparser.ConfigParser()
|
|
|
|
# This is a no-op if the file doesn't exist, so no need to check
|
|
config.read(config_path)
|
|
|
|
update_key(config, 'manifest', 'remote', manifest_url)
|
|
update_key(config, 'manifest', 'revision', manifest_rev)
|
|
|
|
with open(config_path, 'w') as f:
|
|
config.write(f)
|
|
|
|
|
|
def update_key(config, section, key, value):
|
|
'''
|
|
Updates 'key' in section 'section' in ConfigParser 'config', creating
|
|
'section' if it does not exist.
|
|
|
|
If value is None/empty, 'key' is left as-is.
|
|
'''
|
|
if not value:
|
|
return
|
|
|
|
if section not in config:
|
|
config[section] = {}
|
|
|
|
config[section][key] = value
|
|
|
|
|
|
def hide_file(path):
|
|
'''Ensure path is a hidden file.
|
|
|
|
On Windows, this uses attrib to hide the file manually.
|
|
|
|
On UNIX systems, this just checks that the path's basename begins
|
|
with a period ('.'), for it to be hidden already. It's a fatal
|
|
error if it does not begin with a period in this case.
|
|
|
|
On other systems, this just prints a warning.
|
|
'''
|
|
system = platform.system()
|
|
|
|
if system == 'Windows':
|
|
subprocess.check_call(['attrib', '+H', path])
|
|
elif os.name == 'posix': # Try to check for all Unix, not just macOS/Linux
|
|
if not os.path.basename(path).startswith('.'):
|
|
sys.exit("internal error: {} can't be hidden on UNIX".format(path))
|
|
else:
|
|
print("warning: unknown platform {}; {} may not be hidden"
|
|
.format(system, path), file=sys.stderr)
|
|
|
|
|
|
#
|
|
# Wrap a West command
|
|
#
|
|
|
|
def append_to_pythonpath(directory):
|
|
pp = os.environ.get('PYTHONPATH')
|
|
os.environ['PYTHONPATH'] = ':'.join(([pp] if pp else []) + [directory])
|
|
|
|
|
|
def wrap(argv):
|
|
printing_version = False
|
|
printing_help_only = False
|
|
|
|
if argv:
|
|
if argv[0] in ('-V', '--version'):
|
|
print('West bootstrapper version: v{} ({})'.
|
|
format(version.__version__, os.path.dirname(__file__)))
|
|
printing_version = True
|
|
elif len(argv) == 1 and argv[0] in ('-h', '--help'):
|
|
# This only matters if we're called outside of an
|
|
# installation directory. We delegate to the main help if
|
|
# called from within one, because it includes a list of
|
|
# available commands, etc.
|
|
printing_help_only = True
|
|
|
|
start = os.getcwd()
|
|
try:
|
|
topdir = west_topdir(start)
|
|
except WestNotFound:
|
|
if printing_version:
|
|
sys.exit(0) # run outside of an installation directory
|
|
elif printing_help_only:
|
|
# We call print multiple times here and below instead of using
|
|
# \n to be newline agnostic.
|
|
print('To set up a Zephyr installation here, run "west init".')
|
|
print('Run "west init -h" for additional information.')
|
|
sys.exit(0)
|
|
else:
|
|
print('Error: "{}" is not a Zephyr installation directory.'.
|
|
format(start), file=sys.stderr)
|
|
print('Things to try:', file=sys.stderr)
|
|
print(' - Run "west init" to set up an installation here.',
|
|
file=sys.stderr)
|
|
print(' - Run "west init -h" for additional information.',
|
|
file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
west_git_repo = os.path.join(topdir, WEST_DIR, WEST)
|
|
if printing_version:
|
|
try:
|
|
git_describe = subprocess.check_output(
|
|
['git', 'describe', '--tags'],
|
|
stderr=subprocess.DEVNULL,
|
|
cwd=west_git_repo).decode(sys.getdefaultencoding()).strip()
|
|
print('West repository version: {} ({})'.format(git_describe,
|
|
west_git_repo))
|
|
except subprocess.CalledProcessError:
|
|
print('West repository version: unknown; no tags were found')
|
|
sys.exit(0)
|
|
|
|
# Import the west package from the installation and run its main
|
|
# function with the given command-line arguments.
|
|
#
|
|
# This can't be done as a subprocess: that would break the
|
|
# runners' debug handling for GDB, which needs to block the usual
|
|
# control-C signal handling. GDB uses Ctrl-C to halt the debug
|
|
# target. So we really do need to import west and delegate within
|
|
# this bootstrap process.
|
|
#
|
|
# Put this at position 1 to make sure it comes before random stuff
|
|
# that might be on a developer's PYTHONPATH in the import order.
|
|
sys.path.insert(1, os.path.join(west_git_repo, 'src'))
|
|
import west.main
|
|
west.main.main(argv)
|
|
|
|
|
|
#
|
|
# Main entry point
|
|
#
|
|
|
|
|
|
def main(wrap_argv=None):
|
|
'''Entry point to the wrapper script.'''
|
|
if wrap_argv is None:
|
|
wrap_argv = sys.argv[1:]
|
|
|
|
if not wrap_argv or wrap_argv[0] != 'init':
|
|
wrap(wrap_argv)
|
|
else:
|
|
init(wrap_argv[1:])
|
|
sys.exit(0)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|