blob: 7d04b7f588527e8e61403987633665bf184a3200 [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
5"""This module allows adding and deleting of projects to the local manifest."""
6
Brian Harringb0043ab2012-08-05 04:09:56 -07007import logging
8import platform
Brian Harring7fcc02e2012-08-05 04:10:57 -07009import optparse
10import os
Brian Harringb0043ab2012-08-05 04:09:56 -070011import sys
Brian Harring7fcc02e2012-08-05 04:10:57 -070012import xml.etree.ElementTree as ElementTree
Brian Harringb0043ab2012-08-05 04:09:56 -070013from chromite.lib import cros_build_lib
Brian Harring7fcc02e2012-08-05 04:10:57 -070014
15
Brian Harringb0043ab2012-08-05 04:09:56 -070016class Manifest(object):
Brian Harring7fcc02e2012-08-05 04:10:57 -070017 """Class which provides an abstraction for manipulating the local manifest."""
18
Brian Harringb0043ab2012-08-05 04:09:56 -070019 @classmethod
20 def FromPath(cls, path, empty_if_missing=False):
21 if os.path.isfile(path):
22 with open(path) as f:
23 return cls(f.read())
24 elif empty_if_missing:
25 cros_build_lib.Die('Manifest file, %r, not found' % path)
26 return cls()
27
Brian Harring7fcc02e2012-08-05 04:10:57 -070028 def __init__(self, text=None):
29 self._text = text or '<manifest>\n</manifest>'
Brian Harringb0043ab2012-08-05 04:09:56 -070030 self.nodes = ElementTree.fromstring(self._text)
Brian Harring7fcc02e2012-08-05 04:10:57 -070031
Rhyland Kleinb1d1f382012-08-23 11:53:45 -040032 def AddNonWorkonProject(self, name, path, remote=None, revision=None):
Brian Harringb0043ab2012-08-05 04:09:56 -070033 """Add a new nonworkon project element to the manifest tree."""
34 element = ElementTree.Element('project', name=name, path=path,
Rhyland Kleinb1d1f382012-08-23 11:53:45 -040035 remote=remote, revision=revision)
Brian Harringb0043ab2012-08-05 04:09:56 -070036 element.attrib['workon'] = 'False'
37 self.nodes.append(element)
38 return element
Brian Harring7fcc02e2012-08-05 04:10:57 -070039
Brian Harringb0043ab2012-08-05 04:09:56 -070040 def GetProject(self, name, path=None):
Brian Harring7fcc02e2012-08-05 04:10:57 -070041 """Accessor method for getting a project node from the manifest tree.
42
43 Returns:
44 project element node from ElementTree, otherwise, None
45 """
Brian Harringb0043ab2012-08-05 04:09:56 -070046 if path is None:
47 # Use a unique value that can't ever match.
48 path = object()
49 for project in self.nodes.findall('project'):
50 if project.attrib['name'] == name or project.attrib['path'] == path:
Brian Harring7fcc02e2012-08-05 04:10:57 -070051 return project
52 return None
53
54 def ToString(self):
Brian Harringb0043ab2012-08-05 04:09:56 -070055 # Reset the tail for each node, then just do a hacky replace.
56 project = None
57 for project in self.nodes.findall('project'):
58 project.tail = '\n '
59 if project is not None:
60 # Tweak the last project to not have the trailing space.
61 project.tail = '\n'
62 # Fix manifest tag text and tail.
63 self.nodes.text = '\n '
64 self.nodes.tail = '\n'
65 return ElementTree.tostring(self.nodes)
66
67 def GetProjects(self):
68 return list(self.nodes.findall('project'))
69
70
71def _AddProjectsToManifestGroups(options, *projects):
72 """Enable the given manifest groups for the configured repository."""
73
74 groups_to_enable = ['name:%s' % x for x in projects]
75
76 git_config = options.git_config
77
78 enabled_groups = cros_build_lib.RunCommandCaptureOutput(
79 ['git', 'config', '-f', git_config, '--get', 'manifest.groups'],
80 error_code_ok=True, print_cmd=False).output.split(',')
81
82 # Note that ordering actually matters, thus why the following code
83 # is written this way.
84 # Per repo behaviour, enforce an appropriate platform group if
85 # we're converting from a default manifest group to a limited one.
86 # Finally, note we reprocess the existing groups; this is to allow
87 # us to cleanup any user screwups, or our own screwups.
88 requested_groups = (
89 ['minilayout', 'platform-%s' % (platform.system().lower(),)] +
90 enabled_groups + list(groups_to_enable))
91
92 processed_groups = set()
93 finalized_groups = []
94
95 for group in requested_groups:
96 if group not in processed_groups:
97 finalized_groups.append(group)
98 processed_groups.add(group)
99
100 cros_build_lib.RunCommandCaptureOutput(
101 ['git', 'config', '-f', git_config, 'manifest.groups',
102 ','.join(finalized_groups)], print_cmd=False)
103
104
105def _UpgradeMinilayout(options):
106 """Convert a repo checkout away from minilayout.xml to default.xml."""
107
108 full_tree = Manifest.FromPath(options.default_manifest_path)
109 local_manifest_exists = os.path.exists(options.local_manifest_path)
110
111 new_groups = []
112 if local_manifest_exists:
113 local_tree = Manifest.FromPath(options.local_manifest_path)
114 # Identify which projects need to be transferred across.
115 projects = local_tree.GetProjects()
116 new_groups = [x.attrib['name'] for x in projects]
117 allowed = set(x.attrib['name'] for x in full_tree.GetProjects())
118 transferred = [x for x in projects if x.attrib['name'] in allowed]
119 for project in transferred:
120 # Mangle local_manifest object, removing those projects;
121 # note we'll still be adding those projects to the default groups,
122 # including those that didn't intersect the main manifest.
123 local_tree.nodes.remove(project)
124
125 _AddProjectsToManifestGroups(options, *new_groups)
126
127 if local_manifest_exists:
128 # Rewrite the local_manifest now; if there is no settings left in
129 # the local_manifest, wipe it.
130 if local_tree.nodes.getchildren():
131 with open(options.local_manifest_path, 'w') as f:
132 f.write(local_tree.ToString())
133 else:
134 os.unlink(options.local_manifest_path)
135
136 # Finally, move the symlink.
137 os.unlink(options.manifest_sym_path)
138 os.symlink('manifests/default.xml', options.manifest_sym_path)
139 logging.info("Converted the checkout to manifest groups based minilayout.")
Brian Harring7fcc02e2012-08-05 04:10:57 -0700140
141
142def main(argv):
Brian Harringb0043ab2012-08-05 04:09:56 -0700143 parser = optparse.OptionParser(usage='usage: %prog add [options] <name> '
144 '<--workon | <path> --remote <remote> >')
Brian Harring7fcc02e2012-08-05 04:10:57 -0700145 parser.add_option('-w', '--workon', action='store_true', dest='workon',
146 default=False, help='Is this a workon package?')
Brian Harring7fcc02e2012-08-05 04:10:57 -0700147 parser.add_option('-r', '--remote', dest='remote',
148 default=None)
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400149 parser.add_option('-v', '--revision', dest='revision',
150 default=None,
151 help="Use to override the manifest defined default "
152 "revision used for a given project.")
Brian Harringb0043ab2012-08-05 04:09:56 -0700153 parser.add_option('--upgrade-minilayout', default=False, action='store_true',
154 help="Upgrade a minilayout checkout into a full.xml "
155 "checkout utilizing manifest groups.")
156 (options, args) = parser.parse_args(argv)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700157
Brian Harringb0043ab2012-08-05 04:09:56 -0700158 repo_dir = cros_build_lib.FindRepoDir()
159 if not repo_dir:
160 parser.error("This script must be invoked from within a repository "
161 "checkout.")
Brian Harring7fcc02e2012-08-05 04:10:57 -0700162
Brian Harringb0043ab2012-08-05 04:09:56 -0700163 options.git_config = os.path.join(repo_dir, 'manifests.git', 'config')
164 options.repo_dir = repo_dir
165 options.local_manifest_path = os.path.join(repo_dir, 'local_manifest.xml')
166 # This constant is used only when we're doing an upgrade away from
167 # minilayout.xml to default.xml.
168 options.default_manifest_path = os.path.join(repo_dir, 'manifests',
169 'default.xml')
170 options.manifest_sym_path = os.path.join(repo_dir, 'manifest.xml')
171
172 active_manifest = os.path.basename(os.readlink(options.manifest_sym_path))
173 upgrade_required = active_manifest == 'minilayout.xml'
174
175 if options.upgrade_minilayout:
176 if args:
177 parser.error("--upgrade-minilayout takes no arguments.")
178 if not upgrade_required:
179 print "This repository checkout isn't using minilayout.xml; nothing to do"
180 else:
181 _UpgradeMinilayout(options)
182 return 0
183 elif upgrade_required:
184 logging.warn(
185 "Your repository checkout is using the old minilayout.xml workflow; "
186 "auto-upgrading it.")
187 cros_build_lib.RunCommand(
188 [sys.argv[0], '--upgrade-minilayout'], cwd=os.getcwd(),
189 print_cmd=False)
190
191 if not args:
192 parser.error("No command specified.")
193 elif args[0] != 'add':
194 parser.error("Only supported subcommand is add right now.")
195 elif options.workon:
196 if len(args) != 2:
197 parser.error(
198 "Argument count is wrong for --workon; must be add <project>")
199 name, path = args[1], None
Brian Harring7fcc02e2012-08-05 04:10:57 -0700200 else:
Brian Harringb0043ab2012-08-05 04:09:56 -0700201 if options.remote is None:
202 parser.error('Adding non-workon projects requires a remote.')
203 elif len(args) != 3:
204 parser.error(
205 "Argument count is wrong for non-workon mode; "
206 "must be add <project> <path> --remote <remote-arg>")
207 name, path = args[1:]
Brian Harring7fcc02e2012-08-05 04:10:57 -0700208
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400209 revision = options.revision
210 if revision is not None:
211 if (not cros_build_lib.IsRefsTags(revision) and
212 not cros_build_lib.IsSHA1(revision)):
213 revision = cros_build_lib.StripLeadingRefsHeads(revision, False)
214
Brian Harringb0043ab2012-08-05 04:09:56 -0700215 main_manifest = Manifest.FromPath(options.manifest_sym_path,
216 empty_if_missing=False)
217 local_manifest = Manifest.FromPath(options.local_manifest_path)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700218
Brian Harringb0043ab2012-08-05 04:09:56 -0700219 main_element = main_manifest.GetProject(name, path=path)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700220
Brian Harringb0043ab2012-08-05 04:09:56 -0700221 if options.workon:
222 if main_element is None:
223 parser.error('No project named %r in the default manifest.' % name)
224 _AddProjectsToManifestGroups(options, main_element.attrib['name'])
225
226 elif main_element:
227 # Conflict will occur; complain.
228 parser.error("Requested project name=%r path=%r will conflict with "
229 "your current manifest %s" % (name, path, active_manifest))
230
231 elif local_manifest.GetProject(name, path=path) is not None:
232 parser.error("Requested project name=%r path=%r conflicts with "
233 "your local_manifest.xml" % (name, path))
234
235 else:
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400236 element = local_manifest.AddNonWorkonProject(name=name, path=path,
237 remote=options.remote,
238 revision=revision)
Brian Harringb0043ab2012-08-05 04:09:56 -0700239 _AddProjectsToManifestGroups(options, element.attrib['name'])
240
241 with open(options.local_manifest_path, 'w') as f:
242 f.write(local_manifest.ToString())
243 return 0