blob: 89549396867c16b0c98791617df964aa8db83712 [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
Trent Apted593c0742023-05-05 03:50:20 +00009from xml.etree import 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 Klein68b270c2023-04-14 14:42:50 -060018 """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:
Alex Klein68b270c2023-04-14 14:42:50 -060047 project element node from ElementTree, otherwise, None
Alex Klein1699fab2022-09-08 08:46:06 -060048 """
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
Alex Klein68b270c2023-04-14 14:42:50 -060091 # us to clean up any user screwups, or our own screwups.
Alex Klein1699fab2022-09-08 08:46:06 -060092 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:
Alex Klein68b270c2023-04-14 14:42:50 -0600213 # Likely this project wasn't meant to be remote, so workon main
214 # element.
Alex Klein1699fab2022-09-08 08:46:06 -0600215 print(
Alex Klein68b270c2023-04-14 14:42:50 -0600216 "Project already exists in manifest. "
217 "Using that as workon project."
Alex Klein1699fab2022-09-08 08:46:06 -0600218 )
219 _AddProjectsToManifestGroups(
220 options, [checkout["name"] for checkout in main_element]
221 )
222 else:
223 # Conflict will occur; complain.
224 parser.error(
225 "Requested project name=%r path=%r will conflict with "
226 "your current manifest %s"
227 % (name, path, main_manifest.manifest_path)
228 )
229
230 elif local_manifest.GetProject(name, path=path) is not None:
231 parser.error(
232 "Requested project name=%r path=%r conflicts with "
233 "your local_manifest.xml" % (name, path)
234 )
235
236 else:
237 element = local_manifest.AddNonWorkonProject(
238 name=name, path=path, remote=options.remote, revision=revision
239 )
240 _AddProjectsToManifestGroups(options, [element.attrib["name"]])
241
Mike Frysinger31fdddd2023-02-24 15:50:55 -0500242 with open(options.local_manifest_path, "w", encoding="utf-8") as f:
Alex Klein1699fab2022-09-08 08:46:06 -0600243 f.write(local_manifest.ToString())
244 return 0