Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 1 | """Utilities to access an Android tree manifest data.""" |
| 2 | |
Karsten Tausche | da2350f | 2020-04-21 15:13:06 +0200 | [diff] [blame] | 3 | from pathlib import Path, PurePath |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 4 | import re |
| 5 | import xml.dom.minidom |
| 6 | |
| 7 | |
| 8 | class ProjectNotFoundError(Exception): |
| 9 | """When a project is not found. |
| 10 | |
| 11 | Attributes: |
| 12 | project: The project name. |
| 13 | |
| 14 | """ |
Karsten Tausche | 48180b3 | 2018-12-10 17:40:57 +0100 | [diff] [blame] | 15 | |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 16 | def __init__(self, project): |
| 17 | self.project = project |
Karsten Tausche | 9bbfd7f | 2023-02-07 10:41:40 +0100 | [diff] [blame] | 18 | super().__init__(f"Project not found: {project}") |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 19 | |
| 20 | |
| 21 | class AndroidManifest: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 22 | """Wrapper around an Android tree manifest. |
| 23 | |
| 24 | This class extracts projects of an Android manifest XML; it parses manifest include tags as well |
| 25 | as project removal tags in line with the logic of the `repo` tool. |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 26 | |
| 27 | Attributes: |
| 28 | projects: The mapping of AOSP project names to the checked-out project |
| 29 | path. |
| 30 | |
| 31 | """ |
Karsten Tausche | 48180b3 | 2018-12-10 17:40:57 +0100 | [diff] [blame] | 32 | |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 33 | TAG_MANIFEST = "manifest" |
| 34 | TAG_PROJECT = "project" |
| 35 | TAG_INCLUDE_MANIFEST = "include" |
| 36 | TAG_REMOVE_PROJECT = "remove-project" |
Karsten Tausche | c593e59 | 2023-02-07 13:09:15 +0100 | [diff] [blame] | 37 | IGNORED_TAGS = [ |
| 38 | "#comment", |
| 39 | "#text", |
| 40 | "default", |
| 41 | "remote", |
| 42 | ] |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 43 | |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 44 | #: Match the AOSP project name in a Fairphone manifest (special prefixes) |
Karsten Tausche | 9bbfd7f | 2023-02-07 10:41:40 +0100 | [diff] [blame] | 45 | _AOSP_NAME_IN_FP_MANIFEST = re.compile(r"^((fp2|fp2-dev)/)?(?P<name>(kernel|platform)/.*)$") |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 46 | |
Karsten Tausche | 9bbfd7f | 2023-02-07 10:41:40 +0100 | [diff] [blame] | 47 | def __init__(self, manifests_root_path: Path, relative_manifest_path: PurePath): |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 48 | """Create an instance from an Android tree XML file.""" |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 49 | self._manifests_root_path = manifests_root_path |
| 50 | self.projects = {} |
| 51 | self._resolve_manifest_list_projects(relative_manifest_path) |
| 52 | |
| 53 | # Add self-reference to the manifest project, as done in AOSP. We usually don't have these |
| 54 | # entries in Fairphone manifests, but they are relevant when looking for manifest patches. |
| 55 | # Add both AOSP and Fairphone manifest project name for convenience. |
| 56 | self.projects["manifest"] = ".repo/manifests" |
| 57 | self.projects["platform/manifest"] = ".repo/manifests" |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 58 | |
| 59 | @classmethod |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 60 | def _load_manifest_file(cls, manifest_file_path: Path): |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 61 | """Load a XML manifest. |
| 62 | |
| 63 | Arguments: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 64 | manifest_file_path: Path of the manifest XML file to load. |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 65 | Returns: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 66 | The first XML node describing the manifest content. |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 67 | Raises: |
| 68 | ValueError: If the manifest is a malformed XML file or if the |
| 69 | manifest node is missing. |
| 70 | |
| 71 | """ |
| 72 | try: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 73 | root = xml.dom.minidom.parse(str(manifest_file_path)) |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 74 | except (OSError, xml.parsers.expat.ExpatError) as exception: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 75 | raise ValueError( |
| 76 | f"Error parsing manifest {manifest_file_path}: {exception}" |
| 77 | ) from exception |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 78 | |
| 79 | if not root or not root.childNodes: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 80 | raise ValueError(f"No root node in {manifest_file_path}") |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 81 | |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 82 | manifest_node = next((n for n in root.childNodes if n.nodeName == cls.TAG_MANIFEST), None) |
Karsten Tausche | da2350f | 2020-04-21 15:13:06 +0200 | [diff] [blame] | 83 | if not manifest_node: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 84 | raise ValueError(f"No <{cls.TAG_MANIFEST}> node in {manifest_file_path}") |
Karsten Tausche | da2350f | 2020-04-21 15:13:06 +0200 | [diff] [blame] | 85 | |
| 86 | return manifest_node |
| 87 | |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 88 | def _resolve_manifest_list_projects(self, relative_manifest_path: PurePath): |
| 89 | """Resolve effective project list starting from a (sub-)manifest. |
| 90 | |
| 91 | Update self's effective project list based on a the contents of |
| 92 | the manifest at relative_manifest_path. Include sub-manifests |
| 93 | recursively, apply project removals. |
Karsten Tausche | da2350f | 2020-04-21 15:13:06 +0200 | [diff] [blame] | 94 | |
| 95 | Arguments: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 96 | relative_manifest_path: Path of the manifest to parse relative to |
| 97 | self's initial root path. |
Karsten Tausche | da2350f | 2020-04-21 15:13:06 +0200 | [diff] [blame] | 98 | """ |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 99 | manifest_node = self._load_manifest_file(self._manifests_root_path / relative_manifest_path) |
Karsten Tausche | da2350f | 2020-04-21 15:13:06 +0200 | [diff] [blame] | 100 | for node in manifest_node.childNodes: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 101 | if node.nodeName == self.TAG_PROJECT: |
| 102 | self._add_project(node) |
| 103 | elif node.nodeName == self.TAG_INCLUDE_MANIFEST: |
Karsten Tausche | da2350f | 2020-04-21 15:13:06 +0200 | [diff] [blame] | 104 | relative_include_path = node.getAttribute("name") |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 105 | # Process included manifest and extract its projects as well. Don't bother |
| 106 | # actually merging its structure into the current XML; we don't need that merged |
| 107 | # XML tree, only the effective projects. |
| 108 | self._resolve_manifest_list_projects(relative_include_path) |
| 109 | elif node.nodeName == self.TAG_REMOVE_PROJECT: |
| 110 | self._remove_project(node) |
Karsten Tausche | c593e59 | 2023-02-07 13:09:15 +0100 | [diff] [blame] | 111 | elif node.nodeName not in self.IGNORED_TAGS: |
| 112 | raise ValueError(f"Can't parse manifest tag: {node.nodeName}") |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 113 | |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 114 | def _add_project(self, project_node): |
| 115 | """Extract and store relevant properties of a project. |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 116 | |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 117 | This also handles Fairphone-specific differences compared to AOSP manifests: |
| 118 | * Legacy project names are prefixed with "fp2/": "fp2/kernel/*", "fp2/platform/*", |
| 119 | "fp2/vendor/*". Vendor projects are, by definition, not going to be advertised in the |
| 120 | original Android manifest. Kernel and platform projects are in the AOSP manifests and can |
| 121 | thus be targeted by security bulletins. The Fairphone prefix is removed to make the |
| 122 | projects names match those from the AOSP manifest. |
| 123 | * Legacy Fairphone Sibon manifests are using "fp2-dev/" in place of "fp2/". They are also |
| 124 | handled and removed here. |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 125 | |
| 126 | Arguments: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 127 | project_node: XML <project> node from a manifest file. |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 128 | Raises: |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 129 | ValueError: If the project node is invalid or if the project (same name) was already |
| 130 | added. |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 131 | """ |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 132 | name = self._get_project_name(project_node) |
| 133 | if name in self.projects: |
| 134 | raise ValueError(f"Duplicate project {name} in manifest") |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 135 | |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 136 | path = project_node.getAttribute("path") or name |
| 137 | self.projects[name] = path |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 138 | |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 139 | def _remove_project(self, project_node): |
| 140 | """Drop a project according to a 'remove-project' node. |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 141 | |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 142 | Follow the logic of the 'repo' tool to identify and drop a project from a manifest project |
| 143 | list. |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 144 | |
Karsten Tausche | 349e480 | 2023-02-07 10:25:17 +0100 | [diff] [blame] | 145 | Arguments: |
| 146 | project_node: XML <project> node from a manifest file. |
| 147 | Raises: |
| 148 | ValueError: If the project node is invalid or if the project to remove wasn't added |
| 149 | before. |
| 150 | """ |
| 151 | name = self._get_project_name(project_node) |
| 152 | try: |
| 153 | del self.projects[name] |
| 154 | except KeyError as e: |
| 155 | raise ValueError( |
| 156 | f"Project {name} requested to be removed but wasn't added before." |
| 157 | ) from e |
Borjan Tchakaloff | 1043fa3 | 2017-09-05 11:25:52 +0200 | [diff] [blame] | 158 | |
| 159 | @classmethod |
| 160 | def _get_project_name(cls, node): |
| 161 | """Extract the AOSP project name from an XML node. |
| 162 | |
| 163 | Arguments: |
| 164 | node: The project XML node. |
| 165 | Returns: |
| 166 | The project name cleared of Fairphone-specific prefixes. |
| 167 | |
| 168 | """ |
| 169 | name = node.getAttribute("name") |
| 170 | |
| 171 | # Fix Fairphone-specific names |
| 172 | match = cls._AOSP_NAME_IN_FP_MANIFEST.match(name) |
| 173 | if match: |
| 174 | name = match.group("name") |
| 175 | |
| 176 | return name |