234 lines
6.7 KiB
Python
Executable File
234 lines
6.7 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Copyright (C) 2009 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
#
|
|
|
|
#
|
|
# Finds differences between two target files packages
|
|
#
|
|
|
|
from __future__ import print_function
|
|
|
|
import argparse
|
|
import contextlib
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
def ignore(name):
|
|
"""
|
|
Files to ignore when diffing
|
|
|
|
These are packages that we're already diffing elsewhere,
|
|
or files that we expect to be different for every build,
|
|
or known problems.
|
|
"""
|
|
|
|
# We're looking at the files that make the images, so no need to search them
|
|
if name in ['IMAGES']:
|
|
return True
|
|
# These are packages of the recovery partition, which we're already diffing
|
|
if name in ['SYSTEM/etc/recovery-resource.dat',
|
|
'SYSTEM/recovery-from-boot.p']:
|
|
return True
|
|
|
|
# These files are just the BUILD_NUMBER, and will always be different
|
|
if name in ['BOOT/RAMDISK/selinux_version',
|
|
'RECOVERY/RAMDISK/selinux_version']:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def rewrite_build_property(original, new):
|
|
"""
|
|
Rewrite property files to remove values known to change for every build
|
|
"""
|
|
|
|
skipped = ['ro.bootimage.build.date=',
|
|
'ro.bootimage.build.date.utc=',
|
|
'ro.bootimage.build.fingerprint=',
|
|
'ro.build.id=',
|
|
'ro.build.display.id=',
|
|
'ro.build.version.incremental=',
|
|
'ro.build.date=',
|
|
'ro.build.date.utc=',
|
|
'ro.build.host=',
|
|
'ro.build.user=',
|
|
'ro.build.description=',
|
|
'ro.build.fingerprint=',
|
|
'ro.vendor.build.date=',
|
|
'ro.vendor.build.date.utc=',
|
|
'ro.vendor.build.fingerprint=']
|
|
|
|
for line in original:
|
|
skip = False
|
|
for s in skipped:
|
|
if line.startswith(s):
|
|
skip = True
|
|
break
|
|
if not skip:
|
|
new.write(line)
|
|
|
|
|
|
def trim_install_recovery(original, new):
|
|
"""
|
|
Rewrite the install-recovery script to remove the hash of the recovery
|
|
partition.
|
|
"""
|
|
for line in original:
|
|
new.write(re.sub(r'[0-9a-f]{40}', '0'*40, line))
|
|
|
|
def sort_file(original, new):
|
|
"""
|
|
Sort the file. Some OTA metadata files are not in a deterministic order
|
|
currently.
|
|
"""
|
|
lines = original.readlines()
|
|
lines.sort()
|
|
for line in lines:
|
|
new.write(line)
|
|
|
|
# Map files to the functions that will modify them for diffing
|
|
REWRITE_RULES = {
|
|
'BOOT/RAMDISK/default.prop': rewrite_build_property,
|
|
'RECOVERY/RAMDISK/default.prop': rewrite_build_property,
|
|
'SYSTEM/build.prop': rewrite_build_property,
|
|
'VENDOR/build.prop': rewrite_build_property,
|
|
|
|
'SYSTEM/bin/install-recovery.sh': trim_install_recovery,
|
|
|
|
'META/boot_filesystem_config.txt': sort_file,
|
|
'META/filesystem_config.txt': sort_file,
|
|
'META/recovery_filesystem_config.txt': sort_file,
|
|
'META/vendor_filesystem_config.txt': sort_file,
|
|
}
|
|
|
|
@contextlib.contextmanager
|
|
def preprocess(name, filename):
|
|
"""
|
|
Optionally rewrite files before diffing them, to remove known-variable
|
|
information.
|
|
"""
|
|
if name in REWRITE_RULES:
|
|
with tempfile.NamedTemporaryFile() as newfp:
|
|
with open(filename, 'r') as oldfp:
|
|
REWRITE_RULES[name](oldfp, newfp)
|
|
newfp.flush()
|
|
yield newfp.name
|
|
else:
|
|
yield filename
|
|
|
|
def diff(name, file1, file2, out_file):
|
|
"""
|
|
Diff a file pair with diff, running preprocess() on the arguments first.
|
|
"""
|
|
with preprocess(name, file1) as f1:
|
|
with preprocess(name, file2) as f2:
|
|
proc = subprocess.Popen(['diff', f1, f2], stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT)
|
|
(stdout, _) = proc.communicate()
|
|
if proc.returncode == 0:
|
|
return
|
|
stdout = stdout.strip()
|
|
if stdout == 'Binary files %s and %s differ' % (f1, f2):
|
|
print("%s: Binary files differ" % name, file=out_file)
|
|
else:
|
|
for line in stdout.strip().split('\n'):
|
|
print("%s: %s" % (name, line), file=out_file)
|
|
|
|
def recursiveDiff(prefix, dir1, dir2, out_file):
|
|
"""
|
|
Recursively diff two directories, checking metadata then calling diff()
|
|
"""
|
|
list1 = sorted(os.listdir(dir1))
|
|
list2 = sorted(os.listdir(dir2))
|
|
|
|
for entry in list1:
|
|
name = os.path.join(prefix, entry)
|
|
name1 = os.path.join(dir1, entry)
|
|
name2 = os.path.join(dir2, entry)
|
|
|
|
if ignore(name):
|
|
continue
|
|
|
|
if entry in list2:
|
|
if os.path.islink(name1) and os.path.islink(name2):
|
|
link1 = os.readlink(name1)
|
|
link2 = os.readlink(name2)
|
|
if link1 != link2:
|
|
print("%s: Symlinks differ: %s vs %s" % (name, link1, link2),
|
|
file=out_file)
|
|
continue
|
|
elif os.path.islink(name1) or os.path.islink(name2):
|
|
print("%s: File types differ, skipping compare" % name, file=out_file)
|
|
continue
|
|
|
|
stat1 = os.stat(name1)
|
|
stat2 = os.stat(name2)
|
|
type1 = stat1.st_mode & ~0o777
|
|
type2 = stat2.st_mode & ~0o777
|
|
|
|
if type1 != type2:
|
|
print("%s: File types differ, skipping compare" % name, file=out_file)
|
|
continue
|
|
|
|
if stat1.st_mode != stat2.st_mode:
|
|
print("%s: Modes differ: %o vs %o" %
|
|
(name, stat1.st_mode, stat2.st_mode), file=out_file)
|
|
|
|
if os.path.isdir(name1):
|
|
recursiveDiff(name, name1, name2, out_file)
|
|
elif os.path.isfile(name1):
|
|
diff(name, name1, name2, out_file)
|
|
else:
|
|
print("%s: Unknown file type, skipping compare" % name, file=out_file)
|
|
else:
|
|
print("%s: Only in base package" % name, file=out_file)
|
|
|
|
for entry in list2:
|
|
name = os.path.join(prefix, entry)
|
|
name1 = os.path.join(dir1, entry)
|
|
name2 = os.path.join(dir2, entry)
|
|
|
|
if ignore(name):
|
|
continue
|
|
|
|
if entry not in list1:
|
|
print("%s: Only in new package" % name, file=out_file)
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('dir1', help='The base target files package (extracted)')
|
|
parser.add_argument('dir2', help='The new target files package (extracted)')
|
|
parser.add_argument('--output',
|
|
help='The output file, otherwise it prints to stdout')
|
|
args = parser.parse_args()
|
|
|
|
if args.output:
|
|
out_file = open(args.output, 'w')
|
|
else:
|
|
out_file = sys.stdout
|
|
|
|
recursiveDiff('', args.dir1, args.dir2, out_file)
|
|
|
|
if args.output:
|
|
out_file.close()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|