| """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 |