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