| """Utilities to access an Android tree manifest data.""" |
| |
| 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, manifest_file): |
| """Create an instance from an Android tree XML file.""" |
| self._root = self._load_manifest(manifest_file) |
| self.projects = self._extract_projects(self._root) |
| |
| @classmethod |
| def _load_manifest(cls, manifest_file): |
| """Load a XML manifest. |
| |
| Arguments: |
| manifest_file: The path to the XML manifest file. |
| 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(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,)) |
| |
| for node in root.childNodes: |
| if node.nodeName == "manifest": |
| return node |
| |
| raise ValueError("No <manifest> node in %s" % (manifest_file,)) |
| |
| @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 |