xref: /aosp_15_r20/system/apex/tools/apex_compression_tool.py (revision 33f3758387333dbd2962d7edbd98681940d895da)
1*33f37583SAndroid Build Coastguard Worker#!/usr/bin/env python
2*33f37583SAndroid Build Coastguard Worker#
3*33f37583SAndroid Build Coastguard Worker# Copyright (C) 2020 The Android Open Source Project
4*33f37583SAndroid Build Coastguard Worker#
5*33f37583SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
6*33f37583SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
7*33f37583SAndroid Build Coastguard Worker# You may obtain a copy of the License at
8*33f37583SAndroid Build Coastguard Worker#
9*33f37583SAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
10*33f37583SAndroid Build Coastguard Worker#
11*33f37583SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
12*33f37583SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
13*33f37583SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14*33f37583SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
15*33f37583SAndroid Build Coastguard Worker# limitations under the License.
16*33f37583SAndroid Build Coastguard Worker
17*33f37583SAndroid Build Coastguard Worker"""apex_compression_tool is a tool that can compress/decompress APEX.
18*33f37583SAndroid Build Coastguard Worker
19*33f37583SAndroid Build Coastguard WorkerExample:
20*33f37583SAndroid Build Coastguard Worker  apex_compression_tool compress --input /apex/to/compress --output output/path
21*33f37583SAndroid Build Coastguard Worker  apex_compression_tool decompress --input /apex/to/decompress --output dir/
22*33f37583SAndroid Build Coastguard Worker  apex_compression_tool verify-compressed --input /file/to/check
23*33f37583SAndroid Build Coastguard Worker"""
24*33f37583SAndroid Build Coastguard Workerfrom __future__ import print_function
25*33f37583SAndroid Build Coastguard Worker
26*33f37583SAndroid Build Coastguard Workerimport argparse
27*33f37583SAndroid Build Coastguard Workerimport os
28*33f37583SAndroid Build Coastguard Workerimport shutil
29*33f37583SAndroid Build Coastguard Workerimport subprocess
30*33f37583SAndroid Build Coastguard Workerimport sys
31*33f37583SAndroid Build Coastguard Workerimport tempfile
32*33f37583SAndroid Build Coastguard Workerfrom zipfile import ZipFile
33*33f37583SAndroid Build Coastguard Worker
34*33f37583SAndroid Build Coastguard Workerimport apex_manifest_pb2
35*33f37583SAndroid Build Coastguard Worker
36*33f37583SAndroid Build Coastguard Workertool_path_list = None
37*33f37583SAndroid Build Coastguard Worker
38*33f37583SAndroid Build Coastguard Worker
39*33f37583SAndroid Build Coastguard Workerdef FindBinaryPath(binary):
40*33f37583SAndroid Build Coastguard Worker  for path in tool_path_list:
41*33f37583SAndroid Build Coastguard Worker    binary_path = os.path.join(path, binary)
42*33f37583SAndroid Build Coastguard Worker    if os.path.exists(binary_path):
43*33f37583SAndroid Build Coastguard Worker      return binary_path
44*33f37583SAndroid Build Coastguard Worker  raise Exception('Failed to find binary ' + binary + ' in path ' +
45*33f37583SAndroid Build Coastguard Worker                  ':'.join(tool_path_list))
46*33f37583SAndroid Build Coastguard Worker
47*33f37583SAndroid Build Coastguard Worker
48*33f37583SAndroid Build Coastguard Workerdef RunCommand(cmd, verbose=False, env=None, expected_return_values=None):
49*33f37583SAndroid Build Coastguard Worker  expected_return_values = expected_return_values or {0}
50*33f37583SAndroid Build Coastguard Worker  env = env or {}
51*33f37583SAndroid Build Coastguard Worker  env.update(os.environ.copy())
52*33f37583SAndroid Build Coastguard Worker
53*33f37583SAndroid Build Coastguard Worker  cmd[0] = FindBinaryPath(cmd[0])
54*33f37583SAndroid Build Coastguard Worker
55*33f37583SAndroid Build Coastguard Worker  if verbose:
56*33f37583SAndroid Build Coastguard Worker    print('Running: ' + ' '.join(cmd))
57*33f37583SAndroid Build Coastguard Worker  p = subprocess.Popen(
58*33f37583SAndroid Build Coastguard Worker      cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env)
59*33f37583SAndroid Build Coastguard Worker  output, _ = p.communicate()
60*33f37583SAndroid Build Coastguard Worker
61*33f37583SAndroid Build Coastguard Worker  if verbose or p.returncode not in expected_return_values:
62*33f37583SAndroid Build Coastguard Worker    print(output.rstrip())
63*33f37583SAndroid Build Coastguard Worker
64*33f37583SAndroid Build Coastguard Worker  assert p.returncode in expected_return_values, 'Failed to execute: ' \
65*33f37583SAndroid Build Coastguard Worker                                                 + ' '.join(cmd)
66*33f37583SAndroid Build Coastguard Worker
67*33f37583SAndroid Build Coastguard Worker  return output, p.returncode
68*33f37583SAndroid Build Coastguard Worker
69*33f37583SAndroid Build Coastguard Worker
70*33f37583SAndroid Build Coastguard Workerdef RunCompress(args, work_dir):
71*33f37583SAndroid Build Coastguard Worker  """RunCompress takes an uncompressed APEX and compresses into compressed APEX
72*33f37583SAndroid Build Coastguard Worker
73*33f37583SAndroid Build Coastguard Worker  Compressed apex will contain the following items:
74*33f37583SAndroid Build Coastguard Worker      - original_apex: The original uncompressed APEX
75*33f37583SAndroid Build Coastguard Worker      - Duplicates of various meta files inside the input APEX, e.g
76*33f37583SAndroid Build Coastguard Worker        AndroidManifest.xml, public_key
77*33f37583SAndroid Build Coastguard Worker
78*33f37583SAndroid Build Coastguard Worker  Args:
79*33f37583SAndroid Build Coastguard Worker      args.input: file path to uncompressed APEX
80*33f37583SAndroid Build Coastguard Worker      args.output: file path to where compressed APEX will be placed
81*33f37583SAndroid Build Coastguard Worker      work_dir: file path to a temporary folder
82*33f37583SAndroid Build Coastguard Worker  Returns:
83*33f37583SAndroid Build Coastguard Worker      True if compression was executed successfully, otherwise False
84*33f37583SAndroid Build Coastguard Worker  """
85*33f37583SAndroid Build Coastguard Worker  global tool_path_list
86*33f37583SAndroid Build Coastguard Worker  tool_path_list = args.apex_compression_tool_path
87*33f37583SAndroid Build Coastguard Worker
88*33f37583SAndroid Build Coastguard Worker  cmd = ['soong_zip']
89*33f37583SAndroid Build Coastguard Worker  cmd.extend(['-o', args.output])
90*33f37583SAndroid Build Coastguard Worker
91*33f37583SAndroid Build Coastguard Worker  # We want to put the input apex inside the compressed APEX with name
92*33f37583SAndroid Build Coastguard Worker  # "original_apex". Originally this was done by creating a hard link
93*33f37583SAndroid Build Coastguard Worker  # in order to put the renamed file inside the zip, but it causes some issue
94*33f37583SAndroid Build Coastguard Worker  # when running this tool with Bazel in a sandbox which restricts the function
95*33f37583SAndroid Build Coastguard Worker  # of creating cross-device links. So instead of creating hard links, we make a
96*33f37583SAndroid Build Coastguard Worker  # copy of the original_apex here.
97*33f37583SAndroid Build Coastguard Worker  original_apex = os.path.join(work_dir, 'original_apex')
98*33f37583SAndroid Build Coastguard Worker  shutil.copy2(args.input, original_apex)
99*33f37583SAndroid Build Coastguard Worker  cmd.extend(['-C', work_dir])
100*33f37583SAndroid Build Coastguard Worker  cmd.extend(['-f', original_apex])
101*33f37583SAndroid Build Coastguard Worker
102*33f37583SAndroid Build Coastguard Worker  # We also need to extract some files from inside of original_apex and zip
103*33f37583SAndroid Build Coastguard Worker  # together with compressed apex
104*33f37583SAndroid Build Coastguard Worker  with ZipFile(original_apex, 'r') as zip_obj:
105*33f37583SAndroid Build Coastguard Worker    extract_dir = os.path.join(work_dir, 'extract')
106*33f37583SAndroid Build Coastguard Worker    for meta_file in ['apex_manifest.json', 'apex_manifest.pb',
107*33f37583SAndroid Build Coastguard Worker                      'apex_pubkey', 'apex_build_info.pb',
108*33f37583SAndroid Build Coastguard Worker                      'AndroidManifest.xml']:
109*33f37583SAndroid Build Coastguard Worker      if meta_file in zip_obj.namelist():
110*33f37583SAndroid Build Coastguard Worker        zip_obj.extract(meta_file, path=extract_dir)
111*33f37583SAndroid Build Coastguard Worker        file_path = os.path.join(extract_dir, meta_file)
112*33f37583SAndroid Build Coastguard Worker        cmd.extend(['-C', extract_dir])
113*33f37583SAndroid Build Coastguard Worker        cmd.extend(['-f', file_path])
114*33f37583SAndroid Build Coastguard Worker        cmd.extend(['-s', meta_file])
115*33f37583SAndroid Build Coastguard Worker    # Extract the image for retrieving root digest
116*33f37583SAndroid Build Coastguard Worker    zip_obj.extract('apex_payload.img', path= work_dir)
117*33f37583SAndroid Build Coastguard Worker    image_path = os.path.join(work_dir, 'apex_payload.img')
118*33f37583SAndroid Build Coastguard Worker
119*33f37583SAndroid Build Coastguard Worker  # Set digest of original_apex to apex_manifest.pb
120*33f37583SAndroid Build Coastguard Worker  apex_manifest_path = os.path.join(extract_dir, 'apex_manifest.pb')
121*33f37583SAndroid Build Coastguard Worker  assert AddOriginalApexDigestToManifest(apex_manifest_path, image_path, args.verbose)
122*33f37583SAndroid Build Coastguard Worker
123*33f37583SAndroid Build Coastguard Worker  # Don't forget to compress
124*33f37583SAndroid Build Coastguard Worker  cmd.extend(['-L', '9'])
125*33f37583SAndroid Build Coastguard Worker
126*33f37583SAndroid Build Coastguard Worker  RunCommand(cmd, verbose=args.verbose)
127*33f37583SAndroid Build Coastguard Worker
128*33f37583SAndroid Build Coastguard Worker  return True
129*33f37583SAndroid Build Coastguard Worker
130*33f37583SAndroid Build Coastguard Worker
131*33f37583SAndroid Build Coastguard Workerdef AddOriginalApexDigestToManifest(capex_manifest_path, apex_image_path, verbose=False):
132*33f37583SAndroid Build Coastguard Worker  # Retrieve the root digest of the image
133*33f37583SAndroid Build Coastguard Worker  avbtool_cmd = [
134*33f37583SAndroid Build Coastguard Worker        'avbtool',
135*33f37583SAndroid Build Coastguard Worker        'print_partition_digests', '--image',
136*33f37583SAndroid Build Coastguard Worker        apex_image_path]
137*33f37583SAndroid Build Coastguard Worker  # avbtool_cmd output has format "<name>: <value>"
138*33f37583SAndroid Build Coastguard Worker  root_digest = RunCommand(avbtool_cmd, verbose=verbose)[0].decode().split(': ')[1].strip()
139*33f37583SAndroid Build Coastguard Worker  # Update the manifest proto file
140*33f37583SAndroid Build Coastguard Worker  with open(capex_manifest_path, 'rb') as f:
141*33f37583SAndroid Build Coastguard Worker    pb = apex_manifest_pb2.ApexManifest()
142*33f37583SAndroid Build Coastguard Worker    pb.ParseFromString(f.read())
143*33f37583SAndroid Build Coastguard Worker
144*33f37583SAndroid Build Coastguard Worker  assert not pb.supportsRebootlessUpdate, "Rebootless updates not supported for compressed APEXs"
145*33f37583SAndroid Build Coastguard Worker  # Populate CompressedApexMetadata
146*33f37583SAndroid Build Coastguard Worker  capex_metadata = apex_manifest_pb2.ApexManifest().CompressedApexMetadata()
147*33f37583SAndroid Build Coastguard Worker  capex_metadata.originalApexDigest = root_digest
148*33f37583SAndroid Build Coastguard Worker  # Set updated value to protobuf
149*33f37583SAndroid Build Coastguard Worker  pb.capexMetadata.CopyFrom(capex_metadata)
150*33f37583SAndroid Build Coastguard Worker  with open(capex_manifest_path, 'wb') as f:
151*33f37583SAndroid Build Coastguard Worker    f.write(pb.SerializeToString())
152*33f37583SAndroid Build Coastguard Worker  return True
153*33f37583SAndroid Build Coastguard Worker
154*33f37583SAndroid Build Coastguard Worker
155*33f37583SAndroid Build Coastguard Workerdef ParseArgs(argv):
156*33f37583SAndroid Build Coastguard Worker  parser = argparse.ArgumentParser()
157*33f37583SAndroid Build Coastguard Worker  subparsers = parser.add_subparsers(required=True, dest='cmd')
158*33f37583SAndroid Build Coastguard Worker
159*33f37583SAndroid Build Coastguard Worker  # Handle sub-command "compress"
160*33f37583SAndroid Build Coastguard Worker  parser_compress = subparsers.add_parser('compress',
161*33f37583SAndroid Build Coastguard Worker                                          help='compresses an APEX')
162*33f37583SAndroid Build Coastguard Worker  parser_compress.add_argument('-v', '--verbose', action='store_true',
163*33f37583SAndroid Build Coastguard Worker                               help='verbose execution')
164*33f37583SAndroid Build Coastguard Worker  parser_compress.add_argument('--input', type=str, required=True,
165*33f37583SAndroid Build Coastguard Worker                               help='path to input APEX file that will be '
166*33f37583SAndroid Build Coastguard Worker                                    'compressed')
167*33f37583SAndroid Build Coastguard Worker  parser_compress.add_argument('--output', type=str, required=True,
168*33f37583SAndroid Build Coastguard Worker                               help='output path to compressed APEX file')
169*33f37583SAndroid Build Coastguard Worker  apex_compression_tool_path_in_environ = \
170*33f37583SAndroid Build Coastguard Worker    'APEX_COMPRESSION_TOOL_PATH' in os.environ
171*33f37583SAndroid Build Coastguard Worker  parser_compress.add_argument(
172*33f37583SAndroid Build Coastguard Worker      '--apex_compression_tool_path',
173*33f37583SAndroid Build Coastguard Worker      required=not apex_compression_tool_path_in_environ,
174*33f37583SAndroid Build Coastguard Worker      default=os.environ['APEX_COMPRESSION_TOOL_PATH'].split(':')
175*33f37583SAndroid Build Coastguard Worker      if apex_compression_tool_path_in_environ else None,
176*33f37583SAndroid Build Coastguard Worker      type=lambda s: s.split(':'),
177*33f37583SAndroid Build Coastguard Worker      help="""A list of directories containing all the tools used by
178*33f37583SAndroid Build Coastguard Worker        apex_compression_tool (e.g. soong_zip etc.) separated by ':'. Can also
179*33f37583SAndroid Build Coastguard Worker        be set using the APEX_COMPRESSION_TOOL_PATH environment variable""")
180*33f37583SAndroid Build Coastguard Worker  parser_compress.set_defaults(func=RunCompress)
181*33f37583SAndroid Build Coastguard Worker
182*33f37583SAndroid Build Coastguard Worker  return parser.parse_args(argv)
183*33f37583SAndroid Build Coastguard Worker
184*33f37583SAndroid Build Coastguard Worker
185*33f37583SAndroid Build Coastguard Workerclass TempDirectory(object):
186*33f37583SAndroid Build Coastguard Worker
187*33f37583SAndroid Build Coastguard Worker  def __enter__(self):
188*33f37583SAndroid Build Coastguard Worker    self.name = tempfile.mkdtemp()
189*33f37583SAndroid Build Coastguard Worker    return self.name
190*33f37583SAndroid Build Coastguard Worker
191*33f37583SAndroid Build Coastguard Worker  def __exit__(self, *unused):
192*33f37583SAndroid Build Coastguard Worker    shutil.rmtree(self.name)
193*33f37583SAndroid Build Coastguard Worker
194*33f37583SAndroid Build Coastguard Worker
195*33f37583SAndroid Build Coastguard Workerdef main(argv):
196*33f37583SAndroid Build Coastguard Worker  args = ParseArgs(argv)
197*33f37583SAndroid Build Coastguard Worker
198*33f37583SAndroid Build Coastguard Worker  with TempDirectory() as work_dir:
199*33f37583SAndroid Build Coastguard Worker    success = args.func(args, work_dir)
200*33f37583SAndroid Build Coastguard Worker
201*33f37583SAndroid Build Coastguard Worker  if not success:
202*33f37583SAndroid Build Coastguard Worker    sys.exit(1)
203*33f37583SAndroid Build Coastguard Worker
204*33f37583SAndroid Build Coastguard Worker
205*33f37583SAndroid Build Coastguard Workerif __name__ == '__main__':
206*33f37583SAndroid Build Coastguard Worker  main(sys.argv[1:])
207