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