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 json
26import logging
27import os
28import shutil
29import subprocess
30import sys
31import zipfile
32
33import apex_utils
34import common
35from apex_manifest import ParseApexManifest
36
37logger = logging.getLogger(__name__)
38
39OPTIONS = common.OPTIONS
40
41# Keys are paths that VINTF searches. Must keep in sync with libvintf's search
42# paths (VintfObject.cpp).
43# These paths are stored in different directories in target files package, so
44# we have to search for the correct path and tell checkvintf to remap them.
45# Look for TARGET_COPY_OUT_* variables in board_config.mk for possible paths for
46# each partition.
47DIR_SEARCH_PATHS = {
48    '/system': ('SYSTEM',),
49    '/vendor': ('VENDOR', 'SYSTEM/vendor'),
50    '/product': ('PRODUCT', 'SYSTEM/product'),
51    '/odm': ('ODM', 'VENDOR/odm', 'SYSTEM/vendor/odm'),
52    '/system_ext': ('SYSTEM_EXT', 'SYSTEM/system_ext'),
53    # vendor_dlkm, odm_dlkm, and system_dlkm does not have VINTF files.
54}
55
56UNZIP_PATTERN = ['META/*', '*/build.prop']
57
58
59def GetDirmap(input_tmp):
60  dirmap = {}
61  for device_path, target_files_rel_paths in DIR_SEARCH_PATHS.items():
62    for target_files_rel_path in target_files_rel_paths:
63      target_files_path = os.path.join(input_tmp, target_files_rel_path)
64      if os.path.isdir(target_files_path):
65        dirmap[device_path] = target_files_path
66        break
67    if device_path not in dirmap:
68      raise ValueError("Can't determine path for device path " + device_path +
69                       ". Searched the following:" +
70                       ("\n".join(target_files_rel_paths)))
71  return dirmap
72
73
74def GetArgsForSkus(info_dict):
75  odm_skus = info_dict.get('vintf_odm_manifest_skus', '').strip().split()
76  if info_dict.get('vintf_include_empty_odm_sku', '') == "true" or not odm_skus:
77    odm_skus += ['']
78
79  vendor_skus = info_dict.get('vintf_vendor_manifest_skus', '').strip().split()
80  if info_dict.get('vintf_include_empty_vendor_sku', '') == "true" or \
81      not vendor_skus:
82    vendor_skus += ['']
83
84  return [['--property', 'ro.boot.product.hardware.sku=' + odm_sku,
85           '--property', 'ro.boot.product.vendor.sku=' + vendor_sku]
86          for odm_sku in odm_skus for vendor_sku in vendor_skus]
87
88
89def GetArgsForShippingApiLevel(info_dict):
90  shipping_api_level = info_dict['vendor.build.prop'].GetProp(
91      'ro.product.first_api_level')
92  if not shipping_api_level:
93    logger.warning('Cannot determine ro.product.first_api_level')
94    return []
95  return ['--property', 'ro.product.first_api_level=' + shipping_api_level]
96
97
98def GetArgsForKernel(input_tmp):
99  version_path = os.path.join(input_tmp, 'META/kernel_version.txt')
100  config_path = os.path.join(input_tmp, 'META/kernel_configs.txt')
101
102  if not os.path.isfile(version_path) or not os.path.isfile(config_path):
103    logger.info('Skipping kernel config checks because '
104                'PRODUCT_OTA_ENFORCE_VINTF_KERNEL_REQUIREMENTS is not set')
105    return []
106
107  return ['--kernel', '{}:{}'.format(version_path, config_path)]
108
109
110def CheckVintfFromExtractedTargetFiles(input_tmp, info_dict=None):
111  """
112  Checks VINTF metadata of an extracted target files directory.
113
114  Args:
115    inp: path to the directory that contains the extracted target files archive.
116    info_dict: The build-time info dict. If None, it will be loaded from inp.
117
118  Returns:
119    True if VINTF check is skipped or compatible, False if incompatible. Raise
120    a RuntimeError if any error occurs.
121  """
122
123  if info_dict is None:
124    info_dict = common.LoadInfoDict(input_tmp)
125
126  if info_dict.get('vintf_enforce') != 'true':
127    logger.warning('PRODUCT_ENFORCE_VINTF_MANIFEST is not set, skipping checks')
128    return True
129
130
131  dirmap = GetDirmap(input_tmp)
132
133  # Simulate apexd with target-files.
134  # add a mapping('/apex' => ${input_tmp}/APEX) to dirmap
135  PrepareApexDirectory(input_tmp, dirmap)
136
137  args_for_skus = GetArgsForSkus(info_dict)
138  shipping_api_level_args = GetArgsForShippingApiLevel(info_dict)
139  kernel_args = GetArgsForKernel(input_tmp)
140
141  common_command = [
142      'checkvintf',
143      '--check-compat',
144  ]
145
146  for device_path, real_path in sorted(dirmap.items()):
147    common_command += ['--dirmap', '{}:{}'.format(device_path, real_path)]
148  common_command += kernel_args
149  common_command += shipping_api_level_args
150
151  success = True
152  for sku_args in args_for_skus:
153    command = common_command + sku_args
154    proc = common.Run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
155    out, err = proc.communicate()
156    last_out_line = out.split()[-1] if out != "" else out
157    if proc.returncode == 0:
158      logger.info("Command `%s` returns 'compatible'", ' '.join(command))
159    elif last_out_line.strip() == "INCOMPATIBLE":
160      logger.info("Command `%s` returns 'incompatible'", ' '.join(command))
161      success = False
162    else:
163      raise common.ExternalError(
164          "Failed to run command '{}' (exit code {}):\nstdout:{}\nstderr:{}"
165          .format(' '.join(command), proc.returncode, out, err))
166    logger.info("stdout: %s", out)
167    logger.info("stderr: %s", err)
168
169  return success
170
171
172def GetVintfFileList():
173  """
174  Returns a list of VINTF metadata files that should be read from a target files
175  package before executing checkvintf.
176  """
177  def PathToPatterns(path):
178    if path[-1] == '/':
179      path += '**'
180
181    # Loop over all the entries in DIR_SEARCH_PATHS and find one where the key
182    # is a prefix of path. In order to get find the correct prefix, sort the
183    # entries by decreasing length of their keys, so that we check if longer
184    # strings are prefixes before shorter strings. This is so that keys that
185    # are substrings of other keys (like /system vs /system_ext) are checked
186    # later, and we don't mistakenly mark a path that starts with /system_ext
187    # as starting with only /system.
188    for device_path, target_files_rel_paths in sorted(DIR_SEARCH_PATHS.items(), key=lambda i: len(i[0]), reverse=True):
189      if path.startswith(device_path):
190        suffix = path[len(device_path):]
191        return [rel_path + suffix for rel_path in target_files_rel_paths]
192    raise RuntimeError('Unrecognized path from checkvintf --dump-file-list: ' +
193                       path)
194
195  out = common.RunAndCheckOutput(['checkvintf', '--dump-file-list'])
196  paths = out.strip().split('\n')
197  paths = sum((PathToPatterns(path) for path in paths if path), [])
198  return paths
199
200def GetVintfApexUnzipPatterns():
201  """ Build unzip pattern for APEXes. """
202  patterns = []
203  for target_files_rel_paths in DIR_SEARCH_PATHS.values():
204    for target_files_rel_path in target_files_rel_paths:
205      patterns.append(os.path.join(target_files_rel_path,"apex/*"))
206
207  return patterns
208
209
210def PrepareApexDirectory(inp, dirmap):
211  """ Prepare /apex directory before running checkvintf
212
213  Apex binaries do not support dirmaps, in order to use these binaries we
214  need to move the APEXes from the extracted target file archives to the
215  expected device locations.
216
217  This simulates how apexd activates APEXes.
218  1. create {inp}/APEX which is treated as a "/apex" on device.
219  2. invoke apexd_host with APEXes.
220  """
221
222  apex_dir = common.MakeTempDir('APEX')
223  # checkvintf needs /apex dirmap
224  dirmap['/apex'] = apex_dir
225
226  # Always create /apex directory for dirmap
227  os.makedirs(apex_dir, exist_ok=True)
228
229  # Invoke apexd_host to activate APEXes for checkvintf
230  apex_host = os.path.join(OPTIONS.search_path, 'bin', 'apexd_host')
231  cmd = [apex_host, '--tool_path', OPTIONS.search_path]
232  cmd += ['--apex_path', dirmap['/apex']]
233  for p in apex_utils.PARTITIONS:
234    if '/' + p in dirmap:
235      cmd += ['--' + p + '_path', dirmap['/' + p]]
236  common.RunAndCheckOutput(cmd)
237
238
239def CheckVintfFromTargetFiles(inp, info_dict=None):
240  """
241  Checks VINTF metadata of a target files zip.
242
243  Args:
244    inp: path to the target files archive.
245    info_dict: The build-time info dict. If None, it will be loaded from inp.
246
247  Returns:
248    True if VINTF check is skipped or compatible, False if incompatible. Raise
249    a RuntimeError if any error occurs.
250  """
251  input_tmp = common.UnzipTemp(inp, GetVintfFileList() + GetVintfApexUnzipPatterns() + UNZIP_PATTERN)
252  return CheckVintfFromExtractedTargetFiles(input_tmp, info_dict)
253
254
255def CheckVintf(inp, info_dict=None):
256  """
257  Checks VINTF metadata of a target files zip or extracted target files
258  directory.
259
260  Args:
261    inp: path to the (possibly extracted) target files archive.
262    info_dict: The build-time info dict. If None, it will be loaded from inp.
263
264  Returns:
265    True if VINTF check is skipped or compatible, False if incompatible. Raise
266    a RuntimeError if any error occurs.
267  """
268  if os.path.isdir(inp):
269    logger.info('Checking VINTF compatibility extracted target files...')
270    return CheckVintfFromExtractedTargetFiles(inp, info_dict)
271
272  if zipfile.is_zipfile(inp):
273    logger.info('Checking VINTF compatibility target files...')
274    return CheckVintfFromTargetFiles(inp, info_dict)
275
276  raise ValueError('{} is not a valid directory or zip file'.format(inp))
277
278def CheckVintfIfTrebleEnabled(target_files, target_info):
279  """Checks compatibility info of the input target files.
280
281  Metadata used for compatibility verification is retrieved from target_zip.
282
283  Compatibility should only be checked for devices that have enabled
284  Treble support.
285
286  Args:
287    target_files: Path to zip file containing the source files to be included
288        for OTA. Can also be the path to extracted directory.
289    target_info: The BuildInfo instance that holds the target build info.
290  """
291
292  # Will only proceed if the target has enabled the Treble support (as well as
293  # having a /vendor partition).
294  if not HasTrebleEnabled(target_files, target_info):
295    return
296
297  # Skip adding the compatibility package as a workaround for b/114240221. The
298  # compatibility will always fail on devices without qualified kernels.
299  if OPTIONS.skip_compatibility_check:
300    return
301
302  if not CheckVintf(target_files, target_info):
303    raise RuntimeError("VINTF compatibility check failed")
304
305def HasTrebleEnabled(target_files, target_info):
306  def HasVendorPartition(target_files):
307    if os.path.isdir(target_files):
308      return os.path.isdir(os.path.join(target_files, "VENDOR"))
309    if zipfile.is_zipfile(target_files):
310      return HasPartition(zipfile.ZipFile(target_files, allowZip64=True), "vendor")
311    raise ValueError("Unknown target_files argument")
312
313  return (HasVendorPartition(target_files) and
314          target_info.GetBuildProp("ro.treble.enabled") == "true")
315
316
317def HasPartition(target_files_zip, partition):
318  try:
319    target_files_zip.getinfo(partition.upper() + "/")
320    return True
321  except KeyError:
322    return False
323
324
325def main(argv):
326  args = common.ParseOptions(argv, __doc__)
327  if len(args) != 1:
328    common.Usage(__doc__)
329    sys.exit(1)
330  common.InitLogging()
331  if not CheckVintf(args[0]):
332    sys.exit(1)
333
334
335if __name__ == '__main__':
336  try:
337    common.CloseInheritedPipes()
338    main(sys.argv[1:])
339  finally:
340    common.Cleanup()
341