blob: 6c41c763dafef675e4314dbed617dfdfeec8a6dc [file] [log] [blame]
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +02001"""Utilities to access an Android tree manifest data."""
2
Karsten Tauscheda2350f2020-04-21 15:13:06 +02003from pathlib import Path, PurePath
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +02004import re
5import xml.dom.minidom
6
7
8class ProjectNotFoundError(Exception):
9 """When a project is not found.
10
11 Attributes:
12 project: The project name.
13
14 """
Karsten Tausche48180b32018-12-10 17:40:57 +010015
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020016 def __init__(self, project):
17 self.project = project
Karsten Tausche9bbfd7f2023-02-07 10:41:40 +010018 super().__init__(f"Project not found: {project}")
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020019
20
21class AndroidManifest:
Karsten Tausche349e4802023-02-07 10:25:17 +010022 """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 Tchakaloff1043fa32017-09-05 11:25:52 +020026
27 Attributes:
28 projects: The mapping of AOSP project names to the checked-out project
29 path.
30
31 """
Karsten Tausche48180b32018-12-10 17:40:57 +010032
Karsten Tausche349e4802023-02-07 10:25:17 +010033 TAG_MANIFEST = "manifest"
34 TAG_PROJECT = "project"
35 TAG_INCLUDE_MANIFEST = "include"
36 TAG_REMOVE_PROJECT = "remove-project"
Karsten Tauschec593e592023-02-07 13:09:15 +010037 IGNORED_TAGS = [
38 "#comment",
39 "#text",
40 "default",
41 "remote",
42 ]
Karsten Tausche349e4802023-02-07 10:25:17 +010043
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020044 #: Match the AOSP project name in a Fairphone manifest (special prefixes)
Karsten Tausche9bbfd7f2023-02-07 10:41:40 +010045 _AOSP_NAME_IN_FP_MANIFEST = re.compile(r"^((fp2|fp2-dev)/)?(?P<name>(kernel|platform)/.*)$")
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020046
Karsten Tausche9bbfd7f2023-02-07 10:41:40 +010047 def __init__(self, manifests_root_path: Path, relative_manifest_path: PurePath):
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020048 """Create an instance from an Android tree XML file."""
Karsten Tausche349e4802023-02-07 10:25:17 +010049 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 Tchakaloff1043fa32017-09-05 11:25:52 +020058
59 @classmethod
Karsten Tausche349e4802023-02-07 10:25:17 +010060 def _load_manifest_file(cls, manifest_file_path: Path):
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020061 """Load a XML manifest.
62
63 Arguments:
Karsten Tausche349e4802023-02-07 10:25:17 +010064 manifest_file_path: Path of the manifest XML file to load.
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020065 Returns:
Karsten Tausche349e4802023-02-07 10:25:17 +010066 The first XML node describing the manifest content.
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020067 Raises:
68 ValueError: If the manifest is a malformed XML file or if the
69 manifest node is missing.
70
71 """
72 try:
Karsten Tausche349e4802023-02-07 10:25:17 +010073 root = xml.dom.minidom.parse(str(manifest_file_path))
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020074 except (OSError, xml.parsers.expat.ExpatError) as exception:
Karsten Tausche349e4802023-02-07 10:25:17 +010075 raise ValueError(
76 f"Error parsing manifest {manifest_file_path}: {exception}"
77 ) from exception
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020078
79 if not root or not root.childNodes:
Karsten Tausche349e4802023-02-07 10:25:17 +010080 raise ValueError(f"No root node in {manifest_file_path}")
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +020081
Karsten Tausche349e4802023-02-07 10:25:17 +010082 manifest_node = next((n for n in root.childNodes if n.nodeName == cls.TAG_MANIFEST), None)
Karsten Tauscheda2350f2020-04-21 15:13:06 +020083 if not manifest_node:
Karsten Tausche349e4802023-02-07 10:25:17 +010084 raise ValueError(f"No <{cls.TAG_MANIFEST}> node in {manifest_file_path}")
Karsten Tauscheda2350f2020-04-21 15:13:06 +020085
86 return manifest_node
87
Karsten Tausche349e4802023-02-07 10:25:17 +010088 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 Tauscheda2350f2020-04-21 15:13:06 +020094
95 Arguments:
Karsten Tausche349e4802023-02-07 10:25:17 +010096 relative_manifest_path: Path of the manifest to parse relative to
97 self's initial root path.
Karsten Tauscheda2350f2020-04-21 15:13:06 +020098 """
Karsten Tausche349e4802023-02-07 10:25:17 +010099 manifest_node = self._load_manifest_file(self._manifests_root_path / relative_manifest_path)
Karsten Tauscheda2350f2020-04-21 15:13:06 +0200100 for node in manifest_node.childNodes:
Karsten Tausche349e4802023-02-07 10:25:17 +0100101 if node.nodeName == self.TAG_PROJECT:
102 self._add_project(node)
103 elif node.nodeName == self.TAG_INCLUDE_MANIFEST:
Karsten Tauscheda2350f2020-04-21 15:13:06 +0200104 relative_include_path = node.getAttribute("name")
Karsten Tausche349e4802023-02-07 10:25:17 +0100105 # 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 Tauschec593e592023-02-07 13:09:15 +0100111 elif node.nodeName not in self.IGNORED_TAGS:
112 raise ValueError(f"Can't parse manifest tag: {node.nodeName}")
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +0200113
Karsten Tausche349e4802023-02-07 10:25:17 +0100114 def _add_project(self, project_node):
115 """Extract and store relevant properties of a project.
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +0200116
Karsten Tausche349e4802023-02-07 10:25:17 +0100117 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 Tchakaloff1043fa32017-09-05 11:25:52 +0200125
126 Arguments:
Karsten Tausche349e4802023-02-07 10:25:17 +0100127 project_node: XML <project> node from a manifest file.
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +0200128 Raises:
Karsten Tausche349e4802023-02-07 10:25:17 +0100129 ValueError: If the project node is invalid or if the project (same name) was already
130 added.
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +0200131 """
Karsten Tausche349e4802023-02-07 10:25:17 +0100132 name = self._get_project_name(project_node)
133 if name in self.projects:
134 raise ValueError(f"Duplicate project {name} in manifest")
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +0200135
Karsten Tausche349e4802023-02-07 10:25:17 +0100136 path = project_node.getAttribute("path") or name
137 self.projects[name] = path
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +0200138
Karsten Tausche349e4802023-02-07 10:25:17 +0100139 def _remove_project(self, project_node):
140 """Drop a project according to a 'remove-project' node.
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +0200141
Karsten Tausche349e4802023-02-07 10:25:17 +0100142 Follow the logic of the 'repo' tool to identify and drop a project from a manifest project
143 list.
Borjan Tchakaloff1043fa32017-09-05 11:25:52 +0200144
Karsten Tausche349e4802023-02-07 10:25:17 +0100145 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 Tchakaloff1043fa32017-09-05 11:25:52 +0200158
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