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