1*90c8c64dSAndroid Build Coastguard Worker# 2*90c8c64dSAndroid Build Coastguard Worker# Copyright (C) 2019 The Android Open Source Project 3*90c8c64dSAndroid Build Coastguard Worker# 4*90c8c64dSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License"); 5*90c8c64dSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License. 6*90c8c64dSAndroid Build Coastguard Worker# You may obtain a copy of the License at 7*90c8c64dSAndroid Build Coastguard Worker# 8*90c8c64dSAndroid Build Coastguard Worker# http://www.apache.org/licenses/LICENSE-2.0 9*90c8c64dSAndroid Build Coastguard Worker# 10*90c8c64dSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software 11*90c8c64dSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS, 12*90c8c64dSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13*90c8c64dSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and 14*90c8c64dSAndroid Build Coastguard Worker# limitations under the License. 15*90c8c64dSAndroid Build Coastguard Workerimport argparse 16*90c8c64dSAndroid Build Coastguard Workerimport collections 17*90c8c64dSAndroid Build Coastguard Workerimport fnmatch 18*90c8c64dSAndroid Build Coastguard Workerimport hashlib 19*90c8c64dSAndroid Build Coastguard Workerimport os 20*90c8c64dSAndroid Build Coastguard Workerimport pathlib 21*90c8c64dSAndroid Build Coastguard Workerimport subprocess 22*90c8c64dSAndroid Build Coastguard Workerimport tempfile 23*90c8c64dSAndroid Build Coastguard Workerimport zipfile 24*90c8c64dSAndroid Build Coastguard Workerdef silent_call(cmd): 25*90c8c64dSAndroid Build Coastguard Worker return subprocess.call( 26*90c8c64dSAndroid Build Coastguard Worker cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0 27*90c8c64dSAndroid Build Coastguard Workerdef sha1sum(f): 28*90c8c64dSAndroid Build Coastguard Worker with open(f, "rb") as fin: 29*90c8c64dSAndroid Build Coastguard Worker return hashlib.sha1(fin.read()).hexdigest() 30*90c8c64dSAndroid Build Coastguard Workerdef sha1sum_without_signing_key(filepath): 31*90c8c64dSAndroid Build Coastguard Worker apk = zipfile.ZipFile(filepath) 32*90c8c64dSAndroid Build Coastguard Worker l = [] 33*90c8c64dSAndroid Build Coastguard Worker for f in sorted(apk.namelist()): 34*90c8c64dSAndroid Build Coastguard Worker if f.startswith("META-INF/"): 35*90c8c64dSAndroid Build Coastguard Worker continue 36*90c8c64dSAndroid Build Coastguard Worker l.append(hashlib.sha1(apk.read(f)).hexdigest()) 37*90c8c64dSAndroid Build Coastguard Worker l.append(f) 38*90c8c64dSAndroid Build Coastguard Worker return hashlib.sha1(",".join(l).encode()).hexdigest() 39*90c8c64dSAndroid Build Coastguard Workerdef strip_and_sha1sum(filepath): 40*90c8c64dSAndroid Build Coastguard Worker """Strip informations of elf file and calculate sha1 hash.""" 41*90c8c64dSAndroid Build Coastguard Worker tmp_filepath = filepath + ".tmp.no-build-id" 42*90c8c64dSAndroid Build Coastguard Worker llvm_strip = [ 43*90c8c64dSAndroid Build Coastguard Worker "llvm-strip", "--strip-all", "--keep-section=.ARM.attributes", 44*90c8c64dSAndroid Build Coastguard Worker "--remove-section=.note.gnu.build-id", filepath, "-o", tmp_filepath 45*90c8c64dSAndroid Build Coastguard Worker ] 46*90c8c64dSAndroid Build Coastguard Worker strip_all_and_remove_build_id = lambda: silent_call(llvm_strip) 47*90c8c64dSAndroid Build Coastguard Worker try: 48*90c8c64dSAndroid Build Coastguard Worker if strip_all_and_remove_build_id(): 49*90c8c64dSAndroid Build Coastguard Worker return sha1sum(tmp_filepath) 50*90c8c64dSAndroid Build Coastguard Worker else: 51*90c8c64dSAndroid Build Coastguard Worker return sha1sum(filepath) 52*90c8c64dSAndroid Build Coastguard Worker finally: 53*90c8c64dSAndroid Build Coastguard Worker if os.path.exists(tmp_filepath): 54*90c8c64dSAndroid Build Coastguard Worker os.remove(tmp_filepath) 55*90c8c64dSAndroid Build Coastguard Worker return sha1sum(filepath) 56*90c8c64dSAndroid Build Coastguard Workerdef make_filter_from_allowlists(allowlists, all_targets): 57*90c8c64dSAndroid Build Coastguard Worker """Creates a callable filter from a list of allowlist files. 58*90c8c64dSAndroid Build Coastguard Worker Allowlist can contain pathname patterns or skipped lines. Pathnames are case 59*90c8c64dSAndroid Build Coastguard Worker insensitive. 60*90c8c64dSAndroid Build Coastguard Worker Allowlist can contain single-line comments. Comment lines begin with # 61*90c8c64dSAndroid Build Coastguard Worker For example, this ignores the file "system/build.prop": 62*90c8c64dSAndroid Build Coastguard Worker SYSTEM/build.prop 63*90c8c64dSAndroid Build Coastguard Worker This ignores txt files: 64*90c8c64dSAndroid Build Coastguard Worker *.txt 65*90c8c64dSAndroid Build Coastguard Worker This ignores files in directory "system/dontcare/" 66*90c8c64dSAndroid Build Coastguard Worker SYSTEM/dontcare/* 67*90c8c64dSAndroid Build Coastguard Worker This ignores lines prefixed with pat1 or pat2 in file "system/build.prop": 68*90c8c64dSAndroid Build Coastguard Worker SYSTEM/build.prop=pat1 pat2 69*90c8c64dSAndroid Build Coastguard Worker Args: 70*90c8c64dSAndroid Build Coastguard Worker allowlists: A list of allowlist filenames. 71*90c8c64dSAndroid Build Coastguard Worker all_targets: A list of targets to compare. 72*90c8c64dSAndroid Build Coastguard Worker Returns: 73*90c8c64dSAndroid Build Coastguard Worker A callable object that accepts a file pathname and returns True if the file 74*90c8c64dSAndroid Build Coastguard Worker is skipped by the allowlists and False when it is not. 75*90c8c64dSAndroid Build Coastguard Worker """ 76*90c8c64dSAndroid Build Coastguard Worker skipped_patterns = set() 77*90c8c64dSAndroid Build Coastguard Worker skipped_lines = collections.defaultdict(list) 78*90c8c64dSAndroid Build Coastguard Worker for allowlist in allowlists: 79*90c8c64dSAndroid Build Coastguard Worker if not os.path.isfile(allowlist): 80*90c8c64dSAndroid Build Coastguard Worker continue 81*90c8c64dSAndroid Build Coastguard Worker with open(allowlist, "rb") as f: 82*90c8c64dSAndroid Build Coastguard Worker for line in f: 83*90c8c64dSAndroid Build Coastguard Worker pat = line.strip().decode() 84*90c8c64dSAndroid Build Coastguard Worker if pat.startswith("#"): 85*90c8c64dSAndroid Build Coastguard Worker continue 86*90c8c64dSAndroid Build Coastguard Worker if pat and pat[-1] == "\\": 87*90c8c64dSAndroid Build Coastguard Worker pat = pat.rstrip("\\") 88*90c8c64dSAndroid Build Coastguard Worker if "=" in pat: 89*90c8c64dSAndroid Build Coastguard Worker filename, prefixes = pat.split("=", 1) 90*90c8c64dSAndroid Build Coastguard Worker prefixes = prefixes.split() 91*90c8c64dSAndroid Build Coastguard Worker if prefixes: 92*90c8c64dSAndroid Build Coastguard Worker skipped_lines[filename.lower()].extend(prefixes) 93*90c8c64dSAndroid Build Coastguard Worker elif pat: 94*90c8c64dSAndroid Build Coastguard Worker skipped_patterns.add(pat.lower()) 95*90c8c64dSAndroid Build Coastguard Worker def diff_with_skipped_lines(filename, prefixes): 96*90c8c64dSAndroid Build Coastguard Worker """Compares sha1 digest of file while ignoring lines. 97*90c8c64dSAndroid Build Coastguard Worker Args: 98*90c8c64dSAndroid Build Coastguard Worker filename: File to compare among each target. 99*90c8c64dSAndroid Build Coastguard Worker prefixes: A list of prefixes. Lines that start with prefix are skipped. 100*90c8c64dSAndroid Build Coastguard Worker Returns: 101*90c8c64dSAndroid Build Coastguard Worker True if file is identical among each target. 102*90c8c64dSAndroid Build Coastguard Worker """ 103*90c8c64dSAndroid Build Coastguard Worker file_digest_respect_ignore = [] 104*90c8c64dSAndroid Build Coastguard Worker for target in all_targets: 105*90c8c64dSAndroid Build Coastguard Worker pathname = os.path.join(target, filename) 106*90c8c64dSAndroid Build Coastguard Worker if not os.path.isfile(pathname): 107*90c8c64dSAndroid Build Coastguard Worker return False 108*90c8c64dSAndroid Build Coastguard Worker sha1 = hashlib.sha1() 109*90c8c64dSAndroid Build Coastguard Worker with open(pathname, "rb") as f: 110*90c8c64dSAndroid Build Coastguard Worker for line in f: 111*90c8c64dSAndroid Build Coastguard Worker line_text = line.decode() 112*90c8c64dSAndroid Build Coastguard Worker if not any(line_text.startswith(prefix) for prefix in prefixes): 113*90c8c64dSAndroid Build Coastguard Worker sha1.update(line) 114*90c8c64dSAndroid Build Coastguard Worker file_digest_respect_ignore.append(sha1.hexdigest()) 115*90c8c64dSAndroid Build Coastguard Worker return (len(file_digest_respect_ignore) == len(all_targets) and 116*90c8c64dSAndroid Build Coastguard Worker len(set(file_digest_respect_ignore)) == 1) 117*90c8c64dSAndroid Build Coastguard Worker def allowlist_filter(filename): 118*90c8c64dSAndroid Build Coastguard Worker norm_filename = filename.lower() 119*90c8c64dSAndroid Build Coastguard Worker for pattern in skipped_patterns: 120*90c8c64dSAndroid Build Coastguard Worker if fnmatch.fnmatch(norm_filename, pattern): 121*90c8c64dSAndroid Build Coastguard Worker return True 122*90c8c64dSAndroid Build Coastguard Worker if norm_filename in skipped_lines: 123*90c8c64dSAndroid Build Coastguard Worker skipped_prefixes = skipped_lines[norm_filename] 124*90c8c64dSAndroid Build Coastguard Worker return diff_with_skipped_lines(filename, skipped_prefixes) 125*90c8c64dSAndroid Build Coastguard Worker return False 126*90c8c64dSAndroid Build Coastguard Worker return allowlist_filter 127*90c8c64dSAndroid Build Coastguard Workerdef main(all_targets, 128*90c8c64dSAndroid Build Coastguard Worker search_paths, 129*90c8c64dSAndroid Build Coastguard Worker allowlists, 130*90c8c64dSAndroid Build Coastguard Worker ignore_signing_key=False, 131*90c8c64dSAndroid Build Coastguard Worker list_only=False): 132*90c8c64dSAndroid Build Coastguard Worker def run(path): 133*90c8c64dSAndroid Build Coastguard Worker is_executable_component = silent_call(["llvm-objdump", "-a", path]) 134*90c8c64dSAndroid Build Coastguard Worker is_apk = path.endswith(".apk") 135*90c8c64dSAndroid Build Coastguard Worker if is_executable_component: 136*90c8c64dSAndroid Build Coastguard Worker return strip_and_sha1sum(path) 137*90c8c64dSAndroid Build Coastguard Worker elif is_apk and ignore_signing_key: 138*90c8c64dSAndroid Build Coastguard Worker return sha1sum_without_signing_key(path) 139*90c8c64dSAndroid Build Coastguard Worker else: 140*90c8c64dSAndroid Build Coastguard Worker return sha1sum(path) 141*90c8c64dSAndroid Build Coastguard Worker # artifact_sha1_target_map[filename][sha1] = list of targets 142*90c8c64dSAndroid Build Coastguard Worker artifact_sha1_target_map = collections.defaultdict( 143*90c8c64dSAndroid Build Coastguard Worker lambda: collections.defaultdict(list)) 144*90c8c64dSAndroid Build Coastguard Worker for target in all_targets: 145*90c8c64dSAndroid Build Coastguard Worker paths = [] 146*90c8c64dSAndroid Build Coastguard Worker for search_path in search_paths: 147*90c8c64dSAndroid Build Coastguard Worker for path in pathlib.Path(target, search_path).glob("**/*"): 148*90c8c64dSAndroid Build Coastguard Worker if path.exists() and not path.is_dir(): 149*90c8c64dSAndroid Build Coastguard Worker paths.append((str(path), str(path.relative_to(target)))) 150*90c8c64dSAndroid Build Coastguard Worker target_basename = os.path.basename(os.path.normpath(target)) 151*90c8c64dSAndroid Build Coastguard Worker for path, filename in paths: 152*90c8c64dSAndroid Build Coastguard Worker sha1 = 0 153*90c8c64dSAndroid Build Coastguard Worker if not list_only: 154*90c8c64dSAndroid Build Coastguard Worker sha1 = run(path) 155*90c8c64dSAndroid Build Coastguard Worker artifact_sha1_target_map[filename][sha1].append(target_basename) 156*90c8c64dSAndroid Build Coastguard Worker def pretty_print(sha1, filename, targets, exclude_sha1): 157*90c8c64dSAndroid Build Coastguard Worker if exclude_sha1: 158*90c8c64dSAndroid Build Coastguard Worker return "{}, {}\n".format(filename, ";".join(targets)) 159*90c8c64dSAndroid Build Coastguard Worker return "{}, {}, {}\n".format(filename, sha1[:10], ";".join(targets)) 160*90c8c64dSAndroid Build Coastguard Worker def is_common(sha1_target_map): 161*90c8c64dSAndroid Build Coastguard Worker for _, targets in sha1_target_map.items(): 162*90c8c64dSAndroid Build Coastguard Worker return len(sha1_target_map) == 1 and len(targets) == len(all_targets) 163*90c8c64dSAndroid Build Coastguard Worker return False 164*90c8c64dSAndroid Build Coastguard Worker allowlist_filter = make_filter_from_allowlists(allowlists, all_targets) 165*90c8c64dSAndroid Build Coastguard Worker common = [] 166*90c8c64dSAndroid Build Coastguard Worker diff = [] 167*90c8c64dSAndroid Build Coastguard Worker allowlisted_diff = [] 168*90c8c64dSAndroid Build Coastguard Worker for filename, sha1_target_map in artifact_sha1_target_map.items(): 169*90c8c64dSAndroid Build Coastguard Worker if is_common(sha1_target_map): 170*90c8c64dSAndroid Build Coastguard Worker for sha1, targets in sha1_target_map.items(): 171*90c8c64dSAndroid Build Coastguard Worker common.append(pretty_print(sha1, filename, targets, list_only)) 172*90c8c64dSAndroid Build Coastguard Worker else: 173*90c8c64dSAndroid Build Coastguard Worker if allowlist_filter(filename): 174*90c8c64dSAndroid Build Coastguard Worker for sha1, targets in sha1_target_map.items(): 175*90c8c64dSAndroid Build Coastguard Worker allowlisted_diff.append( 176*90c8c64dSAndroid Build Coastguard Worker pretty_print(sha1, filename, targets, list_only)) 177*90c8c64dSAndroid Build Coastguard Worker else: 178*90c8c64dSAndroid Build Coastguard Worker for sha1, targets in sha1_target_map.items(): 179*90c8c64dSAndroid Build Coastguard Worker diff.append(pretty_print(sha1, filename, targets, list_only)) 180*90c8c64dSAndroid Build Coastguard Worker common = sorted(common) 181*90c8c64dSAndroid Build Coastguard Worker diff = sorted(diff) 182*90c8c64dSAndroid Build Coastguard Worker allowlisted_diff = sorted(allowlisted_diff) 183*90c8c64dSAndroid Build Coastguard Worker header = "filename, sha1sum, targets\n" 184*90c8c64dSAndroid Build Coastguard Worker if list_only: 185*90c8c64dSAndroid Build Coastguard Worker header = "filename, targets\n" 186*90c8c64dSAndroid Build Coastguard Worker with open("common.csv", "w") as fout: 187*90c8c64dSAndroid Build Coastguard Worker fout.write(header) 188*90c8c64dSAndroid Build Coastguard Worker fout.writelines(common) 189*90c8c64dSAndroid Build Coastguard Worker with open("diff.csv", "w") as fout: 190*90c8c64dSAndroid Build Coastguard Worker fout.write(header) 191*90c8c64dSAndroid Build Coastguard Worker fout.writelines(diff) 192*90c8c64dSAndroid Build Coastguard Worker with open("allowlisted_diff.csv", "w") as fout: 193*90c8c64dSAndroid Build Coastguard Worker fout.write(header) 194*90c8c64dSAndroid Build Coastguard Worker fout.writelines(allowlisted_diff) 195*90c8c64dSAndroid Build Coastguard Workerdef main_with_zip(extracted_paths, main_args): 196*90c8c64dSAndroid Build Coastguard Worker for origin_path, tmp_path in zip(main_args.target, extracted_paths): 197*90c8c64dSAndroid Build Coastguard Worker unzip_cmd = ["unzip", "-qd", tmp_path, os.path.join(origin_path, "*.zip")] 198*90c8c64dSAndroid Build Coastguard Worker unzip_cmd.extend([os.path.join(s, "*") for s in main_args.search_path]) 199*90c8c64dSAndroid Build Coastguard Worker subprocess.call(unzip_cmd) 200*90c8c64dSAndroid Build Coastguard Worker main( 201*90c8c64dSAndroid Build Coastguard Worker extracted_paths, 202*90c8c64dSAndroid Build Coastguard Worker main_args.search_path, 203*90c8c64dSAndroid Build Coastguard Worker main_args.allowlist, 204*90c8c64dSAndroid Build Coastguard Worker main_args.ignore_signing_key, 205*90c8c64dSAndroid Build Coastguard Worker list_only=main_args.list_only) 206*90c8c64dSAndroid Build Coastguard Workerif __name__ == "__main__": 207*90c8c64dSAndroid Build Coastguard Worker parser = argparse.ArgumentParser( 208*90c8c64dSAndroid Build Coastguard Worker prog="compare_images", 209*90c8c64dSAndroid Build Coastguard Worker usage="compare_images -t model1 model2 [model...] -s dir1 [dir...] [-i] [-u] [-p] [-w allowlist1] [-w allowlist2]" 210*90c8c64dSAndroid Build Coastguard Worker ) 211*90c8c64dSAndroid Build Coastguard Worker parser.add_argument("-t", "--target", nargs="+", required=True) 212*90c8c64dSAndroid Build Coastguard Worker parser.add_argument("-s", "--search_path", nargs="+", required=True) 213*90c8c64dSAndroid Build Coastguard Worker parser.add_argument("-i", "--ignore_signing_key", action="store_true") 214*90c8c64dSAndroid Build Coastguard Worker parser.add_argument( 215*90c8c64dSAndroid Build Coastguard Worker "-l", 216*90c8c64dSAndroid Build Coastguard Worker "--list_only", 217*90c8c64dSAndroid Build Coastguard Worker action="store_true", 218*90c8c64dSAndroid Build Coastguard Worker help="Compare file list only and ignore SHA-1 diff") 219*90c8c64dSAndroid Build Coastguard Worker parser.add_argument("-u", "--unzip", action="store_true") 220*90c8c64dSAndroid Build Coastguard Worker parser.add_argument("-p", "--preserve_extracted_files", action="store_true") 221*90c8c64dSAndroid Build Coastguard Worker parser.add_argument("-w", "--allowlist", action="append", default=[]) 222*90c8c64dSAndroid Build Coastguard Worker args = parser.parse_args() 223*90c8c64dSAndroid Build Coastguard Worker if len(args.target) < 2: 224*90c8c64dSAndroid Build Coastguard Worker parser.error("The number of targets has to be at least two.") 225*90c8c64dSAndroid Build Coastguard Worker if args.unzip: 226*90c8c64dSAndroid Build Coastguard Worker if args.preserve_extracted_files: 227*90c8c64dSAndroid Build Coastguard Worker main_with_zip(args.target, args) 228*90c8c64dSAndroid Build Coastguard Worker else: 229*90c8c64dSAndroid Build Coastguard Worker with tempfile.TemporaryDirectory() as tmpdir: 230*90c8c64dSAndroid Build Coastguard Worker target_in_tmp = [os.path.join(tmpdir, t) for t in args.target] 231*90c8c64dSAndroid Build Coastguard Worker for p in target_in_tmp: 232*90c8c64dSAndroid Build Coastguard Worker os.makedirs(p) 233*90c8c64dSAndroid Build Coastguard Worker main_with_zip(target_in_tmp, args) 234*90c8c64dSAndroid Build Coastguard Worker else: 235*90c8c64dSAndroid Build Coastguard Worker main( 236*90c8c64dSAndroid Build Coastguard Worker args.target, 237*90c8c64dSAndroid Build Coastguard Worker args.search_path, 238*90c8c64dSAndroid Build Coastguard Worker args.allowlist, 239*90c8c64dSAndroid Build Coastguard Worker args.ignore_signing_key, 240*90c8c64dSAndroid Build Coastguard Worker list_only=args.list_only) 241