scripts: update west to latest upstream version.
Update to the latest west. This includes a new 'attach' command. There are also multi-repo commands, but those won't get exposed to the user unless they install Zephyr using "west init" + "west fetch" (and not, say, "git clone"). Replace the launchers; they now detect whether zephyr is part of a multi-repo installation, and run the west code in its own repository if that is the case. This also requires an update to: - the flash/debug CMakeLists.txt, as the new west package is no longer executable as a module and must have its main script run by the interpreter instead. - the documentation, to reflect a rename and with a hack to fix the automodule directive in flash-debug.rst for now Signed-off-by: Marti Bolivar <marti@foundries.io>
This commit is contained in:
parent
b4d856397e
commit
55b462cdfa
38 changed files with 2127 additions and 190 deletions
876
scripts/meta/west/commands/project.py
Normal file
876
scripts/meta/west/commands/project.py
Normal file
|
@ -0,0 +1,876 @@
|
|||
# Copyright (c) 2018, Nordic Semiconductor ASA
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
'''West project commands'''
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
|
||||
import pykwalify.core
|
||||
import yaml
|
||||
|
||||
import log
|
||||
import util
|
||||
from commands import WestCommand
|
||||
|
||||
|
||||
# Branch that points to the revision specified in the manifest (which might be
|
||||
# an SHA). Local branches created with 'west branch' are set to track this
|
||||
# branch.
|
||||
_MANIFEST_REV_BRANCH = 'manifest-rev'
|
||||
|
||||
|
||||
class ListProjects(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'list-projects',
|
||||
_wrap('''
|
||||
List projects.
|
||||
|
||||
Prints the path to the manifest file and lists all projects along
|
||||
with their clone paths and manifest revisions. Also includes
|
||||
information on which projects are currently cloned.
|
||||
'''))
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(parser_adder, self)
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
log.inf("Manifest path: {}\n".format(_manifest_path(args)))
|
||||
|
||||
for project in _all_projects(args):
|
||||
log.inf('{:15} {:30} {:15} {}'.format(
|
||||
project.name,
|
||||
os.path.join(project.path, ''), # Add final '/' if missing
|
||||
project.revision,
|
||||
"(cloned)" if _cloned(project) else "(not cloned)"))
|
||||
|
||||
|
||||
class Fetch(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'fetch',
|
||||
_wrap('''
|
||||
Clone/fetch projects.
|
||||
|
||||
Fetches upstream changes in each of the specified projects
|
||||
(default: all projects). Repositories that do not already exist are
|
||||
cloned.
|
||||
|
||||
Unless --no-update is passed, the manifest and West source code
|
||||
repositories are updated prior to fetching. See the 'update'
|
||||
command.
|
||||
|
||||
''' + _MANIFEST_REV_HELP))
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(parser_adder, self, _no_update_arg,
|
||||
_project_list_arg)
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
if args.update:
|
||||
_update(True, True)
|
||||
|
||||
for project in _projects(args, listed_must_be_cloned=False):
|
||||
log.dbg('fetching:', project, level=log.VERBOSE_VERY)
|
||||
_fetch(project)
|
||||
|
||||
|
||||
class Pull(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'pull',
|
||||
_wrap('''
|
||||
Clone/fetch and rebase projects.
|
||||
|
||||
Fetches upstream changes in each of the specified projects
|
||||
(default: all projects) and rebases the checked-out branch (or
|
||||
detached HEAD state) on top of '{}', effectively bringing the
|
||||
branch up to date. Repositories that do not already exist are
|
||||
cloned.
|
||||
|
||||
Unless --no-update is passed, the manifest and West source code
|
||||
repositories are updated prior to pulling. See the 'update'
|
||||
command.
|
||||
|
||||
'''.format(_MANIFEST_REV_BRANCH) + _MANIFEST_REV_HELP))
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(parser_adder, self, _no_update_arg,
|
||||
_project_list_arg)
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
if args.update:
|
||||
_update(True, True)
|
||||
|
||||
for project in _projects(args, listed_must_be_cloned=False):
|
||||
if _fetch(project):
|
||||
_rebase(project)
|
||||
|
||||
|
||||
class Rebase(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'rebase',
|
||||
_wrap('''
|
||||
Rebase projects.
|
||||
|
||||
Rebases the checked-out branch (or detached HEAD) on top of '{}' in
|
||||
each of the specified projects (default: all cloned projects),
|
||||
effectively bringing the branch up to date.
|
||||
|
||||
'''.format(_MANIFEST_REV_BRANCH) + _MANIFEST_REV_HELP))
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(parser_adder, self, _project_list_arg)
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
for project in _cloned_projects(args):
|
||||
_rebase(project)
|
||||
|
||||
|
||||
class Branch(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'branch',
|
||||
_wrap('''
|
||||
Create a branch or list branches, in multiple projects.
|
||||
|
||||
Creates a branch in each of the specified projects (default: all
|
||||
cloned projects). The new branches are set to track '{}'.
|
||||
|
||||
With no arguments, lists all local branches along with the
|
||||
repositories they appear in.
|
||||
|
||||
'''.format(_MANIFEST_REV_BRANCH) + _MANIFEST_REV_HELP))
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(
|
||||
parser_adder, self,
|
||||
_arg('branch', nargs='?', metavar='BRANCH_NAME'),
|
||||
_project_list_arg)
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
if args.branch:
|
||||
# Create a branch in the specified projects
|
||||
for project in _cloned_projects(args):
|
||||
_create_branch(project, args.branch)
|
||||
else:
|
||||
# No arguments. List local branches from all cloned projects along
|
||||
# with the projects they appear in.
|
||||
|
||||
branch2projs = collections.defaultdict(list)
|
||||
for project in _cloned_projects(args):
|
||||
for branch in _branches(project):
|
||||
branch2projs[branch].append(project.name)
|
||||
|
||||
for branch, projs in sorted(branch2projs.items()):
|
||||
log.inf('{:18} {}'.format(branch, ", ".join(projs)))
|
||||
|
||||
|
||||
class Checkout(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'checkout',
|
||||
_wrap('''
|
||||
Check out topic branch.
|
||||
|
||||
Checks out the specified branch in each of the specified projects
|
||||
(default: all cloned projects). Projects that do not have the
|
||||
branch are left alone.
|
||||
'''))
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(
|
||||
parser_adder, self,
|
||||
_arg('-b',
|
||||
dest='create_branch',
|
||||
action='store_true',
|
||||
help='create the branch before checking it out'),
|
||||
_arg('branch', metavar='BRANCH_NAME'),
|
||||
_project_list_arg)
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
branch_exists = False
|
||||
|
||||
for project in _cloned_projects(args):
|
||||
if args.create_branch:
|
||||
_create_branch(project, args.branch)
|
||||
_checkout(project, args.branch)
|
||||
branch_exists = True
|
||||
elif _has_branch(project, args.branch):
|
||||
_checkout(project, args.branch)
|
||||
branch_exists = True
|
||||
|
||||
if not branch_exists:
|
||||
msg = 'No branch {} exists in any '.format(args.branch)
|
||||
if args.projects:
|
||||
log.die(msg + 'of the listed projects')
|
||||
else:
|
||||
log.die(msg + 'cloned project')
|
||||
|
||||
|
||||
class Diff(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'diff',
|
||||
_wrap('''
|
||||
'git diff' projects.
|
||||
|
||||
Runs 'git diff' for each of the specified projects (default: all
|
||||
cloned projects).
|
||||
|
||||
Extra arguments are passed as-is to 'git diff'.
|
||||
'''),
|
||||
accepts_unknown_args=True)
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(parser_adder, self, _project_list_arg)
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
for project in _cloned_projects(args):
|
||||
# Use paths that are relative to the base directory to make it
|
||||
# easier to see where the changes are
|
||||
_git(project, 'diff --src-prefix=(path)/ --dst-prefix=(path)/',
|
||||
extra_args=user_args)
|
||||
|
||||
|
||||
class Status(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'status',
|
||||
_wrap('''
|
||||
Runs 'git status' for each of the specified projects (default: all
|
||||
cloned projects). Extra arguments are passed as-is to 'git status'.
|
||||
'''),
|
||||
accepts_unknown_args=True)
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(parser_adder, self, _project_list_arg)
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
for project in _cloned_projects(args):
|
||||
_inf(project, 'status of (name-and-path)')
|
||||
_git(project, 'status', extra_args=user_args)
|
||||
|
||||
|
||||
class Update(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'update',
|
||||
_wrap('''
|
||||
Updates the manifest repository and/or the West source code
|
||||
repository.
|
||||
|
||||
There is normally no need to run this command manually, because
|
||||
'west fetch' and 'west pull' automatically update the West and
|
||||
manifest repositories to the latest version before doing anything
|
||||
else.
|
||||
|
||||
Pass --update-west or --update-manifest to update just that
|
||||
repository. With no arguments, both are updated.
|
||||
'''))
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(
|
||||
parser_adder, self,
|
||||
_arg('--update-west',
|
||||
dest='update_west',
|
||||
action='store_true',
|
||||
help='update the West source code repository'),
|
||||
_arg('--update-manifest',
|
||||
dest='update_manifest',
|
||||
action='store_true',
|
||||
help='update the manifest repository'))
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
if not args.update_west and not args.update_manifest:
|
||||
_update(True, True)
|
||||
else:
|
||||
_update(args.update_west, args.update_manifest)
|
||||
|
||||
|
||||
class ForAll(WestCommand):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'forall',
|
||||
_wrap('''
|
||||
Runs a shell (Linux) or batch (Windows) command within the
|
||||
repository of each of the specified projects (default: all cloned
|
||||
projects). Note that you have to quote the command if it consists
|
||||
of more than one word, to prevent the shell you use to run 'west'
|
||||
from splitting it up.
|
||||
|
||||
Since the command is run through the shell, you can use wildcards
|
||||
and the like.
|
||||
|
||||
For example, the following command will list the contents of
|
||||
proj-1's and proj-2's repositories on Linux, in long form:
|
||||
|
||||
west forall -c 'ls -l' proj-1 proj-2
|
||||
'''))
|
||||
|
||||
def do_add_parser(self, parser_adder):
|
||||
return _add_parser(
|
||||
parser_adder, self,
|
||||
_arg('-c',
|
||||
dest='command',
|
||||
metavar='COMMAND',
|
||||
required=True),
|
||||
_project_list_arg)
|
||||
|
||||
def do_run(self, args, user_args):
|
||||
for project in _cloned_projects(args):
|
||||
_inf(project, "Running '{}' in (name-and-path)"
|
||||
.format(args.command))
|
||||
|
||||
subprocess.Popen(args.command, shell=True, cwd=project.abspath) \
|
||||
.wait()
|
||||
|
||||
|
||||
def _arg(*args, **kwargs):
|
||||
# Helper for creating a new argument parser for a single argument,
|
||||
# later passed in parents= to add_parser()
|
||||
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument(*args, **kwargs)
|
||||
return parser
|
||||
|
||||
|
||||
# Arguments shared between more than one command
|
||||
|
||||
_manifest_arg = _arg(
|
||||
'-m', '--manifest',
|
||||
help='path to manifest file (default: west/manifest/default.yml)')
|
||||
|
||||
# For 'fetch' and 'pull'
|
||||
_no_update_arg = _arg(
|
||||
'--no-update',
|
||||
dest='update',
|
||||
action='store_false',
|
||||
help='do not update the manifest or West before fetching project data')
|
||||
|
||||
# List of projects
|
||||
_project_list_arg = _arg('projects', metavar='PROJECT', nargs='*')
|
||||
|
||||
|
||||
def _add_parser(parser_adder, cmd, *extra_args):
|
||||
# Adds and returns a subparser for the project-related WestCommand 'cmd'.
|
||||
# All of these commands (currently) take the manifest path flag, so it's
|
||||
# hardcoded here.
|
||||
|
||||
return parser_adder.add_parser(
|
||||
cmd.name,
|
||||
description=cmd.description,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
parents=(_manifest_arg,) + extra_args)
|
||||
|
||||
|
||||
def _wrap(s):
|
||||
# Wraps help texts for commands. Some of them have variable length (due to
|
||||
# _MANIFEST_REV_BRANCH), so just a textwrap.dedent() can look a bit wonky.
|
||||
|
||||
# [1:] gets rid of the initial newline. It's turned into a space by
|
||||
# textwrap.fill() otherwise.
|
||||
paragraphs = textwrap.dedent(s[1:]).split("\n\n")
|
||||
|
||||
return "\n\n".join(textwrap.fill(paragraph) for paragraph in paragraphs)
|
||||
|
||||
|
||||
_MANIFEST_REV_HELP = """
|
||||
The '{}' branch points to the revision that the manifest specified for the
|
||||
project as of the most recent 'west fetch'/'west pull'.
|
||||
""".format(_MANIFEST_REV_BRANCH)[1:].replace("\n", " ")
|
||||
|
||||
|
||||
# Holds information about a project, taken from the manifest file (or
|
||||
# constructed manually for "special" projects)
|
||||
Project = collections.namedtuple(
|
||||
'Project',
|
||||
'name url revision path abspath clone_depth')
|
||||
|
||||
|
||||
def _cloned_projects(args):
|
||||
# Returns _projects(args, listed_must_be_cloned=True) if a list of projects
|
||||
# was given by the user (i.e., listed projects are required to be cloned).
|
||||
# If no projects were listed, returns all cloned projects.
|
||||
|
||||
# This approach avoids redundant _cloned() checks
|
||||
return _projects(args) if args.projects else \
|
||||
[project for project in _all_projects(args) if _cloned(project)]
|
||||
|
||||
|
||||
def _projects(args, listed_must_be_cloned=True):
|
||||
# Returns a list of project instances for the projects requested in 'args'
|
||||
# (the command-line arguments), in the same order that they were listed by
|
||||
# the user. If args.projects is empty, no projects were listed, and all
|
||||
# projects will be returned. If a non-existent project was listed by the
|
||||
# user, an error is raised.
|
||||
#
|
||||
# Before the manifest is parsed, it is validated agains a pykwalify schema.
|
||||
# An error is raised on validation errors.
|
||||
#
|
||||
# listed_must_be_cloned (default: True):
|
||||
# If True, an error is raised if an uncloned project was listed. This
|
||||
# only applies to projects listed explicitly on the command line.
|
||||
|
||||
projects = _all_projects(args)
|
||||
|
||||
if not args.projects:
|
||||
# No projects specified. Return all projects.
|
||||
return projects
|
||||
|
||||
# Got a list of projects on the command line. First, check that they exist
|
||||
# in the manifest.
|
||||
|
||||
project_names = [project.name for project in projects]
|
||||
nonexistent = set(args.projects) - set(project_names)
|
||||
if nonexistent:
|
||||
log.die('Unknown project{} {} (available projects: {})'
|
||||
.format('s' if len(nonexistent) > 1 else '',
|
||||
', '.join(nonexistent),
|
||||
', '.join(project_names)))
|
||||
|
||||
# Return the projects in the order they were listed
|
||||
res = []
|
||||
for name in args.projects:
|
||||
for project in projects:
|
||||
if project.name == name:
|
||||
res.append(project)
|
||||
break
|
||||
|
||||
# Check that all listed repositories are cloned, if requested
|
||||
if listed_must_be_cloned:
|
||||
uncloned = [prj.name for prj in res if not _cloned(prj)]
|
||||
if uncloned:
|
||||
log.die('The following projects are not cloned: {}. Please clone '
|
||||
"them first (with 'west fetch')."
|
||||
.format(", ".join(uncloned)))
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def _all_projects(args):
|
||||
# Parses the manifest file, returning a list of Project instances.
|
||||
#
|
||||
# Before the manifest is parsed, it is validated against a pykwalify
|
||||
# schema. An error is raised on validation errors.
|
||||
|
||||
manifest_path = _manifest_path(args)
|
||||
|
||||
_validate_manifest(manifest_path)
|
||||
|
||||
with open(manifest_path) as f:
|
||||
manifest = yaml.safe_load(f)['manifest']
|
||||
|
||||
projects = []
|
||||
# Manifest "defaults" keys whose values get copied to each project
|
||||
# that doesn't specify its own value.
|
||||
project_defaults = ('remote', 'revision')
|
||||
|
||||
# mp = manifest project (dictionary with values parsed from the manifest)
|
||||
for mp in manifest['projects']:
|
||||
# Fill in any missing fields in 'mp' with values from the 'defaults'
|
||||
# dictionary
|
||||
if 'defaults' in manifest:
|
||||
for key, val in manifest['defaults'].items():
|
||||
if key in project_defaults:
|
||||
mp.setdefault(key, val)
|
||||
|
||||
# Add the repository URL to 'mp'
|
||||
for remote in manifest['remotes']:
|
||||
if remote['name'] == mp['remote']:
|
||||
mp['url'] = remote['url'] + '/' + mp['name']
|
||||
break
|
||||
else:
|
||||
log.die('Remote {} not defined in {}'
|
||||
.format(mp['remote'], manifest_path))
|
||||
|
||||
# If no clone path is specified, the project's name is used
|
||||
clone_path = mp.get('path', mp['name'])
|
||||
|
||||
# Use named tuples to store project information. That gives nicer
|
||||
# syntax compared to a dict (project.name instead of project['name'],
|
||||
# etc.)
|
||||
projects.append(Project(
|
||||
mp['name'],
|
||||
mp['url'],
|
||||
# If no revision is specified, 'master' is used
|
||||
mp.get('revision', 'master'),
|
||||
clone_path,
|
||||
# Absolute clone path
|
||||
os.path.join(util.west_topdir(), clone_path),
|
||||
# If no clone depth is specified, we fetch the entire history
|
||||
mp.get('clone-depth', None)))
|
||||
|
||||
return projects
|
||||
|
||||
|
||||
def _validate_manifest(manifest_path):
|
||||
# Validates the manifest with pykwalify. schema.yml holds the schema.
|
||||
|
||||
schema_path = os.path.join(os.path.dirname(__file__), "schema.yml")
|
||||
|
||||
try:
|
||||
pykwalify.core.Core(
|
||||
source_file=manifest_path,
|
||||
schema_files=[schema_path]
|
||||
).validate()
|
||||
except pykwalify.errors.SchemaError as e:
|
||||
log.die('{} malformed (schema: {}):\n{}'
|
||||
.format(manifest_path, schema_path, e))
|
||||
|
||||
|
||||
def _manifest_path(args):
|
||||
# Returns the path to the manifest file. Defaults to
|
||||
# .west/manifest/default.yml if the user didn't specify a manifest.
|
||||
|
||||
return args.manifest or os.path.join(util.west_dir(), 'manifest',
|
||||
'default.yml')
|
||||
|
||||
|
||||
def _fetch(project):
|
||||
# Fetches upstream changes for 'project' and updates the 'manifest-rev'
|
||||
# branch to point to the revision specified in the manifest. If the
|
||||
# project's repository does not already exist, it is created first.
|
||||
#
|
||||
# Returns True if the project's repository already existed.
|
||||
|
||||
exists = _cloned(project)
|
||||
|
||||
if not exists:
|
||||
_inf(project, 'Creating repository for (name-and-path)')
|
||||
_git_base(project, 'init (abspath)')
|
||||
_git(project, 'remote add origin (url)')
|
||||
|
||||
if project.clone_depth:
|
||||
_inf(project,
|
||||
'Fetching changes for (name-and-path) with --depth (clone-depth)')
|
||||
|
||||
# If 'clone-depth' is specified, fetch just the specified revision
|
||||
# (probably a branch). That will download the minimum amount of data,
|
||||
# which is probably what's wanted whenever 'clone-depth is used. The
|
||||
# default 'git fetch' behavior is to do a shallow clone of all branches
|
||||
# on the remote.
|
||||
#
|
||||
# Note: Many servers won't allow fetching arbitrary commits by SHA.
|
||||
# Combining --depth with an SHA will break for those.
|
||||
|
||||
# Qualify branch names with refs/heads/, just to be safe. Just the
|
||||
# branch name is likely to work as well though.
|
||||
_git(project,
|
||||
'fetch --depth=(clone-depth) origin ' +
|
||||
(project.revision if _is_sha(project.revision) else \
|
||||
'refs/heads/' + project.revision))
|
||||
|
||||
else:
|
||||
_inf(project, 'Fetching changes for (name-and-path)')
|
||||
|
||||
# If 'clone-depth' is not specified, fetch all branches on the
|
||||
# remote. This gives a more usable repository.
|
||||
_git(project, 'fetch origin')
|
||||
|
||||
# Create/update the 'manifest-rev' branch
|
||||
_git(project,
|
||||
'update-ref refs/heads/(manifest-rev-branch) ' +
|
||||
(project.revision if _is_sha(project.revision) else
|
||||
'remotes/origin/' + project.revision))
|
||||
|
||||
if not exists:
|
||||
# If we just initialized the repository, check out 'manifest-rev' in a
|
||||
# detached HEAD state.
|
||||
#
|
||||
# Otherwise, the initial state would have nothing checked out, and HEAD
|
||||
# would point to a non-existent refs/heads/master branch (that would
|
||||
# get created if the user makes an initial commit). That state causes
|
||||
# e.g. 'west rebase' to fail, and might look confusing.
|
||||
#
|
||||
# (The --detach flag is strictly redundant here, because the
|
||||
# refs/heads/<branch> form already detaches HEAD, but it avoids a
|
||||
# spammy detached HEAD warning from Git.)
|
||||
_git(project, 'checkout --detach refs/heads/(manifest-rev-branch)')
|
||||
|
||||
return exists
|
||||
|
||||
|
||||
def _rebase(project):
|
||||
_inf(project, 'Rebasing (name-and-path) to (manifest-rev-branch)')
|
||||
_git(project, 'rebase (manifest-rev-branch)')
|
||||
|
||||
|
||||
def _cloned(project):
|
||||
# Returns True if the project's path is a directory that looks
|
||||
# like the top-level directory of a Git repository, and False
|
||||
# otherwise.
|
||||
|
||||
def handle(result):
|
||||
log.dbg('project', project.name,
|
||||
'is {}cloned'.format('' if result else 'not '),
|
||||
level=log.VERBOSE_EXTREME)
|
||||
return result
|
||||
|
||||
if not os.path.isdir(project.abspath):
|
||||
return handle(False)
|
||||
|
||||
# --is-inside-work-tree doesn't require that the directory is the top-level
|
||||
# directory of a Git repository. Use --show-cdup instead, which prints an
|
||||
# empty string (i.e., just a newline, which we strip) for the top-level
|
||||
# directory.
|
||||
res = _git(project, 'rev-parse --show-cdup', capture_stdout=True,
|
||||
check=False)
|
||||
|
||||
return handle(not (res.returncode or res.stdout))
|
||||
|
||||
|
||||
def _branches(project):
|
||||
# Returns a sorted list of all local branches in 'project'
|
||||
|
||||
# refname:lstrip=-1 isn't available before Git 2.8 (introduced by commit
|
||||
# 'tag: do not show ambiguous tag names as "tags/foo"'). Strip
|
||||
# 'refs/heads/' manually instead.
|
||||
return [ref[len('refs/heads/'):] for ref in
|
||||
_git(project,
|
||||
'for-each-ref --sort=refname --format=%(refname) refs/heads',
|
||||
capture_stdout=True).stdout.split('\n')]
|
||||
|
||||
|
||||
def _create_branch(project, branch):
|
||||
if _has_branch(project, branch):
|
||||
_inf(project, "Branch '{}' already exists in (name-and-path)"
|
||||
.format(branch))
|
||||
else:
|
||||
_inf(project, "Creating branch '{}' in (name-and-path)"
|
||||
.format(branch))
|
||||
_git(project, 'branch --quiet --track {} (manifest-rev-branch)'
|
||||
.format(branch))
|
||||
|
||||
|
||||
def _has_branch(project, branch):
|
||||
return _git(project, 'show-ref --quiet --verify refs/heads/' + branch,
|
||||
check=False).returncode == 0
|
||||
|
||||
|
||||
def _checkout(project, branch):
|
||||
_inf(project, "Checking out branch '{}' in (name-and-path)".format(branch))
|
||||
_git(project, 'checkout ' + branch)
|
||||
|
||||
|
||||
def _special_project(name):
|
||||
# Returns a Project instance for one of the special repositories in west/,
|
||||
# so that we can reuse the project-related functions for them
|
||||
|
||||
return Project(
|
||||
name,
|
||||
'dummy URL for {} repository'.format(name),
|
||||
'master',
|
||||
os.path.join('west', name.lower()), # Path
|
||||
os.path.join(util.west_dir(), name.lower()), # Absolute path
|
||||
None # Clone depth
|
||||
)
|
||||
|
||||
|
||||
def _update(update_west, update_manifest):
|
||||
# 'try' is a keyword
|
||||
def attempt(project, cmd):
|
||||
res = _git(project, cmd, capture_stdout=True, check=False)
|
||||
if res.returncode:
|
||||
# The Git command's stderr isn't redirected and will also be
|
||||
# available
|
||||
_die(project, _FAILED_UPDATE_MSG.format(cmd))
|
||||
return res.stdout
|
||||
|
||||
projects = []
|
||||
if update_west:
|
||||
projects.append(_special_project('West'))
|
||||
if update_manifest:
|
||||
projects.append(_special_project('manifest'))
|
||||
|
||||
for project in projects:
|
||||
_dbg(project, 'Updating (name-and-path)', level=log.VERBOSE_NORMAL)
|
||||
|
||||
# Fetch changes from upstream
|
||||
attempt(project, 'fetch')
|
||||
|
||||
# Get the SHA of the last commit in common with the upstream branch
|
||||
merge_base = attempt(project, 'merge-base HEAD remotes/origin/master')
|
||||
|
||||
# Get the current SHA of the upstream branch
|
||||
head_sha = attempt(project, 'show-ref --hash remotes/origin/master')
|
||||
|
||||
# If they differ, we need to rebase
|
||||
if merge_base != head_sha:
|
||||
attempt(project, 'rebase remotes/origin/master')
|
||||
|
||||
_inf(project, 'Updated (rebased) (name-and-path) to the '
|
||||
'latest version')
|
||||
|
||||
if project.name == 'west':
|
||||
# Signal self-update, which will cause a restart. This is a bit
|
||||
# nicer than doing the restart here, as callers will have a
|
||||
# chance to flush file buffers, etc.
|
||||
raise WestUpdated()
|
||||
|
||||
|
||||
_FAILED_UPDATE_MSG = """
|
||||
Failed to update (name-and-path), while running command '{}'. Please fix the
|
||||
state of the repository, or pass --no-update to 'west fetch/pull' to skip
|
||||
updating the manifest and West for the duration of the command."""[1:]
|
||||
|
||||
|
||||
class WestUpdated(Exception):
|
||||
'''Raised after West has updated its own source code'''
|
||||
|
||||
|
||||
def _is_sha(s):
|
||||
try:
|
||||
int(s, 16)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
return len(s) == 40
|
||||
|
||||
|
||||
def _git_base(project, cmd, *, extra_args=(), capture_stdout=False,
|
||||
check=True):
|
||||
# Runs a git command in the West top directory. See _git_helper() for
|
||||
# parameter documentation.
|
||||
#
|
||||
# Returns a CompletedProcess instance (see below).
|
||||
|
||||
return _git_helper(project, cmd, extra_args, util.west_topdir(),
|
||||
capture_stdout, check)
|
||||
|
||||
|
||||
def _git(project, cmd, *, extra_args=(), capture_stdout=False, check=True):
|
||||
# Runs a git command within a particular project. See _git_helper() for
|
||||
# parameter documentation.
|
||||
#
|
||||
# Returns a CompletedProcess instance (see below).
|
||||
|
||||
return _git_helper(project, cmd, extra_args, project.abspath,
|
||||
capture_stdout, check)
|
||||
|
||||
|
||||
def _git_helper(project, cmd, extra_args, cwd, capture_stdout, check):
|
||||
# Runs a git command.
|
||||
#
|
||||
# project:
|
||||
# The Project instance for the project, derived from the manifest file.
|
||||
#
|
||||
# cmd:
|
||||
# String with git arguments. Supports some "(foo)" shorthands. See below.
|
||||
#
|
||||
# extra_args:
|
||||
# List of additional arguments to pass to the git command (e.g. from the
|
||||
# user).
|
||||
#
|
||||
# cwd:
|
||||
# Directory to switch to first (None = current directory)
|
||||
#
|
||||
# capture_stdout:
|
||||
# True if stdout should be captured into the returned
|
||||
# subprocess.CompletedProcess instance instead of being printed.
|
||||
#
|
||||
# We never capture stderr, to prevent error messages from being eaten.
|
||||
#
|
||||
# check:
|
||||
# True if an error should be raised if the git command finishes with a
|
||||
# non-zero return code.
|
||||
#
|
||||
# Returns a subprocess.CompletedProcess instance.
|
||||
|
||||
# TODO: Run once somewhere?
|
||||
if shutil.which('git') is None:
|
||||
log.die('Git is not installed or cannot be found')
|
||||
|
||||
args = (('git',) +
|
||||
tuple(_expand_shorthands(project, arg) for arg in cmd.split()) +
|
||||
tuple(extra_args))
|
||||
cmd_str = util.quote_sh_list(args)
|
||||
|
||||
log.dbg("running '{}'".format(cmd_str), 'in', cwd, level=log.VERBOSE_VERY)
|
||||
popen = subprocess.Popen(
|
||||
args, stdout=subprocess.PIPE if capture_stdout else None, cwd=cwd)
|
||||
|
||||
stdout, _ = popen.communicate()
|
||||
|
||||
dbg_msg = "'{}' in {} finished with exit status {}" \
|
||||
.format(cmd_str, cwd, popen.returncode)
|
||||
if capture_stdout:
|
||||
dbg_msg += " and wrote {} to stdout".format(stdout)
|
||||
log.dbg(dbg_msg, level=log.VERBOSE_VERY)
|
||||
|
||||
if check and popen.returncode:
|
||||
_die(project, "Command '{}' failed for (name-and-path)"
|
||||
.format(cmd_str))
|
||||
|
||||
if capture_stdout:
|
||||
# Manual UTF-8 decoding and universal newlines. Before Python 3.6,
|
||||
# Popen doesn't seem to allow using universal newlines mode (which
|
||||
# enables decoding) with a specific encoding (because the encoding=
|
||||
# parameter is missing).
|
||||
#
|
||||
# Also strip all trailing newlines as convenience. The splitlines()
|
||||
# already means we lose a final '\n' anyway.
|
||||
stdout = "\n".join(stdout.decode('utf-8').splitlines()).rstrip("\n")
|
||||
|
||||
return CompletedProcess(popen.args, popen.returncode, stdout)
|
||||
|
||||
|
||||
def _expand_shorthands(project, s):
|
||||
# Expands project-related shorthands in 's' to their values,
|
||||
# returning the expanded string
|
||||
|
||||
return s.replace('(name)', project.name) \
|
||||
.replace('(name-and-path)',
|
||||
'{} ({})'.format(
|
||||
project.name, os.path.join(project.path, ""))) \
|
||||
.replace('(url)', project.url) \
|
||||
.replace('(path)', project.path) \
|
||||
.replace('(abspath)', project.abspath) \
|
||||
.replace('(revision)', project.revision) \
|
||||
.replace('(manifest-rev-branch)', _MANIFEST_REV_BRANCH) \
|
||||
.replace('(clone-depth)', str(project.clone_depth))
|
||||
|
||||
|
||||
def _inf(project, msg):
|
||||
# Print '=== msg' (to clearly separate it from Git output). Supports the
|
||||
# same (foo) shorthands as the git commands.
|
||||
#
|
||||
# Prints the message in green if stdout is a terminal, to clearly separate
|
||||
# it from command (usually Git) output.
|
||||
|
||||
log.inf('=== ' + _expand_shorthands(project, msg), colorize=True)
|
||||
|
||||
|
||||
def _wrn(project, msg):
|
||||
# Warn with 'msg'. Supports the same (foo) shorthands as the git commands.
|
||||
|
||||
log.wrn(_expand_shorthands(project, msg))
|
||||
|
||||
|
||||
def _dbg(project, msg, level):
|
||||
# Like _wrn(), for debug messages
|
||||
|
||||
log.dbg(_expand_shorthands(project, msg), level=level)
|
||||
|
||||
|
||||
def _die(project, msg):
|
||||
# Like _wrn(), for dying
|
||||
|
||||
log.die(_expand_shorthands(project, msg))
|
||||
|
||||
|
||||
# subprocess.CompletedProcess-alike, used instead of the real deal for Python
|
||||
# 3.4 compatibility, and with two small differences:
|
||||
#
|
||||
# - Trailing newlines are stripped from stdout
|
||||
#
|
||||
# - The 'stderr' attribute is omitted, because we never capture stderr
|
||||
CompletedProcess = collections.namedtuple(
|
||||
'CompletedProcess', 'args returncode stdout')
|
Loading…
Add table
Add a link
Reference in a new issue