blob: fa9516edcb8b2a9fb28e5b4364f6bf146186669e [file] [log] [blame]
Kelvin Zhangcff4d762020-07-29 16:37:51 -04001# Copyright (C) 2020 The Android Open Source Project
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import copy
16import itertools
Yifan Hong125d0b62020-09-24 17:07:03 -070017import logging
Kelvin Zhangcff4d762020-07-29 16:37:51 -040018import os
Kelvin Zhangb9fdf2d2022-08-12 14:07:31 -070019import shutil
Kelvin Zhang25ab9982021-06-22 09:51:34 -040020import struct
Kelvin Zhangcff4d762020-07-29 16:37:51 -040021import zipfile
22
Tianjiea2076132020-08-19 17:25:32 -070023import ota_metadata_pb2
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +000024import common
Kelvin Zhang9dbe2ce2023-04-17 16:38:08 -070025import fnmatch
26from common import (ZipDelete, DoesInputFileContain, ReadBytesFromInputFile, OPTIONS, MakeTempFile,
Kelvin Zhangcff4d762020-07-29 16:37:51 -040027 ZipWriteStr, BuildInfo, LoadDictionaryFromFile,
TJ Rhoades6f488e92022-05-01 22:16:22 -070028 SignFile, PARTITIONS_WITH_BUILD_PROP, PartitionBuildProps,
Kelvin Zhangfcd731e2023-04-04 10:28:11 -070029 GetRamdiskFormat, ParseUpdateEngineConfig)
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +000030from payload_signer import PayloadSigner
31
Kelvin Zhangcff4d762020-07-29 16:37:51 -040032
Yifan Hong125d0b62020-09-24 17:07:03 -070033logger = logging.getLogger(__name__)
Kelvin Zhang2e417382020-08-20 11:33:11 -040034
35OPTIONS.no_signing = False
36OPTIONS.force_non_ab = False
37OPTIONS.wipe_user_data = False
38OPTIONS.downgrade = False
39OPTIONS.key_passwords = {}
40OPTIONS.package_key = None
41OPTIONS.incremental_source = None
42OPTIONS.retrofit_dynamic_partitions = False
43OPTIONS.output_metadata_path = None
44OPTIONS.boot_variable_file = None
45
Kelvin Zhangcff4d762020-07-29 16:37:51 -040046METADATA_NAME = 'META-INF/com/android/metadata'
Tianjiea2076132020-08-19 17:25:32 -070047METADATA_PROTO_NAME = 'META-INF/com/android/metadata.pb'
Kelvin Zhang9dbe2ce2023-04-17 16:38:08 -070048UNZIP_PATTERN = ['IMAGES/*', 'META/*', 'OTA/*',
49 'RADIO/*', '*/build.prop', '*/default.prop', '*/build.default', "*/etc/vintf/*"]
Kelvin Zhang05ff7052021-02-10 09:13:26 -050050SECURITY_PATCH_LEVEL_PROP_NAME = "ro.build.version.security_patch"
Kelvin Zhang22680912023-05-19 13:12:59 -070051TARGET_FILES_IMAGES_SUBDIR = ["IMAGES", "PREBUILT_IMAGES", "RADIO"]
Kelvin Zhang05ff7052021-02-10 09:13:26 -050052
Kelvin Zhangcff4d762020-07-29 16:37:51 -040053
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +000054def FinalizeMetadata(metadata, input_file, output_file, needed_property_files=None, package_key=None, pw=None):
Kelvin Zhangcff4d762020-07-29 16:37:51 -040055 """Finalizes the metadata and signs an A/B OTA package.
56
57 In order to stream an A/B OTA package, we need 'ota-streaming-property-files'
58 that contains the offsets and sizes for the ZIP entries. An example
59 property-files string is as follows.
60
61 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379"
62
63 OTA server can pass down this string, in addition to the package URL, to the
64 system update client. System update client can then fetch individual ZIP
65 entries (ZIP_STORED) directly at the given offset of the URL.
66
67 Args:
68 metadata: The metadata dict for the package.
69 input_file: The input ZIP filename that doesn't contain the package METADATA
70 entry yet.
71 output_file: The final output ZIP filename.
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +000072 needed_property_files: The list of PropertyFiles' to be generated. Default is [AbOtaPropertyFiles(), StreamingPropertyFiles()]
73 package_key: The key used to sign this OTA package
74 pw: Password for the package_key
Kelvin Zhangcff4d762020-07-29 16:37:51 -040075 """
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +000076 no_signing = package_key is None
77
78 if needed_property_files is None:
79 # AbOtaPropertyFiles intends to replace StreamingPropertyFiles, as it covers
80 # all the info of the latter. However, system updaters and OTA servers need to
81 # take time to switch to the new flag. We keep both of the flags for
82 # P-timeframe, and will remove StreamingPropertyFiles in later release.
83 needed_property_files = (
84 AbOtaPropertyFiles(),
85 StreamingPropertyFiles(),
86 )
Kelvin Zhangcff4d762020-07-29 16:37:51 -040087
88 def ComputeAllPropertyFiles(input_file, needed_property_files):
89 # Write the current metadata entry with placeholders.
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -070090 with zipfile.ZipFile(input_file, 'r', allowZip64=True) as input_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -040091 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -070092 metadata.property_files[property_files.name] = property_files.Compute(
93 input_zip)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040094
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -070095 ZipDelete(input_file, [METADATA_NAME, METADATA_PROTO_NAME], True)
96 with zipfile.ZipFile(input_file, 'a', allowZip64=True) as output_zip:
97 WriteMetadata(metadata, output_zip)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040098
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +000099 if no_signing:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400100 return input_file
101
102 prelim_signing = MakeTempFile(suffix='.zip')
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000103 SignOutput(input_file, prelim_signing, package_key, pw)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400104 return prelim_signing
105
106 def FinalizeAllPropertyFiles(prelim_signing, needed_property_files):
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -0700107 with zipfile.ZipFile(prelim_signing, 'r', allowZip64=True) as prelim_signing_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400108 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -0700109 metadata.property_files[property_files.name] = property_files.Finalize(
110 prelim_signing_zip,
111 len(metadata.property_files[property_files.name]))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400112
113 # SignOutput(), which in turn calls signapk.jar, will possibly reorder the ZIP
114 # entries, as well as padding the entry headers. We do a preliminary signing
115 # (with an incomplete metadata entry) to allow that to happen. Then compute
116 # the ZIP entry offsets, write back the final metadata and do the final
117 # signing.
118 prelim_signing = ComputeAllPropertyFiles(input_file, needed_property_files)
119 try:
120 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
121 except PropertyFiles.InsufficientSpaceException:
122 # Even with the preliminary signing, the entry orders may change
123 # dramatically, which leads to insufficiently reserved space during the
124 # first call to ComputeAllPropertyFiles(). In that case, we redo all the
125 # preliminary signing works, based on the already ordered ZIP entries, to
126 # address the issue.
127 prelim_signing = ComputeAllPropertyFiles(
128 prelim_signing, needed_property_files)
129 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
130
131 # Replace the METADATA entry.
Tianjiea2076132020-08-19 17:25:32 -0700132 ZipDelete(prelim_signing, [METADATA_NAME, METADATA_PROTO_NAME])
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -0700133 with zipfile.ZipFile(prelim_signing, 'a', allowZip64=True) as output_zip:
134 WriteMetadata(metadata, output_zip)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400135
136 # Re-sign the package after updating the metadata entry.
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000137 if no_signing:
Kelvin Zhangf80e8862023-01-20 10:18:11 -0800138 logger.info(f"Signing disabled for output file {output_file}")
Kelvin Zhangb9fdf2d2022-08-12 14:07:31 -0700139 shutil.copy(prelim_signing, output_file)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400140 else:
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700141 logger.info(
142 f"Signing the output file {output_file} with key {package_key}")
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000143 SignOutput(prelim_signing, output_file, package_key, pw)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400144
145 # Reopen the final signed zip to double check the streaming metadata.
Kelvin Zhang928c2342020-09-22 16:15:57 -0400146 with zipfile.ZipFile(output_file, allowZip64=True) as output_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400147 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -0700148 property_files.Verify(
149 output_zip, metadata.property_files[property_files.name].strip())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400150
151 # If requested, dump the metadata to a separate file.
152 output_metadata_path = OPTIONS.output_metadata_path
153 if output_metadata_path:
154 WriteMetadata(metadata, output_metadata_path)
155
156
Tianjiea2076132020-08-19 17:25:32 -0700157def WriteMetadata(metadata_proto, output):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400158 """Writes the metadata to the zip archive or a file.
159
160 Args:
Tianjiea2076132020-08-19 17:25:32 -0700161 metadata_proto: The metadata protobuf for the package.
162 output: A ZipFile object or a string of the output file path. If a string
163 path is given, the metadata in the protobuf format will be written to
164 {output}.pb, e.g. ota_metadata.pb
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400165 """
166
Tianjiea2076132020-08-19 17:25:32 -0700167 metadata_dict = BuildLegacyOtaMetadata(metadata_proto)
168 legacy_metadata = "".join(["%s=%s\n" % kv for kv in
169 sorted(metadata_dict.items())])
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400170 if isinstance(output, zipfile.ZipFile):
Tianjiea2076132020-08-19 17:25:32 -0700171 ZipWriteStr(output, METADATA_PROTO_NAME, metadata_proto.SerializeToString(),
172 compress_type=zipfile.ZIP_STORED)
173 ZipWriteStr(output, METADATA_NAME, legacy_metadata,
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400174 compress_type=zipfile.ZIP_STORED)
175 return
176
Cole Faustb820bcd2021-10-28 13:59:48 -0700177 with open('{}.pb'.format(output), 'wb') as f:
Tianjiea2076132020-08-19 17:25:32 -0700178 f.write(metadata_proto.SerializeToString())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400179 with open(output, 'w') as f:
Tianjiea2076132020-08-19 17:25:32 -0700180 f.write(legacy_metadata)
181
182
183def UpdateDeviceState(device_state, build_info, boot_variable_values,
184 is_post_build):
185 """Update the fields of the DeviceState proto with build info."""
186
Tianjie2bb14862020-08-28 16:24:34 -0700187 def UpdatePartitionStates(partition_states):
188 """Update the per-partition state according to its build.prop"""
Kelvin Zhang39aea442020-08-17 11:04:25 -0400189 if not build_info.is_ab:
190 return
Tianjie2bb14862020-08-28 16:24:34 -0700191 build_info_set = ComputeRuntimeBuildInfos(build_info,
192 boot_variable_values)
Kelvin Zhang39aea442020-08-17 11:04:25 -0400193 assert "ab_partitions" in build_info.info_dict,\
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500194 "ab_partitions property required for ab update."
Kelvin Zhang39aea442020-08-17 11:04:25 -0400195 ab_partitions = set(build_info.info_dict.get("ab_partitions"))
196
197 # delta_generator will error out on unused timestamps,
198 # so only generate timestamps for dynamic partitions
199 # used in OTA update.
Yifan Hong5057b952021-01-07 14:09:57 -0800200 for partition in sorted(set(PARTITIONS_WITH_BUILD_PROP) & ab_partitions):
Tianjie2bb14862020-08-28 16:24:34 -0700201 partition_prop = build_info.info_dict.get(
202 '{}.build.prop'.format(partition))
203 # Skip if the partition is missing, or it doesn't have a build.prop
204 if not partition_prop or not partition_prop.build_props:
205 continue
206
207 partition_state = partition_states.add()
208 partition_state.partition_name = partition
209 # Update the partition's runtime device names and fingerprints
210 partition_devices = set()
211 partition_fingerprints = set()
212 for runtime_build_info in build_info_set:
213 partition_devices.add(
214 runtime_build_info.GetPartitionBuildProp('ro.product.device',
215 partition))
216 partition_fingerprints.add(
217 runtime_build_info.GetPartitionFingerprint(partition))
218
219 partition_state.device.extend(sorted(partition_devices))
220 partition_state.build.extend(sorted(partition_fingerprints))
221
222 # TODO(xunchang) set the boot image's version with kmi. Note the boot
223 # image doesn't have a file map.
224 partition_state.version = build_info.GetPartitionBuildProp(
225 'ro.build.date.utc', partition)
226
227 # TODO(xunchang), we can save a call to ComputeRuntimeBuildInfos.
Tianjiea2076132020-08-19 17:25:32 -0700228 build_devices, build_fingerprints = \
229 CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values)
230 device_state.device.extend(sorted(build_devices))
231 device_state.build.extend(sorted(build_fingerprints))
232 device_state.build_incremental = build_info.GetBuildProp(
233 'ro.build.version.incremental')
234
Tianjie2bb14862020-08-28 16:24:34 -0700235 UpdatePartitionStates(device_state.partition_state)
Tianjiea2076132020-08-19 17:25:32 -0700236
237 if is_post_build:
238 device_state.sdk_level = build_info.GetBuildProp(
239 'ro.build.version.sdk')
240 device_state.security_patch_level = build_info.GetBuildProp(
241 'ro.build.version.security_patch')
242 # Use the actual post-timestamp, even for a downgrade case.
243 device_state.timestamp = int(build_info.GetBuildProp('ro.build.date.utc'))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400244
245
246def GetPackageMetadata(target_info, source_info=None):
Tianjiea2076132020-08-19 17:25:32 -0700247 """Generates and returns the metadata proto.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400248
Tianjiea2076132020-08-19 17:25:32 -0700249 It generates a ota_metadata protobuf that contains the info to be written
250 into an OTA package (META-INF/com/android/metadata.pb). It also handles the
251 detection of downgrade / data wipe based on the global options.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400252
253 Args:
254 target_info: The BuildInfo instance that holds the target build info.
255 source_info: The BuildInfo instance that holds the source build info, or
256 None if generating full OTA.
257
258 Returns:
Tianjiea2076132020-08-19 17:25:32 -0700259 A protobuf to be written into package metadata entry.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400260 """
261 assert isinstance(target_info, BuildInfo)
262 assert source_info is None or isinstance(source_info, BuildInfo)
263
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400264 boot_variable_values = {}
265 if OPTIONS.boot_variable_file:
266 d = LoadDictionaryFromFile(OPTIONS.boot_variable_file)
267 for key, values in d.items():
268 boot_variable_values[key] = [val.strip() for val in values.split(',')]
269
Tianjiea2076132020-08-19 17:25:32 -0700270 metadata_proto = ota_metadata_pb2.OtaMetadata()
271 # TODO(xunchang) some fields, e.g. post-device isn't necessary. We can
272 # consider skipping them if they aren't used by clients.
273 UpdateDeviceState(metadata_proto.postcondition, target_info,
274 boot_variable_values, True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400275
276 if target_info.is_ab and not OPTIONS.force_non_ab:
Tianjiea2076132020-08-19 17:25:32 -0700277 metadata_proto.type = ota_metadata_pb2.OtaMetadata.AB
278 metadata_proto.required_cache = 0
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400279 else:
Tianjiea2076132020-08-19 17:25:32 -0700280 metadata_proto.type = ota_metadata_pb2.OtaMetadata.BLOCK
281 # cache requirement will be updated by the non-A/B codes.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400282
283 if OPTIONS.wipe_user_data:
Tianjiea2076132020-08-19 17:25:32 -0700284 metadata_proto.wipe = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400285
286 if OPTIONS.retrofit_dynamic_partitions:
Tianjiea2076132020-08-19 17:25:32 -0700287 metadata_proto.retrofit_dynamic_partitions = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400288
289 is_incremental = source_info is not None
290 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700291 UpdateDeviceState(metadata_proto.precondition, source_info,
292 boot_variable_values, False)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400293 else:
Tianjiea2076132020-08-19 17:25:32 -0700294 metadata_proto.precondition.device.extend(
295 metadata_proto.postcondition.device)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400296
297 # Detect downgrades and set up downgrade flags accordingly.
298 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700299 HandleDowngradeMetadata(metadata_proto, target_info, source_info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400300
Tianjiea2076132020-08-19 17:25:32 -0700301 return metadata_proto
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400302
303
Tianjiea2076132020-08-19 17:25:32 -0700304def BuildLegacyOtaMetadata(metadata_proto):
305 """Converts the metadata proto to a legacy metadata dict.
306
307 This metadata dict is used to build the legacy metadata text file for
308 backward compatibility. We won't add new keys to the legacy metadata format.
309 If new information is needed, we should add it as a new field in OtaMetadata
310 proto definition.
311 """
312
313 separator = '|'
314
315 metadata_dict = {}
316 if metadata_proto.type == ota_metadata_pb2.OtaMetadata.AB:
317 metadata_dict['ota-type'] = 'AB'
318 elif metadata_proto.type == ota_metadata_pb2.OtaMetadata.BLOCK:
319 metadata_dict['ota-type'] = 'BLOCK'
320 if metadata_proto.wipe:
321 metadata_dict['ota-wipe'] = 'yes'
322 if metadata_proto.retrofit_dynamic_partitions:
323 metadata_dict['ota-retrofit-dynamic-partitions'] = 'yes'
324 if metadata_proto.downgrade:
325 metadata_dict['ota-downgrade'] = 'yes'
326
327 metadata_dict['ota-required-cache'] = str(metadata_proto.required_cache)
328
329 post_build = metadata_proto.postcondition
330 metadata_dict['post-build'] = separator.join(post_build.build)
331 metadata_dict['post-build-incremental'] = post_build.build_incremental
332 metadata_dict['post-sdk-level'] = post_build.sdk_level
333 metadata_dict['post-security-patch-level'] = post_build.security_patch_level
334 metadata_dict['post-timestamp'] = str(post_build.timestamp)
335
336 pre_build = metadata_proto.precondition
337 metadata_dict['pre-device'] = separator.join(pre_build.device)
338 # incremental updates
339 if len(pre_build.build) != 0:
340 metadata_dict['pre-build'] = separator.join(pre_build.build)
341 metadata_dict['pre-build-incremental'] = pre_build.build_incremental
342
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500343 if metadata_proto.spl_downgrade:
344 metadata_dict['spl-downgrade'] = 'yes'
Tianjiea2076132020-08-19 17:25:32 -0700345 metadata_dict.update(metadata_proto.property_files)
346
347 return metadata_dict
348
349
350def HandleDowngradeMetadata(metadata_proto, target_info, source_info):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400351 # Only incremental OTAs are allowed to reach here.
352 assert OPTIONS.incremental_source is not None
353
354 post_timestamp = target_info.GetBuildProp("ro.build.date.utc")
355 pre_timestamp = source_info.GetBuildProp("ro.build.date.utc")
356 is_downgrade = int(post_timestamp) < int(pre_timestamp)
357
Kelvin Zhang05ff7052021-02-10 09:13:26 -0500358 if OPTIONS.spl_downgrade:
359 metadata_proto.spl_downgrade = True
360
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400361 if OPTIONS.downgrade:
362 if not is_downgrade:
363 raise RuntimeError(
364 "--downgrade or --override_timestamp specified but no downgrade "
365 "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp))
Tianjiea2076132020-08-19 17:25:32 -0700366 metadata_proto.downgrade = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400367 else:
368 if is_downgrade:
369 raise RuntimeError(
370 "Downgrade detected based on timestamp check: pre: %s, post: %s. "
371 "Need to specify --override_timestamp OR --downgrade to allow "
372 "building the incremental." % (pre_timestamp, post_timestamp))
373
374
Tianjie2bb14862020-08-28 16:24:34 -0700375def ComputeRuntimeBuildInfos(default_build_info, boot_variable_values):
376 """Returns a set of build info objects that may exist during runtime."""
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400377
Tianjie2bb14862020-08-28 16:24:34 -0700378 build_info_set = {default_build_info}
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400379 if not boot_variable_values:
Tianjie2bb14862020-08-28 16:24:34 -0700380 return build_info_set
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400381
382 # Calculate all possible combinations of the values for the boot variables.
383 keys = boot_variable_values.keys()
384 value_list = boot_variable_values.values()
385 combinations = [dict(zip(keys, values))
386 for values in itertools.product(*value_list)]
387 for placeholder_values in combinations:
388 # Reload the info_dict as some build properties may change their values
389 # based on the value of ro.boot* properties.
Tianjie2bb14862020-08-28 16:24:34 -0700390 info_dict = copy.deepcopy(default_build_info.info_dict)
Yifan Hong5057b952021-01-07 14:09:57 -0800391 for partition in PARTITIONS_WITH_BUILD_PROP:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400392 partition_prop_key = "{}.build.prop".format(partition)
393 input_file = info_dict[partition_prop_key].input_file
TJ Rhoades6f488e92022-05-01 22:16:22 -0700394 ramdisk = GetRamdiskFormat(info_dict)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400395 if isinstance(input_file, zipfile.ZipFile):
Kelvin Zhang928c2342020-09-22 16:15:57 -0400396 with zipfile.ZipFile(input_file.filename, allowZip64=True) as input_zip:
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400397 info_dict[partition_prop_key] = \
398 PartitionBuildProps.FromInputFile(input_zip, partition,
TJ Rhoades6f488e92022-05-01 22:16:22 -0700399 placeholder_values,
400 ramdisk)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400401 else:
402 info_dict[partition_prop_key] = \
403 PartitionBuildProps.FromInputFile(input_file, partition,
TJ Rhoades6f488e92022-05-01 22:16:22 -0700404 placeholder_values,
405 ramdisk)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400406 info_dict["build.prop"] = info_dict["system.build.prop"]
Tianjie2bb14862020-08-28 16:24:34 -0700407 build_info_set.add(BuildInfo(info_dict, default_build_info.oem_dicts))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400408
Tianjie2bb14862020-08-28 16:24:34 -0700409 return build_info_set
410
411
412def CalculateRuntimeDevicesAndFingerprints(default_build_info,
413 boot_variable_values):
414 """Returns a tuple of sets for runtime devices and fingerprints"""
415
416 device_names = set()
417 fingerprints = set()
418 build_info_set = ComputeRuntimeBuildInfos(default_build_info,
419 boot_variable_values)
420 for runtime_build_info in build_info_set:
421 device_names.add(runtime_build_info.device)
422 fingerprints.add(runtime_build_info.fingerprint)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400423 return device_names, fingerprints
424
425
Kelvin Zhang25ab9982021-06-22 09:51:34 -0400426def GetZipEntryOffset(zfp, entry_info):
427 """Get offset to a beginning of a particular zip entry
428 Args:
429 fp: zipfile.ZipFile
430 entry_info: zipfile.ZipInfo
431
432 Returns:
433 (offset, size) tuple
434 """
435 # Don't use len(entry_info.extra). Because that returns size of extra
436 # fields in central directory. We need to look at local file directory,
437 # as these two might have different sizes.
438
439 # We cannot work with zipfile.ZipFile instances, we need a |fp| for the underlying file.
440 zfp = zfp.fp
441 zfp.seek(entry_info.header_offset)
442 data = zfp.read(zipfile.sizeFileHeader)
443 fheader = struct.unpack(zipfile.structFileHeader, data)
444 # Last two fields of local file header are filename length and
445 # extra length
446 filename_len = fheader[-2]
447 extra_len = fheader[-1]
448 offset = entry_info.header_offset
449 offset += zipfile.sizeFileHeader
450 offset += filename_len + extra_len
451 size = entry_info.file_size
452 return (offset, size)
453
454
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400455class PropertyFiles(object):
456 """A class that computes the property-files string for an OTA package.
457
458 A property-files string is a comma-separated string that contains the
459 offset/size info for an OTA package. The entries, which must be ZIP_STORED,
460 can be fetched directly with the package URL along with the offset/size info.
461 These strings can be used for streaming A/B OTAs, or allowing an updater to
462 download package metadata entry directly, without paying the cost of
463 downloading entire package.
464
465 Computing the final property-files string requires two passes. Because doing
466 the whole package signing (with signapk.jar) will possibly reorder the ZIP
467 entries, which may in turn invalidate earlier computed ZIP entry offset/size
468 values.
469
470 This class provides functions to be called for each pass. The general flow is
471 as follows.
472
473 property_files = PropertyFiles()
474 # The first pass, which writes placeholders before doing initial signing.
475 property_files.Compute()
476 SignOutput()
477
478 # The second pass, by replacing the placeholders with actual data.
479 property_files.Finalize()
480 SignOutput()
481
482 And the caller can additionally verify the final result.
483
484 property_files.Verify()
485 """
486
487 def __init__(self):
488 self.name = None
489 self.required = ()
490 self.optional = ()
491
492 def Compute(self, input_zip):
493 """Computes and returns a property-files string with placeholders.
494
495 We reserve extra space for the offset and size of the metadata entry itself,
496 although we don't know the final values until the package gets signed.
497
498 Args:
499 input_zip: The input ZIP file.
500
501 Returns:
502 A string with placeholders for the metadata offset/size info, e.g.
503 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
504 """
505 return self.GetPropertyFilesString(input_zip, reserve_space=True)
506
507 class InsufficientSpaceException(Exception):
508 pass
509
510 def Finalize(self, input_zip, reserved_length):
511 """Finalizes a property-files string with actual METADATA offset/size info.
512
513 The input ZIP file has been signed, with the ZIP entries in the desired
514 place (signapk.jar will possibly reorder the ZIP entries). Now we compute
515 the ZIP entry offsets and construct the property-files string with actual
516 data. Note that during this process, we must pad the property-files string
517 to the reserved length, so that the METADATA entry size remains the same.
518 Otherwise the entries' offsets and sizes may change again.
519
520 Args:
521 input_zip: The input ZIP file.
522 reserved_length: The reserved length of the property-files string during
523 the call to Compute(). The final string must be no more than this
524 size.
525
526 Returns:
527 A property-files string including the metadata offset/size info, e.g.
528 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379 ".
529
530 Raises:
531 InsufficientSpaceException: If the reserved length is insufficient to hold
532 the final string.
533 """
534 result = self.GetPropertyFilesString(input_zip, reserve_space=False)
535 if len(result) > reserved_length:
536 raise self.InsufficientSpaceException(
537 'Insufficient reserved space: reserved={}, actual={}'.format(
538 reserved_length, len(result)))
539
540 result += ' ' * (reserved_length - len(result))
541 return result
542
543 def Verify(self, input_zip, expected):
544 """Verifies the input ZIP file contains the expected property-files string.
545
546 Args:
547 input_zip: The input ZIP file.
548 expected: The property-files string that's computed from Finalize().
549
550 Raises:
551 AssertionError: On finding a mismatch.
552 """
553 actual = self.GetPropertyFilesString(input_zip)
554 assert actual == expected, \
555 "Mismatching streaming metadata: {} vs {}.".format(actual, expected)
556
557 def GetPropertyFilesString(self, zip_file, reserve_space=False):
558 """
559 Constructs the property-files string per request.
560
561 Args:
562 zip_file: The input ZIP file.
563 reserved_length: The reserved length of the property-files string.
564
565 Returns:
566 A property-files string including the metadata offset/size info, e.g.
567 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
568 """
569
570 def ComputeEntryOffsetSize(name):
571 """Computes the zip entry offset and size."""
572 info = zip_file.getinfo(name)
Kelvin Zhang25ab9982021-06-22 09:51:34 -0400573 (offset, size) = GetZipEntryOffset(zip_file, info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400574 return '%s:%d:%d' % (os.path.basename(name), offset, size)
575
576 tokens = []
577 tokens.extend(self._GetPrecomputed(zip_file))
578 for entry in self.required:
579 tokens.append(ComputeEntryOffsetSize(entry))
580 for entry in self.optional:
581 if entry in zip_file.namelist():
582 tokens.append(ComputeEntryOffsetSize(entry))
583
584 # 'META-INF/com/android/metadata' is required. We don't know its actual
585 # offset and length (as well as the values for other entries). So we reserve
586 # 15-byte as a placeholder ('offset:length'), which is sufficient to cover
587 # the space for metadata entry. Because 'offset' allows a max of 10-digit
588 # (i.e. ~9 GiB), with a max of 4-digit for the length. Note that all the
589 # reserved space serves the metadata entry only.
590 if reserve_space:
591 tokens.append('metadata:' + ' ' * 15)
Tianjiea2076132020-08-19 17:25:32 -0700592 tokens.append('metadata.pb:' + ' ' * 15)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400593 else:
594 tokens.append(ComputeEntryOffsetSize(METADATA_NAME))
Luca Stefanib6075c52021-11-03 17:10:54 +0100595 if METADATA_PROTO_NAME in zip_file.namelist():
Kelvin Zhang2e1ff6e2022-10-10 10:58:57 -0700596 tokens.append(ComputeEntryOffsetSize(METADATA_PROTO_NAME))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400597
598 return ','.join(tokens)
599
600 def _GetPrecomputed(self, input_zip):
601 """Computes the additional tokens to be included into the property-files.
602
603 This applies to tokens without actual ZIP entries, such as
604 payload_metadata.bin. We want to expose the offset/size to updaters, so
605 that they can download the payload metadata directly with the info.
606
607 Args:
608 input_zip: The input zip file.
609
610 Returns:
611 A list of strings (tokens) to be added to the property-files string.
612 """
613 # pylint: disable=no-self-use
614 # pylint: disable=unused-argument
615 return []
616
617
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000618def SignOutput(temp_zip_name, output_zip_name, package_key=None, pw=None):
619 if package_key is None:
620 package_key = OPTIONS.package_key
621 if pw is None and OPTIONS.key_passwords:
622 pw = OPTIONS.key_passwords[package_key]
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400623
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000624 SignFile(temp_zip_name, output_zip_name, package_key, pw,
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400625 whole_file=True)
Tianjiea5fca032021-06-01 22:06:28 -0700626
627
628def ConstructOtaApexInfo(target_zip, source_file=None):
629 """If applicable, add the source version to the apex info."""
630
631 def _ReadApexInfo(input_zip):
Kelvin Zhang9dbe2ce2023-04-17 16:38:08 -0700632 if not DoesInputFileContain(input_zip, "META/apex_info.pb"):
Tianjiea5fca032021-06-01 22:06:28 -0700633 logger.warning("target_file doesn't contain apex_info.pb %s", input_zip)
634 return None
Kelvin Zhang9dbe2ce2023-04-17 16:38:08 -0700635 return ReadBytesFromInputFile(input_zip, "META/apex_info.pb")
Tianjiea5fca032021-06-01 22:06:28 -0700636
637 target_apex_string = _ReadApexInfo(target_zip)
638 # Return early if the target apex info doesn't exist or is empty.
639 if not target_apex_string:
640 return target_apex_string
641
642 # If the source apex info isn't available, just return the target info
643 if not source_file:
644 return target_apex_string
645
Kelvin Zhangca45d7a2023-04-21 09:46:47 -0700646 source_apex_string = _ReadApexInfo(source_file)
Tianjiea5fca032021-06-01 22:06:28 -0700647 if not source_apex_string:
648 return target_apex_string
649
650 source_apex_proto = ota_metadata_pb2.ApexMetadata()
651 source_apex_proto.ParseFromString(source_apex_string)
652 source_apex_versions = {apex.package_name: apex.version for apex in
653 source_apex_proto.apex_info}
654
655 # If the apex package is available in the source build, initialize the source
656 # apex version.
657 target_apex_proto = ota_metadata_pb2.ApexMetadata()
658 target_apex_proto.ParseFromString(target_apex_string)
659 for target_apex in target_apex_proto.apex_info:
660 name = target_apex.package_name
661 if name in source_apex_versions:
662 target_apex.source_version = source_apex_versions[name]
663
664 return target_apex_proto.SerializeToString()
Kelvin Zhang410bb382022-01-06 09:15:54 -0800665
666
Kelvin Zhangf2728d62022-01-10 11:42:36 -0800667def IsLz4diffCompatible(source_file: str, target_file: str):
668 """Check whether lz4diff versions in two builds are compatible
669
670 Args:
671 source_file: Path to source build's target_file.zip
672 target_file: Path to target build's target_file.zip
673
674 Returns:
675 bool true if and only if lz4diff versions are compatible
676 """
677 if source_file is None or target_file is None:
678 return False
679 # Right now we enable lz4diff as long as source build has liblz4.so.
680 # In the future we might introduce version system to lz4diff as well.
681 if zipfile.is_zipfile(source_file):
682 with zipfile.ZipFile(source_file, "r") as zfp:
683 return "META/liblz4.so" in zfp.namelist()
684 else:
685 assert os.path.isdir(source_file)
686 return os.path.exists(os.path.join(source_file, "META", "liblz4.so"))
687
688
Kelvin Zhang410bb382022-01-06 09:15:54 -0800689def IsZucchiniCompatible(source_file: str, target_file: str):
690 """Check whether zucchini versions in two builds are compatible
691
692 Args:
693 source_file: Path to source build's target_file.zip
694 target_file: Path to target build's target_file.zip
695
696 Returns:
697 bool true if and only if zucchini versions are compatible
698 """
699 if source_file is None or target_file is None:
700 return False
701 assert os.path.exists(source_file)
702 assert os.path.exists(target_file)
703
704 assert zipfile.is_zipfile(source_file) or os.path.isdir(source_file)
705 assert zipfile.is_zipfile(target_file) or os.path.isdir(target_file)
706 _ZUCCHINI_CONFIG_ENTRY_NAME = "META/zucchini_config.txt"
707
708 def ReadEntry(path, entry):
709 # Read an entry inside a .zip file or extracted dir of .zip file
710 if zipfile.is_zipfile(path):
711 with zipfile.ZipFile(path, "r", allowZip64=True) as zfp:
712 if entry in zfp.namelist():
713 return zfp.read(entry).decode()
714 else:
Zhou Xuezand0d49f52022-09-14 16:26:55 +0800715 entry_path = os.path.join(path, entry)
Kelvin Zhang410bb382022-01-06 09:15:54 -0800716 if os.path.exists(entry_path):
717 with open(entry_path, "r") as fp:
718 return fp.read()
HÃ¥kan Kvist3db1ef62022-05-03 10:19:41 +0200719 return False
720 sourceEntry = ReadEntry(source_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
721 targetEntry = ReadEntry(target_file, _ZUCCHINI_CONFIG_ENTRY_NAME)
722 return sourceEntry and targetEntry and sourceEntry == targetEntry
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000723
724
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700725def ExtractTargetFiles(path: str):
726 if os.path.isdir(path):
727 logger.info("target files %s is already extracted", path)
728 return path
729 extracted_dir = common.MakeTempDir("target_files")
Kelvin Zhang9dbe2ce2023-04-17 16:38:08 -0700730 common.UnzipToDir(path, extracted_dir, UNZIP_PATTERN + [""])
Kelvin Zhang22680912023-05-19 13:12:59 -0700731 for subdir in TARGET_FILES_IMAGES_SUBDIR:
732 image_dir = os.path.join(extracted_dir, subdir)
733 if not os.path.exists(image_dir):
734 continue
735 for filename in os.listdir(image_dir):
736 if not filename.endswith(".img"):
737 continue
738 common.UnsparseImage(os.path.join(image_dir, filename))
739
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700740 return extracted_dir
741
742
743def LocatePartitionPath(target_files_dir: str, partition: str, allow_empty):
744 path = os.path.join(target_files_dir, "RADIO", partition + ".img")
745 if os.path.exists(path):
746 return path
747 path = os.path.join(target_files_dir, "IMAGES", partition + ".img")
748 if os.path.exists(path):
749 return path
750 if allow_empty:
751 return ""
752 raise common.ExternalError(
753 "Partition {} not found in target files {}".format(partition, target_files_dir))
754
755
756def GetPartitionImages(target_files_dir: str, ab_partitions, allow_empty=True):
757 assert os.path.isdir(target_files_dir)
758 return ":".join([LocatePartitionPath(target_files_dir, partition, allow_empty) for partition in ab_partitions])
759
760
761def LocatePartitionMap(target_files_dir: str, partition: str):
762 path = os.path.join(target_files_dir, "RADIO", partition + ".map")
763 if os.path.exists(path):
764 return path
765 return ""
766
767
768def GetPartitionMaps(target_files_dir: str, ab_partitions):
769 assert os.path.isdir(target_files_dir)
770 return ":".join([LocatePartitionMap(target_files_dir, partition) for partition in ab_partitions])
771
772
Kelvin Zhangfa928692022-08-16 17:01:53 +0000773class PayloadGenerator(object):
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000774 """Manages the creation and the signing of an A/B OTA Payload."""
775
776 PAYLOAD_BIN = 'payload.bin'
777 PAYLOAD_PROPERTIES_TXT = 'payload_properties.txt'
778 SECONDARY_PAYLOAD_BIN = 'secondary/payload.bin'
779 SECONDARY_PAYLOAD_PROPERTIES_TXT = 'secondary/payload_properties.txt'
780
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700781 def __init__(self, secondary=False, wipe_user_data=False, minor_version=None, is_partial_update=False):
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000782 """Initializes a Payload instance.
783
784 Args:
785 secondary: Whether it's generating a secondary payload (default: False).
786 """
787 self.payload_file = None
788 self.payload_properties = None
789 self.secondary = secondary
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000790 self.wipe_user_data = wipe_user_data
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700791 self.minor_version = minor_version
792 self.is_partial_update = is_partial_update
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000793
794 def _Run(self, cmd): # pylint: disable=no-self-use
795 # Don't pipe (buffer) the output if verbose is set. Let
796 # brillo_update_payload write to stdout/stderr directly, so its progress can
797 # be monitored.
798 if OPTIONS.verbose:
799 common.RunAndCheckOutput(cmd, stdout=None, stderr=None)
800 else:
801 common.RunAndCheckOutput(cmd)
802
803 def Generate(self, target_file, source_file=None, additional_args=None):
804 """Generates a payload from the given target-files zip(s).
805
806 Args:
807 target_file: The filename of the target build target-files zip.
808 source_file: The filename of the source build target-files zip; or None if
809 generating a full OTA.
810 additional_args: A list of additional args that should be passed to
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700811 delta_generator binary; or None.
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000812 """
813 if additional_args is None:
814 additional_args = []
815
816 payload_file = common.MakeTempFile(prefix="payload-", suffix=".bin")
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700817 target_dir = ExtractTargetFiles(target_file)
818 cmd = ["delta_generator",
819 "--out_file", payload_file]
820 with open(os.path.join(target_dir, "META", "ab_partitions.txt")) as fp:
821 ab_partitions = fp.read().strip().split("\n")
822 cmd.extend(["--partition_names", ":".join(ab_partitions)])
823 cmd.extend(
824 ["--new_partitions", GetPartitionImages(target_dir, ab_partitions, False)])
825 cmd.extend(
826 ["--new_mapfiles", GetPartitionMaps(target_dir, ab_partitions)])
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000827 if source_file is not None:
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700828 source_dir = ExtractTargetFiles(source_file)
829 cmd.extend(
830 ["--old_partitions", GetPartitionImages(source_dir, ab_partitions, True)])
831 cmd.extend(
832 ["--old_mapfiles", GetPartitionMaps(source_dir, ab_partitions)])
833
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000834 if OPTIONS.disable_fec_computation:
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700835 cmd.extend(["--disable_fec_computation=true"])
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000836 if OPTIONS.disable_verity_computation:
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700837 cmd.extend(["--disable_verity_computation=true"])
838 postinstall_config = os.path.join(
839 target_dir, "META", "postinstall_config.txt")
840
841 if os.path.exists(postinstall_config):
842 cmd.extend(["--new_postinstall_config_file", postinstall_config])
843 dynamic_partition_info = os.path.join(
844 target_dir, "META", "dynamic_partitions_info.txt")
845
846 if os.path.exists(dynamic_partition_info):
847 cmd.extend(["--dynamic_partition_info_file", dynamic_partition_info])
848
849 major_version, minor_version = ParseUpdateEngineConfig(
850 os.path.join(target_dir, "META", "update_engine_config.txt"))
Kelvin Zhang629bc8d2023-04-11 21:08:27 -0700851 if source_file:
852 major_version, minor_version = ParseUpdateEngineConfig(
853 os.path.join(source_dir, "META", "update_engine_config.txt"))
Kelvin Zhangfcd731e2023-04-04 10:28:11 -0700854 if self.minor_version:
855 minor_version = self.minor_version
856 cmd.extend(["--major_version", str(major_version)])
857 if source_file is not None or self.is_partial_update:
858 cmd.extend(["--minor_version", str(minor_version)])
859 if self.is_partial_update:
860 cmd.extend(["--is_partial_update=true"])
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000861 cmd.extend(additional_args)
862 self._Run(cmd)
863
864 self.payload_file = payload_file
865 self.payload_properties = None
866
867 def Sign(self, payload_signer):
868 """Generates and signs the hashes of the payload and metadata.
869
870 Args:
871 payload_signer: A PayloadSigner() instance that serves the signing work.
872
873 Raises:
874 AssertionError: On any failure when calling brillo_update_payload script.
875 """
876 assert isinstance(payload_signer, PayloadSigner)
877
878 # 1. Generate hashes of the payload and metadata files.
879 payload_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin")
880 metadata_sig_file = common.MakeTempFile(prefix="sig-", suffix=".bin")
881 cmd = ["brillo_update_payload", "hash",
882 "--unsigned_payload", self.payload_file,
883 "--signature_size", str(payload_signer.maximum_signature_size),
884 "--metadata_hash_file", metadata_sig_file,
885 "--payload_hash_file", payload_sig_file]
886 self._Run(cmd)
887
888 # 2. Sign the hashes.
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000889 signed_payload_sig_file = payload_signer.SignHashFile(payload_sig_file)
890 signed_metadata_sig_file = payload_signer.SignHashFile(metadata_sig_file)
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000891
892 # 3. Insert the signatures back into the payload file.
893 signed_payload_file = common.MakeTempFile(prefix="signed-payload-",
894 suffix=".bin")
895 cmd = ["brillo_update_payload", "sign",
896 "--unsigned_payload", self.payload_file,
897 "--payload", signed_payload_file,
898 "--signature_size", str(payload_signer.maximum_signature_size),
899 "--metadata_signature_file", signed_metadata_sig_file,
900 "--payload_signature_file", signed_payload_sig_file]
901 self._Run(cmd)
902
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000903 self.payload_file = signed_payload_file
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000904
905 def WriteToZip(self, output_zip):
906 """Writes the payload to the given zip.
907
908 Args:
909 output_zip: The output ZipFile instance.
910 """
911 assert self.payload_file is not None
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +0000912 # 4. Dump the signed payload properties.
913 properties_file = common.MakeTempFile(prefix="payload-properties-",
914 suffix=".txt")
915 cmd = ["brillo_update_payload", "properties",
916 "--payload", self.payload_file,
917 "--properties_file", properties_file]
918 self._Run(cmd)
919
920 if self.secondary:
921 with open(properties_file, "a") as f:
922 f.write("SWITCH_SLOT_ON_REBOOT=0\n")
923
924 if self.wipe_user_data:
925 with open(properties_file, "a") as f:
926 f.write("POWERWASH=1\n")
927
928 self.payload_properties = properties_file
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000929
930 if self.secondary:
Kelvin Zhangfa928692022-08-16 17:01:53 +0000931 payload_arcname = PayloadGenerator.SECONDARY_PAYLOAD_BIN
932 payload_properties_arcname = PayloadGenerator.SECONDARY_PAYLOAD_PROPERTIES_TXT
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000933 else:
Kelvin Zhangfa928692022-08-16 17:01:53 +0000934 payload_arcname = PayloadGenerator.PAYLOAD_BIN
935 payload_properties_arcname = PayloadGenerator.PAYLOAD_PROPERTIES_TXT
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +0000936
937 # Add the signed payload file and properties into the zip. In order to
938 # support streaming, we pack them as ZIP_STORED. So these entries can be
939 # read directly with the offset and length pairs.
940 common.ZipWrite(output_zip, self.payload_file, arcname=payload_arcname,
941 compress_type=zipfile.ZIP_STORED)
942 common.ZipWrite(output_zip, self.payload_properties,
943 arcname=payload_properties_arcname,
944 compress_type=zipfile.ZIP_STORED)
945
946
947class StreamingPropertyFiles(PropertyFiles):
948 """A subclass for computing the property-files for streaming A/B OTAs."""
949
950 def __init__(self):
951 super(StreamingPropertyFiles, self).__init__()
952 self.name = 'ota-streaming-property-files'
953 self.required = (
954 # payload.bin and payload_properties.txt must exist.
955 'payload.bin',
956 'payload_properties.txt',
957 )
958 self.optional = (
959 # apex_info.pb isn't directly used in the update flow
960 'apex_info.pb',
961 # care_map is available only if dm-verity is enabled.
962 'care_map.pb',
963 'care_map.txt',
964 # compatibility.zip is available only if target supports Treble.
965 'compatibility.zip',
966 )
967
968
969class AbOtaPropertyFiles(StreamingPropertyFiles):
970 """The property-files for A/B OTA that includes payload_metadata.bin info.
971
972 Since P, we expose one more token (aka property-file), in addition to the ones
973 for streaming A/B OTA, for a virtual entry of 'payload_metadata.bin'.
974 'payload_metadata.bin' is the header part of a payload ('payload.bin'), which
975 doesn't exist as a separate ZIP entry, but can be used to verify if the
976 payload can be applied on the given device.
977
978 For backward compatibility, we keep both of the 'ota-streaming-property-files'
979 and the newly added 'ota-property-files' in P. The new token will only be
980 available in 'ota-property-files'.
981 """
982
983 def __init__(self):
984 super(AbOtaPropertyFiles, self).__init__()
985 self.name = 'ota-property-files'
986
987 def _GetPrecomputed(self, input_zip):
988 offset, size = self._GetPayloadMetadataOffsetAndSize(input_zip)
989 return ['payload_metadata.bin:{}:{}'.format(offset, size)]
990
991 @staticmethod
992 def _GetPayloadMetadataOffsetAndSize(input_zip):
993 """Computes the offset and size of the payload metadata for a given package.
994
995 (From system/update_engine/update_metadata.proto)
996 A delta update file contains all the deltas needed to update a system from
997 one specific version to another specific version. The update format is
998 represented by this struct pseudocode:
999
1000 struct delta_update_file {
1001 char magic[4] = "CrAU";
1002 uint64 file_format_version;
1003 uint64 manifest_size; // Size of protobuf DeltaArchiveManifest
1004
1005 // Only present if format_version > 1:
1006 uint32 metadata_signature_size;
1007
1008 // The Bzip2 compressed DeltaArchiveManifest
1009 char manifest[metadata_signature_size];
1010
1011 // The signature of the metadata (from the beginning of the payload up to
1012 // this location, not including the signature itself). This is a
1013 // serialized Signatures message.
1014 char medatada_signature_message[metadata_signature_size];
1015
1016 // Data blobs for files, no specific format. The specific offset
1017 // and length of each data blob is recorded in the DeltaArchiveManifest.
1018 struct {
1019 char data[];
1020 } blobs[];
1021
1022 // These two are not signed:
1023 uint64 payload_signatures_message_size;
1024 char payload_signatures_message[];
1025 };
1026
1027 'payload-metadata.bin' contains all the bytes from the beginning of the
1028 payload, till the end of 'medatada_signature_message'.
1029 """
1030 payload_info = input_zip.getinfo('payload.bin')
1031 (payload_offset, payload_size) = GetZipEntryOffset(input_zip, payload_info)
1032
1033 # Read the underlying raw zipfile at specified offset
1034 payload_fp = input_zip.fp
1035 payload_fp.seek(payload_offset)
1036 header_bin = payload_fp.read(24)
1037
1038 # network byte order (big-endian)
1039 header = struct.unpack("!IQQL", header_bin)
1040
1041 # 'CrAU'
1042 magic = header[0]
1043 assert magic == 0x43724155, "Invalid magic: {:x}, computed offset {}" \
1044 .format(magic, payload_offset)
1045
1046 manifest_size = header[2]
1047 metadata_signature_size = header[3]
1048 metadata_total = 24 + manifest_size + metadata_signature_size
Kelvin Zhangbf01f8b2022-08-30 18:25:43 +00001049 assert metadata_total <= payload_size
Kelvin Zhang62a7f6e2022-08-30 17:41:29 +00001050
1051 return (payload_offset, metadata_total)
Kelvin Zhang9dbe2ce2023-04-17 16:38:08 -07001052
1053
1054def Fnmatch(filename, pattersn):
1055 return any([fnmatch.fnmatch(filename, pat) for pat in pattersn])
1056
1057
1058def CopyTargetFilesDir(input_dir):
1059 output_dir = common.MakeTempDir("target_files")
Kelvin Zhang22680912023-05-19 13:12:59 -07001060
1061 def SymlinkIfNotSparse(src, dst):
1062 if common.IsSparseImage(src):
1063 return common.UnsparseImage(src, dst)
1064 else:
1065 return os.link(src, dst)
1066
1067 for subdir in TARGET_FILES_IMAGES_SUBDIR:
Kelvin Zhang6b10e152023-05-02 15:48:16 -07001068 if not os.path.exists(os.path.join(input_dir, subdir)):
1069 continue
1070 shutil.copytree(os.path.join(input_dir, subdir), os.path.join(
Kelvin Zhang22680912023-05-19 13:12:59 -07001071 output_dir, subdir), dirs_exist_ok=True, copy_function=SymlinkIfNotSparse)
Kelvin Zhang9dbe2ce2023-04-17 16:38:08 -07001072 shutil.copytree(os.path.join(input_dir, "META"), os.path.join(
1073 output_dir, "META"), dirs_exist_ok=True)
Kelvin Zhang6b10e152023-05-02 15:48:16 -07001074
Kelvin Zhang9dbe2ce2023-04-17 16:38:08 -07001075 for (dirpath, _, filenames) in os.walk(input_dir):
1076 for filename in filenames:
1077 path = os.path.join(dirpath, filename)
1078 relative_path = path.removeprefix(input_dir).removeprefix("/")
1079 if not Fnmatch(relative_path, UNZIP_PATTERN):
1080 continue
1081 if filename.endswith(".prop") or filename == "prop.default" or "/etc/vintf/" in relative_path:
1082 target_path = os.path.join(
1083 output_dir, relative_path)
1084 os.makedirs(os.path.dirname(target_path), exist_ok=True)
1085 shutil.copy(path, target_path)
1086 return output_dir