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