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