1# Copyright (C) 2020 The Android Open Source Project 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""Compares two repo manifest xml files. 15 16Checks to see if the manifests contain same projects. And if those projects 17contain the same attributes, linkfile elements and copyfile elements. 18""" 19 20import argparse 21import sys 22import textwrap 23from typing import Set 24import xml.etree.ElementTree as ET 25import dataclasses 26from treble.split import xml_diff 27 28Element = ET.Element 29Change = xml_diff.Change 30ChangeMap = xml_diff.ChangeMap 31 32_SINGLE_NODE_ELEMENTS = ('default', 'manifest-server', 'repo-hooks', 'include') 33_INDENT = (' ' * 2) 34 35 36@dataclasses.dataclass 37class ProjectChanges: 38 """A collection of changes between project elements. 39 40 Attributes: 41 attributes: A ChangeMap of attributes changes. Keyed by attribute name. 42 linkfiles: A ChangeMap of linkfile elements changes. Keyed by dest. 43 copyfiles: A ChangeMap of copyfile elements changes. Keyed by dest. 44 """ 45 attributes: ChangeMap = dataclasses.field(default_factory=ChangeMap) 46 linkfiles: ChangeMap = dataclasses.field(default_factory=ChangeMap) 47 copyfiles: ChangeMap = dataclasses.field(default_factory=ChangeMap) 48 49 def __bool__(self): 50 return bool(self.attributes) or bool(self.linkfiles) or bool(self.copyfiles) 51 52 def __repr__(self): 53 if not self: 54 return 'No changes' 55 56 ret_str = '' 57 58 if self.attributes: 59 ret_str += 'Attributes:\n' 60 ret_str += textwrap.indent(str(self.attributes), _INDENT) 61 if self.linkfiles: 62 ret_str += 'Link Files:\n' 63 ret_str += textwrap.indent(str(self.linkfiles), _INDENT) 64 if self.copyfiles: 65 ret_str += 'Copy Files:\n' 66 ret_str += textwrap.indent(str(self.copyfiles), _INDENT) 67 68 return ret_str 69 70 71@dataclasses.dataclass 72class ManifestChanges: 73 """A collection of changes between manifests. 74 75 Attributes: 76 projects: A ChangeMap of changes to project elements. Keyed by project path. 77 remotes: A ChangeMap of changes to remote elements. Keyed by remote name. 78 other: A ChangeMap of changes to other elements. Keyed by element tag. 79 """ 80 projects: ChangeMap = dataclasses.field(default_factory=ChangeMap) 81 remotes: ChangeMap = dataclasses.field(default_factory=ChangeMap) 82 other: ChangeMap = dataclasses.field(default_factory=ChangeMap) 83 84 def has_changes(self): 85 return self.projects or self.remotes or self.other 86 87 def __repr__(self): 88 ret_str = 'Project Changes:\n' 89 ret_str += (textwrap.indent(str(self.projects) + '\n', _INDENT) 90 if self.projects else _INDENT + 'No changes found.\n\n') 91 ret_str += 'Remote Changes:\n' 92 ret_str += (textwrap.indent(str(self.remotes) + '\n', _INDENT) 93 if self.remotes else _INDENT + 'No changes found.\n\n') 94 ret_str += 'Other Changes:\n' 95 ret_str += (textwrap.indent(str(self.other) + '\n', _INDENT) 96 if self.other else _INDENT + 'No changes found.\n\n') 97 98 return ret_str 99 100 101def subelement_file_changes(tag: str, p1: Element, p2: Element) -> ChangeMap: 102 """Get the changes copyfile or linkfile elements between two project elements. 103 104 Arguments: 105 tag: The tag of the element. 106 p1: the xml element for the base project. 107 p2: the xml element for the new roject. 108 109 Returns: 110 A ChangeMap of copyfile or linkfile changes. Keyed by dest attribute. 111 """ 112 return xml_diff.compare_subelements( 113 tag=tag, 114 p1=p1, 115 p2=p2, 116 ignored_attrs=set(), 117 key_fn=lambda x: x.get('dest'), 118 diff_fn=xml_diff.attribute_changes) 119 120 121def project_changes(p1: Element, p2: Element, 122 ignored_attrs: Set[str]) -> ProjectChanges: 123 """Get the changes between two project elements. 124 125 Arguments: 126 p1: the xml element for the base project. 127 p2: the xml element for the new project. 128 ignored_attrs: a set of attribute names to ignore changes. 129 130 Returns: 131 A ProjectChanges object of the changes. 132 """ 133 return ProjectChanges( 134 attributes=xml_diff.attribute_changes(p1, p2, ignored_attrs), 135 linkfiles=subelement_file_changes('linkfile', p1, p2), 136 copyfiles=subelement_file_changes('copyfile', p1, p2)) 137 138 139def compare_single_node_elements(manifest_e1: Element, manifest_e2: Element, 140 ignored_attrs: Set[str]) -> ChangeMap: 141 """Get the changes between single element nodes such as <defaults> in a manifest. 142 143 Arguments: 144 manifest_e1: the xml element for the base manifest. 145 manifest_e2: the xml element for the new manifest. 146 ignored_attrs: a set of attribute names to ignore changes. 147 148 Returns: 149 A ChangeMap of changes. Keyed by elements tag name. 150 """ 151 changes = ChangeMap() 152 for tag in _SINGLE_NODE_ELEMENTS: 153 e1 = manifest_e1.find(tag) 154 e2 = manifest_e2.find(tag) 155 if e1 is None and e2 is None: 156 continue 157 elif e1 is None: 158 changes.added[tag] = xml_diff.element_string(e2) 159 elif e2 is None: 160 changes.removed[tag] = xml_diff.element_string(e1) 161 else: 162 attr_changes = xml_diff.attribute_changes(e1, e2, ignored_attrs) 163 if attr_changes: 164 changes.modified[tag] = attr_changes 165 return changes 166 167 168def compare_remote_elements(manifest_e1: Element, manifest_e2: Element, 169 ignored_attrs: Set[str]) -> ChangeMap: 170 """Get the changes to remote elements between two manifests. 171 172 Arguments: 173 manifest_e1: the xml element for the base manifest. 174 manifest_e2: the xml element for the new manifest. 175 ignored_attrs: a set of attribute names to ignore changes. 176 177 Returns: 178 A ChangeMap of changes to remote elements. Keyed by name attribute. 179 """ 180 return xml_diff.compare_subelements( 181 tag='remote', 182 p1=manifest_e1, 183 p2=manifest_e2, 184 ignored_attrs=ignored_attrs, 185 key_fn=lambda x: x.get('name'), 186 diff_fn=xml_diff.attribute_changes) 187 188 189def compare_project_elements(manifest_e1, manifest_e2, 190 ignored_attrs: Set[str]) -> ChangeMap: 191 """Get the changes to project elements between two manifests. 192 193 Arguments: 194 manifest_e1: the xml element for the base manifest. 195 manifest_e2: the xml element for the new manifest. 196 ignored_attrs: a set of attribute names to ignore changes. 197 198 Returns: 199 A ChangeMap of changes to project elements. Keyed by path/name attribute. 200 """ 201 # Ignore path attribute since it's already keyed on that value and avoid false 202 # detection when path == name on one element and path == None on the other. 203 project_ignored_attrs = ignored_attrs | set(['path']) 204 return xml_diff.compare_subelements( 205 tag='project', 206 p1=manifest_e1, 207 p2=manifest_e2, 208 ignored_attrs=project_ignored_attrs, 209 key_fn=lambda x: x.get('path', x.get('name')), 210 diff_fn=project_changes) 211 212 213def compare_manifest_elements(manifest_e1, manifest_e2, 214 ignored_attrs: Set[str]) -> ManifestChanges: 215 """Get the changes between two manifests xml elements. 216 217 Arguments: 218 manifest_e1: the xml element for the base manifest. 219 manifest_e2: the xml element for the new manifest. 220 ignored_attrs: a set of attribute names to ignore changes. 221 222 Returns: 223 A ManifestChanges. 224 """ 225 return ManifestChanges( 226 projects=compare_project_elements(manifest_e1, manifest_e2, 227 ignored_attrs), 228 remotes=compare_remote_elements(manifest_e1, manifest_e2, ignored_attrs), 229 other=compare_single_node_elements(manifest_e1, manifest_e2, 230 ignored_attrs)) 231 232 233def compare_manifest_files(manifest_a: str, manifest_b: str, 234 ignored_attrs: Set[str]) -> ManifestChanges: 235 """Get the changes between two manifests files. 236 237 Arguments: 238 manifest_a: Path to the base manifest xml file. 239 manifest_b: Path to the manifest xml file to compare against. 240 ignored_attrs: a set of attribute names to ignore changes. 241 242 Returns: 243 A ManifestChanges. 244 """ 245 e1 = ET.parse(manifest_a).getroot() 246 e2 = ET.parse(manifest_b).getroot() 247 return compare_manifest_elements( 248 manifest_e1=e1, manifest_e2=e2, ignored_attrs=ignored_attrs) 249 250 251def main(): 252 parser = argparse.ArgumentParser( 253 description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) 254 parser.add_argument( 255 '--ignored_attributes', 256 type=str, 257 help='A comma separated list of attributes to ignore when comparing ' + 258 'project elements.') 259 parser.add_argument('manifest_a', help='Path to the base manifest xml file.') 260 parser.add_argument( 261 'manifest_b', help='Path to the manifest xml file to compare against.') 262 args = parser.parse_args() 263 264 ignored_attributes = set( 265 args.ignored_attributes.split(',')) if args.ignored_attributes else set() 266 changes = compare_manifest_files(args.manifest_a, args.manifest_b, 267 ignored_attributes) 268 269 print(changes) 270 if changes: 271 sys.exit(1) 272 273 274if __name__ == '__main__': 275 main() 276