blob: 6fe3099d2f23a0ba9f7d550d93c59b76c21d476b [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"""Functions for merging META/* files from partial builds.
18
19Expects items in OPTIONS prepared by merge_target_files.py.
20"""
21
22import logging
23import os
24import re
25import shutil
26
27import build_image
28import common
29import merge_utils
30import sparse_img
31import verity_utils
32
33from common import ExternalError
34
35logger = logging.getLogger(__name__)
36
37OPTIONS = common.OPTIONS
38
39# In apexkeys.txt or apkcerts.txt, we will find partition tags on each entry in
40# the file. We use these partition tags to filter the entries in those files
41# from the two different target files packages to produce a merged apexkeys.txt
42# or apkcerts.txt file. A partition tag (e.g., for the product partition) looks
43# like this: 'partition="product"'. We use the group syntax grab the value of
44# the tag. We use non-greedy matching in case there are other fields on the
45# same line.
46
47PARTITION_TAG_PATTERN = re.compile(r'partition="(.*?)"')
48
49# The sorting algorithm for apexkeys.txt and apkcerts.txt does not include the
50# ".apex" or ".apk" suffix, so we use the following pattern to extract a key.
51
52MODULE_KEY_PATTERN = re.compile(r'name="(.+)\.(apex|apk)"')
53
54
Kelvin Zhangfa40c042022-11-09 10:59:25 -080055def ParseUpdateEngineConfig(path: str):
56 """Parse the update_engine config stored in file `path`
57 Args
58 path: Path to update_engine_config.txt file in target_files
59
60 Returns
61 A tuple of (major, minor) version number . E.g. (2, 8)
62 """
63 with open(path, "r") as fp:
64 # update_engine_config.txt is only supposed to contain two lines,
65 # PAYLOAD_MAJOR_VERSION and PAYLOAD_MINOR_VERSION. 1024 should be more than
66 # sufficient. If the length is more than that, something is wrong.
67 data = fp.read(1024)
68 major = re.search(r"PAYLOAD_MAJOR_VERSION=(\d+)", data)
69 if not major:
70 raise ValueError(
71 f"{path} is an invalid update_engine config, missing PAYLOAD_MAJOR_VERSION {data}")
72 minor = re.search(r"PAYLOAD_MINOR_VERSION=(\d+)", data)
73 if not minor:
74 raise ValueError(
75 f"{path} is an invalid update_engine config, missing PAYLOAD_MINOR_VERSION {data}")
76 return (int(major.group(1)), int(minor.group(1)))
77
78
79def MergeUpdateEngineConfig(input_metadir1, input_metadir2, merged_meta_dir):
80 UPDATE_ENGINE_CONFIG_NAME = "update_engine_config.txt"
81 config1_path = os.path.join(
82 input_metadir1, UPDATE_ENGINE_CONFIG_NAME)
83 config2_path = os.path.join(
84 input_metadir2, UPDATE_ENGINE_CONFIG_NAME)
85 config1 = ParseUpdateEngineConfig(config1_path)
86 config2 = ParseUpdateEngineConfig(config2_path)
87 # Copy older config to merged target files for maximum compatibility
88 # update_engine in system partition is from system side, but
89 # update_engine_sideload in recovery is from vendor side.
90 if config1 < config2:
91 shutil.copy(config1_path, os.path.join(
92 merged_meta_dir, UPDATE_ENGINE_CONFIG_NAME))
93 else:
94 shutil.copy(config2_path, os.path.join(
95 merged_meta_dir, UPDATE_ENGINE_CONFIG_NAME))
96
97
Daniel Norman2465fc82022-03-02 12:01:20 -080098def MergeMetaFiles(temp_dir, merged_dir):
99 """Merges various files in META/*."""
100
101 framework_meta_dir = os.path.join(temp_dir, 'framework_meta', 'META')
Dennis Song5bfa43e2023-03-30 18:28:00 +0800102 merge_utils.CollectTargetFiles(
103 input_zipfile_or_dir=OPTIONS.framework_target_files,
Daniel Norman2465fc82022-03-02 12:01:20 -0800104 output_dir=os.path.dirname(framework_meta_dir),
Dennis Song5bfa43e2023-03-30 18:28:00 +0800105 item_list=('META/*',))
Daniel Norman2465fc82022-03-02 12:01:20 -0800106
107 vendor_meta_dir = os.path.join(temp_dir, 'vendor_meta', 'META')
Dennis Song5bfa43e2023-03-30 18:28:00 +0800108 merge_utils.CollectTargetFiles(
109 input_zipfile_or_dir=OPTIONS.vendor_target_files,
Daniel Norman2465fc82022-03-02 12:01:20 -0800110 output_dir=os.path.dirname(vendor_meta_dir),
Dennis Song5bfa43e2023-03-30 18:28:00 +0800111 item_list=('META/*',))
Daniel Norman2465fc82022-03-02 12:01:20 -0800112
113 merged_meta_dir = os.path.join(merged_dir, 'META')
114
115 # Merge META/misc_info.txt into OPTIONS.merged_misc_info,
116 # but do not write it yet. The following functions may further
117 # modify this dict.
118 OPTIONS.merged_misc_info = MergeMiscInfo(
119 framework_meta_dir=framework_meta_dir,
120 vendor_meta_dir=vendor_meta_dir,
121 merged_meta_dir=merged_meta_dir)
122
123 CopyNamedFileContexts(
124 framework_meta_dir=framework_meta_dir,
125 vendor_meta_dir=vendor_meta_dir,
126 merged_meta_dir=merged_meta_dir)
127
128 if OPTIONS.merged_misc_info.get('use_dynamic_partitions') == 'true':
129 MergeDynamicPartitionsInfo(
130 framework_meta_dir=framework_meta_dir,
131 vendor_meta_dir=vendor_meta_dir,
132 merged_meta_dir=merged_meta_dir)
133
134 if OPTIONS.merged_misc_info.get('ab_update') == 'true':
135 MergeAbPartitions(
136 framework_meta_dir=framework_meta_dir,
137 vendor_meta_dir=vendor_meta_dir,
138 merged_meta_dir=merged_meta_dir)
139 UpdateCareMapImageSizeProps(images_dir=os.path.join(merged_dir, 'IMAGES'))
140
141 for file_name in ('apkcerts.txt', 'apexkeys.txt'):
142 MergePackageKeys(
143 framework_meta_dir=framework_meta_dir,
144 vendor_meta_dir=vendor_meta_dir,
145 merged_meta_dir=merged_meta_dir,
146 file_name=file_name)
147
Kelvin Zhangfa40c042022-11-09 10:59:25 -0800148 MergeUpdateEngineConfig(
149 framework_meta_dir,
150 vendor_meta_dir, merged_meta_dir,
151 )
152
Daniel Norman2465fc82022-03-02 12:01:20 -0800153 # Write the now-finalized OPTIONS.merged_misc_info.
154 merge_utils.WriteSortedData(
155 data=OPTIONS.merged_misc_info,
156 path=os.path.join(merged_meta_dir, 'misc_info.txt'))
157
158
159def MergeAbPartitions(framework_meta_dir, vendor_meta_dir, merged_meta_dir):
160 """Merges META/ab_partitions.txt.
161
162 The output contains the union of the partition names.
163 """
164 with open(os.path.join(framework_meta_dir, 'ab_partitions.txt')) as f:
165 framework_ab_partitions = f.read().splitlines()
166
167 with open(os.path.join(vendor_meta_dir, 'ab_partitions.txt')) as f:
168 vendor_ab_partitions = f.read().splitlines()
169
170 merge_utils.WriteSortedData(
171 data=set(framework_ab_partitions + vendor_ab_partitions),
172 path=os.path.join(merged_meta_dir, 'ab_partitions.txt'))
173
174
175def MergeMiscInfo(framework_meta_dir, vendor_meta_dir, merged_meta_dir):
176 """Merges META/misc_info.txt.
177
178 The output contains a combination of key=value pairs from both inputs.
179 Most pairs are taken from the vendor input, while some are taken from
180 the framework input.
181 """
182
183 OPTIONS.framework_misc_info = common.LoadDictionaryFromFile(
184 os.path.join(framework_meta_dir, 'misc_info.txt'))
185 OPTIONS.vendor_misc_info = common.LoadDictionaryFromFile(
186 os.path.join(vendor_meta_dir, 'misc_info.txt'))
187
188 # Merged misc info is a combination of vendor misc info plus certain values
189 # from the framework misc info.
190
191 merged_dict = OPTIONS.vendor_misc_info
192 for key in OPTIONS.framework_misc_info_keys:
Daniel Norman5f476772022-03-02 15:46:34 -0800193 if key in OPTIONS.framework_misc_info:
194 merged_dict[key] = OPTIONS.framework_misc_info[key]
Daniel Norman2465fc82022-03-02 12:01:20 -0800195
196 # If AVB is enabled then ensure that we build vbmeta.img.
197 # Partial builds with AVB enabled may set PRODUCT_BUILD_VBMETA_IMAGE=false to
198 # skip building an incomplete vbmeta.img.
199 if merged_dict.get('avb_enable') == 'true':
200 merged_dict['avb_building_vbmeta_image'] = 'true'
201
202 return merged_dict
203
204
205def MergeDynamicPartitionsInfo(framework_meta_dir, vendor_meta_dir,
206 merged_meta_dir):
207 """Merge META/dynamic_partitions_info.txt."""
208 framework_dynamic_partitions_dict = common.LoadDictionaryFromFile(
209 os.path.join(framework_meta_dir, 'dynamic_partitions_info.txt'))
210 vendor_dynamic_partitions_dict = common.LoadDictionaryFromFile(
211 os.path.join(vendor_meta_dir, 'dynamic_partitions_info.txt'))
212
213 merged_dynamic_partitions_dict = common.MergeDynamicPartitionInfoDicts(
214 framework_dict=framework_dynamic_partitions_dict,
215 vendor_dict=vendor_dynamic_partitions_dict)
216
217 merge_utils.WriteSortedData(
218 data=merged_dynamic_partitions_dict,
219 path=os.path.join(merged_meta_dir, 'dynamic_partitions_info.txt'))
220
221 # Merge misc info keys used for Dynamic Partitions.
222 OPTIONS.merged_misc_info.update(merged_dynamic_partitions_dict)
223 # Ensure that add_img_to_target_files rebuilds super split images for
224 # devices that retrofit dynamic partitions. This flag may have been set to
225 # false in the partial builds to prevent duplicate building of super.img.
226 OPTIONS.merged_misc_info['build_super_partition'] = 'true'
227
228
229def MergePackageKeys(framework_meta_dir, vendor_meta_dir, merged_meta_dir,
230 file_name):
231 """Merges APK/APEX key list files."""
232
233 if file_name not in ('apkcerts.txt', 'apexkeys.txt'):
234 raise ExternalError(
235 'Unexpected file_name provided to merge_package_keys_txt: %s',
236 file_name)
237
238 def read_helper(d):
239 temp = {}
240 with open(os.path.join(d, file_name)) as f:
241 for line in f.read().splitlines():
242 line = line.strip()
243 if line:
244 name_search = MODULE_KEY_PATTERN.search(line.split()[0])
245 temp[name_search.group(1)] = line
246 return temp
247
248 framework_dict = read_helper(framework_meta_dir)
249 vendor_dict = read_helper(vendor_meta_dir)
250 merged_dict = {}
251
252 def filter_into_merged_dict(item_dict, partition_set):
253 for key, value in item_dict.items():
254 tag_search = PARTITION_TAG_PATTERN.search(value)
255
256 if tag_search is None:
257 raise ValueError('Entry missing partition tag: %s' % value)
258
259 partition_tag = tag_search.group(1)
260
261 if partition_tag in partition_set:
262 if key in merged_dict:
263 if OPTIONS.allow_duplicate_apkapex_keys:
264 # TODO(b/150582573) Always raise on duplicates.
265 logger.warning('Duplicate key %s' % key)
266 continue
267 else:
268 raise ValueError('Duplicate key %s' % key)
269
270 merged_dict[key] = value
271
272 # Prioritize framework keys first.
273 # Duplicate keys from vendor are an error, or ignored.
274 filter_into_merged_dict(framework_dict, OPTIONS.framework_partition_set)
275 filter_into_merged_dict(vendor_dict, OPTIONS.vendor_partition_set)
276
277 # The following code is similar to WriteSortedData, but different enough
278 # that we couldn't use that function. We need the output to be sorted by the
279 # basename of the apex/apk (without the ".apex" or ".apk" suffix). This
280 # allows the sort to be consistent with the framework/vendor input data and
281 # eases comparison of input data with merged data.
282 with open(os.path.join(merged_meta_dir, file_name), 'w') as output:
283 for key, value in sorted(merged_dict.items()):
284 output.write(value + '\n')
285
286
287def CopyNamedFileContexts(framework_meta_dir, vendor_meta_dir, merged_meta_dir):
288 """Creates named copies of each partial build's file_contexts.bin.
289
290 Used when regenerating images from the partial build.
291 """
292
293 def copy_fc_file(source_dir, file_name):
294 for name in (file_name, 'file_contexts.bin'):
295 fc_path = os.path.join(source_dir, name)
296 if os.path.exists(fc_path):
297 shutil.copyfile(fc_path, os.path.join(merged_meta_dir, file_name))
298 return
299 raise ValueError('Missing file_contexts file from %s: %s', source_dir,
300 file_name)
301
302 copy_fc_file(framework_meta_dir, 'framework_file_contexts.bin')
303 copy_fc_file(vendor_meta_dir, 'vendor_file_contexts.bin')
304
305 # Replace <image>_selinux_fc values with framework or vendor file_contexts.bin
306 # depending on which dictionary the key came from.
307 # Only the file basename is required because all selinux_fc properties are
308 # replaced with the full path to the file under META/ when misc_info.txt is
309 # loaded from target files for repacking. See common.py LoadInfoDict().
310 for key in OPTIONS.vendor_misc_info:
311 if key.endswith('_selinux_fc'):
312 OPTIONS.merged_misc_info[key] = 'vendor_file_contexts.bin'
313 for key in OPTIONS.framework_misc_info:
314 if key.endswith('_selinux_fc'):
315 OPTIONS.merged_misc_info[key] = 'framework_file_contexts.bin'
316
317
318def UpdateCareMapImageSizeProps(images_dir):
319 """Sets <partition>_image_size props in misc_info.
320
321 add_images_to_target_files uses these props to generate META/care_map.pb.
322 Regenerated images will have this property set during regeneration.
323
324 However, images copied directly from input partial target files packages
325 need this value calculated here.
326 """
327 for partition in common.PARTITIONS_WITH_CARE_MAP:
328 image_path = os.path.join(images_dir, '{}.img'.format(partition))
329 if os.path.exists(image_path):
330 partition_size = sparse_img.GetImagePartitionSize(image_path)
331 image_props = build_image.ImagePropFromGlobalDict(
332 OPTIONS.merged_misc_info, partition)
333 verity_image_builder = verity_utils.CreateVerityImageBuilder(image_props)
334 image_size = verity_image_builder.CalculateMaxImageSize(partition_size)
335 OPTIONS.merged_misc_info['{}_image_size'.format(partition)] = image_size