blob: 97a628029a9fdbbeb35ab720d73990144346ba8b [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
17import os
18import zipfile
19
Tianjiea2076132020-08-19 17:25:32 -070020import ota_metadata_pb2
Kelvin Zhangcff4d762020-07-29 16:37:51 -040021from common import (ZipDelete, ZipClose, OPTIONS, MakeTempFile,
22 ZipWriteStr, BuildInfo, LoadDictionaryFromFile,
23 SignFile, PARTITIONS_WITH_CARE_MAP, PartitionBuildProps)
24
Kelvin Zhang2e417382020-08-20 11:33:11 -040025
26OPTIONS.no_signing = False
27OPTIONS.force_non_ab = False
28OPTIONS.wipe_user_data = False
29OPTIONS.downgrade = False
30OPTIONS.key_passwords = {}
31OPTIONS.package_key = None
32OPTIONS.incremental_source = None
33OPTIONS.retrofit_dynamic_partitions = False
34OPTIONS.output_metadata_path = None
35OPTIONS.boot_variable_file = None
36
Kelvin Zhangcff4d762020-07-29 16:37:51 -040037METADATA_NAME = 'META-INF/com/android/metadata'
Tianjiea2076132020-08-19 17:25:32 -070038METADATA_PROTO_NAME = 'META-INF/com/android/metadata.pb'
Kelvin Zhangcff4d762020-07-29 16:37:51 -040039UNZIP_PATTERN = ['IMAGES/*', 'META/*', 'OTA/*', 'RADIO/*']
40
41
42def FinalizeMetadata(metadata, input_file, output_file, needed_property_files):
43 """Finalizes the metadata and signs an A/B OTA package.
44
45 In order to stream an A/B OTA package, we need 'ota-streaming-property-files'
46 that contains the offsets and sizes for the ZIP entries. An example
47 property-files string is as follows.
48
49 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379"
50
51 OTA server can pass down this string, in addition to the package URL, to the
52 system update client. System update client can then fetch individual ZIP
53 entries (ZIP_STORED) directly at the given offset of the URL.
54
55 Args:
56 metadata: The metadata dict for the package.
57 input_file: The input ZIP filename that doesn't contain the package METADATA
58 entry yet.
59 output_file: The final output ZIP filename.
60 needed_property_files: The list of PropertyFiles' to be generated.
61 """
62
63 def ComputeAllPropertyFiles(input_file, needed_property_files):
64 # Write the current metadata entry with placeholders.
65 with zipfile.ZipFile(input_file) as input_zip:
66 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -070067 metadata.property_files[property_files.name] = property_files.Compute(
68 input_zip)
Kelvin Zhangcff4d762020-07-29 16:37:51 -040069 namelist = input_zip.namelist()
70
Tianjiea2076132020-08-19 17:25:32 -070071 if METADATA_NAME in namelist or METADATA_PROTO_NAME in namelist:
72 ZipDelete(input_file, [METADATA_NAME, METADATA_PROTO_NAME])
Kelvin Zhangcff4d762020-07-29 16:37:51 -040073 output_zip = zipfile.ZipFile(input_file, 'a')
74 WriteMetadata(metadata, output_zip)
75 ZipClose(output_zip)
76
77 if OPTIONS.no_signing:
78 return input_file
79
80 prelim_signing = MakeTempFile(suffix='.zip')
81 SignOutput(input_file, prelim_signing)
82 return prelim_signing
83
84 def FinalizeAllPropertyFiles(prelim_signing, needed_property_files):
85 with zipfile.ZipFile(prelim_signing) as prelim_signing_zip:
86 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -070087 metadata.property_files[property_files.name] = property_files.Finalize(
88 prelim_signing_zip,
89 len(metadata.property_files[property_files.name]))
Kelvin Zhangcff4d762020-07-29 16:37:51 -040090
91 # SignOutput(), which in turn calls signapk.jar, will possibly reorder the ZIP
92 # entries, as well as padding the entry headers. We do a preliminary signing
93 # (with an incomplete metadata entry) to allow that to happen. Then compute
94 # the ZIP entry offsets, write back the final metadata and do the final
95 # signing.
96 prelim_signing = ComputeAllPropertyFiles(input_file, needed_property_files)
97 try:
98 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
99 except PropertyFiles.InsufficientSpaceException:
100 # Even with the preliminary signing, the entry orders may change
101 # dramatically, which leads to insufficiently reserved space during the
102 # first call to ComputeAllPropertyFiles(). In that case, we redo all the
103 # preliminary signing works, based on the already ordered ZIP entries, to
104 # address the issue.
105 prelim_signing = ComputeAllPropertyFiles(
106 prelim_signing, needed_property_files)
107 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
108
109 # Replace the METADATA entry.
Tianjiea2076132020-08-19 17:25:32 -0700110 ZipDelete(prelim_signing, [METADATA_NAME, METADATA_PROTO_NAME])
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400111 output_zip = zipfile.ZipFile(prelim_signing, 'a')
112 WriteMetadata(metadata, output_zip)
113 ZipClose(output_zip)
114
115 # Re-sign the package after updating the metadata entry.
116 if OPTIONS.no_signing:
117 output_file = prelim_signing
118 else:
119 SignOutput(prelim_signing, output_file)
120
121 # Reopen the final signed zip to double check the streaming metadata.
122 with zipfile.ZipFile(output_file) as output_zip:
123 for property_files in needed_property_files:
Tianjiea2076132020-08-19 17:25:32 -0700124 property_files.Verify(
125 output_zip, metadata.property_files[property_files.name].strip())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400126
127 # If requested, dump the metadata to a separate file.
128 output_metadata_path = OPTIONS.output_metadata_path
129 if output_metadata_path:
130 WriteMetadata(metadata, output_metadata_path)
131
132
Tianjiea2076132020-08-19 17:25:32 -0700133def WriteMetadata(metadata_proto, output):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400134 """Writes the metadata to the zip archive or a file.
135
136 Args:
Tianjiea2076132020-08-19 17:25:32 -0700137 metadata_proto: The metadata protobuf for the package.
138 output: A ZipFile object or a string of the output file path. If a string
139 path is given, the metadata in the protobuf format will be written to
140 {output}.pb, e.g. ota_metadata.pb
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400141 """
142
Tianjiea2076132020-08-19 17:25:32 -0700143 metadata_dict = BuildLegacyOtaMetadata(metadata_proto)
144 legacy_metadata = "".join(["%s=%s\n" % kv for kv in
145 sorted(metadata_dict.items())])
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400146 if isinstance(output, zipfile.ZipFile):
Tianjiea2076132020-08-19 17:25:32 -0700147 ZipWriteStr(output, METADATA_PROTO_NAME, metadata_proto.SerializeToString(),
148 compress_type=zipfile.ZIP_STORED)
149 ZipWriteStr(output, METADATA_NAME, legacy_metadata,
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400150 compress_type=zipfile.ZIP_STORED)
151 return
152
Tianjiea2076132020-08-19 17:25:32 -0700153 with open('{}.pb'.format(output), 'w') as f:
154 f.write(metadata_proto.SerializeToString())
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400155 with open(output, 'w') as f:
Tianjiea2076132020-08-19 17:25:32 -0700156 f.write(legacy_metadata)
157
158
159def UpdateDeviceState(device_state, build_info, boot_variable_values,
160 is_post_build):
161 """Update the fields of the DeviceState proto with build info."""
162
163 build_devices, build_fingerprints = \
164 CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values)
165 device_state.device.extend(sorted(build_devices))
166 device_state.build.extend(sorted(build_fingerprints))
167 device_state.build_incremental = build_info.GetBuildProp(
168 'ro.build.version.incremental')
169
170 # TODO(xunchang) update the partition state
171
172 if is_post_build:
173 device_state.sdk_level = build_info.GetBuildProp(
174 'ro.build.version.sdk')
175 device_state.security_patch_level = build_info.GetBuildProp(
176 'ro.build.version.security_patch')
177 # Use the actual post-timestamp, even for a downgrade case.
178 device_state.timestamp = int(build_info.GetBuildProp('ro.build.date.utc'))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400179
180
181def GetPackageMetadata(target_info, source_info=None):
Tianjiea2076132020-08-19 17:25:32 -0700182 """Generates and returns the metadata proto.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400183
Tianjiea2076132020-08-19 17:25:32 -0700184 It generates a ota_metadata protobuf that contains the info to be written
185 into an OTA package (META-INF/com/android/metadata.pb). It also handles the
186 detection of downgrade / data wipe based on the global options.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400187
188 Args:
189 target_info: The BuildInfo instance that holds the target build info.
190 source_info: The BuildInfo instance that holds the source build info, or
191 None if generating full OTA.
192
193 Returns:
Tianjiea2076132020-08-19 17:25:32 -0700194 A protobuf to be written into package metadata entry.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400195 """
196 assert isinstance(target_info, BuildInfo)
197 assert source_info is None or isinstance(source_info, BuildInfo)
198
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400199 boot_variable_values = {}
200 if OPTIONS.boot_variable_file:
201 d = LoadDictionaryFromFile(OPTIONS.boot_variable_file)
202 for key, values in d.items():
203 boot_variable_values[key] = [val.strip() for val in values.split(',')]
204
Tianjiea2076132020-08-19 17:25:32 -0700205 metadata_proto = ota_metadata_pb2.OtaMetadata()
206 # TODO(xunchang) some fields, e.g. post-device isn't necessary. We can
207 # consider skipping them if they aren't used by clients.
208 UpdateDeviceState(metadata_proto.postcondition, target_info,
209 boot_variable_values, True)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400210
211 if target_info.is_ab and not OPTIONS.force_non_ab:
Tianjiea2076132020-08-19 17:25:32 -0700212 metadata_proto.type = ota_metadata_pb2.OtaMetadata.AB
213 metadata_proto.required_cache = 0
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400214 else:
Tianjiea2076132020-08-19 17:25:32 -0700215 metadata_proto.type = ota_metadata_pb2.OtaMetadata.BLOCK
216 # cache requirement will be updated by the non-A/B codes.
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400217
218 if OPTIONS.wipe_user_data:
Tianjiea2076132020-08-19 17:25:32 -0700219 metadata_proto.wipe = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400220
221 if OPTIONS.retrofit_dynamic_partitions:
Tianjiea2076132020-08-19 17:25:32 -0700222 metadata_proto.retrofit_dynamic_partitions = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400223
224 is_incremental = source_info is not None
225 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700226 UpdateDeviceState(metadata_proto.precondition, source_info,
227 boot_variable_values, False)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400228 else:
Tianjiea2076132020-08-19 17:25:32 -0700229 metadata_proto.precondition.device.extend(
230 metadata_proto.postcondition.device)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400231
232 # Detect downgrades and set up downgrade flags accordingly.
233 if is_incremental:
Tianjiea2076132020-08-19 17:25:32 -0700234 HandleDowngradeMetadata(metadata_proto, target_info, source_info)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400235
Tianjiea2076132020-08-19 17:25:32 -0700236 return metadata_proto
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400237
238
Tianjiea2076132020-08-19 17:25:32 -0700239def BuildLegacyOtaMetadata(metadata_proto):
240 """Converts the metadata proto to a legacy metadata dict.
241
242 This metadata dict is used to build the legacy metadata text file for
243 backward compatibility. We won't add new keys to the legacy metadata format.
244 If new information is needed, we should add it as a new field in OtaMetadata
245 proto definition.
246 """
247
248 separator = '|'
249
250 metadata_dict = {}
251 if metadata_proto.type == ota_metadata_pb2.OtaMetadata.AB:
252 metadata_dict['ota-type'] = 'AB'
253 elif metadata_proto.type == ota_metadata_pb2.OtaMetadata.BLOCK:
254 metadata_dict['ota-type'] = 'BLOCK'
255 if metadata_proto.wipe:
256 metadata_dict['ota-wipe'] = 'yes'
257 if metadata_proto.retrofit_dynamic_partitions:
258 metadata_dict['ota-retrofit-dynamic-partitions'] = 'yes'
259 if metadata_proto.downgrade:
260 metadata_dict['ota-downgrade'] = 'yes'
261
262 metadata_dict['ota-required-cache'] = str(metadata_proto.required_cache)
263
264 post_build = metadata_proto.postcondition
265 metadata_dict['post-build'] = separator.join(post_build.build)
266 metadata_dict['post-build-incremental'] = post_build.build_incremental
267 metadata_dict['post-sdk-level'] = post_build.sdk_level
268 metadata_dict['post-security-patch-level'] = post_build.security_patch_level
269 metadata_dict['post-timestamp'] = str(post_build.timestamp)
270
271 pre_build = metadata_proto.precondition
272 metadata_dict['pre-device'] = separator.join(pre_build.device)
273 # incremental updates
274 if len(pre_build.build) != 0:
275 metadata_dict['pre-build'] = separator.join(pre_build.build)
276 metadata_dict['pre-build-incremental'] = pre_build.build_incremental
277
278 metadata_dict.update(metadata_proto.property_files)
279
280 return metadata_dict
281
282
283def HandleDowngradeMetadata(metadata_proto, target_info, source_info):
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400284 # Only incremental OTAs are allowed to reach here.
285 assert OPTIONS.incremental_source is not None
286
287 post_timestamp = target_info.GetBuildProp("ro.build.date.utc")
288 pre_timestamp = source_info.GetBuildProp("ro.build.date.utc")
289 is_downgrade = int(post_timestamp) < int(pre_timestamp)
290
291 if OPTIONS.downgrade:
292 if not is_downgrade:
293 raise RuntimeError(
294 "--downgrade or --override_timestamp specified but no downgrade "
295 "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp))
Tianjiea2076132020-08-19 17:25:32 -0700296 metadata_proto.downgrade = True
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400297 else:
298 if is_downgrade:
299 raise RuntimeError(
300 "Downgrade detected based on timestamp check: pre: %s, post: %s. "
301 "Need to specify --override_timestamp OR --downgrade to allow "
302 "building the incremental." % (pre_timestamp, post_timestamp))
303
304
305def CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values):
306 """Returns a tuple of sets for runtime devices and fingerprints"""
307
308 device_names = {build_info.device}
309 fingerprints = {build_info.fingerprint}
310
311 if not boot_variable_values:
312 return device_names, fingerprints
313
314 # Calculate all possible combinations of the values for the boot variables.
315 keys = boot_variable_values.keys()
316 value_list = boot_variable_values.values()
317 combinations = [dict(zip(keys, values))
318 for values in itertools.product(*value_list)]
319 for placeholder_values in combinations:
320 # Reload the info_dict as some build properties may change their values
321 # based on the value of ro.boot* properties.
322 info_dict = copy.deepcopy(build_info.info_dict)
323 for partition in PARTITIONS_WITH_CARE_MAP:
324 partition_prop_key = "{}.build.prop".format(partition)
325 input_file = info_dict[partition_prop_key].input_file
326 if isinstance(input_file, zipfile.ZipFile):
327 with zipfile.ZipFile(input_file.filename) as input_zip:
328 info_dict[partition_prop_key] = \
329 PartitionBuildProps.FromInputFile(input_zip, partition,
330 placeholder_values)
331 else:
332 info_dict[partition_prop_key] = \
333 PartitionBuildProps.FromInputFile(input_file, partition,
334 placeholder_values)
335 info_dict["build.prop"] = info_dict["system.build.prop"]
336
337 new_build_info = BuildInfo(info_dict, build_info.oem_dicts)
338 device_names.add(new_build_info.device)
339 fingerprints.add(new_build_info.fingerprint)
340 return device_names, fingerprints
341
342
343class PropertyFiles(object):
344 """A class that computes the property-files string for an OTA package.
345
346 A property-files string is a comma-separated string that contains the
347 offset/size info for an OTA package. The entries, which must be ZIP_STORED,
348 can be fetched directly with the package URL along with the offset/size info.
349 These strings can be used for streaming A/B OTAs, or allowing an updater to
350 download package metadata entry directly, without paying the cost of
351 downloading entire package.
352
353 Computing the final property-files string requires two passes. Because doing
354 the whole package signing (with signapk.jar) will possibly reorder the ZIP
355 entries, which may in turn invalidate earlier computed ZIP entry offset/size
356 values.
357
358 This class provides functions to be called for each pass. The general flow is
359 as follows.
360
361 property_files = PropertyFiles()
362 # The first pass, which writes placeholders before doing initial signing.
363 property_files.Compute()
364 SignOutput()
365
366 # The second pass, by replacing the placeholders with actual data.
367 property_files.Finalize()
368 SignOutput()
369
370 And the caller can additionally verify the final result.
371
372 property_files.Verify()
373 """
374
375 def __init__(self):
376 self.name = None
377 self.required = ()
378 self.optional = ()
379
380 def Compute(self, input_zip):
381 """Computes and returns a property-files string with placeholders.
382
383 We reserve extra space for the offset and size of the metadata entry itself,
384 although we don't know the final values until the package gets signed.
385
386 Args:
387 input_zip: The input ZIP file.
388
389 Returns:
390 A string with placeholders for the metadata offset/size info, e.g.
391 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
392 """
393 return self.GetPropertyFilesString(input_zip, reserve_space=True)
394
395 class InsufficientSpaceException(Exception):
396 pass
397
398 def Finalize(self, input_zip, reserved_length):
399 """Finalizes a property-files string with actual METADATA offset/size info.
400
401 The input ZIP file has been signed, with the ZIP entries in the desired
402 place (signapk.jar will possibly reorder the ZIP entries). Now we compute
403 the ZIP entry offsets and construct the property-files string with actual
404 data. Note that during this process, we must pad the property-files string
405 to the reserved length, so that the METADATA entry size remains the same.
406 Otherwise the entries' offsets and sizes may change again.
407
408 Args:
409 input_zip: The input ZIP file.
410 reserved_length: The reserved length of the property-files string during
411 the call to Compute(). The final string must be no more than this
412 size.
413
414 Returns:
415 A property-files string including the metadata offset/size info, e.g.
416 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379 ".
417
418 Raises:
419 InsufficientSpaceException: If the reserved length is insufficient to hold
420 the final string.
421 """
422 result = self.GetPropertyFilesString(input_zip, reserve_space=False)
423 if len(result) > reserved_length:
424 raise self.InsufficientSpaceException(
425 'Insufficient reserved space: reserved={}, actual={}'.format(
426 reserved_length, len(result)))
427
428 result += ' ' * (reserved_length - len(result))
429 return result
430
431 def Verify(self, input_zip, expected):
432 """Verifies the input ZIP file contains the expected property-files string.
433
434 Args:
435 input_zip: The input ZIP file.
436 expected: The property-files string that's computed from Finalize().
437
438 Raises:
439 AssertionError: On finding a mismatch.
440 """
441 actual = self.GetPropertyFilesString(input_zip)
442 assert actual == expected, \
443 "Mismatching streaming metadata: {} vs {}.".format(actual, expected)
444
445 def GetPropertyFilesString(self, zip_file, reserve_space=False):
446 """
447 Constructs the property-files string per request.
448
449 Args:
450 zip_file: The input ZIP file.
451 reserved_length: The reserved length of the property-files string.
452
453 Returns:
454 A property-files string including the metadata offset/size info, e.g.
455 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
456 """
457
458 def ComputeEntryOffsetSize(name):
459 """Computes the zip entry offset and size."""
460 info = zip_file.getinfo(name)
461 offset = info.header_offset
462 offset += zipfile.sizeFileHeader
463 offset += len(info.extra) + len(info.filename)
464 size = info.file_size
465 return '%s:%d:%d' % (os.path.basename(name), offset, size)
466
467 tokens = []
468 tokens.extend(self._GetPrecomputed(zip_file))
469 for entry in self.required:
470 tokens.append(ComputeEntryOffsetSize(entry))
471 for entry in self.optional:
472 if entry in zip_file.namelist():
473 tokens.append(ComputeEntryOffsetSize(entry))
474
475 # 'META-INF/com/android/metadata' is required. We don't know its actual
476 # offset and length (as well as the values for other entries). So we reserve
477 # 15-byte as a placeholder ('offset:length'), which is sufficient to cover
478 # the space for metadata entry. Because 'offset' allows a max of 10-digit
479 # (i.e. ~9 GiB), with a max of 4-digit for the length. Note that all the
480 # reserved space serves the metadata entry only.
481 if reserve_space:
482 tokens.append('metadata:' + ' ' * 15)
Tianjiea2076132020-08-19 17:25:32 -0700483 tokens.append('metadata.pb:' + ' ' * 15)
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400484 else:
485 tokens.append(ComputeEntryOffsetSize(METADATA_NAME))
Tianjiea2076132020-08-19 17:25:32 -0700486 tokens.append(ComputeEntryOffsetSize(METADATA_PROTO_NAME))
Kelvin Zhangcff4d762020-07-29 16:37:51 -0400487
488 return ','.join(tokens)
489
490 def _GetPrecomputed(self, input_zip):
491 """Computes the additional tokens to be included into the property-files.
492
493 This applies to tokens without actual ZIP entries, such as
494 payload_metadata.bin. We want to expose the offset/size to updaters, so
495 that they can download the payload metadata directly with the info.
496
497 Args:
498 input_zip: The input zip file.
499
500 Returns:
501 A list of strings (tokens) to be added to the property-files string.
502 """
503 # pylint: disable=no-self-use
504 # pylint: disable=unused-argument
505 return []
506
507
508def SignOutput(temp_zip_name, output_zip_name):
509 pw = OPTIONS.key_passwords[OPTIONS.package_key]
510
511 SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
512 whole_file=True)