#!/usr/bin/env python # # Copyright (C) 2022 The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); you may not # use this file except in compliance with the License. You may obtain a copy of # the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations under # the License. # """Common utility functions shared by merge_* scripts. Expects items in OPTIONS prepared by merge_target_files.py. """ import fnmatch import logging import os import re import shutil import zipfile import common logger = logging.getLogger(__name__) OPTIONS = common.OPTIONS def ExtractItems(input_zip, output_dir, extract_item_list): """Extracts items in extract_item_list from a zip to a dir.""" # Filter the extract_item_list to remove any items that do not exist in the # zip file. Otherwise, the extraction step will fail. with zipfile.ZipFile(input_zip, allowZip64=True) as input_zipfile: input_namelist = input_zipfile.namelist() filtered_extract_item_list = [] for pattern in extract_item_list: if fnmatch.filter(input_namelist, pattern): filtered_extract_item_list.append(pattern) common.UnzipToDir(input_zip, output_dir, filtered_extract_item_list) def CopyItems(from_dir, to_dir, patterns): """Similar to ExtractItems() except uses an input dir instead of zip.""" file_paths = [] for dirpath, _, filenames in os.walk(from_dir): file_paths.extend( os.path.relpath(path=os.path.join(dirpath, filename), start=from_dir) for filename in filenames) filtered_file_paths = set() for pattern in patterns: filtered_file_paths.update(fnmatch.filter(file_paths, pattern)) for file_path in filtered_file_paths: original_file_path = os.path.join(from_dir, file_path) copied_file_path = os.path.join(to_dir, file_path) copied_file_dir = os.path.dirname(copied_file_path) if not os.path.exists(copied_file_dir): os.makedirs(copied_file_dir) if os.path.islink(original_file_path): os.symlink(os.readlink(original_file_path), copied_file_path) else: shutil.copyfile(original_file_path, copied_file_path) def WriteSortedData(data, path): """Writes the sorted contents of either a list or dict to file. This function sorts the contents of the list or dict and then writes the resulting sorted contents to a file specified by path. Args: data: The list or dict to sort and write. path: Path to the file to write the sorted values to. The file at path will be overridden if it exists. """ with open(path, 'w') as output: for entry in sorted(data): out_str = '{}={}\n'.format(entry, data[entry]) if isinstance( data, dict) else '{}\n'.format(entry) output.write(out_str) # The merge config lists should not attempt to extract items from both # builds for any of the following partitions. The partitions in # SINGLE_BUILD_PARTITIONS should come entirely from a single build (either # framework or vendor, but not both). _SINGLE_BUILD_PARTITIONS = ( 'BOOT/', 'DATA/', 'ODM/', 'PRODUCT/', 'SYSTEM_EXT/', 'RADIO/', 'RECOVERY/', 'ROOT/', 'SYSTEM/', 'SYSTEM_OTHER/', 'VENDOR/', 'VENDOR_DLKM/', 'ODM_DLKM/', 'SYSTEM_DLKM/', ) def ValidateConfigLists(): """Performs validations on the merge config lists. Returns: False if a validation fails, otherwise true. """ has_error = False # Check that partitions only come from one input. for partition in _SINGLE_BUILD_PARTITIONS: image_path = 'IMAGES/{}.img'.format(partition.lower().replace('/', '')) in_framework = ( any(item.startswith(partition) for item in OPTIONS.framework_item_list) or image_path in OPTIONS.framework_item_list) in_vendor = ( any(item.startswith(partition) for item in OPTIONS.vendor_item_list) or image_path in OPTIONS.vendor_item_list) if in_framework and in_vendor: logger.error( 'Cannot extract items from %s for both the framework and vendor' ' builds. Please ensure only one merge config item list' ' includes %s.', partition, partition) has_error = True if any([ key in OPTIONS.framework_misc_info_keys for key in ('dynamic_partition_list', 'super_partition_groups') ]): logger.error('Dynamic partition misc info keys should come from ' 'the vendor instance of META/misc_info.txt.') has_error = True return not has_error # In an item list (framework or vendor), we may see entries that select whole # partitions. Such an entry might look like this 'SYSTEM/*' (e.g., for the # system partition). The following regex matches this and extracts the # partition name. _PARTITION_ITEM_PATTERN = re.compile(r'^([A-Z_]+)/\*$') def ItemListToPartitionSet(item_list): """Converts a target files item list to a partition set. The item list contains items that might look like 'SYSTEM/*' or 'VENDOR/*' or 'OTA/android-info.txt'. Items that end in '/*' are assumed to match entire directories where 'SYSTEM' or 'VENDOR' is a directory name that identifies the contents of a partition of the same name. Other items in the list, such as the 'OTA' example contain metadata. This function iterates such a list, returning a set that contains the partition entries. Args: item_list: A list of items in a target files package. Returns: A set of partitions extracted from the list of items. """ partition_set = set() for item in item_list: partition_match = _PARTITION_ITEM_PATTERN.search(item.strip()) partition_tag = partition_match.group( 1).lower() if partition_match else None if partition_tag: partition_set.add(partition_tag) return partition_set