xref: /aosp_15_r20/system/update_engine/scripts/simulate_ota.py (revision 5a9231315b4521097b8dc3750bc806fcafe0c72f)
1*5a923131SAndroid Build Coastguard Worker#!/usr/bin/env python3
2*5a923131SAndroid Build Coastguard Worker#
3*5a923131SAndroid Build Coastguard Worker# Copyright (C) 2020 The Android Open Source Project
4*5a923131SAndroid Build Coastguard Worker#
5*5a923131SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
6*5a923131SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
7*5a923131SAndroid Build Coastguard Worker# You may obtain a copy of the License at
8*5a923131SAndroid Build Coastguard Worker#
9*5a923131SAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
10*5a923131SAndroid Build Coastguard Worker#
11*5a923131SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
12*5a923131SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
13*5a923131SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*5a923131SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
15*5a923131SAndroid Build Coastguard Worker# limitations under the License.
16*5a923131SAndroid Build Coastguard Worker#
17*5a923131SAndroid Build Coastguard Worker
18*5a923131SAndroid Build Coastguard Worker"""Tools for running host side simulation of an OTA update."""
19*5a923131SAndroid Build Coastguard Worker
20*5a923131SAndroid Build Coastguard Worker
21*5a923131SAndroid Build Coastguard Workerimport argparse
22*5a923131SAndroid Build Coastguard Workerimport filecmp
23*5a923131SAndroid Build Coastguard Workerimport os
24*5a923131SAndroid Build Coastguard Workerimport shutil
25*5a923131SAndroid Build Coastguard Workerimport subprocess
26*5a923131SAndroid Build Coastguard Workerimport sys
27*5a923131SAndroid Build Coastguard Workerimport tempfile
28*5a923131SAndroid Build Coastguard Workerimport zipfile
29*5a923131SAndroid Build Coastguard Worker
30*5a923131SAndroid Build Coastguard Workerimport update_payload
31*5a923131SAndroid Build Coastguard Worker
32*5a923131SAndroid Build Coastguard Worker
33*5a923131SAndroid Build Coastguard Workerdef extract_file(zip_file_path, entry_name, target_file_path):
34*5a923131SAndroid Build Coastguard Worker  """Extract a file from zip archive into |target_file_path|"""
35*5a923131SAndroid Build Coastguard Worker  with open(target_file_path, 'wb') as out_fp:
36*5a923131SAndroid Build Coastguard Worker    if isinstance(zip_file_path, zipfile.ZipFile):
37*5a923131SAndroid Build Coastguard Worker      with zip_file_path.open(entry_name) as fp:
38*5a923131SAndroid Build Coastguard Worker        shutil.copyfileobj(fp, out_fp)
39*5a923131SAndroid Build Coastguard Worker    elif os.path.isdir(zip_file_path):
40*5a923131SAndroid Build Coastguard Worker      with open(os.path.join(zip_file_path, entry_name), "rb") as fp:
41*5a923131SAndroid Build Coastguard Worker        shutil.copyfileobj(fp, out_fp)
42*5a923131SAndroid Build Coastguard Worker
43*5a923131SAndroid Build Coastguard Worker
44*5a923131SAndroid Build Coastguard Workerdef is_sparse_image(filepath):
45*5a923131SAndroid Build Coastguard Worker  with open(filepath, 'rb') as fp:
46*5a923131SAndroid Build Coastguard Worker    # Magic for android sparse image format
47*5a923131SAndroid Build Coastguard Worker    # https://source.android.com/devices/bootloader/images
48*5a923131SAndroid Build Coastguard Worker    return fp.read(4) == b'\x3A\xFF\x26\xED'
49*5a923131SAndroid Build Coastguard Worker
50*5a923131SAndroid Build Coastguard Worker
51*5a923131SAndroid Build Coastguard Workerdef extract_img(zip_archive: zipfile.ZipFile, img_name, output_path, is_source):
52*5a923131SAndroid Build Coastguard Worker  """ Extract and unsparse partition image from zip archive """
53*5a923131SAndroid Build Coastguard Worker  entry_name = "IMAGES/" + img_name + ".img"
54*5a923131SAndroid Build Coastguard Worker  try:
55*5a923131SAndroid Build Coastguard Worker    extract_file(zip_archive, entry_name, output_path)
56*5a923131SAndroid Build Coastguard Worker  except (KeyError, FileNotFoundError) as e:
57*5a923131SAndroid Build Coastguard Worker    print("Faild to extract", img_name, "from IMAGES/ dir, trying RADIO/", e)
58*5a923131SAndroid Build Coastguard Worker    extract_file(zip_archive, "RADIO/" + img_name + ".img", output_path)
59*5a923131SAndroid Build Coastguard Worker  if is_sparse_image(output_path):
60*5a923131SAndroid Build Coastguard Worker    raw_img_path = output_path + ".raw"
61*5a923131SAndroid Build Coastguard Worker    subprocess.check_output(["simg2img", output_path, raw_img_path])
62*5a923131SAndroid Build Coastguard Worker    os.rename(raw_img_path, output_path)
63*5a923131SAndroid Build Coastguard Worker
64*5a923131SAndroid Build Coastguard Worker  # delta_generator only supports images multiple of 4 KiB. For target images
65*5a923131SAndroid Build Coastguard Worker  # we pad the data with zeros if needed, but for source images we truncate
66*5a923131SAndroid Build Coastguard Worker  # down the data since the last block of the old image could be padded on
67*5a923131SAndroid Build Coastguard Worker  # disk with unknown data.
68*5a923131SAndroid Build Coastguard Worker  file_size = os.path.getsize(output_path)
69*5a923131SAndroid Build Coastguard Worker  if file_size % 4096 != 0:
70*5a923131SAndroid Build Coastguard Worker    if is_source:
71*5a923131SAndroid Build Coastguard Worker      print("Rounding DOWN partition {} to a multiple of 4 KiB."
72*5a923131SAndroid Build Coastguard Worker            .format(output_path))
73*5a923131SAndroid Build Coastguard Worker      file_size = file_size & -4096
74*5a923131SAndroid Build Coastguard Worker    else:
75*5a923131SAndroid Build Coastguard Worker      print("Rounding UP partition {} to a multiple of 4 KiB."
76*5a923131SAndroid Build Coastguard Worker            .format(output_path))
77*5a923131SAndroid Build Coastguard Worker      file_size = (file_size + 4095) & -4096
78*5a923131SAndroid Build Coastguard Worker    with open(output_path, 'a') as f:
79*5a923131SAndroid Build Coastguard Worker      f.truncate(file_size)
80*5a923131SAndroid Build Coastguard Worker
81*5a923131SAndroid Build Coastguard Workerdef run_ota(source, target, payload_path, tempdir, output_dir):
82*5a923131SAndroid Build Coastguard Worker  """Run an OTA on host side"""
83*5a923131SAndroid Build Coastguard Worker  payload = update_payload.Payload(payload_path)
84*5a923131SAndroid Build Coastguard Worker  payload.Init()
85*5a923131SAndroid Build Coastguard Worker  if source and zipfile.is_zipfile(source):
86*5a923131SAndroid Build Coastguard Worker    source = zipfile.ZipFile(source)
87*5a923131SAndroid Build Coastguard Worker  if target and zipfile.is_zipfile(target):
88*5a923131SAndroid Build Coastguard Worker    target = zipfile.ZipFile(target)
89*5a923131SAndroid Build Coastguard Worker  source_exist = source and (isinstance(
90*5a923131SAndroid Build Coastguard Worker      source, zipfile.ZipFile) or os.path.exists(source))
91*5a923131SAndroid Build Coastguard Worker  target_exist = target and (isinstance(
92*5a923131SAndroid Build Coastguard Worker      target, zipfile.ZipFile) or os.path.exists(target))
93*5a923131SAndroid Build Coastguard Worker
94*5a923131SAndroid Build Coastguard Worker  old_partitions = []
95*5a923131SAndroid Build Coastguard Worker  new_partitions = []
96*5a923131SAndroid Build Coastguard Worker  expected_new_partitions = []
97*5a923131SAndroid Build Coastguard Worker  for part in payload.manifest.partitions:
98*5a923131SAndroid Build Coastguard Worker    name = part.partition_name
99*5a923131SAndroid Build Coastguard Worker    old_image = os.path.join(tempdir, "source_" + name + ".img")
100*5a923131SAndroid Build Coastguard Worker    new_image = os.path.join(tempdir, "target_" + name + ".img")
101*5a923131SAndroid Build Coastguard Worker    if part.HasField("old_partition_info"):
102*5a923131SAndroid Build Coastguard Worker      assert source_exist, \
103*5a923131SAndroid Build Coastguard Worker          "source target file must point to a valid zipfile or directory " + \
104*5a923131SAndroid Build Coastguard Worker          source
105*5a923131SAndroid Build Coastguard Worker      print("Extracting source image for", name)
106*5a923131SAndroid Build Coastguard Worker      extract_img(source, name, old_image, True)
107*5a923131SAndroid Build Coastguard Worker    if target_exist:
108*5a923131SAndroid Build Coastguard Worker      print("Extracting target image for", name)
109*5a923131SAndroid Build Coastguard Worker      extract_img(target, name, new_image, False)
110*5a923131SAndroid Build Coastguard Worker
111*5a923131SAndroid Build Coastguard Worker    old_partitions.append(old_image)
112*5a923131SAndroid Build Coastguard Worker    scratch_image_name = new_image + ".actual"
113*5a923131SAndroid Build Coastguard Worker    new_partitions.append(scratch_image_name)
114*5a923131SAndroid Build Coastguard Worker    with open(scratch_image_name, "wb") as fp:
115*5a923131SAndroid Build Coastguard Worker      fp.truncate(part.new_partition_info.size)
116*5a923131SAndroid Build Coastguard Worker    expected_new_partitions.append(new_image)
117*5a923131SAndroid Build Coastguard Worker
118*5a923131SAndroid Build Coastguard Worker  delta_generator_args = ["delta_generator", "--in_file=" + payload_path]
119*5a923131SAndroid Build Coastguard Worker  partition_names = [
120*5a923131SAndroid Build Coastguard Worker      part.partition_name for part in payload.manifest.partitions
121*5a923131SAndroid Build Coastguard Worker  ]
122*5a923131SAndroid Build Coastguard Worker  if payload.manifest.partial_update:
123*5a923131SAndroid Build Coastguard Worker    delta_generator_args.append("--is_partial_update")
124*5a923131SAndroid Build Coastguard Worker  if payload.is_incremental:
125*5a923131SAndroid Build Coastguard Worker    delta_generator_args.append("--old_partitions=" + ":".join(old_partitions))
126*5a923131SAndroid Build Coastguard Worker  delta_generator_args.append("--partition_names=" + ":".join(partition_names))
127*5a923131SAndroid Build Coastguard Worker  delta_generator_args.append("--new_partitions=" + ":".join(new_partitions))
128*5a923131SAndroid Build Coastguard Worker
129*5a923131SAndroid Build Coastguard Worker  print("Running ", " ".join(delta_generator_args))
130*5a923131SAndroid Build Coastguard Worker  subprocess.check_output(delta_generator_args)
131*5a923131SAndroid Build Coastguard Worker
132*5a923131SAndroid Build Coastguard Worker  valid = True
133*5a923131SAndroid Build Coastguard Worker  if not target_exist:
134*5a923131SAndroid Build Coastguard Worker    for part in new_partitions:
135*5a923131SAndroid Build Coastguard Worker      print("Output written to", part)
136*5a923131SAndroid Build Coastguard Worker      shutil.copy(part, output_dir)
137*5a923131SAndroid Build Coastguard Worker    return
138*5a923131SAndroid Build Coastguard Worker  for (expected_part, actual_part, part_name) in \
139*5a923131SAndroid Build Coastguard Worker          zip(expected_new_partitions, new_partitions, partition_names):
140*5a923131SAndroid Build Coastguard Worker    if filecmp.cmp(expected_part, actual_part):
141*5a923131SAndroid Build Coastguard Worker      print("Partition `{}` is valid".format(part_name))
142*5a923131SAndroid Build Coastguard Worker    else:
143*5a923131SAndroid Build Coastguard Worker      valid = False
144*5a923131SAndroid Build Coastguard Worker      print(
145*5a923131SAndroid Build Coastguard Worker          "Partition `{}` is INVALID expected image: {} actual image: {}"
146*5a923131SAndroid Build Coastguard Worker          .format(part_name, expected_part, actual_part))
147*5a923131SAndroid Build Coastguard Worker
148*5a923131SAndroid Build Coastguard Worker  if not valid and sys.stdout.isatty():
149*5a923131SAndroid Build Coastguard Worker    input("Paused to investigate invalid partitions, press any key to exit.")
150*5a923131SAndroid Build Coastguard Worker
151*5a923131SAndroid Build Coastguard Worker
152*5a923131SAndroid Build Coastguard Workerdef main():
153*5a923131SAndroid Build Coastguard Worker  parser = argparse.ArgumentParser(
154*5a923131SAndroid Build Coastguard Worker      description="Run host side simulation of OTA package")
155*5a923131SAndroid Build Coastguard Worker  parser.add_argument(
156*5a923131SAndroid Build Coastguard Worker      "--source",
157*5a923131SAndroid Build Coastguard Worker      help="Target file zip for the source build",
158*5a923131SAndroid Build Coastguard Worker      required=False)
159*5a923131SAndroid Build Coastguard Worker  parser.add_argument(
160*5a923131SAndroid Build Coastguard Worker      "--target",
161*5a923131SAndroid Build Coastguard Worker      help="Target file zip for the target build",
162*5a923131SAndroid Build Coastguard Worker      required=False)
163*5a923131SAndroid Build Coastguard Worker  parser.add_argument(
164*5a923131SAndroid Build Coastguard Worker      "-o",
165*5a923131SAndroid Build Coastguard Worker      dest="output_dir",
166*5a923131SAndroid Build Coastguard Worker      help="Output directory to put all images, current directory by default"
167*5a923131SAndroid Build Coastguard Worker  )
168*5a923131SAndroid Build Coastguard Worker  parser.add_argument(
169*5a923131SAndroid Build Coastguard Worker      "payload",
170*5a923131SAndroid Build Coastguard Worker      help="payload.bin for the OTA package, or a zip of OTA package itself",
171*5a923131SAndroid Build Coastguard Worker      nargs=1)
172*5a923131SAndroid Build Coastguard Worker  args = parser.parse_args()
173*5a923131SAndroid Build Coastguard Worker  print(args)
174*5a923131SAndroid Build Coastguard Worker
175*5a923131SAndroid Build Coastguard Worker  # pylint: disable=no-member
176*5a923131SAndroid Build Coastguard Worker  with tempfile.TemporaryDirectory() as tempdir:
177*5a923131SAndroid Build Coastguard Worker    payload_path = args.payload[0]
178*5a923131SAndroid Build Coastguard Worker    if zipfile.is_zipfile(payload_path):
179*5a923131SAndroid Build Coastguard Worker      with zipfile.ZipFile(payload_path, "r") as zfp:
180*5a923131SAndroid Build Coastguard Worker        payload_entry_name = 'payload.bin'
181*5a923131SAndroid Build Coastguard Worker        zfp.extract(payload_entry_name, tempdir)
182*5a923131SAndroid Build Coastguard Worker        payload_path = os.path.join(tempdir, payload_entry_name)
183*5a923131SAndroid Build Coastguard Worker    if args.output_dir is None:
184*5a923131SAndroid Build Coastguard Worker      args.output_dir = "."
185*5a923131SAndroid Build Coastguard Worker    if not os.path.exists(args.output_dir):
186*5a923131SAndroid Build Coastguard Worker      os.makedirs(args.output_dir, exist_ok=True)
187*5a923131SAndroid Build Coastguard Worker    assert os.path.isdir(args.output_dir)
188*5a923131SAndroid Build Coastguard Worker    run_ota(args.source, args.target, payload_path, tempdir, args.output_dir)
189*5a923131SAndroid Build Coastguard Worker
190*5a923131SAndroid Build Coastguard Worker
191*5a923131SAndroid Build Coastguard Workerif __name__ == '__main__':
192*5a923131SAndroid Build Coastguard Worker  main()
193