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