Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 1 | # 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 Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 7 | import logging |
| 8 | import platform |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 9 | import optparse |
| 10 | import os |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 11 | import sys |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 12 | import xml.etree.ElementTree as ElementTree |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 13 | from chromite.lib import cros_build_lib |
David James | 97d9587 | 2012-11-16 15:09:56 -0800 | [diff] [blame] | 14 | from chromite.lib import git |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 15 | |
| 16 | |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 17 | class Manifest(object): |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 18 | """Class which provides an abstraction for manipulating the local manifest.""" |
| 19 | |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 20 | @classmethod |
| 21 | def FromPath(cls, path, empty_if_missing=False): |
| 22 | if os.path.isfile(path): |
| 23 | with open(path) as f: |
| 24 | return cls(f.read()) |
| 25 | elif empty_if_missing: |
| 26 | cros_build_lib.Die('Manifest file, %r, not found' % path) |
| 27 | return cls() |
| 28 | |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 29 | def __init__(self, text=None): |
| 30 | self._text = text or '<manifest>\n</manifest>' |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 31 | self.nodes = ElementTree.fromstring(self._text) |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 32 | |
Rhyland Klein | b1d1f38 | 2012-08-23 11:53:45 -0400 | [diff] [blame] | 33 | def AddNonWorkonProject(self, name, path, remote=None, revision=None): |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 34 | """Add a new nonworkon project element to the manifest tree.""" |
| 35 | element = ElementTree.Element('project', name=name, path=path, |
Rhyland Klein | dd8ebbb | 2012-09-06 11:51:30 -0400 | [diff] [blame] | 36 | remote=remote) |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 37 | element.attrib['workon'] = 'False' |
Rhyland Klein | dd8ebbb | 2012-09-06 11:51:30 -0400 | [diff] [blame] | 38 | if revision is not None: |
| 39 | element.attrib['revision'] = revision |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 40 | self.nodes.append(element) |
| 41 | return element |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 42 | |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 43 | def GetProject(self, name, path=None): |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 44 | """Accessor method for getting a project node from the manifest tree. |
| 45 | |
| 46 | Returns: |
| 47 | project element node from ElementTree, otherwise, None |
| 48 | """ |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 49 | if path is None: |
| 50 | # Use a unique value that can't ever match. |
| 51 | path = object() |
| 52 | for project in self.nodes.findall('project'): |
| 53 | if project.attrib['name'] == name or project.attrib['path'] == path: |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 54 | return project |
| 55 | return None |
| 56 | |
| 57 | def ToString(self): |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 58 | # Reset the tail for each node, then just do a hacky replace. |
| 59 | project = None |
| 60 | for project in self.nodes.findall('project'): |
| 61 | project.tail = '\n ' |
| 62 | if project is not None: |
| 63 | # Tweak the last project to not have the trailing space. |
| 64 | project.tail = '\n' |
| 65 | # Fix manifest tag text and tail. |
| 66 | self.nodes.text = '\n ' |
| 67 | self.nodes.tail = '\n' |
| 68 | return ElementTree.tostring(self.nodes) |
| 69 | |
| 70 | def GetProjects(self): |
| 71 | return list(self.nodes.findall('project')) |
| 72 | |
| 73 | |
| 74 | def _AddProjectsToManifestGroups(options, *projects): |
| 75 | """Enable the given manifest groups for the configured repository.""" |
| 76 | |
| 77 | groups_to_enable = ['name:%s' % x for x in projects] |
| 78 | |
| 79 | git_config = options.git_config |
| 80 | |
David James | 67d7325 | 2013-09-19 17:33:12 -0700 | [diff] [blame^] | 81 | cmd = ['config', '-f', git_config, '--get', 'manifest.groups'] |
| 82 | enabled_groups = git.RunGit('.', cmd, error_code_ok=True).output.split(',') |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 83 | |
| 84 | # Note that ordering actually matters, thus why the following code |
| 85 | # is written this way. |
| 86 | # Per repo behaviour, enforce an appropriate platform group if |
| 87 | # we're converting from a default manifest group to a limited one. |
| 88 | # Finally, note we reprocess the existing groups; this is to allow |
| 89 | # us to cleanup any user screwups, or our own screwups. |
| 90 | requested_groups = ( |
| 91 | ['minilayout', 'platform-%s' % (platform.system().lower(),)] + |
| 92 | enabled_groups + list(groups_to_enable)) |
| 93 | |
| 94 | processed_groups = set() |
| 95 | finalized_groups = [] |
| 96 | |
| 97 | for group in requested_groups: |
| 98 | if group not in processed_groups: |
| 99 | finalized_groups.append(group) |
| 100 | processed_groups.add(group) |
| 101 | |
David James | 67d7325 | 2013-09-19 17:33:12 -0700 | [diff] [blame^] | 102 | cmd = ['config', '-f', git_config, 'manifest.groups', |
| 103 | ','.join(finalized_groups)] |
| 104 | git.RunGit('.', cmd) |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 105 | |
| 106 | |
| 107 | def _UpgradeMinilayout(options): |
| 108 | """Convert a repo checkout away from minilayout.xml to default.xml.""" |
| 109 | |
| 110 | full_tree = Manifest.FromPath(options.default_manifest_path) |
| 111 | local_manifest_exists = os.path.exists(options.local_manifest_path) |
| 112 | |
| 113 | new_groups = [] |
| 114 | if local_manifest_exists: |
| 115 | local_tree = Manifest.FromPath(options.local_manifest_path) |
| 116 | # Identify which projects need to be transferred across. |
| 117 | projects = local_tree.GetProjects() |
| 118 | new_groups = [x.attrib['name'] for x in projects] |
| 119 | allowed = set(x.attrib['name'] for x in full_tree.GetProjects()) |
| 120 | transferred = [x for x in projects if x.attrib['name'] in allowed] |
| 121 | for project in transferred: |
| 122 | # Mangle local_manifest object, removing those projects; |
| 123 | # note we'll still be adding those projects to the default groups, |
| 124 | # including those that didn't intersect the main manifest. |
| 125 | local_tree.nodes.remove(project) |
| 126 | |
| 127 | _AddProjectsToManifestGroups(options, *new_groups) |
| 128 | |
| 129 | if local_manifest_exists: |
| 130 | # Rewrite the local_manifest now; if there is no settings left in |
| 131 | # the local_manifest, wipe it. |
| 132 | if local_tree.nodes.getchildren(): |
| 133 | with open(options.local_manifest_path, 'w') as f: |
| 134 | f.write(local_tree.ToString()) |
| 135 | else: |
| 136 | os.unlink(options.local_manifest_path) |
| 137 | |
| 138 | # Finally, move the symlink. |
| 139 | os.unlink(options.manifest_sym_path) |
| 140 | os.symlink('manifests/default.xml', options.manifest_sym_path) |
| 141 | logging.info("Converted the checkout to manifest groups based minilayout.") |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 142 | |
| 143 | |
| 144 | def main(argv): |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 145 | parser = optparse.OptionParser(usage='usage: %prog add [options] <name> ' |
| 146 | '<--workon | <path> --remote <remote> >') |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 147 | parser.add_option('-w', '--workon', action='store_true', dest='workon', |
| 148 | default=False, help='Is this a workon package?') |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 149 | parser.add_option('-r', '--remote', dest='remote', |
| 150 | default=None) |
Rhyland Klein | b1d1f38 | 2012-08-23 11:53:45 -0400 | [diff] [blame] | 151 | parser.add_option('-v', '--revision', dest='revision', |
| 152 | default=None, |
| 153 | help="Use to override the manifest defined default " |
| 154 | "revision used for a given project.") |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 155 | parser.add_option('--upgrade-minilayout', default=False, action='store_true', |
| 156 | help="Upgrade a minilayout checkout into a full.xml " |
| 157 | "checkout utilizing manifest groups.") |
| 158 | (options, args) = parser.parse_args(argv) |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 159 | |
Ryan Cui | 0b1b94b | 2012-12-21 12:09:57 -0800 | [diff] [blame] | 160 | repo_dir = git.FindRepoDir(os.getcwd()) |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 161 | if not repo_dir: |
| 162 | parser.error("This script must be invoked from within a repository " |
| 163 | "checkout.") |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 164 | |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 165 | options.git_config = os.path.join(repo_dir, 'manifests.git', 'config') |
| 166 | options.repo_dir = repo_dir |
| 167 | options.local_manifest_path = os.path.join(repo_dir, 'local_manifest.xml') |
| 168 | # This constant is used only when we're doing an upgrade away from |
| 169 | # minilayout.xml to default.xml. |
| 170 | options.default_manifest_path = os.path.join(repo_dir, 'manifests', |
| 171 | 'default.xml') |
| 172 | options.manifest_sym_path = os.path.join(repo_dir, 'manifest.xml') |
| 173 | |
| 174 | active_manifest = os.path.basename(os.readlink(options.manifest_sym_path)) |
| 175 | upgrade_required = active_manifest == 'minilayout.xml' |
| 176 | |
| 177 | if options.upgrade_minilayout: |
| 178 | if args: |
| 179 | parser.error("--upgrade-minilayout takes no arguments.") |
| 180 | if not upgrade_required: |
| 181 | print "This repository checkout isn't using minilayout.xml; nothing to do" |
| 182 | else: |
| 183 | _UpgradeMinilayout(options) |
| 184 | return 0 |
| 185 | elif upgrade_required: |
| 186 | logging.warn( |
| 187 | "Your repository checkout is using the old minilayout.xml workflow; " |
| 188 | "auto-upgrading it.") |
| 189 | cros_build_lib.RunCommand( |
| 190 | [sys.argv[0], '--upgrade-minilayout'], cwd=os.getcwd(), |
| 191 | print_cmd=False) |
| 192 | |
| 193 | if not args: |
| 194 | parser.error("No command specified.") |
| 195 | elif args[0] != 'add': |
| 196 | parser.error("Only supported subcommand is add right now.") |
| 197 | elif options.workon: |
| 198 | if len(args) != 2: |
| 199 | parser.error( |
| 200 | "Argument count is wrong for --workon; must be add <project>") |
| 201 | name, path = args[1], None |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 202 | else: |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 203 | if options.remote is None: |
| 204 | parser.error('Adding non-workon projects requires a remote.') |
| 205 | elif len(args) != 3: |
| 206 | parser.error( |
| 207 | "Argument count is wrong for non-workon mode; " |
| 208 | "must be add <project> <path> --remote <remote-arg>") |
| 209 | name, path = args[1:] |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 210 | |
Rhyland Klein | b1d1f38 | 2012-08-23 11:53:45 -0400 | [diff] [blame] | 211 | revision = options.revision |
| 212 | if revision is not None: |
David James | 97d9587 | 2012-11-16 15:09:56 -0800 | [diff] [blame] | 213 | if (not git.IsRefsTags(revision) and |
| 214 | not git.IsSHA1(revision)): |
| 215 | revision = git.StripRefsHeads(revision, False) |
Rhyland Klein | b1d1f38 | 2012-08-23 11:53:45 -0400 | [diff] [blame] | 216 | |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 217 | main_manifest = Manifest.FromPath(options.manifest_sym_path, |
| 218 | empty_if_missing=False) |
| 219 | local_manifest = Manifest.FromPath(options.local_manifest_path) |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 220 | |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 221 | main_element = main_manifest.GetProject(name, path=path) |
Brian Harring | 7fcc02e | 2012-08-05 04:10:57 -0700 | [diff] [blame] | 222 | |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 223 | if options.workon: |
| 224 | if main_element is None: |
| 225 | parser.error('No project named %r in the default manifest.' % name) |
| 226 | _AddProjectsToManifestGroups(options, main_element.attrib['name']) |
| 227 | |
Rhyland Klein | f33f622 | 2012-10-25 12:15:42 -0400 | [diff] [blame] | 228 | elif main_element is not None: |
Rhyland Klein | e5faad5 | 2012-10-31 11:58:19 -0400 | [diff] [blame] | 229 | if options.remote is not None: |
| 230 | # Likely this project wasn't meant to be remote, so workon main element |
| 231 | print "Project already exists in manifest. Using that as workon project." |
| 232 | _AddProjectsToManifestGroups(options, main_element.attrib['name']) |
| 233 | else: |
| 234 | # Conflict will occur; complain. |
| 235 | parser.error("Requested project name=%r path=%r will conflict with " |
| 236 | "your current manifest %s" % (name, path, active_manifest)) |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 237 | |
| 238 | elif local_manifest.GetProject(name, path=path) is not None: |
| 239 | parser.error("Requested project name=%r path=%r conflicts with " |
| 240 | "your local_manifest.xml" % (name, path)) |
| 241 | |
| 242 | else: |
Rhyland Klein | b1d1f38 | 2012-08-23 11:53:45 -0400 | [diff] [blame] | 243 | element = local_manifest.AddNonWorkonProject(name=name, path=path, |
| 244 | remote=options.remote, |
| 245 | revision=revision) |
Brian Harring | b0043ab | 2012-08-05 04:09:56 -0700 | [diff] [blame] | 246 | _AddProjectsToManifestGroups(options, element.attrib['name']) |
| 247 | |
| 248 | with open(options.local_manifest_path, 'w') as f: |
| 249 | f.write(local_manifest.ToString()) |
| 250 | return 0 |