blob: a52d181c6e1ce5890aecf3efc5dc142c88f25084 [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(ProjectNotFoundError, self).__init__(
"Project not found: %s" % (project,)
)
class AndroidManifest:
"""A simple wrapper around an Android tree manifest.
Attributes:
projects: The mapping of AOSP project names to the checked-out project
path.
"""
#: 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._root = self._load_manifest(
manifests_root_path, relative_manifest_path
)
self.projects = self._extract_projects(self._root)
@classmethod
def _load_manifest(
cls, manifests_root_path: Path, relative_manifest_path: PurePath
):
"""Load a XML manifest.
Arguments:
manifests_root_path: Path of the manifests git repository in the
tree (usually AOSP_ROOT/.repo/manifests). This is used to
resolve include directives in manifests.
relative_manifest_path: Path of the manifest to parse relative to
the manifest_root_path.
Returns:
The first XML node describing the manifest content, with included
manifests recursively resolved.
Raises:
ValueError: If the manifest is a malformed XML file or if the
manifest node is missing.
"""
manifest_file = manifests_root_path / relative_manifest_path
try:
root = xml.dom.minidom.parse(str(manifest_file))
except (OSError, xml.parsers.expat.ExpatError) as exception:
raise ValueError(
"Error parsing manifest %s: %s" % (manifest_file, exception)
)
if not root or not root.childNodes:
raise ValueError("No root node in %s" % (manifest_file,))
manifest_node = next(
(n for n in root.childNodes if n.nodeName == "manifest"), None
)
if not manifest_node:
raise ValueError("No <manifest> node in %s" % (manifest_file,))
cls._resolve_manifest_includes(manifest_node, manifests_root_path)
return manifest_node
@classmethod
def _resolve_manifest_includes(
cls, manifest_node, manifests_root_path: Path
):
"""Resolve include tags in Android manifests.
Arguments:
manifest_node: <manifest> node of the manifest to process. It will
be processed in place.
manifests_root_path: Path of the manifests git repository in the
tree (usually AOSP_ROOT/.repo/manifests). This is used to
resolve include directives in manifests.
"""
for node in manifest_node.childNodes:
if node.nodeName == "include":
relative_include_path = node.getAttribute("name")
included_manifest = cls._load_manifest(
manifests_root_path, relative_include_path
)
for included_node in included_manifest.childNodes:
if included_node.nodeName == "project":
manifest_node.insertBefore(included_node, node)
manifest_node.removeChild(node)
@classmethod
def _extract_projects(cls, root):
"""Extract the projects from an Android tree XML manifest.
The Fairphone manifests have changed some project names by adding the
prefix "fp2/" to them: "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.
The Fairphone Sibon manifests are using "fp2-dev/" in place of "fp2/".
They are also handled and removed here.
Another exception is the manifest project itself. The Fairphone
manifests do not include a self reference like the AOSP manifests do.
This is worked around by pointing to the checked-out manifests project.
Arguments:
root: The XML root node describing the manifest.
Returns:
The mapping of project names to real project paths (the check-outs).
Raises:
ValueError: If the manifest is a malformed XML file, if the
manifest node is missing, or if a project name appears more than
once.
"""
# The Fairphone manifests do not include self-references, add them here
projects = {
"manifest": ".repo/manifests",
"platform/manifest": ".repo/manifests",
}
for node in root.childNodes:
if node.nodeName == "project":
name = cls._get_project_name(node)
if name in projects:
raise ValueError("Duplicate project %s in manifest" % name)
path = node.getAttribute("path") or name
projects[name] = path
return projects
@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