blob: 5de8c2f422631f5d9c41787a01e030d5d59ee7c8 [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
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 optparse
11import os
Brian Harringb0043ab2012-08-05 04:09:56 -070012import sys
Brian Harring7fcc02e2012-08-05 04:10:57 -070013import xml.etree.ElementTree as ElementTree
Mike Frysinger750c5f52014-09-16 16:16:57 -040014
Brian Harringb0043ab2012-08-05 04:09:56 -070015from chromite.lib import cros_build_lib
Ralph Nathan91874ca2015-03-19 13:29:41 -070016from chromite.lib import cros_logging as logging
David James97d95872012-11-16 15:09:56 -080017from chromite.lib import git
Brian Harring7fcc02e2012-08-05 04:10:57 -070018
19
Brian Harringb0043ab2012-08-05 04:09:56 -070020class Manifest(object):
Brian Harring7fcc02e2012-08-05 04:10:57 -070021 """Class which provides an abstraction for manipulating the local manifest."""
22
Brian Harringb0043ab2012-08-05 04:09:56 -070023 @classmethod
24 def FromPath(cls, path, empty_if_missing=False):
25 if os.path.isfile(path):
26 with open(path) as f:
27 return cls(f.read())
28 elif empty_if_missing:
29 cros_build_lib.Die('Manifest file, %r, not found' % path)
30 return cls()
31
Brian Harring7fcc02e2012-08-05 04:10:57 -070032 def __init__(self, text=None):
33 self._text = text or '<manifest>\n</manifest>'
Brian Harringb0043ab2012-08-05 04:09:56 -070034 self.nodes = ElementTree.fromstring(self._text)
Brian Harring7fcc02e2012-08-05 04:10:57 -070035
Rhyland Kleinb1d1f382012-08-23 11:53:45 -040036 def AddNonWorkonProject(self, name, path, remote=None, revision=None):
Brian Harringb0043ab2012-08-05 04:09:56 -070037 """Add a new nonworkon project element to the manifest tree."""
38 element = ElementTree.Element('project', name=name, path=path,
Rhyland Kleindd8ebbb2012-09-06 11:51:30 -040039 remote=remote)
Brian Harringb0043ab2012-08-05 04:09:56 -070040 element.attrib['workon'] = 'False'
Rhyland Kleindd8ebbb2012-09-06 11:51:30 -040041 if revision is not None:
42 element.attrib['revision'] = revision
Brian Harringb0043ab2012-08-05 04:09:56 -070043 self.nodes.append(element)
44 return element
Brian Harring7fcc02e2012-08-05 04:10:57 -070045
Brian Harringb0043ab2012-08-05 04:09:56 -070046 def GetProject(self, name, path=None):
Brian Harring7fcc02e2012-08-05 04:10:57 -070047 """Accessor method for getting a project node from the manifest tree.
48
49 Returns:
50 project element node from ElementTree, otherwise, None
51 """
Brian Harringb0043ab2012-08-05 04:09:56 -070052 if path is None:
53 # Use a unique value that can't ever match.
54 path = object()
55 for project in self.nodes.findall('project'):
56 if project.attrib['name'] == name or project.attrib['path'] == path:
Brian Harring7fcc02e2012-08-05 04:10:57 -070057 return project
58 return None
59
60 def ToString(self):
Brian Harringb0043ab2012-08-05 04:09:56 -070061 # Reset the tail for each node, then just do a hacky replace.
62 project = None
63 for project in self.nodes.findall('project'):
64 project.tail = '\n '
65 if project is not None:
66 # Tweak the last project to not have the trailing space.
67 project.tail = '\n'
68 # Fix manifest tag text and tail.
69 self.nodes.text = '\n '
70 self.nodes.tail = '\n'
71 return ElementTree.tostring(self.nodes)
72
73 def GetProjects(self):
74 return list(self.nodes.findall('project'))
75
76
Mike Frysingerc15efa52013-12-12 01:13:56 -050077def _AddProjectsToManifestGroups(options, *args):
Brian Harringb0043ab2012-08-05 04:09:56 -070078 """Enable the given manifest groups for the configured repository."""
79
Mike Frysingerc15efa52013-12-12 01:13:56 -050080 groups_to_enable = ['name:%s' % x for x in args]
Brian Harringb0043ab2012-08-05 04:09:56 -070081
82 git_config = options.git_config
83
David James67d73252013-09-19 17:33:12 -070084 cmd = ['config', '-f', git_config, '--get', 'manifest.groups']
85 enabled_groups = git.RunGit('.', cmd, error_code_ok=True).output.split(',')
Brian Harringb0043ab2012-08-05 04:09:56 -070086
87 # Note that ordering actually matters, thus why the following code
88 # is written this way.
89 # Per repo behaviour, enforce an appropriate platform group if
90 # we're converting from a default manifest group to a limited one.
91 # Finally, note we reprocess the existing groups; this is to allow
92 # us to cleanup any user screwups, or our own screwups.
93 requested_groups = (
94 ['minilayout', 'platform-%s' % (platform.system().lower(),)] +
95 enabled_groups + list(groups_to_enable))
96
97 processed_groups = set()
98 finalized_groups = []
99
100 for group in requested_groups:
101 if group not in processed_groups:
102 finalized_groups.append(group)
103 processed_groups.add(group)
104
David James67d73252013-09-19 17:33:12 -0700105 cmd = ['config', '-f', git_config, 'manifest.groups',
106 ','.join(finalized_groups)]
107 git.RunGit('.', cmd)
Brian Harringb0043ab2012-08-05 04:09:56 -0700108
109
110def _UpgradeMinilayout(options):
111 """Convert a repo checkout away from minilayout.xml to default.xml."""
112
113 full_tree = Manifest.FromPath(options.default_manifest_path)
114 local_manifest_exists = os.path.exists(options.local_manifest_path)
115
116 new_groups = []
117 if local_manifest_exists:
118 local_tree = Manifest.FromPath(options.local_manifest_path)
119 # Identify which projects need to be transferred across.
120 projects = local_tree.GetProjects()
121 new_groups = [x.attrib['name'] for x in projects]
122 allowed = set(x.attrib['name'] for x in full_tree.GetProjects())
123 transferred = [x for x in projects if x.attrib['name'] in allowed]
124 for project in transferred:
125 # Mangle local_manifest object, removing those projects;
126 # note we'll still be adding those projects to the default groups,
127 # including those that didn't intersect the main manifest.
128 local_tree.nodes.remove(project)
129
130 _AddProjectsToManifestGroups(options, *new_groups)
131
132 if local_manifest_exists:
133 # Rewrite the local_manifest now; if there is no settings left in
134 # the local_manifest, wipe it.
135 if local_tree.nodes.getchildren():
136 with open(options.local_manifest_path, 'w') as f:
137 f.write(local_tree.ToString())
138 else:
139 os.unlink(options.local_manifest_path)
140
141 # Finally, move the symlink.
142 os.unlink(options.manifest_sym_path)
143 os.symlink('manifests/default.xml', options.manifest_sym_path)
144 logging.info("Converted the checkout to manifest groups based minilayout.")
Brian Harring7fcc02e2012-08-05 04:10:57 -0700145
146
147def main(argv):
Brian Harringb0043ab2012-08-05 04:09:56 -0700148 parser = optparse.OptionParser(usage='usage: %prog add [options] <name> '
149 '<--workon | <path> --remote <remote> >')
Brian Harring7fcc02e2012-08-05 04:10:57 -0700150 parser.add_option('-w', '--workon', action='store_true', dest='workon',
151 default=False, help='Is this a workon package?')
Brian Harring7fcc02e2012-08-05 04:10:57 -0700152 parser.add_option('-r', '--remote', dest='remote',
153 default=None)
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400154 parser.add_option('-v', '--revision', dest='revision',
155 default=None,
156 help="Use to override the manifest defined default "
157 "revision used for a given project.")
Brian Harringb0043ab2012-08-05 04:09:56 -0700158 parser.add_option('--upgrade-minilayout', default=False, action='store_true',
159 help="Upgrade a minilayout checkout into a full.xml "
160 "checkout utilizing manifest groups.")
161 (options, args) = parser.parse_args(argv)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700162
Ryan Cui0b1b94b2012-12-21 12:09:57 -0800163 repo_dir = git.FindRepoDir(os.getcwd())
Brian Harringb0043ab2012-08-05 04:09:56 -0700164 if not repo_dir:
165 parser.error("This script must be invoked from within a repository "
166 "checkout.")
Brian Harring7fcc02e2012-08-05 04:10:57 -0700167
Brian Harringb0043ab2012-08-05 04:09:56 -0700168 options.git_config = os.path.join(repo_dir, 'manifests.git', 'config')
169 options.repo_dir = repo_dir
170 options.local_manifest_path = os.path.join(repo_dir, 'local_manifest.xml')
171 # This constant is used only when we're doing an upgrade away from
172 # minilayout.xml to default.xml.
173 options.default_manifest_path = os.path.join(repo_dir, 'manifests',
174 'default.xml')
175 options.manifest_sym_path = os.path.join(repo_dir, 'manifest.xml')
176
177 active_manifest = os.path.basename(os.readlink(options.manifest_sym_path))
178 upgrade_required = active_manifest == 'minilayout.xml'
179
180 if options.upgrade_minilayout:
181 if args:
182 parser.error("--upgrade-minilayout takes no arguments.")
183 if not upgrade_required:
Mike Frysinger383367e2014-09-16 15:06:17 -0400184 print("This repository checkout isn't using minilayout.xml; "
185 "nothing to do")
Brian Harringb0043ab2012-08-05 04:09:56 -0700186 else:
187 _UpgradeMinilayout(options)
188 return 0
189 elif upgrade_required:
190 logging.warn(
191 "Your repository checkout is using the old minilayout.xml workflow; "
192 "auto-upgrading it.")
193 cros_build_lib.RunCommand(
Mike Frysingerd6e2df02014-11-26 02:55:04 -0500194 [sys.argv[0], '--upgrade-minilayout'], cwd=os.getcwd(), print_cmd=False)
Brian Harringb0043ab2012-08-05 04:09:56 -0700195
196 if not args:
197 parser.error("No command specified.")
198 elif args[0] != 'add':
199 parser.error("Only supported subcommand is add right now.")
200 elif options.workon:
201 if len(args) != 2:
202 parser.error(
203 "Argument count is wrong for --workon; must be add <project>")
204 name, path = args[1], None
Brian Harring7fcc02e2012-08-05 04:10:57 -0700205 else:
Brian Harringb0043ab2012-08-05 04:09:56 -0700206 if options.remote is None:
207 parser.error('Adding non-workon projects requires a remote.')
208 elif len(args) != 3:
209 parser.error(
210 "Argument count is wrong for non-workon mode; "
211 "must be add <project> <path> --remote <remote-arg>")
212 name, path = args[1:]
Brian Harring7fcc02e2012-08-05 04:10:57 -0700213
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400214 revision = options.revision
215 if revision is not None:
David James97d95872012-11-16 15:09:56 -0800216 if (not git.IsRefsTags(revision) and
217 not git.IsSHA1(revision)):
218 revision = git.StripRefsHeads(revision, False)
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400219
Brian Harringb0043ab2012-08-05 04:09:56 -0700220 main_manifest = Manifest.FromPath(options.manifest_sym_path,
221 empty_if_missing=False)
222 local_manifest = Manifest.FromPath(options.local_manifest_path)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700223
Brian Harringb0043ab2012-08-05 04:09:56 -0700224 main_element = main_manifest.GetProject(name, path=path)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700225
Brian Harringb0043ab2012-08-05 04:09:56 -0700226 if options.workon:
227 if main_element is None:
228 parser.error('No project named %r in the default manifest.' % name)
229 _AddProjectsToManifestGroups(options, main_element.attrib['name'])
230
Rhyland Kleinf33f6222012-10-25 12:15:42 -0400231 elif main_element is not None:
Rhyland Kleine5faad52012-10-31 11:58:19 -0400232 if options.remote is not None:
233 # Likely this project wasn't meant to be remote, so workon main element
Mike Frysinger383367e2014-09-16 15:06:17 -0400234 print("Project already exists in manifest. Using that as workon project.")
Rhyland Kleine5faad52012-10-31 11:58:19 -0400235 _AddProjectsToManifestGroups(options, main_element.attrib['name'])
236 else:
237 # Conflict will occur; complain.
238 parser.error("Requested project name=%r path=%r will conflict with "
239 "your current manifest %s" % (name, path, active_manifest))
Brian Harringb0043ab2012-08-05 04:09:56 -0700240
241 elif local_manifest.GetProject(name, path=path) is not None:
242 parser.error("Requested project name=%r path=%r conflicts with "
243 "your local_manifest.xml" % (name, path))
244
245 else:
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400246 element = local_manifest.AddNonWorkonProject(name=name, path=path,
247 remote=options.remote,
248 revision=revision)
Brian Harringb0043ab2012-08-05 04:09:56 -0700249 _AddProjectsToManifestGroups(options, element.attrib['name'])
250
251 with open(options.local_manifest_path, 'w') as f:
252 f.write(local_manifest.ToString())
253 return 0