blob: 56bbffecc4aef2ac6271d8804fa50e0282240c46 [file] [log] [blame]
Mike Frysingerf1ba7ad2022-09-12 05:42:57 -04001# Copyright 2010 The ChromiumOS Authors
Brian Harring7fcc02e2012-08-05 04:10:57 -07002# 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
Brian Harring7fcc02e2012-08-05 04:10:57 -07007import os
Mike Frysinger807d8282022-04-28 22:45:17 -04008import platform
Brian Harring7fcc02e2012-08-05 04:10:57 -07009import xml.etree.ElementTree as ElementTree
Mike Frysinger750c5f52014-09-16 16:16:57 -040010
Mike Frysinger4ca60152016-09-01 00:13:36 -040011from chromite.lib import commandline
Brian Harringb0043ab2012-08-05 04:09:56 -070012from chromite.lib import cros_build_lib
David James97d95872012-11-16 15:09:56 -080013from chromite.lib import git
Mike Frysinger462dbd62019-10-19 20:26:46 -040014from chromite.lib import osutils
Brian Harring7fcc02e2012-08-05 04:10:57 -070015
16
Gwendal Grignou89afc082016-09-29 21:03:20 -070017class LocalManifest(object):
Alex Klein1699fab2022-09-08 08:46:06 -060018 """Class which provides an abstraction for manipulating the local manifest."""
Brian Harring7fcc02e2012-08-05 04:10:57 -070019
Alex Klein1699fab2022-09-08 08:46:06 -060020 @classmethod
21 def FromPath(cls, path, empty_if_missing=False):
22 if os.path.isfile(path):
23 return cls(osutils.ReadFile(path))
24 elif empty_if_missing:
Alex Kleindf8ee502022-10-18 09:48:15 -060025 cros_build_lib.Die("Manifest file, %r, not found", path)
Alex Klein1699fab2022-09-08 08:46:06 -060026 return cls()
Brian Harringb0043ab2012-08-05 04:09:56 -070027
Alex Klein1699fab2022-09-08 08:46:06 -060028 def __init__(self, text=None):
29 self._text = text or "<manifest>\n</manifest>"
30 self.nodes = ElementTree.fromstring(self._text)
Brian Harring7fcc02e2012-08-05 04:10:57 -070031
Alex Klein1699fab2022-09-08 08:46:06 -060032 def AddNonWorkonProject(self, name, path, remote=None, revision=None):
33 """Add a new nonworkon project element to the manifest tree."""
34 element = ElementTree.Element(
35 "project", name=name, path=path, remote=remote
36 )
37 element.attrib["workon"] = "False"
38 if revision is not None:
39 element.attrib["revision"] = revision
40 self.nodes.append(element)
41 return element
Brian Harring7fcc02e2012-08-05 04:10:57 -070042
Alex Klein1699fab2022-09-08 08:46:06 -060043 def GetProject(self, name, path=None):
44 """Accessor method for getting a project node from the manifest tree.
Brian Harring7fcc02e2012-08-05 04:10:57 -070045
Alex Klein1699fab2022-09-08 08:46:06 -060046 Returns:
47 project element node from ElementTree, otherwise, None
48 """
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:
54 return project
55 return None
Brian Harring7fcc02e2012-08-05 04:10:57 -070056
Alex Klein1699fab2022-09-08 08:46:06 -060057 def ToString(self):
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, encoding="unicode")
Brian Harringb0043ab2012-08-05 04:09:56 -070069
Alex Klein1699fab2022-09-08 08:46:06 -060070 def GetProjects(self):
71 return list(self.nodes.findall("project"))
Brian Harringb0043ab2012-08-05 04:09:56 -070072
73
Gwendal Grignou89afc082016-09-29 21:03:20 -070074def _AddProjectsToManifestGroups(options, new_group):
Alex Klein1699fab2022-09-08 08:46:06 -060075 """Enable the given manifest groups for the configured repository."""
Brian Harringb0043ab2012-08-05 04:09:56 -070076
Alex Klein1699fab2022-09-08 08:46:06 -060077 groups_to_enable = ["name:%s" % x for x in new_group]
Brian Harringb0043ab2012-08-05 04:09:56 -070078
Alex Klein1699fab2022-09-08 08:46:06 -060079 git_config = options.git_config
Brian Harringb0043ab2012-08-05 04:09:56 -070080
Alex Klein1699fab2022-09-08 08:46:06 -060081 cmd = ["config", "-f", git_config, "--get", "manifest.groups"]
82 enabled_groups = (
83 git.RunGit(".", cmd, check=False).stdout.rstrip().split(",")
84 )
Brian Harringb0043ab2012-08-05 04:09:56 -070085
Alex Klein1699fab2022-09-08 08:46:06 -060086 # 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
95 + list(groups_to_enable)
96 )
Brian Harringb0043ab2012-08-05 04:09:56 -070097
Alex Klein1699fab2022-09-08 08:46:06 -060098 processed_groups = set()
99 finalized_groups = []
Brian Harringb0043ab2012-08-05 04:09:56 -0700100
Alex Klein1699fab2022-09-08 08:46:06 -0600101 for group in requested_groups:
102 if group not in processed_groups:
103 finalized_groups.append(group)
104 processed_groups.add(group)
Brian Harringb0043ab2012-08-05 04:09:56 -0700105
Alex Klein1699fab2022-09-08 08:46:06 -0600106 cmd = [
107 "config",
108 "-f",
109 git_config,
110 "manifest.groups",
111 ",".join(finalized_groups),
112 ]
113 git.RunGit(".", cmd)
Brian Harringb0043ab2012-08-05 04:09:56 -0700114
115
Gwendal Grignouf9d6d362016-09-30 09:29:20 -0700116def _AssertNotMiniLayout():
Alex Klein1699fab2022-09-08 08:46:06 -0600117 cros_build_lib.Die(
118 "Your repository checkout is using the old minilayout.xml workflow; "
119 "Autoupdate is no longer supported, reinstall your tree."
120 )
Brian Harring7fcc02e2012-08-05 04:10:57 -0700121
122
Mike Frysinger4ca60152016-09-01 00:13:36 -0400123def GetParser():
Alex Klein1699fab2022-09-08 08:46:06 -0600124 """Return a command line parser."""
125 parser = commandline.ArgumentParser(description=__doc__)
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400126
Alex Klein1699fab2022-09-08 08:46:06 -0600127 # Subparsers are required by default under Python 2. Python 3 changed to
128 # not required, but didn't include a required option until 3.7. Setting
129 # the required member works in all versions (and setting dest name).
130 subparsers = parser.add_subparsers(dest="command")
131 subparsers.required = True
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400132
Alex Klein1699fab2022-09-08 08:46:06 -0600133 subparser = subparsers.add_parser(
134 "add", help="Add projects to the manifest."
135 )
136 subparser.add_argument(
137 "-w",
138 "--workon",
139 action="store_true",
140 default=False,
141 help="Is this a workon package?",
142 )
143 subparser.add_argument(
144 "-r", "--remote", help="Remote project name (for non-workon packages)."
145 )
146 subparser.add_argument(
147 "--revision",
148 help="Use to override the manifest defined default "
149 "revision used for a given project.",
150 )
151 subparser.add_argument("project", help="Name of project in the manifest.")
152 subparser.add_argument("path", nargs="?", help="Local path to the project.")
Mike Frysinger1b8565b2016-09-13 16:03:49 -0400153
Alex Klein1699fab2022-09-08 08:46:06 -0600154 return parser
Mike Frysinger4ca60152016-09-01 00:13:36 -0400155
156
Brian Harring7fcc02e2012-08-05 04:10:57 -0700157def main(argv):
Alex Klein1699fab2022-09-08 08:46:06 -0600158 parser = GetParser()
159 options = parser.parse_args(argv)
160 repo_dir = git.FindRepoDir(os.getcwd())
161 if not repo_dir:
162 parser.error(
163 "This script must be invoked from within a repository " "checkout."
164 )
Brian Harring7fcc02e2012-08-05 04:10:57 -0700165
Alex Klein1699fab2022-09-08 08:46:06 -0600166 options.git_config = os.path.join(repo_dir, "manifests.git", "config")
167 options.local_manifest_path = os.path.join(repo_dir, "local_manifest.xml")
Brian Harringb0043ab2012-08-05 04:09:56 -0700168
Alex Klein1699fab2022-09-08 08:46:06 -0600169 manifest_sym_path = os.path.join(repo_dir, "manifest.xml")
170 if (
171 os.path.basename(os.path.realpath(manifest_sym_path))
172 == "minilayout.xml"
173 ):
174 _AssertNotMiniLayout()
Brian Harringb0043ab2012-08-05 04:09:56 -0700175
Alex Klein1699fab2022-09-08 08:46:06 -0600176 # For now, we only support the add command.
177 assert options.command == "add"
178 if options.workon:
179 if options.path is not None:
180 parser.error("Adding workon projects do not set project.")
Rhyland Kleine5faad52012-10-31 11:58:19 -0400181 else:
Alex Klein1699fab2022-09-08 08:46:06 -0600182 if options.remote is None:
183 parser.error("Adding non-workon projects requires a remote.")
184 if options.path is None:
185 parser.error("Adding non-workon projects requires a path.")
186 name = options.project
187 path = options.path
188 revision = options.revision
189 if revision is not None:
190 if not git.IsRefsTags(revision) and not git.IsSHA1(revision):
191 revision = git.StripRefsHeads(revision, False)
Brian Harringb0043ab2012-08-05 04:09:56 -0700192
Alex Klein1699fab2022-09-08 08:46:06 -0600193 main_manifest = git.ManifestCheckout(os.getcwd())
194 main_element = main_manifest.FindCheckouts(name)
195 if path is not None:
196 main_element_from_path = main_manifest.FindCheckoutFromPath(
197 path, strict=False
198 )
199 if main_element_from_path is not None:
200 main_element.append(main_element_from_path)
Brian Harringb0043ab2012-08-05 04:09:56 -0700201
Alex Klein1699fab2022-09-08 08:46:06 -0600202 local_manifest = LocalManifest.FromPath(options.local_manifest_path)
Brian Harringb0043ab2012-08-05 04:09:56 -0700203
Alex Klein1699fab2022-09-08 08:46:06 -0600204 if options.workon:
205 if not main_element:
206 parser.error("No project named %r in the default manifest." % name)
207 _AddProjectsToManifestGroups(
208 options, [checkout["name"] for checkout in main_element]
209 )
210
211 elif main_element:
212 if options.remote is not None:
213 # Likely this project wasn't meant to be remote, so workon main element
214 print(
215 "Project already exists in manifest. Using that as workon project."
216 )
217 _AddProjectsToManifestGroups(
218 options, [checkout["name"] for checkout in main_element]
219 )
220 else:
221 # Conflict will occur; complain.
222 parser.error(
223 "Requested project name=%r path=%r will conflict with "
224 "your current manifest %s"
225 % (name, path, main_manifest.manifest_path)
226 )
227
228 elif local_manifest.GetProject(name, path=path) is not None:
229 parser.error(
230 "Requested project name=%r path=%r conflicts with "
231 "your local_manifest.xml" % (name, path)
232 )
233
234 else:
235 element = local_manifest.AddNonWorkonProject(
236 name=name, path=path, remote=options.remote, revision=revision
237 )
238 _AddProjectsToManifestGroups(options, [element.attrib["name"]])
239
Mike Frysinger31fdddd2023-02-24 15:50:55 -0500240 with open(options.local_manifest_path, "w", encoding="utf-8") as f:
Alex Klein1699fab2022-09-08 08:46:06 -0600241 f.write(local_manifest.ToString())
242 return 0