1#!/usr/bin/env python 2# 3# Copyright (C) 2022 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); you may not 6# use this file except in compliance with the License. You may obtain a copy of 7# 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, WITHOUT 13# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14# License for the specific language governing permissions and limitations under 15# the License. 16# 17"""Common utility functions shared by merge_* scripts. 18 19Expects items in OPTIONS prepared by merge_target_files.py. 20""" 21 22import fnmatch 23import logging 24import os 25import re 26import shutil 27import zipfile 28 29import common 30 31logger = logging.getLogger(__name__) 32OPTIONS = common.OPTIONS 33 34 35def ExtractItems(input_zip, output_dir, extract_item_list): 36 """Extracts items in extract_item_list from a zip to a dir.""" 37 38 # Filter the extract_item_list to remove any items that do not exist in the 39 # zip file. Otherwise, the extraction step will fail. 40 41 with zipfile.ZipFile(input_zip, allowZip64=True) as input_zipfile: 42 input_namelist = input_zipfile.namelist() 43 44 filtered_extract_item_list = [] 45 for pattern in extract_item_list: 46 if fnmatch.filter(input_namelist, pattern): 47 filtered_extract_item_list.append(pattern) 48 49 common.UnzipToDir(input_zip, output_dir, filtered_extract_item_list) 50 51 52def CopyItems(from_dir, to_dir, patterns): 53 """Similar to ExtractItems() except uses an input dir instead of zip.""" 54 file_paths = [] 55 for dirpath, _, filenames in os.walk(from_dir): 56 file_paths.extend( 57 os.path.relpath(path=os.path.join(dirpath, filename), start=from_dir) 58 for filename in filenames) 59 60 filtered_file_paths = set() 61 for pattern in patterns: 62 filtered_file_paths.update(fnmatch.filter(file_paths, pattern)) 63 64 for file_path in filtered_file_paths: 65 original_file_path = os.path.join(from_dir, file_path) 66 copied_file_path = os.path.join(to_dir, file_path) 67 copied_file_dir = os.path.dirname(copied_file_path) 68 if not os.path.exists(copied_file_dir): 69 os.makedirs(copied_file_dir) 70 if os.path.islink(original_file_path): 71 os.symlink(os.readlink(original_file_path), copied_file_path) 72 else: 73 shutil.copyfile(original_file_path, copied_file_path) 74 75 76def WriteSortedData(data, path): 77 """Writes the sorted contents of either a list or dict to file. 78 79 This function sorts the contents of the list or dict and then writes the 80 resulting sorted contents to a file specified by path. 81 82 Args: 83 data: The list or dict to sort and write. 84 path: Path to the file to write the sorted values to. The file at path will 85 be overridden if it exists. 86 """ 87 with open(path, 'w') as output: 88 for entry in sorted(data): 89 out_str = '{}={}\n'.format(entry, data[entry]) if isinstance( 90 data, dict) else '{}\n'.format(entry) 91 output.write(out_str) 92 93 94def ValidateConfigLists(): 95 """Performs validations on the merge config lists. 96 97 Returns: 98 False if a validation fails, otherwise true. 99 """ 100 has_error = False 101 102 # Check that partitions only come from one input. 103 for partition in _FRAMEWORK_PARTITIONS.union(_VENDOR_PARTITIONS): 104 image_path = 'IMAGES/{}.img'.format(partition.lower().replace('/', '')) 105 in_framework = ( 106 any(item.startswith(partition) for item in OPTIONS.framework_item_list) 107 or image_path in OPTIONS.framework_item_list) 108 in_vendor = ( 109 any(item.startswith(partition) for item in OPTIONS.vendor_item_list) or 110 image_path in OPTIONS.vendor_item_list) 111 if in_framework and in_vendor: 112 logger.error( 113 'Cannot extract items from %s for both the framework and vendor' 114 ' builds. Please ensure only one merge config item list' 115 ' includes %s.', partition, partition) 116 has_error = True 117 118 if any([ 119 key in OPTIONS.framework_misc_info_keys 120 for key in ('dynamic_partition_list', 'super_partition_groups') 121 ]): 122 logger.error('Dynamic partition misc info keys should come from ' 123 'the vendor instance of META/misc_info.txt.') 124 has_error = True 125 126 return not has_error 127 128 129# In an item list (framework or vendor), we may see entries that select whole 130# partitions. Such an entry might look like this 'SYSTEM/*' (e.g., for the 131# system partition). The following regex matches this and extracts the 132# partition name. 133 134_PARTITION_ITEM_PATTERN = re.compile(r'^([A-Z_]+)/\*$') 135 136 137def ItemListToPartitionSet(item_list): 138 """Converts a target files item list to a partition set. 139 140 The item list contains items that might look like 'SYSTEM/*' or 'VENDOR/*' or 141 'OTA/android-info.txt'. Items that end in '/*' are assumed to match entire 142 directories where 'SYSTEM' or 'VENDOR' is a directory name that identifies the 143 contents of a partition of the same name. Other items in the list, such as the 144 'OTA' example contain metadata. This function iterates such a list, returning 145 a set that contains the partition entries. 146 147 Args: 148 item_list: A list of items in a target files package. 149 150 Returns: 151 A set of partitions extracted from the list of items. 152 """ 153 154 partition_set = set() 155 156 for item in item_list: 157 partition_match = _PARTITION_ITEM_PATTERN.search(item.strip()) 158 partition_tag = partition_match.group( 159 1).lower() if partition_match else None 160 161 if partition_tag: 162 partition_set.add(partition_tag) 163 164 return partition_set 165 166 167# Partitions that are grabbed from the framework partial build by default. 168_FRAMEWORK_PARTITIONS = { 169 'system', 'product', 'system_ext', 'system_other', 'root', 'system_dlkm' 170} 171# Partitions that are grabbed from the vendor partial build by default. 172_VENDOR_PARTITIONS = { 173 'vendor', 'odm', 'oem', 'boot', 'vendor_boot', 'recovery', 174 'prebuilt_images', 'radio', 'data', 'vendor_dlkm', 'odm_dlkm' 175} 176 177 178def InferItemList(input_namelist, framework): 179 item_list = [] 180 181 # Some META items are grabbed from partial builds directly. 182 # Others are combined in merge_meta.py. 183 if framework: 184 item_list.extend([ 185 'META/liblz4.so', 186 'META/postinstall_config.txt', 187 'META/update_engine_config.txt', 188 'META/zucchini_config.txt', 189 ]) 190 else: # vendor 191 item_list.extend([ 192 'META/kernel_configs.txt', 193 'META/kernel_version.txt', 194 'META/otakeys.txt', 195 'META/releasetools.py', 196 'OTA/android-info.txt', 197 ]) 198 199 # Grab a set of items for the expected partitions in the partial build. 200 for partition in (_FRAMEWORK_PARTITIONS if framework else _VENDOR_PARTITIONS): 201 for namelist in input_namelist: 202 if namelist.startswith('%s/' % partition.upper()): 203 fs_config_prefix = '' if partition == 'system' else '%s_' % partition 204 item_list.extend([ 205 '%s/*' % partition.upper(), 206 'IMAGES/%s.img' % partition, 207 'IMAGES/%s.map' % partition, 208 'META/%sfilesystem_config.txt' % fs_config_prefix, 209 ]) 210 break 211 212 return sorted(item_list) 213 214 215def InferFrameworkMiscInfoKeys(input_namelist): 216 keys = [ 217 'ab_update', 218 'avb_vbmeta_system', 219 'avb_vbmeta_system_algorithm', 220 'avb_vbmeta_system_key_path', 221 'avb_vbmeta_system_rollback_index_location', 222 'default_system_dev_certificate', 223 ] 224 225 for partition in _FRAMEWORK_PARTITIONS: 226 for namelist in input_namelist: 227 if namelist.startswith('%s/' % partition.upper()): 228 fs_type_prefix = '' if partition == 'system' else '%s_' % partition 229 keys.extend([ 230 'avb_%s_hashtree_enable' % partition, 231 'avb_%s_add_hashtree_footer_args' % partition, 232 '%s_disable_sparse' % partition, 233 'building_%s_image' % partition, 234 '%sfs_type' % fs_type_prefix, 235 ]) 236 237 return sorted(keys) 238