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