• 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 from target-files.
133  dirmap['/apex'] = PrepareApexDirectory(input_tmp)
134
135  args_for_skus = GetArgsForSkus(info_dict)
136  shipping_api_level_args = GetArgsForShippingApiLevel(info_dict)
137  kernel_args = GetArgsForKernel(input_tmp)
138
139  common_command = [
140      'checkvintf',
141      '--check-compat',
142  ]
143
144  for device_path, real_path in sorted(dirmap.items()):
145    common_command += ['--dirmap', '{}:{}'.format(device_path, real_path)]
146  common_command += kernel_args
147  common_command += shipping_api_level_args
148
149  success = True
150  for sku_args in args_for_skus:
151    command = common_command + sku_args
152    proc = common.Run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
153    out, err = proc.communicate()
154    last_out_line = out.split()[-1] if out != "" else out
155    if proc.returncode == 0:
156      logger.info("Command `%s` returns 'compatible'", ' '.join(command))
157    elif last_out_line.strip() == "INCOMPATIBLE":
158      logger.info("Command `%s` returns 'incompatible'", ' '.join(command))
159      success = False
160    else:
161      raise common.ExternalError(
162          "Failed to run command '{}' (exit code {}):\nstdout:{}\nstderr:{}"
163          .format(' '.join(command), proc.returncode, out, err))
164    logger.info("stdout: %s", out)
165    logger.info("stderr: %s", err)
166
167  return success
168
169
170def GetVintfFileList():
171  """
172  Returns a list of VINTF metadata files that should be read from a target files
173  package before executing checkvintf.
174  """
175  def PathToPatterns(path):
176    if path[-1] == '/':
177      path += '**'
178
179    # Loop over all the entries in DIR_SEARCH_PATHS and find one where the key
180    # is a prefix of path. In order to get find the correct prefix, sort the
181    # entries by decreasing length of their keys, so that we check if longer
182    # strings are prefixes before shorter strings. This is so that keys that
183    # are substrings of other keys (like /system vs /system_ext) are checked
184    # later, and we don't mistakenly mark a path that starts with /system_ext
185    # as starting with only /system.
186    for device_path, target_files_rel_paths in sorted(DIR_SEARCH_PATHS.items(), key=lambda i: len(i[0]), reverse=True):
187      if path.startswith(device_path):
188        suffix = path[len(device_path):]
189        return [rel_path + suffix for rel_path in target_files_rel_paths]
190    raise RuntimeError('Unrecognized path from checkvintf --dump-file-list: ' +
191                       path)
192
193  out = common.RunAndCheckOutput(['checkvintf', '--dump-file-list'])
194  paths = out.strip().split('\n')
195  paths = sum((PathToPatterns(path) for path in paths if path), [])
196  return paths
197
198def GetVintfApexUnzipPatterns():
199  """ Build unzip pattern for APEXes. """
200  patterns = []
201  for target_files_rel_paths in DIR_SEARCH_PATHS.values():
202    for target_files_rel_path in target_files_rel_paths:
203      patterns.append(os.path.join(target_files_rel_path,"apex/*"))
204
205  return patterns
206
207def PrepareApexDirectory(inp):
208  """ Prepare /apex directory before running checkvintf
209
210  Apex binaries do not support dirmaps, in order to use these binaries we
211  need to move the APEXes from the extracted target file archives to the
212  expected device locations.
213
214  This simulates how apexd activates APEXes.
215  1. create {inp}/APEX which is treated as a "/" on device.
216  2. copy apexes from target-files to {root}/{partition}/apex.
217  3. mount apexes under {root}/{partition}/apex at {root}/apex.
218  4. generate info files with dump_apex_info.
219
220  We'll get the following layout
221       {inp}/APEX/apex             # Activated APEXes + some info files
222       {inp}/APEX/system/apex      # System APEXes
223       {inp}/APEX/vendor/apex      # Vendor APEXes
224       ...
225
226  Args:
227    inp: path to the directory that contains the extracted target files archive.
228
229  Returns:
230    directory representing /apex on device
231  """
232
233  deapexer = 'deapexer'
234  debugfs_path = 'debugfs'
235  blkid_path = 'blkid'
236  fsckerofs_path = 'fsck.erofs'
237  if OPTIONS.search_path:
238    debugfs_path = os.path.join(OPTIONS.search_path, 'bin', 'debugfs_static')
239    deapexer_path = os.path.join(OPTIONS.search_path, 'bin', 'deapexer')
240    blkid_path = os.path.join(OPTIONS.search_path, 'bin', 'blkid_static')
241    fsckerofs_path = os.path.join(OPTIONS.search_path, 'bin', 'fsck.erofs')
242    if os.path.isfile(deapexer_path):
243      deapexer = deapexer_path
244
245  def ExtractApexes(path, outp):
246    # Extract all APEXes found in input path.
247    logger.info('Extracting APEXs in %s', path)
248    for f in os.listdir(path):
249      logger.info('  adding APEX %s', os.path.basename(f))
250      apex = os.path.join(path, f)
251      if os.path.isdir(apex) and os.path.isfile(os.path.join(apex, 'apex_manifest.pb')):
252        info = ParseApexManifest(os.path.join(apex, 'apex_manifest.pb'))
253        # Flattened APEXes may have symlinks for libs (linked to /system/lib)
254        # We need to blindly copy them all.
255        shutil.copytree(apex, os.path.join(outp, info.name), symlinks=True)
256      elif os.path.isfile(apex) and apex.endswith(('.apex', '.capex')):
257        cmd = [deapexer,
258               '--debugfs_path', debugfs_path,
259               'info',
260               apex]
261        info = json.loads(common.RunAndCheckOutput(cmd))
262
263        cmd = [deapexer,
264               '--debugfs_path', debugfs_path,
265               '--fsckerofs_path', fsckerofs_path,
266               '--blkid_path', blkid_path,
267               'extract',
268               apex,
269               os.path.join(outp, info['name'])]
270        common.RunAndCheckOutput(cmd)
271      else:
272        logger.info('  .. skipping %s (is it APEX?)', path)
273
274  root_dir_name = 'APEX'
275  root_dir = os.path.join(inp, root_dir_name)
276  extracted_root = os.path.join(root_dir, 'apex')
277
278  # Always create /apex directory for dirmap
279  os.makedirs(extracted_root)
280
281  create_info_file = False
282
283  # Loop through search path looking for and processing apex/ directories.
284  for device_path, target_files_rel_paths in DIR_SEARCH_PATHS.items():
285    # checkvintf only needs vendor apexes. skip other partitions for efficiency
286    if device_path not in ['/vendor', '/odm']:
287      continue
288    # First, copy VENDOR/apex/foo.apex to APEX/vendor/apex/foo.apex
289    # Then, extract the contents to APEX/apex/foo/
290    for target_files_rel_path in target_files_rel_paths:
291      inp_partition = os.path.join(inp, target_files_rel_path,"apex")
292      if os.path.exists(inp_partition):
293        apex_dir = root_dir + os.path.join(device_path + "/apex");
294        os.makedirs(root_dir + device_path)
295        shutil.copytree(inp_partition, apex_dir, symlinks=True)
296        ExtractApexes(apex_dir, extracted_root)
297        create_info_file = True
298
299  if create_info_file:
300    ### Dump apex info files
301    dump_cmd = ['dump_apex_info', '--root_dir', root_dir]
302    common.RunAndCheckOutput(dump_cmd)
303
304  return extracted_root
305
306def CheckVintfFromTargetFiles(inp, info_dict=None):
307  """
308  Checks VINTF metadata of a target files zip.
309
310  Args:
311    inp: path to the target files archive.
312    info_dict: The build-time info dict. If None, it will be loaded from inp.
313
314  Returns:
315    True if VINTF check is skipped or compatible, False if incompatible. Raise
316    a RuntimeError if any error occurs.
317  """
318  input_tmp = common.UnzipTemp(inp, GetVintfFileList() + GetVintfApexUnzipPatterns() + UNZIP_PATTERN)
319  return CheckVintfFromExtractedTargetFiles(input_tmp, info_dict)
320
321
322def CheckVintf(inp, info_dict=None):
323  """
324  Checks VINTF metadata of a target files zip or extracted target files
325  directory.
326
327  Args:
328    inp: path to the (possibly extracted) target files archive.
329    info_dict: The build-time info dict. If None, it will be loaded from inp.
330
331  Returns:
332    True if VINTF check is skipped or compatible, False if incompatible. Raise
333    a RuntimeError if any error occurs.
334  """
335  if os.path.isdir(inp):
336    logger.info('Checking VINTF compatibility extracted target files...')
337    return CheckVintfFromExtractedTargetFiles(inp, info_dict)
338
339  if zipfile.is_zipfile(inp):
340    logger.info('Checking VINTF compatibility target files...')
341    return CheckVintfFromTargetFiles(inp, info_dict)
342
343  raise ValueError('{} is not a valid directory or zip file'.format(inp))
344
345def CheckVintfIfTrebleEnabled(target_files, target_info):
346  """Checks compatibility info of the input target files.
347
348  Metadata used for compatibility verification is retrieved from target_zip.
349
350  Compatibility should only be checked for devices that have enabled
351  Treble support.
352
353  Args:
354    target_files: Path to zip file containing the source files to be included
355        for OTA. Can also be the path to extracted directory.
356    target_info: The BuildInfo instance that holds the target build info.
357  """
358
359  # Will only proceed if the target has enabled the Treble support (as well as
360  # having a /vendor partition).
361  if not HasTrebleEnabled(target_files, target_info):
362    return
363
364  # Skip adding the compatibility package as a workaround for b/114240221. The
365  # compatibility will always fail on devices without qualified kernels.
366  if OPTIONS.skip_compatibility_check:
367    return
368
369  if not CheckVintf(target_files, target_info):
370    raise RuntimeError("VINTF compatibility check failed")
371
372def HasTrebleEnabled(target_files, target_info):
373  def HasVendorPartition(target_files):
374    if os.path.isdir(target_files):
375      return os.path.isdir(os.path.join(target_files, "VENDOR"))
376    if zipfile.is_zipfile(target_files):
377      return HasPartition(zipfile.ZipFile(target_files, allowZip64=True), "vendor")
378    raise ValueError("Unknown target_files argument")
379
380  return (HasVendorPartition(target_files) and
381          target_info.GetBuildProp("ro.treble.enabled") == "true")
382
383
384def HasPartition(target_files_zip, partition):
385  try:
386    target_files_zip.getinfo(partition.upper() + "/")
387    return True
388  except KeyError:
389    return False
390
391
392def main(argv):
393  args = common.ParseOptions(argv, __doc__)
394  if len(args) != 1:
395    common.Usage(__doc__)
396    sys.exit(1)
397  common.InitLogging()
398  if not CheckVintf(args[0]):
399    sys.exit(1)
400
401
402if __name__ == '__main__':
403  try:
404    common.CloseInheritedPipes()
405    main(sys.argv[1:])
406  finally:
407    common.Cleanup()
408