Daniel Norman | 2465fc8 | 2022-03-02 12:01:20 -0800 | [diff] [blame^] | 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 | |
| 19 | Expects items in OPTIONS prepared by merge_target_files.py. |
| 20 | """ |
| 21 | |
| 22 | import fnmatch |
| 23 | import logging |
| 24 | import os |
| 25 | import re |
| 26 | import shutil |
| 27 | import zipfile |
| 28 | |
| 29 | import common |
| 30 | |
| 31 | logger = logging.getLogger(__name__) |
| 32 | OPTIONS = common.OPTIONS |
| 33 | |
| 34 | |
| 35 | def 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 | |
| 52 | def 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 | |
| 76 | def 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 | |
| 94 | # The merge config lists should not attempt to extract items from both |
| 95 | # builds for any of the following partitions. The partitions in |
| 96 | # SINGLE_BUILD_PARTITIONS should come entirely from a single build (either |
| 97 | # framework or vendor, but not both). |
| 98 | |
| 99 | _SINGLE_BUILD_PARTITIONS = ( |
| 100 | 'BOOT/', |
| 101 | 'DATA/', |
| 102 | 'ODM/', |
| 103 | 'PRODUCT/', |
| 104 | 'SYSTEM_EXT/', |
| 105 | 'RADIO/', |
| 106 | 'RECOVERY/', |
| 107 | 'ROOT/', |
| 108 | 'SYSTEM/', |
| 109 | 'SYSTEM_OTHER/', |
| 110 | 'VENDOR/', |
| 111 | 'VENDOR_DLKM/', |
| 112 | 'ODM_DLKM/', |
| 113 | 'SYSTEM_DLKM/', |
| 114 | ) |
| 115 | |
| 116 | |
| 117 | def ValidateConfigLists(): |
| 118 | """Performs validations on the merge config lists. |
| 119 | |
| 120 | Returns: |
| 121 | False if a validation fails, otherwise true. |
| 122 | """ |
| 123 | has_error = False |
| 124 | |
| 125 | # Check that partitions only come from one input. |
| 126 | for partition in _SINGLE_BUILD_PARTITIONS: |
| 127 | image_path = 'IMAGES/{}.img'.format(partition.lower().replace('/', '')) |
| 128 | in_framework = ( |
| 129 | any(item.startswith(partition) for item in OPTIONS.framework_item_list) |
| 130 | or image_path in OPTIONS.framework_item_list) |
| 131 | in_vendor = ( |
| 132 | any(item.startswith(partition) for item in OPTIONS.vendor_item_list) or |
| 133 | image_path in OPTIONS.vendor_item_list) |
| 134 | if in_framework and in_vendor: |
| 135 | logger.error( |
| 136 | 'Cannot extract items from %s for both the framework and vendor' |
| 137 | ' builds. Please ensure only one merge config item list' |
| 138 | ' includes %s.', partition, partition) |
| 139 | has_error = True |
| 140 | |
| 141 | if any([ |
| 142 | key in OPTIONS.framework_misc_info_keys |
| 143 | for key in ('dynamic_partition_list', 'super_partition_groups') |
| 144 | ]): |
| 145 | logger.error('Dynamic partition misc info keys should come from ' |
| 146 | 'the vendor instance of META/misc_info.txt.') |
| 147 | has_error = True |
| 148 | |
| 149 | return not has_error |
| 150 | |
| 151 | |
| 152 | # In an item list (framework or vendor), we may see entries that select whole |
| 153 | # partitions. Such an entry might look like this 'SYSTEM/*' (e.g., for the |
| 154 | # system partition). The following regex matches this and extracts the |
| 155 | # partition name. |
| 156 | |
| 157 | _PARTITION_ITEM_PATTERN = re.compile(r'^([A-Z_]+)/\*$') |
| 158 | |
| 159 | |
| 160 | def ItemListToPartitionSet(item_list): |
| 161 | """Converts a target files item list to a partition set. |
| 162 | |
| 163 | The item list contains items that might look like 'SYSTEM/*' or 'VENDOR/*' or |
| 164 | 'OTA/android-info.txt'. Items that end in '/*' are assumed to match entire |
| 165 | directories where 'SYSTEM' or 'VENDOR' is a directory name that identifies the |
| 166 | contents of a partition of the same name. Other items in the list, such as the |
| 167 | 'OTA' example contain metadata. This function iterates such a list, returning |
| 168 | a set that contains the partition entries. |
| 169 | |
| 170 | Args: |
| 171 | item_list: A list of items in a target files package. |
| 172 | |
| 173 | Returns: |
| 174 | A set of partitions extracted from the list of items. |
| 175 | """ |
| 176 | |
| 177 | partition_set = set() |
| 178 | |
| 179 | for item in item_list: |
| 180 | partition_match = _PARTITION_ITEM_PATTERN.search(item.strip()) |
| 181 | partition_tag = partition_match.group( |
| 182 | 1).lower() if partition_match else None |
| 183 | |
| 184 | if partition_tag: |
| 185 | partition_set.add(partition_tag) |
| 186 | |
| 187 | return partition_set |