blob: 6c41c763dafef675e4314dbed617dfdfeec8a6dc [file] [log] [blame]
"""Utilities to access an Android tree manifest data."""
from pathlib import Path, PurePath
import re
import xml.dom.minidom
class ProjectNotFoundError(Exception):
"""When a project is not found.
Attributes:
project: The project name.
"""
def __init__(self, project):
self.project = project
super().__init__(f"Project not found: {project}")
class AndroidManifest:
"""Wrapper around an Android tree manifest.
This class extracts projects of an Android manifest XML; it parses manifest include tags as well
as project removal tags in line with the logic of the `repo` tool.
Attributes:
projects: The mapping of AOSP project names to the checked-out project
path.
"""
TAG_MANIFEST = "manifest"
TAG_PROJECT = "project"
TAG_INCLUDE_MANIFEST = "include"
TAG_REMOVE_PROJECT = "remove-project"
IGNORED_TAGS = [
"#comment",
"#text",
"default",
"remote",
]
#: Match the AOSP project name in a Fairphone manifest (special prefixes)
_AOSP_NAME_IN_FP_MANIFEST = re.compile(r"^((fp2|fp2-dev)/)?(?P<name>(kernel|platform)/.*)$")
def __init__(self, manifests_root_path: Path, relative_manifest_path: PurePath):
"""Create an instance from an Android tree XML file."""
self._manifests_root_path = manifests_root_path
self.projects = {}
self._resolve_manifest_list_projects(relative_manifest_path)
# Add self-reference to the manifest project, as done in AOSP. We usually don't have these
# entries in Fairphone manifests, but they are relevant when looking for manifest patches.
# Add both AOSP and Fairphone manifest project name for convenience.
self.projects["manifest"] = ".repo/manifests"
self.projects["platform/manifest"] = ".repo/manifests"
@classmethod
def _load_manifest_file(cls, manifest_file_path: Path):
"""Load a XML manifest.
Arguments:
manifest_file_path: Path of the manifest XML file to load.
Returns:
The first XML node describing the manifest content.
Raises:
ValueError: If the manifest is a malformed XML file or if the
manifest node is missing.
"""
try:
root = xml.dom.minidom.parse(str(manifest_file_path))
except (OSError, xml.parsers.expat.ExpatError) as exception:
raise ValueError(
f"Error parsing manifest {manifest_file_path}: {exception}"
) from exception
if not root or not root.childNodes:
raise ValueError(f"No root node in {manifest_file_path}")
manifest_node = next((n for n in root.childNodes if n.nodeName == cls.TAG_MANIFEST), None)
if not manifest_node:
raise ValueError(f"No <{cls.TAG_MANIFEST}> node in {manifest_file_path}")
return manifest_node
def _resolve_manifest_list_projects(self, relative_manifest_path: PurePath):
"""Resolve effective project list starting from a (sub-)manifest.
Update self's effective project list based on a the contents of
the manifest at relative_manifest_path. Include sub-manifests
recursively, apply project removals.
Arguments:
relative_manifest_path: Path of the manifest to parse relative to
self's initial root path.
"""
manifest_node = self._load_manifest_file(self._manifests_root_path / relative_manifest_path)
for node in manifest_node.childNodes:
if node.nodeName == self.TAG_PROJECT:
self._add_project(node)
elif node.nodeName == self.TAG_INCLUDE_MANIFEST:
relative_include_path = node.getAttribute("name")
# Process included manifest and extract its projects as well. Don't bother
# actually merging its structure into the current XML; we don't need that merged
# XML tree, only the effective projects.
self._resolve_manifest_list_projects(relative_include_path)
elif node.nodeName == self.TAG_REMOVE_PROJECT:
self._remove_project(node)
elif node.nodeName not in self.IGNORED_TAGS:
raise ValueError(f"Can't parse manifest tag: {node.nodeName}")
def _add_project(self, project_node):
"""Extract and store relevant properties of a project.
This also handles Fairphone-specific differences compared to AOSP manifests:
* Legacy project names are prefixed with "fp2/": "fp2/kernel/*", "fp2/platform/*",
"fp2/vendor/*". Vendor projects are, by definition, not going to be advertised in the
original Android manifest. Kernel and platform projects are in the AOSP manifests and can
thus be targeted by security bulletins. The Fairphone prefix is removed to make the
projects names match those from the AOSP manifest.
* Legacy Fairphone Sibon manifests are using "fp2-dev/" in place of "fp2/". They are also
handled and removed here.
Arguments:
project_node: XML <project> node from a manifest file.
Raises:
ValueError: If the project node is invalid or if the project (same name) was already
added.
"""
name = self._get_project_name(project_node)
if name in self.projects:
raise ValueError(f"Duplicate project {name} in manifest")
path = project_node.getAttribute("path") or name
self.projects[name] = path
def _remove_project(self, project_node):
"""Drop a project according to a 'remove-project' node.
Follow the logic of the 'repo' tool to identify and drop a project from a manifest project
list.
Arguments:
project_node: XML <project> node from a manifest file.
Raises:
ValueError: If the project node is invalid or if the project to remove wasn't added
before.
"""
name = self._get_project_name(project_node)
try:
del self.projects[name]
except KeyError as e:
raise ValueError(
f"Project {name} requested to be removed but wasn't added before."
) from e
@classmethod
def _get_project_name(cls, node):
"""Extract the AOSP project name from an XML node.
Arguments:
node: The project XML node.
Returns:
The project name cleared of Fairphone-specific prefixes.
"""
name = node.getAttribute("name")
# Fix Fairphone-specific names
match = cls._AOSP_NAME_IN_FP_MANIFEST.match(name)
if match:
name = match.group("name")
return name