blob: 48de07b2057098a9aa60039157cbdd8fe56b26b6 [file] [log] [blame]
Brian Harring7fcc02e2012-08-05 04:10:57 -07001# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
Mike Frysinger4ca60152016-09-01 00:13:36 -04005"""Manage projects in the local manifest."""
Brian Harring7fcc02e2012-08-05 04:10:57 -07006
Mike Frysinger383367e2014-09-16 15:06:17 -04007from __future__ import print_function
8
Brian Harringb0043ab2012-08-05 04:09:56 -07009import platform
Brian Harring7fcc02e2012-08-05 04:10:57 -070010import os
11import xml.etree.ElementTree as ElementTree
Mike Frysinger750c5f52014-09-16 16:16:57 -040012
Mike Frysinger4ca60152016-09-01 00:13:36 -040013from chromite.lib import commandline
Brian Harringb0043ab2012-08-05 04:09:56 -070014from chromite.lib import cros_build_lib
Ralph Nathan91874ca2015-03-19 13:29:41 -070015from chromite.lib import cros_logging as logging
David James97d95872012-11-16 15:09:56 -080016from chromite.lib import git
Brian Harring7fcc02e2012-08-05 04:10:57 -070017
18
Brian Harringb0043ab2012-08-05 04:09:56 -070019class Manifest(object):
Brian Harring7fcc02e2012-08-05 04:10:57 -070020 """Class which provides an abstraction for manipulating the local manifest."""
21
Brian Harringb0043ab2012-08-05 04:09:56 -070022 @classmethod
23 def FromPath(cls, path, empty_if_missing=False):
24 if os.path.isfile(path):
25 with open(path) as f:
26 return cls(f.read())
27 elif empty_if_missing:
28 cros_build_lib.Die('Manifest file, %r, not found' % path)
29 return cls()
30
Brian Harring7fcc02e2012-08-05 04:10:57 -070031 def __init__(self, text=None):
32 self._text = text or '<manifest>\n</manifest>'
Brian Harringb0043ab2012-08-05 04:09:56 -070033 self.nodes = ElementTree.fromstring(self._text)
Brian Harring7fcc02e2012-08-05 04:10:57 -070034
Rhyland Kleinb1d1f382012-08-23 11:53:45 -040035 def AddNonWorkonProject(self, name, path, remote=None, revision=None):
Brian Harringb0043ab2012-08-05 04:09:56 -070036 """Add a new nonworkon project element to the manifest tree."""
37 element = ElementTree.Element('project', name=name, path=path,
Rhyland Kleindd8ebbb2012-09-06 11:51:30 -040038 remote=remote)
Brian Harringb0043ab2012-08-05 04:09:56 -070039 element.attrib['workon'] = 'False'
Rhyland Kleindd8ebbb2012-09-06 11:51:30 -040040 if revision is not None:
41 element.attrib['revision'] = revision
Brian Harringb0043ab2012-08-05 04:09:56 -070042 self.nodes.append(element)
43 return element
Brian Harring7fcc02e2012-08-05 04:10:57 -070044
Brian Harringb0043ab2012-08-05 04:09:56 -070045 def GetProject(self, name, path=None):
Brian Harring7fcc02e2012-08-05 04:10:57 -070046 """Accessor method for getting a project node from the manifest tree.
47
48 Returns:
49 project element node from ElementTree, otherwise, None
50 """
Brian Harringb0043ab2012-08-05 04:09:56 -070051 if path is None:
52 # Use a unique value that can't ever match.
53 path = object()
54 for project in self.nodes.findall('project'):
55 if project.attrib['name'] == name or project.attrib['path'] == path:
Brian Harring7fcc02e2012-08-05 04:10:57 -070056 return project
57 return None
58
59 def ToString(self):
Brian Harringb0043ab2012-08-05 04:09:56 -070060 # Reset the tail for each node, then just do a hacky replace.
61 project = None
62 for project in self.nodes.findall('project'):
63 project.tail = '\n '
64 if project is not None:
65 # Tweak the last project to not have the trailing space.
66 project.tail = '\n'
67 # Fix manifest tag text and tail.
68 self.nodes.text = '\n '
69 self.nodes.tail = '\n'
70 return ElementTree.tostring(self.nodes)
71
72 def GetProjects(self):
73 return list(self.nodes.findall('project'))
74
75
Mike Frysingerc15efa52013-12-12 01:13:56 -050076def _AddProjectsToManifestGroups(options, *args):
Brian Harringb0043ab2012-08-05 04:09:56 -070077 """Enable the given manifest groups for the configured repository."""
78
Mike Frysingerc15efa52013-12-12 01:13:56 -050079 groups_to_enable = ['name:%s' % x for x in args]
Brian Harringb0043ab2012-08-05 04:09:56 -070080
81 git_config = options.git_config
82
David James67d73252013-09-19 17:33:12 -070083 cmd = ['config', '-f', git_config, '--get', 'manifest.groups']
84 enabled_groups = git.RunGit('.', cmd, error_code_ok=True).output.split(',')
Brian Harringb0043ab2012-08-05 04:09:56 -070085
86 # Note that ordering actually matters, thus why the following code
87 # is written this way.
88 # Per repo behaviour, enforce an appropriate platform group if
89 # we're converting from a default manifest group to a limited one.
90 # Finally, note we reprocess the existing groups; this is to allow
91 # us to cleanup any user screwups, or our own screwups.
92 requested_groups = (
93 ['minilayout', 'platform-%s' % (platform.system().lower(),)] +
94 enabled_groups + list(groups_to_enable))
95
96 processed_groups = set()
97 finalized_groups = []
98
99 for group in requested_groups:
100 if group not in processed_groups:
101 finalized_groups.append(group)
102 processed_groups.add(group)
103
David James67d73252013-09-19 17:33:12 -0700104 cmd = ['config', '-f', git_config, 'manifest.groups',
105 ','.join(finalized_groups)]
106 git.RunGit('.', cmd)
Brian Harringb0043ab2012-08-05 04:09:56 -0700107
108
109def _UpgradeMinilayout(options):
110 """Convert a repo checkout away from minilayout.xml to default.xml."""
111
112 full_tree = Manifest.FromPath(options.default_manifest_path)
113 local_manifest_exists = os.path.exists(options.local_manifest_path)
114
115 new_groups = []
116 if local_manifest_exists:
117 local_tree = Manifest.FromPath(options.local_manifest_path)
118 # Identify which projects need to be transferred across.
119 projects = local_tree.GetProjects()
120 new_groups = [x.attrib['name'] for x in projects]
121 allowed = set(x.attrib['name'] for x in full_tree.GetProjects())
122 transferred = [x for x in projects if x.attrib['name'] in allowed]
123 for project in transferred:
124 # Mangle local_manifest object, removing those projects;
125 # note we'll still be adding those projects to the default groups,
126 # including those that didn't intersect the main manifest.
127 local_tree.nodes.remove(project)
128
129 _AddProjectsToManifestGroups(options, *new_groups)
130
131 if local_manifest_exists:
132 # Rewrite the local_manifest now; if there is no settings left in
133 # the local_manifest, wipe it.
134 if local_tree.nodes.getchildren():
135 with open(options.local_manifest_path, 'w') as f:
136 f.write(local_tree.ToString())
137 else:
138 os.unlink(options.local_manifest_path)
139
140 # Finally, move the symlink.
141 os.unlink(options.manifest_sym_path)
142 os.symlink('manifests/default.xml', options.manifest_sym_path)
143 logging.info("Converted the checkout to manifest groups based minilayout.")
Brian Harring7fcc02e2012-08-05 04:10:57 -0700144
145
Mike Frysinger4ca60152016-09-01 00:13:36 -0400146def GetParser():
147 """Return a command line parser."""
148 parser = commandline.ArgumentParser(description=__doc__)
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400149
150 subparsers = parser.add_subparsers(dest='command')
151
152 subparser = subparsers.add_parser(
153 'add',
154 help='Add projects to the manifest.')
155 subparser.add_argument('-w', '--workon', action='store_true',
156 default=False, help='Is this a workon package?')
157 subparser.add_argument('-r', '--remote',
158 help='Remote project name (for non-workon packages).')
159 subparser.add_argument('-v', '--revision',
160 help='Use to override the manifest defined default '
161 'revision used for a given project.')
162 subparser.add_argument('project', help='Name of project in the manifest.')
163 subparser.add_argument('path', nargs='?', help='Local path to the project.')
164
165 subparser = subparsers.add_parser(
166 'upgrade-minilayout',
167 help='Upgrade a minilayout checkout into a full.xml checkout utilizing '
168 'manifest groups.')
169
Mike Frysinger4ca60152016-09-01 00:13:36 -0400170 return parser
171
172
Brian Harring7fcc02e2012-08-05 04:10:57 -0700173def main(argv):
Mike Frysinger4ca60152016-09-01 00:13:36 -0400174 parser = GetParser()
175 options = parser.parse_args(argv)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700176
Ryan Cui0b1b94b2012-12-21 12:09:57 -0800177 repo_dir = git.FindRepoDir(os.getcwd())
Brian Harringb0043ab2012-08-05 04:09:56 -0700178 if not repo_dir:
179 parser.error("This script must be invoked from within a repository "
180 "checkout.")
Brian Harring7fcc02e2012-08-05 04:10:57 -0700181
Brian Harringb0043ab2012-08-05 04:09:56 -0700182 options.git_config = os.path.join(repo_dir, 'manifests.git', 'config')
183 options.repo_dir = repo_dir
184 options.local_manifest_path = os.path.join(repo_dir, 'local_manifest.xml')
185 # This constant is used only when we're doing an upgrade away from
186 # minilayout.xml to default.xml.
187 options.default_manifest_path = os.path.join(repo_dir, 'manifests',
188 'default.xml')
189 options.manifest_sym_path = os.path.join(repo_dir, 'manifest.xml')
190
191 active_manifest = os.path.basename(os.readlink(options.manifest_sym_path))
192 upgrade_required = active_manifest == 'minilayout.xml'
193
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400194 if options.command == 'upgrade-minilayout':
Brian Harringb0043ab2012-08-05 04:09:56 -0700195 if not upgrade_required:
Mike Frysinger383367e2014-09-16 15:06:17 -0400196 print("This repository checkout isn't using minilayout.xml; "
197 "nothing to do")
Brian Harringb0043ab2012-08-05 04:09:56 -0700198 else:
199 _UpgradeMinilayout(options)
200 return 0
201 elif upgrade_required:
Bertrand SIMONNET0ddf7762015-05-20 14:38:00 -0700202 logging.warning(
Brian Harringb0043ab2012-08-05 04:09:56 -0700203 "Your repository checkout is using the old minilayout.xml workflow; "
204 "auto-upgrading it.")
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400205 main(['upgrade-minilayout'])
Brian Harringb0043ab2012-08-05 04:09:56 -0700206
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400207 # For now, we only support the add command.
208 assert options.command == 'add'
209 if options.workon:
210 if options.path is not None:
211 parser.error('Adding workon projects do not set project.')
Brian Harring7fcc02e2012-08-05 04:10:57 -0700212 else:
Brian Harringb0043ab2012-08-05 04:09:56 -0700213 if options.remote is None:
214 parser.error('Adding non-workon projects requires a remote.')
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400215 if options.path is None:
216 parser.error('Adding non-workon projects requires a path.')
217 name = options.project
218 path = options.path
Brian Harring7fcc02e2012-08-05 04:10:57 -0700219
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400220 revision = options.revision
221 if revision is not None:
David James97d95872012-11-16 15:09:56 -0800222 if (not git.IsRefsTags(revision) and
223 not git.IsSHA1(revision)):
224 revision = git.StripRefsHeads(revision, False)
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400225
Brian Harringb0043ab2012-08-05 04:09:56 -0700226 main_manifest = Manifest.FromPath(options.manifest_sym_path,
227 empty_if_missing=False)
228 local_manifest = Manifest.FromPath(options.local_manifest_path)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700229
Brian Harringb0043ab2012-08-05 04:09:56 -0700230 main_element = main_manifest.GetProject(name, path=path)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700231
Brian Harringb0043ab2012-08-05 04:09:56 -0700232 if options.workon:
233 if main_element is None:
234 parser.error('No project named %r in the default manifest.' % name)
235 _AddProjectsToManifestGroups(options, main_element.attrib['name'])
236
Rhyland Kleinf33f6222012-10-25 12:15:42 -0400237 elif main_element is not None:
Rhyland Kleine5faad52012-10-31 11:58:19 -0400238 if options.remote is not None:
239 # Likely this project wasn't meant to be remote, so workon main element
Mike Frysinger383367e2014-09-16 15:06:17 -0400240 print("Project already exists in manifest. Using that as workon project.")
Rhyland Kleine5faad52012-10-31 11:58:19 -0400241 _AddProjectsToManifestGroups(options, main_element.attrib['name'])
242 else:
243 # Conflict will occur; complain.
244 parser.error("Requested project name=%r path=%r will conflict with "
245 "your current manifest %s" % (name, path, active_manifest))
Brian Harringb0043ab2012-08-05 04:09:56 -0700246
247 elif local_manifest.GetProject(name, path=path) is not None:
248 parser.error("Requested project name=%r path=%r conflicts with "
249 "your local_manifest.xml" % (name, path))
250
251 else:
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400252 element = local_manifest.AddNonWorkonProject(name=name, path=path,
253 remote=options.remote,
254 revision=revision)
Brian Harringb0043ab2012-08-05 04:09:56 -0700255 _AddProjectsToManifestGroups(options, element.attrib['name'])
256
257 with open(options.local_manifest_path, 'w') as f:
258 f.write(local_manifest.ToString())
259 return 0