1#!/usr/bin/env python3
2
3import argparse
4import csv
5import glob
6import json
7import os
8import sys
9
10HELP_MSG = '''
11This script computes the differences between two system images (system1 -
12system2), and lists the files grouped by package. The difference is just based
13on the existence of the file, not on its contents.
14'''
15
16VENDOR_PATH_MAP = {
17    'vendor/google' : 'Google',
18    'vendor/unbundled_google': 'Google',
19    'vendor/verizon' : 'Verizon',
20    'vendor/qcom' : 'Qualcomm',
21    'vendor/tmobile' : 'TMobile',
22    'vendor/mediatek' : 'Mediatek',
23    'vendor/htc' : 'HTC',
24    'vendor/realtek' : 'Realtek'
25}
26
27def _get_relative_out_path_from_root(out_path):
28  """Given a path to a target out directory, get the relative path from the
29  Android root.
30
31  The module-info.json file paths are relative to the root source folder
32  ie. one directory before out."""
33  system_path = os.path.normpath(os.path.join(out_path, 'system'))
34  system_path_dirs = system_path.split(os.sep)
35  out_index = system_path_dirs.index("out")
36  return os.path.join(*system_path_dirs[out_index:])
37
38def system_files(path):
39  """Returns an array of the files under /system, recursively, and ignoring
40  symbolic-links"""
41  system_files = []
42  system_prefix = os.path.join(path, 'system')
43  # Skip trailing '/'
44  system_prefix_len = len(system_prefix) + 1
45
46  for root, dirs, files in os.walk(system_prefix, topdown=True):
47    for file in files:
48      # Ignore symbolic links.
49      if not os.path.islink(os.path.join(root, file)):
50        system_files.append(os.path.join(root[system_prefix_len:], file))
51
52  return system_files
53
54def system_files_to_package_map(path):
55  """Returns a dictionary mapping from each file in the /system partition to its
56  package, according to modules-info.json."""
57  system_files_to_package_map = {}
58  system_prefix = _get_relative_out_path_from_root(path)
59  # Skip trailing '/'
60  system_prefix_len = len(system_prefix) + 1
61
62  with open(os.path.join(path, 'module-info.json')) as module_info_json:
63    module_info = json.load(module_info_json)
64    for module in module_info:
65      installs = module_info[module]['installed']
66      for install in installs:
67        if install.startswith(system_prefix):
68          system_file = install[system_prefix_len:]
69          # Not clear if collisions can ever happen in modules-info.json (e.g.
70          # the same file installed by multiple packages), but it doesn't hurt
71          # to check.
72          if system_file in system_files_to_package_map:
73            system_files_to_package_map[system_file] = "--multiple--"
74          else:
75            system_files_to_package_map[system_file] = module
76
77  return system_files_to_package_map
78
79def package_to_vendor_map(path):
80  """Returns a dictionary mapping from each package in modules-info.json to its
81  vendor. If a vendor cannot be found, it maps to "--unknown--". Those cases
82  are:
83
84    1. The package maps to multiple modules (e.g., one in APPS and one in
85       SHARED_LIBRARIES.
86    2. The path to the module is not one of the recognized vendor paths in
87       VENDOR_PATH_MAP."""
88  package_vendor_map = {}
89  system_prefix = os.path.join(path, 'system')
90  # Skip trailing '/'
91  system_prefix_len = len(system_prefix) + 1
92  vendor_prefixes = VENDOR_PATH_MAP.keys()
93
94  with open(os.path.join(path, 'module-info.json')) as module_info_json:
95    module_info = json.load(module_info_json)
96    for module in module_info:
97      paths = module_info[module]['path']
98      vendor = ""
99      if len(paths) == 1:
100        path = paths[0]
101        for prefix in vendor_prefixes:
102          if path.startswith(prefix):
103            vendor = VENDOR_PATH_MAP[prefix]
104            break
105        if vendor == "":
106          vendor = "--unknown--"
107      else:
108        vendor = "--multiple--"
109      package_vendor_map[module] = vendor
110
111  return package_vendor_map
112
113def main():
114  parser = argparse.ArgumentParser(description=HELP_MSG)
115  parser.add_argument("out1", help="First $OUT directory")
116  parser.add_argument("out2", help="Second $OUT directory")
117  args = parser.parse_args()
118
119  system_files1 = system_files(args.out1)
120  system_files2 = system_files(args.out2)
121  system_files_diff = set(system_files1) - set(system_files2)
122
123  system_files_map = system_files_to_package_map(args.out1)
124  package_vendor_map = package_to_vendor_map(args.out1)
125  packages = {}
126
127  for file in system_files_diff:
128    if file in system_files_map:
129      package = system_files_map[file]
130    else:
131      package = "--unknown--"
132
133    if package in packages:
134      packages[package].append(file)
135    else:
136      packages[package] = [file]
137
138  with open(os.path.join(args.out1, 'module-info.json')) as module_info_json:
139    module_info = json.load(module_info_json)
140
141  writer = csv.writer(sys.stdout, quoting = csv.QUOTE_NONNUMERIC,
142                      delimiter = ',', lineterminator = '\n')
143  for package, files in packages.iteritems():
144    for file in files:
145      # Group sources of the deltas.
146      if package in package_vendor_map:
147        vendor = package_vendor_map[package]
148      else:
149        vendor = "--unknown--"
150      # Get file size.
151      full_path = os.path.join(args.out1, 'system', file)
152      size = os.stat(full_path).st_size
153      if package in module_info.keys():
154        module_path = module_info[package]['path']
155      else:
156        module_path = ''
157      writer.writerow([
158          # File that exists in out1 but not out2.
159          file,
160          # Module name that the file came from.
161          package,
162          # Path to the module.
163          module_path,
164          # File size.
165          size,
166          # Vendor owner.
167          vendor])
168
169if __name__ == '__main__':
170  main()
171