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) and os.path.islink(name2): 176 link1 = os.readlink(name1) 177 link2 = os.readlink(name2) 178 if link1 != link2: 179 print("%s: Symlinks differ: %s vs %s" % (name, link1, link2), 180 file=out_file) 181 continue 182 elif os.path.islink(name1) or os.path.islink(name2): 183 print("%s: File types differ, skipping compare" % name, file=out_file) 184 continue 185 186 stat1 = os.stat(name1) 187 stat2 = os.stat(name2) 188 type1 = stat1.st_mode & ~0o777 189 type2 = stat2.st_mode & ~0o777 190 191 if type1 != type2: 192 print("%s: File types differ, skipping compare" % name, file=out_file) 193 continue 194 195 if stat1.st_mode != stat2.st_mode: 196 print("%s: Modes differ: %o vs %o" % 197 (name, stat1.st_mode, stat2.st_mode), file=out_file) 198 199 if os.path.isdir(name1): 200 recursiveDiff(name, name1, name2, out_file) 201 elif os.path.isfile(name1): 202 diff(name, name1, name2, out_file) 203 else: 204 print("%s: Unknown file type, skipping compare" % name, file=out_file) 205 else: 206 print("%s: Only in base package" % name, file=out_file) 207 208 for entry in list2: 209 name = os.path.join(prefix, entry) 210 name1 = os.path.join(dir1, entry) 211 name2 = os.path.join(dir2, entry) 212 213 if ignore(name): 214 continue 215 216 if entry not in list1: 217 print("%s: Only in new package" % name, file=out_file) 218 219def main(): 220 parser = argparse.ArgumentParser() 221 parser.add_argument('dir1', help='The base target files package (extracted)') 222 parser.add_argument('dir2', help='The new target files package (extracted)') 223 parser.add_argument('--output', 224 help='The output file, otherwise it prints to stdout') 225 args = parser.parse_args() 226 227 if args.output: 228 out_file = open(args.output, 'w') 229 else: 230 out_file = sys.stdout 231 232 recursiveDiff('', args.dir1, args.dir2, out_file) 233 234 if args.output: 235 out_file.close() 236 237if __name__ == '__main__': 238 main() 239