blob: 4bb2b61ee5c2250d855ffbbdc3fb28f7aa47d2b3 [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
20from common import (ZipDelete, ZipClose, OPTIONS, MakeTempFile,
21 ZipWriteStr, BuildInfo, LoadDictionaryFromFile,
22 SignFile, PARTITIONS_WITH_CARE_MAP, PartitionBuildProps)
23
Kelvin Zhang2e417382020-08-20 11:33:11 -040024
25OPTIONS.no_signing = False
26OPTIONS.force_non_ab = False
27OPTIONS.wipe_user_data = False
28OPTIONS.downgrade = False
29OPTIONS.key_passwords = {}
30OPTIONS.package_key = None
31OPTIONS.incremental_source = None
32OPTIONS.retrofit_dynamic_partitions = False
33OPTIONS.output_metadata_path = None
34OPTIONS.boot_variable_file = None
35
Kelvin Zhangcff4d762020-07-29 16:37:51 -040036METADATA_NAME = 'META-INF/com/android/metadata'
37UNZIP_PATTERN = ['IMAGES/*', 'META/*', 'OTA/*', 'RADIO/*']
38
39
40def FinalizeMetadata(metadata, input_file, output_file, needed_property_files):
41 """Finalizes the metadata and signs an A/B OTA package.
42
43 In order to stream an A/B OTA package, we need 'ota-streaming-property-files'
44 that contains the offsets and sizes for the ZIP entries. An example
45 property-files string is as follows.
46
47 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379"
48
49 OTA server can pass down this string, in addition to the package URL, to the
50 system update client. System update client can then fetch individual ZIP
51 entries (ZIP_STORED) directly at the given offset of the URL.
52
53 Args:
54 metadata: The metadata dict for the package.
55 input_file: The input ZIP filename that doesn't contain the package METADATA
56 entry yet.
57 output_file: The final output ZIP filename.
58 needed_property_files: The list of PropertyFiles' to be generated.
59 """
60
61 def ComputeAllPropertyFiles(input_file, needed_property_files):
62 # Write the current metadata entry with placeholders.
63 with zipfile.ZipFile(input_file) as input_zip:
64 for property_files in needed_property_files:
65 metadata[property_files.name] = property_files.Compute(input_zip)
66 namelist = input_zip.namelist()
67
68 if METADATA_NAME in namelist:
69 ZipDelete(input_file, METADATA_NAME)
70 output_zip = zipfile.ZipFile(input_file, 'a')
71 WriteMetadata(metadata, output_zip)
72 ZipClose(output_zip)
73
74 if OPTIONS.no_signing:
75 return input_file
76
77 prelim_signing = MakeTempFile(suffix='.zip')
78 SignOutput(input_file, prelim_signing)
79 return prelim_signing
80
81 def FinalizeAllPropertyFiles(prelim_signing, needed_property_files):
82 with zipfile.ZipFile(prelim_signing) as prelim_signing_zip:
83 for property_files in needed_property_files:
84 metadata[property_files.name] = property_files.Finalize(
85 prelim_signing_zip, len(metadata[property_files.name]))
86
87 # SignOutput(), which in turn calls signapk.jar, will possibly reorder the ZIP
88 # entries, as well as padding the entry headers. We do a preliminary signing
89 # (with an incomplete metadata entry) to allow that to happen. Then compute
90 # the ZIP entry offsets, write back the final metadata and do the final
91 # signing.
92 prelim_signing = ComputeAllPropertyFiles(input_file, needed_property_files)
93 try:
94 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
95 except PropertyFiles.InsufficientSpaceException:
96 # Even with the preliminary signing, the entry orders may change
97 # dramatically, which leads to insufficiently reserved space during the
98 # first call to ComputeAllPropertyFiles(). In that case, we redo all the
99 # preliminary signing works, based on the already ordered ZIP entries, to
100 # address the issue.
101 prelim_signing = ComputeAllPropertyFiles(
102 prelim_signing, needed_property_files)
103 FinalizeAllPropertyFiles(prelim_signing, needed_property_files)
104
105 # Replace the METADATA entry.
106 ZipDelete(prelim_signing, METADATA_NAME)
107 output_zip = zipfile.ZipFile(prelim_signing, 'a')
108 WriteMetadata(metadata, output_zip)
109 ZipClose(output_zip)
110
111 # Re-sign the package after updating the metadata entry.
112 if OPTIONS.no_signing:
113 output_file = prelim_signing
114 else:
115 SignOutput(prelim_signing, output_file)
116
117 # Reopen the final signed zip to double check the streaming metadata.
118 with zipfile.ZipFile(output_file) as output_zip:
119 for property_files in needed_property_files:
120 property_files.Verify(output_zip, metadata[property_files.name].strip())
121
122 # If requested, dump the metadata to a separate file.
123 output_metadata_path = OPTIONS.output_metadata_path
124 if output_metadata_path:
125 WriteMetadata(metadata, output_metadata_path)
126
127
128def WriteMetadata(metadata, output):
129 """Writes the metadata to the zip archive or a file.
130
131 Args:
132 metadata: The metadata dict for the package.
133 output: A ZipFile object or a string of the output file path.
134 """
135
136 value = "".join(["%s=%s\n" % kv for kv in sorted(metadata.items())])
137 if isinstance(output, zipfile.ZipFile):
138 ZipWriteStr(output, METADATA_NAME, value,
139 compress_type=zipfile.ZIP_STORED)
140 return
141
142 with open(output, 'w') as f:
143 f.write(value)
144
145
146def GetPackageMetadata(target_info, source_info=None):
147 """Generates and returns the metadata dict.
148
149 It generates a dict() that contains the info to be written into an OTA
150 package (META-INF/com/android/metadata). It also handles the detection of
151 downgrade / data wipe based on the global options.
152
153 Args:
154 target_info: The BuildInfo instance that holds the target build info.
155 source_info: The BuildInfo instance that holds the source build info, or
156 None if generating full OTA.
157
158 Returns:
159 A dict to be written into package metadata entry.
160 """
161 assert isinstance(target_info, BuildInfo)
162 assert source_info is None or isinstance(source_info, BuildInfo)
163
164 separator = '|'
165
166 boot_variable_values = {}
167 if OPTIONS.boot_variable_file:
168 d = LoadDictionaryFromFile(OPTIONS.boot_variable_file)
169 for key, values in d.items():
170 boot_variable_values[key] = [val.strip() for val in values.split(',')]
171
172 post_build_devices, post_build_fingerprints = \
173 CalculateRuntimeDevicesAndFingerprints(target_info, boot_variable_values)
174 metadata = {
175 'post-build': separator.join(sorted(post_build_fingerprints)),
176 'post-build-incremental': target_info.GetBuildProp(
177 'ro.build.version.incremental'),
178 'post-sdk-level': target_info.GetBuildProp(
179 'ro.build.version.sdk'),
180 'post-security-patch-level': target_info.GetBuildProp(
181 'ro.build.version.security_patch'),
182 }
183
184 if target_info.is_ab and not OPTIONS.force_non_ab:
185 metadata['ota-type'] = 'AB'
186 metadata['ota-required-cache'] = '0'
187 else:
188 metadata['ota-type'] = 'BLOCK'
189
190 if OPTIONS.wipe_user_data:
191 metadata['ota-wipe'] = 'yes'
192
193 if OPTIONS.retrofit_dynamic_partitions:
194 metadata['ota-retrofit-dynamic-partitions'] = 'yes'
195
196 is_incremental = source_info is not None
197 if is_incremental:
198 pre_build_devices, pre_build_fingerprints = \
199 CalculateRuntimeDevicesAndFingerprints(source_info,
200 boot_variable_values)
201 metadata['pre-build'] = separator.join(sorted(pre_build_fingerprints))
202 metadata['pre-build-incremental'] = source_info.GetBuildProp(
203 'ro.build.version.incremental')
204 metadata['pre-device'] = separator.join(sorted(pre_build_devices))
205 else:
206 metadata['pre-device'] = separator.join(sorted(post_build_devices))
207
208 # Use the actual post-timestamp, even for a downgrade case.
209 metadata['post-timestamp'] = target_info.GetBuildProp('ro.build.date.utc')
210
211 # Detect downgrades and set up downgrade flags accordingly.
212 if is_incremental:
213 HandleDowngradeMetadata(metadata, target_info, source_info)
214
215 return metadata
216
217
218def HandleDowngradeMetadata(metadata, target_info, source_info):
219 # Only incremental OTAs are allowed to reach here.
220 assert OPTIONS.incremental_source is not None
221
222 post_timestamp = target_info.GetBuildProp("ro.build.date.utc")
223 pre_timestamp = source_info.GetBuildProp("ro.build.date.utc")
224 is_downgrade = int(post_timestamp) < int(pre_timestamp)
225
226 if OPTIONS.downgrade:
227 if not is_downgrade:
228 raise RuntimeError(
229 "--downgrade or --override_timestamp specified but no downgrade "
230 "detected: pre: %s, post: %s" % (pre_timestamp, post_timestamp))
231 metadata["ota-downgrade"] = "yes"
232 else:
233 if is_downgrade:
234 raise RuntimeError(
235 "Downgrade detected based on timestamp check: pre: %s, post: %s. "
236 "Need to specify --override_timestamp OR --downgrade to allow "
237 "building the incremental." % (pre_timestamp, post_timestamp))
238
239
240def CalculateRuntimeDevicesAndFingerprints(build_info, boot_variable_values):
241 """Returns a tuple of sets for runtime devices and fingerprints"""
242
243 device_names = {build_info.device}
244 fingerprints = {build_info.fingerprint}
245
246 if not boot_variable_values:
247 return device_names, fingerprints
248
249 # Calculate all possible combinations of the values for the boot variables.
250 keys = boot_variable_values.keys()
251 value_list = boot_variable_values.values()
252 combinations = [dict(zip(keys, values))
253 for values in itertools.product(*value_list)]
254 for placeholder_values in combinations:
255 # Reload the info_dict as some build properties may change their values
256 # based on the value of ro.boot* properties.
257 info_dict = copy.deepcopy(build_info.info_dict)
258 for partition in PARTITIONS_WITH_CARE_MAP:
259 partition_prop_key = "{}.build.prop".format(partition)
260 input_file = info_dict[partition_prop_key].input_file
261 if isinstance(input_file, zipfile.ZipFile):
262 with zipfile.ZipFile(input_file.filename) as input_zip:
263 info_dict[partition_prop_key] = \
264 PartitionBuildProps.FromInputFile(input_zip, partition,
265 placeholder_values)
266 else:
267 info_dict[partition_prop_key] = \
268 PartitionBuildProps.FromInputFile(input_file, partition,
269 placeholder_values)
270 info_dict["build.prop"] = info_dict["system.build.prop"]
271
272 new_build_info = BuildInfo(info_dict, build_info.oem_dicts)
273 device_names.add(new_build_info.device)
274 fingerprints.add(new_build_info.fingerprint)
275 return device_names, fingerprints
276
277
278class PropertyFiles(object):
279 """A class that computes the property-files string for an OTA package.
280
281 A property-files string is a comma-separated string that contains the
282 offset/size info for an OTA package. The entries, which must be ZIP_STORED,
283 can be fetched directly with the package URL along with the offset/size info.
284 These strings can be used for streaming A/B OTAs, or allowing an updater to
285 download package metadata entry directly, without paying the cost of
286 downloading entire package.
287
288 Computing the final property-files string requires two passes. Because doing
289 the whole package signing (with signapk.jar) will possibly reorder the ZIP
290 entries, which may in turn invalidate earlier computed ZIP entry offset/size
291 values.
292
293 This class provides functions to be called for each pass. The general flow is
294 as follows.
295
296 property_files = PropertyFiles()
297 # The first pass, which writes placeholders before doing initial signing.
298 property_files.Compute()
299 SignOutput()
300
301 # The second pass, by replacing the placeholders with actual data.
302 property_files.Finalize()
303 SignOutput()
304
305 And the caller can additionally verify the final result.
306
307 property_files.Verify()
308 """
309
310 def __init__(self):
311 self.name = None
312 self.required = ()
313 self.optional = ()
314
315 def Compute(self, input_zip):
316 """Computes and returns a property-files string with placeholders.
317
318 We reserve extra space for the offset and size of the metadata entry itself,
319 although we don't know the final values until the package gets signed.
320
321 Args:
322 input_zip: The input ZIP file.
323
324 Returns:
325 A string with placeholders for the metadata offset/size info, e.g.
326 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
327 """
328 return self.GetPropertyFilesString(input_zip, reserve_space=True)
329
330 class InsufficientSpaceException(Exception):
331 pass
332
333 def Finalize(self, input_zip, reserved_length):
334 """Finalizes a property-files string with actual METADATA offset/size info.
335
336 The input ZIP file has been signed, with the ZIP entries in the desired
337 place (signapk.jar will possibly reorder the ZIP entries). Now we compute
338 the ZIP entry offsets and construct the property-files string with actual
339 data. Note that during this process, we must pad the property-files string
340 to the reserved length, so that the METADATA entry size remains the same.
341 Otherwise the entries' offsets and sizes may change again.
342
343 Args:
344 input_zip: The input ZIP file.
345 reserved_length: The reserved length of the property-files string during
346 the call to Compute(). The final string must be no more than this
347 size.
348
349 Returns:
350 A property-files string including the metadata offset/size info, e.g.
351 "payload.bin:679:343,payload_properties.txt:378:45,metadata:69:379 ".
352
353 Raises:
354 InsufficientSpaceException: If the reserved length is insufficient to hold
355 the final string.
356 """
357 result = self.GetPropertyFilesString(input_zip, reserve_space=False)
358 if len(result) > reserved_length:
359 raise self.InsufficientSpaceException(
360 'Insufficient reserved space: reserved={}, actual={}'.format(
361 reserved_length, len(result)))
362
363 result += ' ' * (reserved_length - len(result))
364 return result
365
366 def Verify(self, input_zip, expected):
367 """Verifies the input ZIP file contains the expected property-files string.
368
369 Args:
370 input_zip: The input ZIP file.
371 expected: The property-files string that's computed from Finalize().
372
373 Raises:
374 AssertionError: On finding a mismatch.
375 """
376 actual = self.GetPropertyFilesString(input_zip)
377 assert actual == expected, \
378 "Mismatching streaming metadata: {} vs {}.".format(actual, expected)
379
380 def GetPropertyFilesString(self, zip_file, reserve_space=False):
381 """
382 Constructs the property-files string per request.
383
384 Args:
385 zip_file: The input ZIP file.
386 reserved_length: The reserved length of the property-files string.
387
388 Returns:
389 A property-files string including the metadata offset/size info, e.g.
390 "payload.bin:679:343,payload_properties.txt:378:45,metadata: ".
391 """
392
393 def ComputeEntryOffsetSize(name):
394 """Computes the zip entry offset and size."""
395 info = zip_file.getinfo(name)
396 offset = info.header_offset
397 offset += zipfile.sizeFileHeader
398 offset += len(info.extra) + len(info.filename)
399 size = info.file_size
400 return '%s:%d:%d' % (os.path.basename(name), offset, size)
401
402 tokens = []
403 tokens.extend(self._GetPrecomputed(zip_file))
404 for entry in self.required:
405 tokens.append(ComputeEntryOffsetSize(entry))
406 for entry in self.optional:
407 if entry in zip_file.namelist():
408 tokens.append(ComputeEntryOffsetSize(entry))
409
410 # 'META-INF/com/android/metadata' is required. We don't know its actual
411 # offset and length (as well as the values for other entries). So we reserve
412 # 15-byte as a placeholder ('offset:length'), which is sufficient to cover
413 # the space for metadata entry. Because 'offset' allows a max of 10-digit
414 # (i.e. ~9 GiB), with a max of 4-digit for the length. Note that all the
415 # reserved space serves the metadata entry only.
416 if reserve_space:
417 tokens.append('metadata:' + ' ' * 15)
418 else:
419 tokens.append(ComputeEntryOffsetSize(METADATA_NAME))
420
421 return ','.join(tokens)
422
423 def _GetPrecomputed(self, input_zip):
424 """Computes the additional tokens to be included into the property-files.
425
426 This applies to tokens without actual ZIP entries, such as
427 payload_metadata.bin. We want to expose the offset/size to updaters, so
428 that they can download the payload metadata directly with the info.
429
430 Args:
431 input_zip: The input zip file.
432
433 Returns:
434 A list of strings (tokens) to be added to the property-files string.
435 """
436 # pylint: disable=no-self-use
437 # pylint: disable=unused-argument
438 return []
439
440
441def SignOutput(temp_zip_name, output_zip_name):
442 pw = OPTIONS.key_passwords[OPTIONS.package_key]
443
444 SignFile(temp_zip_name, output_zip_name, OPTIONS.package_key, pw,
445 whole_file=True)