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