xref: /aosp_15_r20/development/vndk/tools/image-diff-tool/diff.py (revision 90c8c64db3049935a07c6143d7fd006e26f8ecca)
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