1#!/usr/bin/env python
2#
3# Copyright (C) 2019 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"""
18Check VINTF compatibility from a target files package.
19
20Usage: check_target_files_vintf target_files
21
22target_files can be a ZIP file or an extracted target files directory.
23"""
24
25import logging
26import subprocess
27import sys
28import os
29import zipfile
30
31import common
32
33logger = logging.getLogger(__name__)
34
35OPTIONS = common.OPTIONS
36
37# Keys are paths that VINTF searches. Must keep in sync with libvintf's search
38# paths (VintfObject.cpp).
39# These paths are stored in different directories in target files package, so
40# we have to search for the correct path and tell checkvintf to remap them.
41# Look for TARGET_COPY_OUT_* variables in board_config.mk for possible paths for
42# each partition.
43DIR_SEARCH_PATHS = {
44    '/system': ('SYSTEM',),
45    '/vendor': ('VENDOR', 'SYSTEM/vendor'),
46    '/product': ('PRODUCT', 'SYSTEM/product'),
47    '/odm': ('ODM', 'VENDOR/odm', 'SYSTEM/vendor/odm'),
48    '/system_ext': ('SYSTEM_EXT', 'SYSTEM/system_ext'),
49    # vendor_dlkm and odm_dlkm does not have VINTF files.
50}
51
52UNZIP_PATTERN = ['META/*', '*/build.prop']
53
54
55def GetDirmap(input_tmp):
56  dirmap = {}
57  for device_path, target_files_rel_paths in DIR_SEARCH_PATHS.items():
58    for target_files_rel_path in target_files_rel_paths:
59      target_files_path = os.path.join(input_tmp, target_files_rel_path)
60      if os.path.isdir(target_files_path):
61        dirmap[device_path] = target_files_path
62        break
63    if device_path not in dirmap:
64      raise ValueError("Can't determine path for device path " + device_path +
65                       ". Searched the following:" +
66                       ("\n".join(target_files_rel_paths)))
67  return dirmap
68
69
70def GetArgsForSkus(info_dict):
71  odm_skus = info_dict.get('vintf_odm_manifest_skus', '').strip().split()
72  if info_dict.get('vintf_include_empty_odm_sku', '') == "true" or not odm_skus:
73    odm_skus += ['']
74
75  vendor_skus = info_dict.get('vintf_vendor_manifest_skus', '').strip().split()
76  if info_dict.get('vintf_include_empty_vendor_sku', '') == "true" or \
77      not vendor_skus:
78    vendor_skus += ['']
79
80  return [['--property', 'ro.boot.product.hardware.sku=' + odm_sku,
81           '--property', 'ro.boot.product.vendor.sku=' + vendor_sku]
82          for odm_sku in odm_skus for vendor_sku in vendor_skus]
83
84
85def GetArgsForShippingApiLevel(info_dict):
86  shipping_api_level = info_dict['vendor.build.prop'].GetProp(
87      'ro.product.first_api_level')
88  if not shipping_api_level:
89    logger.warning('Cannot determine ro.product.first_api_level')
90    return []
91  return ['--property', 'ro.product.first_api_level=' + shipping_api_level]
92
93
94def GetArgsForKernel(input_tmp):
95  version_path = os.path.join(input_tmp, 'META/kernel_version.txt')
96  config_path = os.path.join(input_tmp, 'META/kernel_configs.txt')
97
98  if not os.path.isfile(version_path) or not os.path.isfile(config_path):
99    logger.info('Skipping kernel config checks because '
100                'PRODUCT_OTA_ENFORCE_VINTF_KERNEL_REQUIREMENTS is not set')
101    return []
102
103  return ['--kernel', '{}:{}'.format(version_path, config_path)]
104
105
106def CheckVintfFromExtractedTargetFiles(input_tmp, info_dict=None):
107  """
108  Checks VINTF metadata of an extracted target files directory.
109
110  Args:
111    inp: path to the directory that contains the extracted target files archive.
112    info_dict: The build-time info dict. If None, it will be loaded from inp.
113
114  Returns:
115    True if VINTF check is skipped or compatible, False if incompatible. Raise
116    a RuntimeError if any error occurs.
117  """
118
119  if info_dict is None:
120    info_dict = common.LoadInfoDict(input_tmp)
121
122  if info_dict.get('vintf_enforce') != 'true':
123    logger.warning('PRODUCT_ENFORCE_VINTF_MANIFEST is not set, skipping checks')
124    return True
125
126  dirmap = GetDirmap(input_tmp)
127  args_for_skus = GetArgsForSkus(info_dict)
128  shipping_api_level_args = GetArgsForShippingApiLevel(info_dict)
129  kernel_args = GetArgsForKernel(input_tmp)
130
131  common_command = [
132      'checkvintf',
133      '--check-compat',
134  ]
135  for device_path, real_path in dirmap.items():
136    common_command += ['--dirmap', '{}:{}'.format(device_path, real_path)]
137  common_command += kernel_args
138  common_command += shipping_api_level_args
139
140  success = True
141  for sku_args in args_for_skus:
142    command = common_command + sku_args
143    proc = common.Run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
144    out, err = proc.communicate()
145    if proc.returncode == 0:
146      logger.info("Command `%s` returns 'compatible'", ' '.join(command))
147    elif out.strip() == "INCOMPATIBLE":
148      logger.info("Command `%s` returns 'incompatible'", ' '.join(command))
149      success = False
150    else:
151      raise common.ExternalError(
152          "Failed to run command '{}' (exit code {}):\nstdout:{}\nstderr:{}"
153          .format(' '.join(command), proc.returncode, out, err))
154    logger.info("stdout: %s", out)
155    logger.info("stderr: %s", err)
156
157  return success
158
159
160def GetVintfFileList():
161  """
162  Returns a list of VINTF metadata files that should be read from a target files
163  package before executing checkvintf.
164  """
165  def PathToPatterns(path):
166    if path[-1] == '/':
167      path += '*'
168    for device_path, target_files_rel_paths in DIR_SEARCH_PATHS.items():
169      if path.startswith(device_path):
170        suffix = path[len(device_path):]
171        return [rel_path + suffix for rel_path in target_files_rel_paths]
172    raise RuntimeError('Unrecognized path from checkvintf --dump-file-list: ' +
173                       path)
174
175  out = common.RunAndCheckOutput(['checkvintf', '--dump-file-list'])
176  paths = out.strip().split('\n')
177  paths = sum((PathToPatterns(path) for path in paths if path), [])
178  return paths
179
180
181def CheckVintfFromTargetFiles(inp, info_dict=None):
182  """
183  Checks VINTF metadata of a target files zip.
184
185  Args:
186    inp: path to the target files archive.
187    info_dict: The build-time info dict. If None, it will be loaded from inp.
188
189  Returns:
190    True if VINTF check is skipped or compatible, False if incompatible. Raise
191    a RuntimeError if any error occurs.
192  """
193  input_tmp = common.UnzipTemp(inp, GetVintfFileList() + UNZIP_PATTERN)
194  return CheckVintfFromExtractedTargetFiles(input_tmp, info_dict)
195
196
197def CheckVintf(inp, info_dict=None):
198  """
199  Checks VINTF metadata of a target files zip or extracted target files
200  directory.
201
202  Args:
203    inp: path to the (possibly extracted) target files archive.
204    info_dict: The build-time info dict. If None, it will be loaded from inp.
205
206  Returns:
207    True if VINTF check is skipped or compatible, False if incompatible. Raise
208    a RuntimeError if any error occurs.
209  """
210  if os.path.isdir(inp):
211    logger.info('Checking VINTF compatibility extracted target files...')
212    return CheckVintfFromExtractedTargetFiles(inp, info_dict)
213
214  if zipfile.is_zipfile(inp):
215    logger.info('Checking VINTF compatibility target files...')
216    return CheckVintfFromTargetFiles(inp, info_dict)
217
218  raise ValueError('{} is not a valid directory or zip file'.format(inp))
219
220def CheckVintfIfTrebleEnabled(target_files, target_info):
221  """Checks compatibility info of the input target files.
222
223  Metadata used for compatibility verification is retrieved from target_zip.
224
225  Compatibility should only be checked for devices that have enabled
226  Treble support.
227
228  Args:
229    target_files: Path to zip file containing the source files to be included
230        for OTA. Can also be the path to extracted directory.
231    target_info: The BuildInfo instance that holds the target build info.
232  """
233
234  # Will only proceed if the target has enabled the Treble support (as well as
235  # having a /vendor partition).
236  if not HasTrebleEnabled(target_files, target_info):
237    return
238
239  # Skip adding the compatibility package as a workaround for b/114240221. The
240  # compatibility will always fail on devices without qualified kernels.
241  if OPTIONS.skip_compatibility_check:
242    return
243
244  if not CheckVintf(target_files, target_info):
245    raise RuntimeError("VINTF compatibility check failed")
246
247def HasTrebleEnabled(target_files, target_info):
248  def HasVendorPartition(target_files):
249    if os.path.isdir(target_files):
250      return os.path.isdir(os.path.join(target_files, "VENDOR"))
251    if zipfile.is_zipfile(target_files):
252      return HasPartition(zipfile.ZipFile(target_files, allowZip64=True), "vendor")
253    raise ValueError("Unknown target_files argument")
254
255  return (HasVendorPartition(target_files) and
256          target_info.GetBuildProp("ro.treble.enabled") == "true")
257
258
259def HasPartition(target_files_zip, partition):
260  try:
261    target_files_zip.getinfo(partition.upper() + "/")
262    return True
263  except KeyError:
264    return False
265
266
267def main(argv):
268  args = common.ParseOptions(argv, __doc__)
269  if len(args) != 1:
270    common.Usage(__doc__)
271    sys.exit(1)
272  common.InitLogging()
273  if not CheckVintf(args[0]):
274    sys.exit(1)
275
276
277if __name__ == '__main__':
278  try:
279    common.CloseInheritedPipes()
280    main(sys.argv[1:])
281  except common.ExternalError:
282    logger.exception('\n   ERROR:\n')
283    sys.exit(1)
284  finally:
285    common.Cleanup()
286