blob: 7097067a9aeb9afa426b269f45fb1c41c7521e1c [file] [log] [blame]
# -*- coding: utf-8 -*-
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Manage projects in the local manifest."""
from __future__ import print_function
import platform
import os
import sys
import xml.etree.ElementTree as ElementTree
from chromite.lib import commandline
from chromite.lib import cros_build_lib
from chromite.lib import git
from chromite.lib import osutils
from chromite.lib import repo_manifest
assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
class LocalManifest(object):
"""Class which provides an abstraction for manipulating the local manifest."""
@classmethod
def FromPath(cls, path, empty_if_missing=False):
if os.path.isfile(path):
return cls(osutils.ReadFile(path))
elif empty_if_missing:
cros_build_lib.Die('Manifest file, %r, not found' % path)
return cls()
def __init__(self, text=None):
self._text = text or '<manifest>\n</manifest>'
self.nodes = ElementTree.fromstring(self._text)
def AddNonWorkonProject(self, name, path, remote=None, revision=None):
"""Add a new nonworkon project element to the manifest tree."""
element = ElementTree.Element('project', name=name, path=path,
remote=remote)
element.attrib['workon'] = 'False'
if revision is not None:
element.attrib['revision'] = revision
self.nodes.append(element)
return element
def GetProject(self, name, path=None):
"""Accessor method for getting a project node from the manifest tree.
Returns:
project element node from ElementTree, otherwise, None
"""
if path is None:
# Use a unique value that can't ever match.
path = object()
for project in self.nodes.findall('project'):
if project.attrib['name'] == name or project.attrib['path'] == path:
return project
return None
def ToString(self):
# Reset the tail for each node, then just do a hacky replace.
project = None
for project in self.nodes.findall('project'):
project.tail = '\n '
if project is not None:
# Tweak the last project to not have the trailing space.
project.tail = '\n'
# Fix manifest tag text and tail.
self.nodes.text = '\n '
self.nodes.tail = '\n'
return ElementTree.tostring(
self.nodes, encoding=repo_manifest.TOSTRING_ENCODING)
def GetProjects(self):
return list(self.nodes.findall('project'))
def _AddProjectsToManifestGroups(options, new_group):
"""Enable the given manifest groups for the configured repository."""
groups_to_enable = ['name:%s' % x for x in new_group]
git_config = options.git_config
cmd = ['config', '-f', git_config, '--get', 'manifest.groups']
enabled_groups = git.RunGit('.', cmd, check=False).output.split(',')
# Note that ordering actually matters, thus why the following code
# is written this way.
# Per repo behaviour, enforce an appropriate platform group if
# we're converting from a default manifest group to a limited one.
# Finally, note we reprocess the existing groups; this is to allow
# us to cleanup any user screwups, or our own screwups.
requested_groups = (
['minilayout', 'platform-%s' % (platform.system().lower(),)] +
enabled_groups + list(groups_to_enable))
processed_groups = set()
finalized_groups = []
for group in requested_groups:
if group not in processed_groups:
finalized_groups.append(group)
processed_groups.add(group)
cmd = ['config', '-f', git_config, 'manifest.groups',
','.join(finalized_groups)]
git.RunGit('.', cmd)
def _AssertNotMiniLayout():
cros_build_lib.Die(
'Your repository checkout is using the old minilayout.xml workflow; '
'Autoupdate is no longer supported, reinstall your tree.')
def GetParser():
"""Return a command line parser."""
parser = commandline.ArgumentParser(description=__doc__)
# Subparsers are required by default under Python 2. Python 3 changed to
# not required, but didn't include a required option until 3.7. Setting
# the required member works in all versions (and setting dest name).
subparsers = parser.add_subparsers(dest='command')
subparsers.required = True
subparser = subparsers.add_parser(
'add',
help='Add projects to the manifest.')
subparser.add_argument('-w', '--workon', action='store_true',
default=False, help='Is this a workon package?')
subparser.add_argument('-r', '--remote',
help='Remote project name (for non-workon packages).')
subparser.add_argument('--revision',
help='Use to override the manifest defined default '
'revision used for a given project.')
subparser.add_argument('project', help='Name of project in the manifest.')
subparser.add_argument('path', nargs='?', help='Local path to the project.')
return parser
def main(argv):
parser = GetParser()
options = parser.parse_args(argv)
repo_dir = git.FindRepoDir(os.getcwd())
if not repo_dir:
parser.error('This script must be invoked from within a repository '
'checkout.')
options.git_config = os.path.join(repo_dir, 'manifests.git', 'config')
options.local_manifest_path = os.path.join(repo_dir, 'local_manifest.xml')
manifest_sym_path = os.path.join(repo_dir, 'manifest.xml')
if os.path.basename(os.path.realpath(manifest_sym_path)) == 'minilayout.xml':
_AssertNotMiniLayout()
# For now, we only support the add command.
assert options.command == 'add'
if options.workon:
if options.path is not None:
parser.error('Adding workon projects do not set project.')
else:
if options.remote is None:
parser.error('Adding non-workon projects requires a remote.')
if options.path is None:
parser.error('Adding non-workon projects requires a path.')
name = options.project
path = options.path
revision = options.revision
if revision is not None:
if (not git.IsRefsTags(revision) and
not git.IsSHA1(revision)):
revision = git.StripRefsHeads(revision, False)
main_manifest = git.ManifestCheckout(os.getcwd())
main_element = main_manifest.FindCheckouts(name)
if path is not None:
main_element_from_path = main_manifest.FindCheckoutFromPath(
path, strict=False)
if main_element_from_path is not None:
main_element.append(main_element_from_path)
local_manifest = LocalManifest.FromPath(options.local_manifest_path)
if options.workon:
if not main_element:
parser.error('No project named %r in the default manifest.' % name)
_AddProjectsToManifestGroups(
options, [checkout['name'] for checkout in main_element])
elif main_element:
if options.remote is not None:
# Likely this project wasn't meant to be remote, so workon main element
print('Project already exists in manifest. Using that as workon project.')
_AddProjectsToManifestGroups(
options, [checkout['name'] for checkout in main_element])
else:
# Conflict will occur; complain.
parser.error('Requested project name=%r path=%r will conflict with '
'your current manifest %s' % (
name, path, main_manifest.manifest_path))
elif local_manifest.GetProject(name, path=path) is not None:
parser.error('Requested project name=%r path=%r conflicts with '
'your local_manifest.xml' % (name, path))
else:
element = local_manifest.AddNonWorkonProject(name=name, path=path,
remote=options.remote,
revision=revision)
_AddProjectsToManifestGroups(options, [element.attrib['name']])
with open(options.local_manifest_path, 'w') as f:
f.write(local_manifest.ToString())
return 0