blob: af581efd61ca80169a670e84e3de5b2c2fedb932 [file] [log] [blame]
Mike Frysingere58c0e22017-10-04 15:43:30 -04001# -*- coding: utf-8 -*-
Brian Harring7fcc02e2012-08-05 04:10:57 -07002# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysinger4ca60152016-09-01 00:13:36 -04006"""Manage projects in the local manifest."""
Brian Harring7fcc02e2012-08-05 04:10:57 -07007
Mike Frysinger383367e2014-09-16 15:06:17 -04008from __future__ import print_function
9
Brian Harringb0043ab2012-08-05 04:09:56 -070010import platform
Brian Harring7fcc02e2012-08-05 04:10:57 -070011import os
Mike Frysinger1c76d4c2020-02-08 23:35:29 -050012import sys
Brian Harring7fcc02e2012-08-05 04:10:57 -070013import xml.etree.ElementTree as ElementTree
Mike Frysinger750c5f52014-09-16 16:16:57 -040014
Mike Frysinger4ca60152016-09-01 00:13:36 -040015from chromite.lib import commandline
Brian Harringb0043ab2012-08-05 04:09:56 -070016from chromite.lib import cros_build_lib
David James97d95872012-11-16 15:09:56 -080017from chromite.lib import git
Mike Frysinger462dbd62019-10-19 20:26:46 -040018from chromite.lib import osutils
19from chromite.lib import repo_manifest
Brian Harring7fcc02e2012-08-05 04:10:57 -070020
21
Mike Frysinger1c76d4c2020-02-08 23:35:29 -050022assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
23
24
Gwendal Grignou89afc082016-09-29 21:03:20 -070025class LocalManifest(object):
Brian Harring7fcc02e2012-08-05 04:10:57 -070026 """Class which provides an abstraction for manipulating the local manifest."""
27
Brian Harringb0043ab2012-08-05 04:09:56 -070028 @classmethod
29 def FromPath(cls, path, empty_if_missing=False):
30 if os.path.isfile(path):
Mike Frysinger462dbd62019-10-19 20:26:46 -040031 return cls(osutils.ReadFile(path))
Brian Harringb0043ab2012-08-05 04:09:56 -070032 elif empty_if_missing:
33 cros_build_lib.Die('Manifest file, %r, not found' % path)
34 return cls()
35
Brian Harring7fcc02e2012-08-05 04:10:57 -070036 def __init__(self, text=None):
37 self._text = text or '<manifest>\n</manifest>'
Brian Harringb0043ab2012-08-05 04:09:56 -070038 self.nodes = ElementTree.fromstring(self._text)
Brian Harring7fcc02e2012-08-05 04:10:57 -070039
Rhyland Kleinb1d1f382012-08-23 11:53:45 -040040 def AddNonWorkonProject(self, name, path, remote=None, revision=None):
Brian Harringb0043ab2012-08-05 04:09:56 -070041 """Add a new nonworkon project element to the manifest tree."""
42 element = ElementTree.Element('project', name=name, path=path,
Rhyland Kleindd8ebbb2012-09-06 11:51:30 -040043 remote=remote)
Brian Harringb0043ab2012-08-05 04:09:56 -070044 element.attrib['workon'] = 'False'
Rhyland Kleindd8ebbb2012-09-06 11:51:30 -040045 if revision is not None:
46 element.attrib['revision'] = revision
Brian Harringb0043ab2012-08-05 04:09:56 -070047 self.nodes.append(element)
48 return element
Brian Harring7fcc02e2012-08-05 04:10:57 -070049
Brian Harringb0043ab2012-08-05 04:09:56 -070050 def GetProject(self, name, path=None):
Brian Harring7fcc02e2012-08-05 04:10:57 -070051 """Accessor method for getting a project node from the manifest tree.
52
53 Returns:
54 project element node from ElementTree, otherwise, None
55 """
Brian Harringb0043ab2012-08-05 04:09:56 -070056 if path is None:
57 # Use a unique value that can't ever match.
58 path = object()
59 for project in self.nodes.findall('project'):
60 if project.attrib['name'] == name or project.attrib['path'] == path:
Brian Harring7fcc02e2012-08-05 04:10:57 -070061 return project
62 return None
63
64 def ToString(self):
Brian Harringb0043ab2012-08-05 04:09:56 -070065 # Reset the tail for each node, then just do a hacky replace.
66 project = None
67 for project in self.nodes.findall('project'):
68 project.tail = '\n '
69 if project is not None:
70 # Tweak the last project to not have the trailing space.
71 project.tail = '\n'
72 # Fix manifest tag text and tail.
73 self.nodes.text = '\n '
74 self.nodes.tail = '\n'
Mike Frysinger462dbd62019-10-19 20:26:46 -040075 return ElementTree.tostring(
76 self.nodes, encoding=repo_manifest.TOSTRING_ENCODING)
Brian Harringb0043ab2012-08-05 04:09:56 -070077
78 def GetProjects(self):
79 return list(self.nodes.findall('project'))
80
81
Gwendal Grignou89afc082016-09-29 21:03:20 -070082def _AddProjectsToManifestGroups(options, new_group):
Brian Harringb0043ab2012-08-05 04:09:56 -070083 """Enable the given manifest groups for the configured repository."""
84
Gwendal Grignou89afc082016-09-29 21:03:20 -070085 groups_to_enable = ['name:%s' % x for x in new_group]
Brian Harringb0043ab2012-08-05 04:09:56 -070086
87 git_config = options.git_config
88
David James67d73252013-09-19 17:33:12 -070089 cmd = ['config', '-f', git_config, '--get', 'manifest.groups']
Mike Frysingerf5a3b2d2019-12-12 14:36:17 -050090 enabled_groups = git.RunGit('.', cmd, check=False).output.split(',')
Brian Harringb0043ab2012-08-05 04:09:56 -070091
92 # Note that ordering actually matters, thus why the following code
93 # is written this way.
94 # Per repo behaviour, enforce an appropriate platform group if
95 # we're converting from a default manifest group to a limited one.
96 # Finally, note we reprocess the existing groups; this is to allow
97 # us to cleanup any user screwups, or our own screwups.
98 requested_groups = (
99 ['minilayout', 'platform-%s' % (platform.system().lower(),)] +
100 enabled_groups + list(groups_to_enable))
101
102 processed_groups = set()
103 finalized_groups = []
104
105 for group in requested_groups:
106 if group not in processed_groups:
107 finalized_groups.append(group)
108 processed_groups.add(group)
109
David James67d73252013-09-19 17:33:12 -0700110 cmd = ['config', '-f', git_config, 'manifest.groups',
111 ','.join(finalized_groups)]
112 git.RunGit('.', cmd)
Brian Harringb0043ab2012-08-05 04:09:56 -0700113
114
Gwendal Grignouf9d6d362016-09-30 09:29:20 -0700115def _AssertNotMiniLayout():
116 cros_build_lib.Die(
Mike Frysinger80de5012019-08-01 14:10:53 -0400117 'Your repository checkout is using the old minilayout.xml workflow; '
118 'Autoupdate is no longer supported, reinstall your tree.')
Brian Harring7fcc02e2012-08-05 04:10:57 -0700119
120
Mike Frysinger4ca60152016-09-01 00:13:36 -0400121def GetParser():
122 """Return a command line parser."""
123 parser = commandline.ArgumentParser(description=__doc__)
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400124
Mike Frysinger342be272019-10-19 20:33:37 -0400125 # Subparsers are required by default under Python 2. Python 3 changed to
126 # not required, but didn't include a required option until 3.7. Setting
127 # the required member works in all versions (and setting dest name).
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400128 subparsers = parser.add_subparsers(dest='command')
Mike Frysinger342be272019-10-19 20:33:37 -0400129 subparsers.required = True
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400130
131 subparser = subparsers.add_parser(
132 'add',
133 help='Add projects to the manifest.')
134 subparser.add_argument('-w', '--workon', action='store_true',
135 default=False, help='Is this a workon package?')
136 subparser.add_argument('-r', '--remote',
137 help='Remote project name (for non-workon packages).')
138 subparser.add_argument('-v', '--revision',
139 help='Use to override the manifest defined default '
140 'revision used for a given project.')
141 subparser.add_argument('project', help='Name of project in the manifest.')
142 subparser.add_argument('path', nargs='?', help='Local path to the project.')
143
Mike Frysinger4ca60152016-09-01 00:13:36 -0400144 return parser
145
146
Brian Harring7fcc02e2012-08-05 04:10:57 -0700147def main(argv):
Mike Frysinger4ca60152016-09-01 00:13:36 -0400148 parser = GetParser()
149 options = parser.parse_args(argv)
Ryan Cui0b1b94b2012-12-21 12:09:57 -0800150 repo_dir = git.FindRepoDir(os.getcwd())
Brian Harringb0043ab2012-08-05 04:09:56 -0700151 if not repo_dir:
Mike Frysinger80de5012019-08-01 14:10:53 -0400152 parser.error('This script must be invoked from within a repository '
153 'checkout.')
Brian Harring7fcc02e2012-08-05 04:10:57 -0700154
Brian Harringb0043ab2012-08-05 04:09:56 -0700155 options.git_config = os.path.join(repo_dir, 'manifests.git', 'config')
Brian Harringb0043ab2012-08-05 04:09:56 -0700156 options.local_manifest_path = os.path.join(repo_dir, 'local_manifest.xml')
Brian Harringb0043ab2012-08-05 04:09:56 -0700157
Gwendal Grignou89afc082016-09-29 21:03:20 -0700158 manifest_sym_path = os.path.join(repo_dir, 'manifest.xml')
159 if os.path.basename(os.readlink(manifest_sym_path)) == 'minilayout.xml':
Gwendal Grignouf9d6d362016-09-30 09:29:20 -0700160 _AssertNotMiniLayout()
Brian Harringb0043ab2012-08-05 04:09:56 -0700161
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400162 # For now, we only support the add command.
163 assert options.command == 'add'
164 if options.workon:
165 if options.path is not None:
166 parser.error('Adding workon projects do not set project.')
Brian Harring7fcc02e2012-08-05 04:10:57 -0700167 else:
Brian Harringb0043ab2012-08-05 04:09:56 -0700168 if options.remote is None:
169 parser.error('Adding non-workon projects requires a remote.')
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400170 if options.path is None:
171 parser.error('Adding non-workon projects requires a path.')
172 name = options.project
173 path = options.path
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400174 revision = options.revision
175 if revision is not None:
David James97d95872012-11-16 15:09:56 -0800176 if (not git.IsRefsTags(revision) and
177 not git.IsSHA1(revision)):
178 revision = git.StripRefsHeads(revision, False)
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400179
Gwendal Grignou89afc082016-09-29 21:03:20 -0700180 main_manifest = git.ManifestCheckout(os.getcwd())
181 main_element = main_manifest.FindCheckouts(name)
182 if path is not None:
183 main_element_from_path = main_manifest.FindCheckoutFromPath(
184 path, strict=False)
185 if main_element_from_path is not None:
186 main_element.append(main_element_from_path)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700187
Gwendal Grignou89afc082016-09-29 21:03:20 -0700188 local_manifest = LocalManifest.FromPath(options.local_manifest_path)
Brian Harring7fcc02e2012-08-05 04:10:57 -0700189
Brian Harringb0043ab2012-08-05 04:09:56 -0700190 if options.workon:
Gwendal Grignou89afc082016-09-29 21:03:20 -0700191 if not main_element:
Brian Harringb0043ab2012-08-05 04:09:56 -0700192 parser.error('No project named %r in the default manifest.' % name)
Gwendal Grignou89afc082016-09-29 21:03:20 -0700193 _AddProjectsToManifestGroups(
194 options, [checkout['name'] for checkout in main_element])
Brian Harringb0043ab2012-08-05 04:09:56 -0700195
Gwendal Grignou89afc082016-09-29 21:03:20 -0700196 elif main_element:
Rhyland Kleine5faad52012-10-31 11:58:19 -0400197 if options.remote is not None:
198 # Likely this project wasn't meant to be remote, so workon main element
Mike Frysinger80de5012019-08-01 14:10:53 -0400199 print('Project already exists in manifest. Using that as workon project.')
Gwendal Grignou89afc082016-09-29 21:03:20 -0700200 _AddProjectsToManifestGroups(
201 options, [checkout['name'] for checkout in main_element])
Rhyland Kleine5faad52012-10-31 11:58:19 -0400202 else:
203 # Conflict will occur; complain.
Mike Frysinger80de5012019-08-01 14:10:53 -0400204 parser.error('Requested project name=%r path=%r will conflict with '
205 'your current manifest %s' % (
Gwendal Grignou89afc082016-09-29 21:03:20 -0700206 name, path, main_manifest.manifest_path))
Brian Harringb0043ab2012-08-05 04:09:56 -0700207
208 elif local_manifest.GetProject(name, path=path) is not None:
Mike Frysinger80de5012019-08-01 14:10:53 -0400209 parser.error('Requested project name=%r path=%r conflicts with '
210 'your local_manifest.xml' % (name, path))
Brian Harringb0043ab2012-08-05 04:09:56 -0700211
212 else:
Rhyland Kleinb1d1f382012-08-23 11:53:45 -0400213 element = local_manifest.AddNonWorkonProject(name=name, path=path,
214 remote=options.remote,
215 revision=revision)
Gwendal Grignou89afc082016-09-29 21:03:20 -0700216 _AddProjectsToManifestGroups(options, [element.attrib['name']])
Brian Harringb0043ab2012-08-05 04:09:56 -0700217
218 with open(options.local_manifest_path, 'w') as f:
219 f.write(local_manifest.ToString())
220 return 0