1#!/usr/bin/env python3
2#
3# Copyright (C) 2018 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#      http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""
18Given a OTA package file, produces update config JSON file.
19
20Example:
21      $ PYTHONPATH=$ANDROID_BUILD_TOP/build/make/tools/releasetools:$PYTHONPATH \\
22            bootable/recovery/updater_sample/tools/gen_update_config.py \\
23                --ab_install_type=STREAMING \\
24                ota-build-001.zip  \\
25                my-config-001.json \\
26                http://foo.bar/ota-builds/ota-build-001.zip
27"""
28
29import argparse
30import json
31import os.path
32import sys
33import zipfile
34
35import ota_from_target_files  # pylint: disable=import-error
36
37
38class GenUpdateConfig(object):
39    """
40    A class that generates update configuration file from an OTA package.
41
42    Currently supports only A/B (seamless) OTA packages.
43    TODO: add non-A/B packages support.
44    """
45
46    AB_INSTALL_TYPE_STREAMING = 'STREAMING'
47    AB_INSTALL_TYPE_NON_STREAMING = 'NON_STREAMING'
48
49    def __init__(self,
50                 package,
51                 url,
52                 ab_install_type,
53                 ab_force_switch_slot,
54                 ab_verify_payload_metadata):
55        self.package = package
56        self.url = url
57        self.ab_install_type = ab_install_type
58        self.ab_force_switch_slot = ab_force_switch_slot
59        self.ab_verify_payload_metadata = ab_verify_payload_metadata
60        self.streaming_required = (
61            # payload.bin and payload_properties.txt must exist.
62            'payload.bin',
63            'payload_properties.txt',
64        )
65        self.streaming_optional = (
66            # care_map.txt is available only if dm-verity is enabled.
67            'care_map.txt',
68            # compatibility.zip is available only if target supports Treble.
69            'compatibility.zip',
70        )
71        self._config = None
72
73    @property
74    def config(self):
75        """Returns generated config object."""
76        return self._config
77
78    def run(self):
79        """Generates config."""
80        self._config = {
81            '__': '*** Generated using tools/gen_update_config.py ***',
82            'name': self.ab_install_type[0] + ' ' + os.path.basename(self.package)[:-4],
83            'url': self.url,
84            'ab_config': self._gen_ab_config(),
85            'ab_install_type': self.ab_install_type,
86        }
87
88    def _gen_ab_config(self):
89        """Builds config required for A/B update."""
90        with zipfile.ZipFile(self.package, 'r') as package_zip:
91            config = {
92                'property_files': self._get_property_files(package_zip),
93                'verify_payload_metadata': self.ab_verify_payload_metadata,
94                'force_switch_slot': self.ab_force_switch_slot,
95            }
96
97        return config
98
99    @staticmethod
100    def _get_property_files(package_zip):
101        """Constructs the property-files list for A/B streaming metadata."""
102
103        ab_ota = ota_from_target_files.AbOtaPropertyFiles()
104        property_str = ab_ota.GetPropertyFilesString(package_zip, False)
105        property_files = []
106        for file in property_str.split(','):
107            filename, offset, size = file.split(':')
108            inner_file = {
109                'filename': filename,
110                'offset': int(offset),
111                'size': int(size)
112            }
113            property_files.append(inner_file)
114
115        return property_files
116
117    def write(self, out):
118        """Writes config to the output file."""
119        with open(out, 'w') as out_file:
120            json.dump(self.config, out_file, indent=4, separators=(',', ': '), sort_keys=True)
121
122
123def main():  # pylint: disable=missing-docstring
124    ab_install_type_choices = [
125        GenUpdateConfig.AB_INSTALL_TYPE_STREAMING,
126        GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING]
127    parser = argparse.ArgumentParser(description=__doc__,
128                                     formatter_class=argparse.RawDescriptionHelpFormatter)
129    parser.add_argument('--ab_install_type',
130                        type=str,
131                        default=GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING,
132                        choices=ab_install_type_choices,
133                        help='A/B update installation type')
134    parser.add_argument('--ab_force_switch_slot',
135                        default=False,
136                        action='store_true',
137                        help='if set device will boot to a new slot, otherwise user '
138                              'manually switches slot on the screen')
139    parser.add_argument('--ab_verify_payload_metadata',
140                        default=False,
141                        action='store_true',
142                        help='if set the app will verify the update payload metadata using '
143                             'update_engine before downloading the whole package.')
144    parser.add_argument('package',
145                        type=str,
146                        help='OTA package zip file')
147    parser.add_argument('out',
148                        type=str,
149                        help='Update configuration JSON file')
150    parser.add_argument('url',
151                        type=str,
152                        help='OTA package download url')
153    args = parser.parse_args()
154
155    if not args.out.endswith('.json'):
156        print('out must be a json file')
157        sys.exit(1)
158
159    gen = GenUpdateConfig(
160        package=args.package,
161        url=args.url,
162        ab_install_type=args.ab_install_type,
163        ab_force_switch_slot=args.ab_force_switch_slot,
164        ab_verify_payload_metadata=args.ab_verify_payload_metadata)
165    gen.run()
166    gen.write(args.out)
167    print('Config is written to ' + args.out)
168
169
170if __name__ == '__main__':
171    main()
172