blob: c284338a6ea497ddb0114e28196469185f51f7b9 [file] [log] [blame]
Daniel Norman2465fc82022-03-02 12:01:20 -08001#!/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
Dennis Song5bfa43e2023-03-30 18:28:00 +080052def CopyItems(from_dir, to_dir, copy_item_list):
53 """Copies the items in copy_item_list from source to destination directory.
Daniel Norman2465fc82022-03-02 12:01:20 -080054
Dennis Song5bfa43e2023-03-30 18:28:00 +080055 copy_item_list may include files and directories. Will copy the matched
56 files and create the matched directories.
Daniel Norman2465fc82022-03-02 12:01:20 -080057
Dennis Song5bfa43e2023-03-30 18:28:00 +080058 Args:
59 from_dir: The source directory.
60 to_dir: The destination directory.
61 copy_item_list: Items to be copied.
62 """
63 item_paths = []
64 for root, dirs, files in os.walk(from_dir):
65 item_paths.extend(
66 os.path.relpath(path=os.path.join(root, item_name), start=from_dir)
67 for item_name in files + dirs)
68
69 filtered = set()
70 for pattern in copy_item_list:
71 filtered.update(fnmatch.filter(item_paths, pattern))
72
73 for item in filtered:
74 original_path = os.path.join(from_dir, item)
75 copied_path = os.path.join(to_dir, item)
76 copied_parent_path = os.path.dirname(copied_path)
77 if not os.path.exists(copied_parent_path):
78 os.makedirs(copied_parent_path)
79 if os.path.islink(original_path):
80 os.symlink(os.readlink(original_path), copied_path)
81 elif os.path.isdir(original_path):
82 if not os.path.exists(copied_path):
83 os.makedirs(copied_path)
Daniel Norman2465fc82022-03-02 12:01:20 -080084 else:
Dennis Song5bfa43e2023-03-30 18:28:00 +080085 shutil.copyfile(original_path, copied_path)
86
87
88def GetTargetFilesItems(target_files_zipfile_or_dir):
89 """Gets a list of target files items."""
90 if zipfile.is_zipfile(target_files_zipfile_or_dir):
91 with zipfile.ZipFile(target_files_zipfile_or_dir, allowZip64=True) as fz:
92 return fz.namelist()
93 elif os.path.isdir(target_files_zipfile_or_dir):
94 item_list = []
95 for root, dirs, files in os.walk(target_files_zipfile_or_dir):
96 item_list.extend(
97 os.path.relpath(path=os.path.join(root, item),
98 start=target_files_zipfile_or_dir)
99 for item in dirs + files)
100 return item_list
101 else:
102 raise ValueError('Target files should be either zipfile or directory.')
103
104
105def CollectTargetFiles(input_zipfile_or_dir, output_dir, item_list=None):
106 """Extracts input zipfile or copy input directory to output directory.
107
108 Extracts the input zipfile if `input_zipfile_or_dir` is a zip archive, or
109 copies the items if `input_zipfile_or_dir` is a directory.
110
111 Args:
112 input_zipfile_or_dir: The input target files, could be either a zipfile to
113 extract or a directory to copy.
114 output_dir: The output directory that the input files are either extracted
115 or copied.
116 item_list: Files to be extracted or copied. Will extract or copy all files
117 if omitted.
118 """
119 patterns = item_list if item_list else ('*',)
120 if zipfile.is_zipfile(input_zipfile_or_dir):
121 ExtractItems(input_zipfile_or_dir, output_dir, patterns)
122 elif os.path.isdir(input_zipfile_or_dir):
123 CopyItems(input_zipfile_or_dir, output_dir, patterns)
124 else:
125 raise ValueError('Target files should be either zipfile or directory.')
Daniel Norman2465fc82022-03-02 12:01:20 -0800126
127
128def WriteSortedData(data, path):
129 """Writes the sorted contents of either a list or dict to file.
130
131 This function sorts the contents of the list or dict and then writes the
132 resulting sorted contents to a file specified by path.
133
134 Args:
135 data: The list or dict to sort and write.
136 path: Path to the file to write the sorted values to. The file at path will
137 be overridden if it exists.
138 """
139 with open(path, 'w') as output:
140 for entry in sorted(data):
141 out_str = '{}={}\n'.format(entry, data[entry]) if isinstance(
142 data, dict) else '{}\n'.format(entry)
143 output.write(out_str)
144
145
Daniel Norman2465fc82022-03-02 12:01:20 -0800146def ValidateConfigLists():
147 """Performs validations on the merge config lists.
148
149 Returns:
150 False if a validation fails, otherwise true.
151 """
152 has_error = False
153
154 # Check that partitions only come from one input.
Daniel Norman679242b2022-03-18 15:46:27 -0700155 framework_partitions = ItemListToPartitionSet(OPTIONS.framework_item_list)
156 vendor_partitions = ItemListToPartitionSet(OPTIONS.vendor_item_list)
157 from_both = framework_partitions.intersection(vendor_partitions)
158 if from_both:
159 logger.error(
160 'Cannot extract items from the same partition in both the '
161 'framework and vendor builds. Please ensure only one merge config '
162 'item list (or inferred list) includes each partition: %s' %
163 ','.join(from_both))
164 has_error = True
Daniel Norman2465fc82022-03-02 12:01:20 -0800165
166 if any([
167 key in OPTIONS.framework_misc_info_keys
168 for key in ('dynamic_partition_list', 'super_partition_groups')
169 ]):
170 logger.error('Dynamic partition misc info keys should come from '
171 'the vendor instance of META/misc_info.txt.')
172 has_error = True
173
174 return not has_error
175
176
177# In an item list (framework or vendor), we may see entries that select whole
178# partitions. Such an entry might look like this 'SYSTEM/*' (e.g., for the
179# system partition). The following regex matches this and extracts the
180# partition name.
181
Daniel Norman679242b2022-03-18 15:46:27 -0700182_PARTITION_ITEM_PATTERN = re.compile(r'^([A-Z_]+)/.*$')
183_IMAGE_PARTITION_PATTERN = re.compile(r'^IMAGES/(.*)\.img$')
Daniel Norman2465fc82022-03-02 12:01:20 -0800184
185
186def ItemListToPartitionSet(item_list):
187 """Converts a target files item list to a partition set.
188
189 The item list contains items that might look like 'SYSTEM/*' or 'VENDOR/*' or
190 'OTA/android-info.txt'. Items that end in '/*' are assumed to match entire
191 directories where 'SYSTEM' or 'VENDOR' is a directory name that identifies the
192 contents of a partition of the same name. Other items in the list, such as the
193 'OTA' example contain metadata. This function iterates such a list, returning
194 a set that contains the partition entries.
195
196 Args:
197 item_list: A list of items in a target files package.
198
199 Returns:
200 A set of partitions extracted from the list of items.
201 """
202
203 partition_set = set()
204
205 for item in item_list:
Daniel Norman679242b2022-03-18 15:46:27 -0700206 for pattern in (_PARTITION_ITEM_PATTERN, _IMAGE_PARTITION_PATTERN):
207 partition_match = pattern.search(item.strip())
208 if partition_match:
209 partition = partition_match.group(1).lower()
210 # These directories in target-files are not actual partitions.
211 if partition not in ('meta', 'images'):
212 partition_set.add(partition)
Daniel Norman2465fc82022-03-02 12:01:20 -0800213
214 return partition_set
Daniel Norman5f476772022-03-02 15:46:34 -0800215
216
217# Partitions that are grabbed from the framework partial build by default.
218_FRAMEWORK_PARTITIONS = {
Daniel Norman679242b2022-03-18 15:46:27 -0700219 'system', 'product', 'system_ext', 'system_other', 'root', 'system_dlkm',
220 'vbmeta_system'
Daniel Norman5f476772022-03-02 15:46:34 -0800221}
222
223
224def InferItemList(input_namelist, framework):
Daniel Norman679242b2022-03-18 15:46:27 -0700225 item_set = set()
Daniel Norman5f476772022-03-02 15:46:34 -0800226
Daniel Norman679242b2022-03-18 15:46:27 -0700227 # Some META items are always grabbed from partial builds directly.
Daniel Norman5f476772022-03-02 15:46:34 -0800228 # Others are combined in merge_meta.py.
229 if framework:
Daniel Norman679242b2022-03-18 15:46:27 -0700230 item_set.update([
Daniel Norman5f476772022-03-02 15:46:34 -0800231 'META/liblz4.so',
232 'META/postinstall_config.txt',
Daniel Norman5f476772022-03-02 15:46:34 -0800233 'META/zucchini_config.txt',
234 ])
235 else: # vendor
Daniel Norman679242b2022-03-18 15:46:27 -0700236 item_set.update([
Daniel Norman5f476772022-03-02 15:46:34 -0800237 'META/kernel_configs.txt',
238 'META/kernel_version.txt',
239 'META/otakeys.txt',
Daniel Norman679242b2022-03-18 15:46:27 -0700240 'META/pack_radioimages.txt',
Daniel Norman5f476772022-03-02 15:46:34 -0800241 'META/releasetools.py',
Daniel Norman5f476772022-03-02 15:46:34 -0800242 ])
243
244 # Grab a set of items for the expected partitions in the partial build.
Daniel Norman679242b2022-03-18 15:46:27 -0700245 seen_partitions = []
246 for namelist in input_namelist:
247 if namelist.endswith('/'):
248 continue
Daniel Norman5f476772022-03-02 15:46:34 -0800249
Daniel Norman679242b2022-03-18 15:46:27 -0700250 partition = namelist.split('/')[0].lower()
251
252 # META items are grabbed above, or merged later.
253 if partition == 'meta':
254 continue
255
256 if partition == 'images':
257 image_partition, extension = os.path.splitext(os.path.basename(namelist))
258 if image_partition == 'vbmeta':
259 # Always regenerate vbmeta.img since it depends on hash information
260 # from both builds.
261 continue
262 if extension in ('.img', '.map'):
263 # Include image files in IMAGES/* if the partition comes from
264 # the expected set.
265 if (framework and image_partition in _FRAMEWORK_PARTITIONS) or (
266 not framework and image_partition not in _FRAMEWORK_PARTITIONS):
267 item_set.add(namelist)
268 elif not framework:
269 # Include all miscellaneous non-image files in IMAGES/* from
270 # the vendor build.
271 item_set.add(namelist)
272 continue
273
274 # Skip already-visited partitions.
275 if partition in seen_partitions:
276 continue
277 seen_partitions.append(partition)
278
279 if (framework and partition in _FRAMEWORK_PARTITIONS) or (
280 not framework and partition not in _FRAMEWORK_PARTITIONS):
281 fs_config_prefix = '' if partition == 'system' else '%s_' % partition
282 item_set.update([
283 '%s/*' % partition.upper(),
284 'META/%sfilesystem_config.txt' % fs_config_prefix,
285 ])
286
287 return sorted(item_set)
Daniel Norman5f476772022-03-02 15:46:34 -0800288
289
290def InferFrameworkMiscInfoKeys(input_namelist):
291 keys = [
292 'ab_update',
293 'avb_vbmeta_system',
294 'avb_vbmeta_system_algorithm',
295 'avb_vbmeta_system_key_path',
296 'avb_vbmeta_system_rollback_index_location',
297 'default_system_dev_certificate',
298 ]
299
300 for partition in _FRAMEWORK_PARTITIONS:
Daniel Norman679242b2022-03-18 15:46:27 -0700301 for partition_dir in ('%s/' % partition.upper(), 'SYSTEM/%s/' % partition):
302 if partition_dir in input_namelist:
Daniel Norman5f476772022-03-02 15:46:34 -0800303 fs_type_prefix = '' if partition == 'system' else '%s_' % partition
304 keys.extend([
305 'avb_%s_hashtree_enable' % partition,
306 'avb_%s_add_hashtree_footer_args' % partition,
307 '%s_disable_sparse' % partition,
308 'building_%s_image' % partition,
309 '%sfs_type' % fs_type_prefix,
310 ])
311
312 return sorted(keys)