1#!/usr/bin/env python
2#
3# Copyright (C) 2009 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18#
19# Finds differences between two target files packages
20#
21
22from __future__ import print_function
23
24import argparse
25import contextlib
26import os
27import re
28import subprocess
29import sys
30import tempfile
31
32def ignore(name):
33  """
34  Files to ignore when diffing
35
36  These are packages that we're already diffing elsewhere,
37  or files that we expect to be different for every build,
38  or known problems.
39  """
40
41  # We're looking at the files that make the images, so no need to search them
42  if name in ['IMAGES']:
43    return True
44  # These are packages of the recovery partition, which we're already diffing
45  if name in ['SYSTEM/etc/recovery-resource.dat',
46              'SYSTEM/recovery-from-boot.p']:
47    return True
48
49  # These files are just the BUILD_NUMBER, and will always be different
50  if name in ['BOOT/RAMDISK/selinux_version',
51              'RECOVERY/RAMDISK/selinux_version']:
52    return True
53
54  return False
55
56
57def rewrite_build_property(original, new):
58  """
59  Rewrite property files to remove values known to change for every build
60  """
61
62  skipped = ['ro.bootimage.build.date=',
63             'ro.bootimage.build.date.utc=',
64             'ro.bootimage.build.fingerprint=',
65             'ro.build.id=',
66             'ro.build.display.id=',
67             'ro.build.version.incremental=',
68             'ro.build.date=',
69             'ro.build.date.utc=',
70             'ro.build.host=',
71             'ro.build.user=',
72             'ro.build.description=',
73             'ro.build.fingerprint=',
74             'ro.expect.recovery_id=',
75             'ro.vendor.build.date=',
76             'ro.vendor.build.date.utc=',
77             'ro.vendor.build.fingerprint=']
78
79  for line in original:
80    skip = False
81    for s in skipped:
82      if line.startswith(s):
83        skip = True
84        break
85    if not skip:
86      new.write(line)
87
88
89def trim_install_recovery(original, new):
90  """
91  Rewrite the install-recovery script to remove the hash of the recovery
92  partition.
93  """
94  for line in original:
95    new.write(re.sub(r'[0-9a-f]{40}', '0'*40, line))
96
97def sort_file(original, new):
98  """
99  Sort the file. Some OTA metadata files are not in a deterministic order
100  currently.
101  """
102  lines = original.readlines()
103  lines.sort()
104  for line in lines:
105    new.write(line)
106
107# Map files to the functions that will modify them for diffing
108REWRITE_RULES = {
109    'BOOT/RAMDISK/default.prop': rewrite_build_property,
110    'RECOVERY/RAMDISK/default.prop': rewrite_build_property,
111    'SYSTEM/build.prop': rewrite_build_property,
112    'VENDOR/build.prop': rewrite_build_property,
113
114    'SYSTEM/bin/install-recovery.sh': trim_install_recovery,
115
116    'META/boot_filesystem_config.txt': sort_file,
117    'META/filesystem_config.txt': sort_file,
118    'META/recovery_filesystem_config.txt': sort_file,
119    'META/vendor_filesystem_config.txt': sort_file,
120}
121
122@contextlib.contextmanager
123def preprocess(name, filename):
124  """
125  Optionally rewrite files before diffing them, to remove known-variable
126  information.
127  """
128  if name in REWRITE_RULES:
129    with tempfile.NamedTemporaryFile() as newfp:
130      with open(filename, 'r') as oldfp:
131        REWRITE_RULES[name](oldfp, newfp)
132      newfp.flush()
133      yield newfp.name
134  else:
135    yield filename
136
137def diff(name, file1, file2, out_file):
138  """
139  Diff a file pair with diff, running preprocess() on the arguments first.
140  """
141  with preprocess(name, file1) as f1:
142    with preprocess(name, file2) as f2:
143      proc = subprocess.Popen(['diff', f1, f2], stdout=subprocess.PIPE,
144                              stderr=subprocess.STDOUT)
145      (stdout, _) = proc.communicate()
146      if proc.returncode == 0:
147        return
148      stdout = stdout.strip()
149      if stdout == 'Binary files %s and %s differ' % (f1, f2):
150        print("%s: Binary files differ" % name, file=out_file)
151      else:
152        for line in stdout.strip().split('\n'):
153          print("%s: %s" % (name, line), file=out_file)
154
155def recursiveDiff(prefix, dir1, dir2, out_file):
156  """
157  Recursively diff two directories, checking metadata then calling diff()
158  """
159  list1 = sorted(os.listdir(dir1))
160  list2 = sorted(os.listdir(dir2))
161
162  for entry in list1:
163    name = os.path.join(prefix, entry)
164    name1 = os.path.join(dir1, entry)
165    name2 = os.path.join(dir2, entry)
166
167    if ignore(name):
168      continue
169
170    if entry in list2:
171      if os.path.islink(name1) and os.path.islink(name2):
172        link1 = os.readlink(name1)
173        link2 = os.readlink(name2)
174        if link1 != link2:
175          print("%s: Symlinks differ: %s vs %s" % (name, link1, link2),
176                file=out_file)
177        continue
178      elif os.path.islink(name1) or os.path.islink(name2):
179        print("%s: File types differ, skipping compare" % name, file=out_file)
180        continue
181
182      stat1 = os.stat(name1)
183      stat2 = os.stat(name2)
184      type1 = stat1.st_mode & ~0o777
185      type2 = stat2.st_mode & ~0o777
186
187      if type1 != type2:
188        print("%s: File types differ, skipping compare" % name, file=out_file)
189        continue
190
191      if stat1.st_mode != stat2.st_mode:
192        print("%s: Modes differ: %o vs %o" %
193            (name, stat1.st_mode, stat2.st_mode), file=out_file)
194
195      if os.path.isdir(name1):
196        recursiveDiff(name, name1, name2, out_file)
197      elif os.path.isfile(name1):
198        diff(name, name1, name2, out_file)
199      else:
200        print("%s: Unknown file type, skipping compare" % name, file=out_file)
201    else:
202      print("%s: Only in base package" % name, file=out_file)
203
204  for entry in list2:
205    name = os.path.join(prefix, entry)
206    name1 = os.path.join(dir1, entry)
207    name2 = os.path.join(dir2, entry)
208
209    if ignore(name):
210      continue
211
212    if entry not in list1:
213      print("%s: Only in new package" % name, file=out_file)
214
215def main():
216  parser = argparse.ArgumentParser()
217  parser.add_argument('dir1', help='The base target files package (extracted)')
218  parser.add_argument('dir2', help='The new target files package (extracted)')
219  parser.add_argument('--output',
220      help='The output file, otherwise it prints to stdout')
221  args = parser.parse_args()
222
223  if args.output:
224    out_file = open(args.output, 'w')
225  else:
226    out_file = sys.stdout
227
228  recursiveDiff('', args.dir1, args.dir2, out_file)
229
230  if args.output:
231    out_file.close()
232
233if __name__ == '__main__':
234  main()
235