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