blob: 3d4320c472ee61ec471c0c88937dbda828ae99df [file] [edit]
# Copyright (C) 2024 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Parses the repo manifest from a build."""
import dataclasses
import pathlib
import re
import xml.dom.minidom
import xml.parsers.expat
from typing import TextIO
from init.init_errors import KleafProjectSetterError
_TOOLS_BAZEL = "tools/bazel"
@dataclasses.dataclass(frozen=True)
class ProjectState:
# Paths are relative to repo root.
# Original path in the repo manifest in the build
original_path: pathlib.Path
# Fixed up path below the current repo root.
fixed_path: pathlib.Path
@dataclasses.dataclass
class RepoManifestParser:
"""Parses the repo manifest from a build."""
manifest: str
project_prefix: pathlib.Path
# If None, add all projects. If a set, only add projects that matches
# any of these groups. If both an empty set, no project is added.
# list of projects with paths fixed up below project_prefix
fixup_groups: set[str] | None
# list of projects with paths untouched
preserve_groups: set[str] | None
def write_transformed_dom(self, file: TextIO) \
-> set[ProjectState]:
"""Transforms manifest from the build and write result to file.
Returns:
set of ProjectState objects describing old and new paths.
"""
try:
with xml.dom.minidom.parseString(self.manifest) as dom:
project_states = self._transform_dom(dom)
dom.writexml(file)
return project_states
except xml.parsers.expat.ExpatError as err:
raise KleafProjectSetterError("Unable to parse repo manifest") \
from err
def _transform_dom(self, dom: xml.dom.minidom.Document) \
-> set[ProjectState]:
"""Transforms manifest from the build.
- Append project_prefix to each project.
- Filter out projects of mismatching groups
- Drop elements that may conflict with the main manifest
Returns:
set of ProjectState objects describing old and new paths.
"""
root: xml.dom.minidom.Element = dom.documentElement
projects = root.getElementsByTagName("project")
defaults = self._parse_repo_manifest_defaults(root)
project_states = set()
for project in projects:
category = self._match_group(project)
if category == "delete":
root.removeChild(project).unlink()
continue
for key, value in defaults.items():
if not project.hasAttribute(key):
project.setAttribute(key, value)
# https://gerrit.googlesource.com/git-repo/+/master/docs/manifest-format.md#element-project
orig_path_below_repo = pathlib.Path(project.getAttribute("path") or
project.getAttribute("name"))
if category == "preserve":
project_states.add(ProjectState(orig_path_below_repo,
orig_path_below_repo))
continue
path_below_repo = self.project_prefix / orig_path_below_repo
project_states.add(ProjectState(
orig_path_below_repo, path_below_repo))
project.setAttribute("path", str(path_below_repo))
for link in project.getElementsByTagName("linkfile"):
orig_dest = link.getAttribute("dest")
# b/355523169 special case which should be in the top directory.
if orig_dest == _TOOLS_BAZEL:
continue
orig_dest = pathlib.Path(orig_dest)
link.setAttribute("dest", str(self.project_prefix / orig_dest))
# Avoid <superproject> and <default> in Kleaf manifest conflicting with
# the one in main manifest
for superproject in root.getElementsByTagName("superproject"):
root.removeChild(superproject).unlink()
for default_element in root.getElementsByTagName("default"):
root.removeChild(default_element).unlink()
return project_states
def _match_group(self, project: xml.dom.minidom.Element) -> str:
"""Returns category of the groups if project matches any of groups."""
# preserve_groups has higher priority.
if self._match_group_internal(project, self.preserve_groups):
return "preserve"
if self._match_group_internal(project, self.fixup_groups):
return "fixup"
return "delete"
def _match_group_internal(self, project: xml.dom.minidom.Element,
expect_groups: set[str] | None):
if expect_groups is None:
return True
project_groups = re.split(r",| ", project.getAttribute("groups"))
return bool(set(project_groups) & expect_groups)
def _parse_repo_manifest_defaults(self, root: xml.dom.minidom.Element):
"""Parses <default> in a repo manifest. """
ret = dict[str, str]()
for default_element in root.getElementsByTagName("default"):
attrs = default_element.attributes
for index in range(attrs.length):
attr = attrs.item(index)
assert isinstance(attr, xml.dom.minidom.Attr)
ret[attr.name] = attr.value
return ret