blob: 010ea847b9425e261e31c85ffebcdcc6c5482e52 [file] [log] [blame]
Dan Willemsen99568622015-11-06 18:36:16 -08001#!/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 tempfile
30
31def ignore(name):
32 """
33 Files to ignore when diffing
34
35 These are packages that we're already diffing elsewhere,
36 or files that we expect to be different for every build,
37 or known problems.
38 """
39
40 # We're looking at the files that make the images, so no need to search them
41 if name in ['IMAGES']:
42 return True
43 # These are packages of the recovery partition, which we're already diffing
44 if name in ['SYSTEM/etc/recovery-resource.dat',
45 'SYSTEM/recovery-from-boot.p']:
46 return True
47
48 # These files are just the BUILD_NUMBER, and will always be different
49 if name in ['BOOT/RAMDISK/selinux_version',
50 'RECOVERY/RAMDISK/selinux_version']:
51 return True
52
53 # b/24201956 .art/.oat/.odex files are different with every build
54 if name.endswith('.art') or name.endswith('.oat') or name.endswith('.odex'):
55 return True
56 # b/25348136 libpac.so changes with every build
57 if name in ['SYSTEM/lib/libpac.so',
58 'SYSTEM/lib64/libpac.so']:
59 return True
60
61 return False
62
63
64def rewrite_build_property(original, new):
65 """
66 Rewrite property files to remove values known to change for every build
67 """
68
69 skipped = ['ro.bootimage.build.date=',
70 'ro.bootimage.build.date.utc=',
71 'ro.bootimage.build.fingerprint=',
72 'ro.build.id=',
73 'ro.build.display.id=',
74 'ro.build.version.incremental=',
75 'ro.build.date=',
76 'ro.build.date.utc=',
77 'ro.build.host=',
78 'ro.build.description=',
79 'ro.build.fingerprint=',
80 'ro.expect.recovery_id=',
81 'ro.vendor.build.date=',
82 'ro.vendor.build.date.utc=',
83 'ro.vendor.build.fingerprint=']
84
85 for line in original:
86 skip = False
87 for s in skipped:
88 if line.startswith(s):
89 skip = True
90 break
91 if not skip:
92 new.write(line)
93
94
95def trim_install_recovery(original, new):
96 """
97 Rewrite the install-recovery script to remove the hash of the recovery 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 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 information.
130 """
131 if name in REWRITE_RULES:
132 with tempfile.NamedTemporaryFile() as newfp:
133 with open(filename, 'r') as oldfp:
134 REWRITE_RULES[name](oldfp, newfp)
135 newfp.flush()
136 yield newfp.name
137 else:
138 yield filename
139
140def diff(name, file1, file2):
141 """
142 Diff a file pair with diff, running preprocess() on the arguments first.
143 """
144 with preprocess(name, file1) as f1:
145 with preprocess(name, file2) as f2:
146 proc = subprocess.Popen(['diff', f1, f2], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
147 (stdout, ignore) = proc.communicate()
148 if proc.returncode == 0:
149 return
150 stdout = stdout.strip()
151 if stdout == 'Binary files %s and %s differ' % (f1, f2):
152 print("%s: Binary files differ" % name)
153 else:
154 for line in stdout.strip().split('\n'):
155 print("%s: %s" % (name, line))
156
157def recursiveDiff(prefix, dir1, dir2):
158 """
159 Recursively diff two directories, checking metadata then calling diff()
160 """
161 list1 = sorted(os.listdir(dir1))
162 list2 = sorted(os.listdir(dir2))
163
164 for entry in list1:
165 name = os.path.join(prefix, entry)
166 name1 = os.path.join(dir1, entry)
167 name2 = os.path.join(dir2, entry)
168
169 if ignore(name):
170 continue
171
172 if entry in list2:
173 if os.path.islink(name1):
174 if os.path.islink(name2):
175 link1 = os.readlink(name1)
176 link2 = os.readlink(name2)
177 if link1 != link2:
178 print("%s: Symlinks differ: %s vs %s" % (name, link1, link2))
179 else:
180 print("%s: File types differ, skipping compare" % name)
181 continue
182
183 stat1 = os.stat(name1)
184 stat2 = os.stat(name2)
185 type1 = stat1.st_mode & ~0777
186 type2 = stat2.st_mode & ~0777
187
188 if type1 != type2:
189 print("%s: File types differ, skipping compare" % name)
190 continue
191
192 if stat1.st_mode != stat2.st_mode:
193 print("%s: Modes differ: %o vs %o" % (name, stat1.st_mode, stat2.st_mode))
194
195 if os.path.isdir(name1):
196 recursiveDiff(name, name1, name2)
197 elif os.path.isfile(name1):
198 diff(name, name1, name2)
199 else:
200 print("%s: Unknown file type, skipping compare" % name)
201 else:
202 print("%s: Only in base package" % name)
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)
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 args = parser.parse_args()
220
221 recursiveDiff('', args.dir1, args.dir2)
222
223if __name__ == '__main__':
224 main()