blob: d068c4edbcc2c7f0e938698dc789bce93bddeaa5 [file] [log] [blame]
# 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.
"""This module allows adding and deleting of projects to the local manifest."""
from __future__ import print_function
import platform
import optparse
import os
import sys
import xml.etree.ElementTree as ElementTree
from chromite.lib import cros_build_lib
from chromite.lib import cros_logging as logging
from chromite.lib import git
class Manifest(object):
"""Class which provides an abstraction for manipulating the local manifest."""
def FromPath(cls, path, empty_if_missing=False):
if os.path.isfile(path):
with open(path) as f:
return cls(
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,
element.attrib['workon'] = 'False'
if revision is not None:
element.attrib['revision'] = revision
return element
def GetProject(self, name, path=None):
"""Accessor method for getting a project node from the manifest tree.
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)
def GetProjects(self):
return list(self.nodes.findall('project'))
def _AddProjectsToManifestGroups(options, *args):
"""Enable the given manifest groups for the configured repository."""
groups_to_enable = ['name:%s' % x for x in args]
git_config = options.git_config
cmd = ['config', '-f', git_config, '--get', 'manifest.groups']
enabled_groups = git.RunGit('.', cmd, error_code_ok=True).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:
cmd = ['config', '-f', git_config, 'manifest.groups',
git.RunGit('.', cmd)
def _UpgradeMinilayout(options):
"""Convert a repo checkout away from minilayout.xml to default.xml."""
full_tree = Manifest.FromPath(options.default_manifest_path)
local_manifest_exists = os.path.exists(options.local_manifest_path)
new_groups = []
if local_manifest_exists:
local_tree = Manifest.FromPath(options.local_manifest_path)
# Identify which projects need to be transferred across.
projects = local_tree.GetProjects()
new_groups = [x.attrib['name'] for x in projects]
allowed = set(x.attrib['name'] for x in full_tree.GetProjects())
transferred = [x for x in projects if x.attrib['name'] in allowed]
for project in transferred:
# Mangle local_manifest object, removing those projects;
# note we'll still be adding those projects to the default groups,
# including those that didn't intersect the main manifest.
_AddProjectsToManifestGroups(options, *new_groups)
if local_manifest_exists:
# Rewrite the local_manifest now; if there is no settings left in
# the local_manifest, wipe it.
if local_tree.nodes.getchildren():
with open(options.local_manifest_path, 'w') as f:
# Finally, move the symlink.
os.symlink('manifests/default.xml', options.manifest_sym_path)"Converted the checkout to manifest groups based minilayout.")
def main(argv):
parser = optparse.OptionParser(usage='usage: %prog add [options] <name> '
'<--workon | <path> --remote <remote> >')
parser.add_option('-w', '--workon', action='store_true', dest='workon',
default=False, help='Is this a workon package?')
parser.add_option('-r', '--remote', dest='remote',
parser.add_option('-v', '--revision', dest='revision',
help="Use to override the manifest defined default "
"revision used for a given project.")
parser.add_option('--upgrade-minilayout', default=False, action='store_true',
help="Upgrade a minilayout checkout into a full.xml "
"checkout utilizing manifest groups.")
(options, args) = 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 "
options.git_config = os.path.join(repo_dir, 'manifests.git', 'config')
options.repo_dir = repo_dir
options.local_manifest_path = os.path.join(repo_dir, 'local_manifest.xml')
# This constant is used only when we're doing an upgrade away from
# minilayout.xml to default.xml.
options.default_manifest_path = os.path.join(repo_dir, 'manifests',
options.manifest_sym_path = os.path.join(repo_dir, 'manifest.xml')
active_manifest = os.path.basename(os.readlink(options.manifest_sym_path))
upgrade_required = active_manifest == 'minilayout.xml'
if options.upgrade_minilayout:
if args:
parser.error("--upgrade-minilayout takes no arguments.")
if not upgrade_required:
print("This repository checkout isn't using minilayout.xml; "
"nothing to do")
return 0
elif upgrade_required:
"Your repository checkout is using the old minilayout.xml workflow; "
"auto-upgrading it.")
[sys.argv[0], '--upgrade-minilayout'], cwd=os.getcwd(), print_cmd=False)
if not args:
parser.error("No command specified.")
elif args[0] != 'add':
parser.error("Only supported subcommand is add right now.")
elif options.workon:
if len(args) != 2:
"Argument count is wrong for --workon; must be add <project>")
name, path = args[1], None
if options.remote is None:
parser.error('Adding non-workon projects requires a remote.')
elif len(args) != 3:
"Argument count is wrong for non-workon mode; "
"must be add <project> <path> --remote <remote-arg>")
name, path = args[1:]
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 = Manifest.FromPath(options.manifest_sym_path,
local_manifest = Manifest.FromPath(options.local_manifest_path)
main_element = main_manifest.GetProject(name, path=path)
if options.workon:
if main_element is None:
parser.error('No project named %r in the default manifest.' % name)
_AddProjectsToManifestGroups(options, main_element.attrib['name'])
elif main_element is not None:
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, main_element.attrib['name'])
# Conflict will occur; complain.
parser.error("Requested project name=%r path=%r will conflict with "
"your current manifest %s" % (name, path, active_manifest))
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))
element = local_manifest.AddNonWorkonProject(name=name, path=path,
_AddProjectsToManifestGroups(options, element.attrib['name'])
with open(options.local_manifest_path, 'w') as f:
return 0