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