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