• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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