xref: /aosp_15_r20/tools/treble/split/manifest_diff.py (revision 105f628577ac4ba0e277a494fbb614ed8c12a994)
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