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.vendor.build.date=',
75             'ro.vendor.build.date.utc=',
76             'ro.vendor.build.fingerprint=']
77
78  for line in original:
79    skip = False
80    for s in skipped:
81      if line.startswith(s):
82        skip = True
83        break
84    if not skip:
85      new.write(line)
86
87
88def trim_install_recovery(original, new):
89  """
90  Rewrite the install-recovery script to remove the hash of the recovery
91  partition.
92  """
93  for line in original:
94    new.write(re.sub(r'[0-9a-f]{40}', '0'*40, line))
95
96def sort_file(original, new):
97  """
98  Sort the file. Some OTA metadata files are not in a deterministic order
99  currently.
100  """
101  lines = original.readlines()
102  lines.sort()
103  for line in lines:
104    new.write(line)
105
106# Map files to the functions that will modify them for diffing
107REWRITE_RULES = {
108    'BOOT/RAMDISK/default.prop': rewrite_build_property,
109    'RECOVERY/RAMDISK/default.prop': rewrite_build_property,
110    'SYSTEM/build.prop': rewrite_build_property,
111    'VENDOR/build.prop': rewrite_build_property,
112
113    'SYSTEM/bin/install-recovery.sh': trim_install_recovery,
114
115    'META/boot_filesystem_config.txt': sort_file,
116    'META/filesystem_config.txt': sort_file,
117    'META/recovery_filesystem_config.txt': sort_file,
118    'META/vendor_filesystem_config.txt': sort_file,
119}
120
121@contextlib.contextmanager
122def preprocess(name, filename):
123  """
124  Optionally rewrite files before diffing them, to remove known-variable
125  information.
126  """
127  if name in REWRITE_RULES:
128    with tempfile.NamedTemporaryFile() as newfp:
129      with open(filename, 'r') as oldfp:
130        REWRITE_RULES[name](oldfp, newfp)
131      newfp.flush()
132      yield newfp.name
133  else:
134    yield filename
135
136def diff(name, file1, file2, out_file):
137  """
138  Diff a file pair with diff, running preprocess() on the arguments first.
139  """
140  with preprocess(name, file1) as f1:
141    with preprocess(name, file2) as f2:
142      proc = subprocess.Popen(['diff', f1, f2], stdout=subprocess.PIPE,
143                              stderr=subprocess.STDOUT)
144      (stdout, _) = proc.communicate()
145      if proc.returncode == 0:
146        return
147      stdout = stdout.strip()
148      if stdout == 'Binary files %s and %s differ' % (f1, f2):
149        print("%s: Binary files differ" % name, file=out_file)
150      else:
151        for line in stdout.strip().split('\n'):
152          print("%s: %s" % (name, line), file=out_file)
153
154def recursiveDiff(prefix, dir1, dir2, out_file):
155  """
156  Recursively diff two directories, checking metadata then calling diff()
157  """
158  list1 = sorted(os.listdir(dir1))
159  list2 = sorted(os.listdir(dir2))
160
161  for entry in list1:
162    name = os.path.join(prefix, entry)
163    name1 = os.path.join(dir1, entry)
164    name2 = os.path.join(dir2, entry)
165
166    if ignore(name):
167      continue
168
169    if entry in list2:
170      if os.path.islink(name1) and os.path.islink(name2):
171        link1 = os.readlink(name1)
172        link2 = os.readlink(name2)
173        if link1 != link2:
174          print("%s: Symlinks differ: %s vs %s" % (name, link1, link2),
175                file=out_file)
176        continue
177      elif os.path.islink(name1) or os.path.islink(name2):
178        print("%s: File types differ, skipping compare" % name, file=out_file)
179        continue
180
181      stat1 = os.stat(name1)
182      stat2 = os.stat(name2)
183      type1 = stat1.st_mode & ~0o777
184      type2 = stat2.st_mode & ~0o777
185
186      if type1 != type2:
187        print("%s: File types differ, skipping compare" % name, file=out_file)
188        continue
189
190      if stat1.st_mode != stat2.st_mode:
191        print("%s: Modes differ: %o vs %o" %
192            (name, stat1.st_mode, stat2.st_mode), file=out_file)
193
194      if os.path.isdir(name1):
195        recursiveDiff(name, name1, name2, out_file)
196      elif os.path.isfile(name1):
197        diff(name, name1, name2, out_file)
198      else:
199        print("%s: Unknown file type, skipping compare" % name, file=out_file)
200    else:
201      print("%s: Only in base package" % name, file=out_file)
202
203  for entry in list2:
204    name = os.path.join(prefix, entry)
205    name1 = os.path.join(dir1, entry)
206    name2 = os.path.join(dir2, entry)
207
208    if ignore(name):
209      continue
210
211    if entry not in list1:
212      print("%s: Only in new package" % name, file=out_file)
213
214def main():
215  parser = argparse.ArgumentParser()
216  parser.add_argument('dir1', help='The base target files package (extracted)')
217  parser.add_argument('dir2', help='The new target files package (extracted)')
218  parser.add_argument('--output',
219      help='The output file, otherwise it prints to stdout')
220  args = parser.parse_args()
221
222  if args.output:
223    out_file = open(args.output, 'w')
224  else:
225    out_file = sys.stdout
226
227  recursiveDiff('', args.dir1, args.dir2, out_file)
228
229  if args.output:
230    out_file.close()
231
232if __name__ == '__main__':
233  main()
234