#!/usr/bin/env python # Copyright 2024 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import print_function from struct import pack, unpack from collections import OrderedDict import os import sys import zipfile import argparse import json import time import stm32_crc import socket import pprint MANIFEST_VERSION = 2 BUNDLE_PREFIX = 'bundle' class MissingFileException(Exception): def __init__(self, filename): self.filename = filename def flen(path): statinfo = os.stat(path) return statinfo.st_size def stm32crc(path): with open(path, 'r+b') as f: binfile = f.read() return stm32_crc.crc32(binfile) & 0xFFFFFFFF def check_paths(*args): for path in args: if not os.path.exists(path): raise MissingFileException(path) class PebbleBundle(object): def __init__(self, subfolder=None): self.generated_at = int(time.time()) self.bundle_manifest = { 'manifestVersion' : MANIFEST_VERSION, 'generatedAt' : self.generated_at, 'generatedBy' : socket.gethostname(), 'debug' : {}, } self.bundle_files = [] self.subfolder = subfolder self.has_firmware = False self.has_appinfo = False self.has_layouts = False self.has_watchapp = False self.has_worker = False self.has_resources = False self.has_jsapp = False self.has_loghash = False self.has_children = False self.has_license = False self.has_jstooling = False self.rocky_info = {} def add_firmware(self, firmware_path, firmware_type, firmware_timestamp, firmware_commit, firmware_hwrev, firmware_version_tag): if self.has_firmware: raise Exception("Added multiple firmwares to a single bundle") if self.has_watchapp or self.has_worker: raise Exception("Cannot add firmware and watchapp to a single bundle") if firmware_type != 'normal' and \ firmware_type != 'recovery': raise Exception("Invalid firmware type!") check_paths(firmware_path) self.type = 'firmware' self.bundle_files.append(firmware_path) self.bundle_manifest['firmware'] = { 'name' : os.path.basename(firmware_path), 'type' : firmware_type, 'timestamp' : firmware_timestamp, 'commit' : firmware_commit, 'hwrev' : firmware_hwrev, 'size' : flen(firmware_path), 'crc' : stm32crc(firmware_path), 'versionTag' : firmware_version_tag, } self.has_firmware = True return True def add_resources(self, resources_path, resources_timestamp, sdk_version=None): if self.has_resources: raise Exception("Added multiple resource packs to a single bundle") check_paths(resources_path) self.bundle_files.append(resources_path) self.bundle_manifest['resources'] = { 'name' : os.path.basename(resources_path), 'timestamp' : resources_timestamp, 'size' : flen(resources_path), 'crc' : stm32crc(resources_path), } # If this is a SDK-built project that is 3.x or later, check for a layouts.json file if sdk_version is not None and sdk_version['major'] >= 5 and sdk_version['minor'] > 19: timeline_resource_path = os.path.join('build', self.subfolder, 'layouts.json') # If a project doesn't contain a resource json file, don't create an app_layouts object if os.path.exists(timeline_resource_path): self.bundle_files.append(timeline_resource_path) self.bundle_manifest['app_layouts'] = os.path.basename(timeline_resource_path) self.has_resources = True return True def add_loghash(self, loghash_path): if self.has_loghash: raise Exception("Added multiple loghash to a single bundle") check_paths(loghash_path) self.bundle_files.append(loghash_path) self.has_loghash = True return True def add_license(self, license_path): if self.has_license: raise Exception("Added multiple license to a single bundle") check_paths(license_path) self.bundle_files.append(license_path) self.has_license = True return True def add_jstooling(self, jstooling_path, bytecode_version): if self.has_jstooling: raise Exception("Added multiple js_toolings to a single bundle") if not (1 <= bytecode_version <= 31): raise Exception("Invalid bytecode version {}".format(bytecode_version)) check_paths(jstooling_path) self.bundle_files.append(jstooling_path) self.bundle_manifest['js_tooling'] = { 'bytecode_version': bytecode_version } self.has_jstooling = True return True def add_rockyjs(self, rocky_path, parent_bundle=None): """ Add a rocky-app.js source file to the PBW bundle :param rocky_path: the path to the source rocky-app.js file in the project build folder :param parent_bundle: the parent PebbleBundle to write rocky-app.js to, or None if rocky-app.js should be written to the current platform subfolder :return: boolean indicating success or failure of addition """ if self.rocky_info: raise Exception("PBW already has a rocky-app.js file") # Check to see that rocky-app.js source file exists in the build folder check_paths(rocky_path) if parent_bundle: # If rocky-app.js should be written to the parent_bundle (PBW root) of this # platform/bundle, construct a relative path for 'source_path' in manifest.json if rocky_path not in parent_bundle.bundle_files: parent_bundle.bundle_files.append(rocky_path) rocky_relative_path = '../' + os.path.basename(rocky_path) else: # If rocky-app.js should be written to this platform/subfolder, use the rocky-app.js # basename for 'source_path' in manifest.json self.bundle_files.append(rocky_path) rocky_relative_path = os.path.basename(rocky_path) self.rocky_info = { 'source_path': rocky_relative_path } return True def add_appinfo(self, appinfo_path): if self.has_appinfo: raise Exception("Added multiple appinfo to a single bundle") check_paths(appinfo_path) self.bundle_files.append(appinfo_path) self.has_appinfo = True return True def add_layouts(self, layouts_path): if self.has_layouts: raise Exception("Added multiple layouts maps to a single bundle") check_paths(layouts_path) self.bundle_files.append(layouts_path) self.has_layouts = True return True def add_watchapp(self, watchapp_path, app_timestamp, sdk_version): if self.has_watchapp: raise Exception("Added multiple apps to a single bundle") if self.has_firmware: raise Exception("Cannot add watchapp and firmware to a single bundle") if sdk_version['major'] == 5 and sdk_version['minor'] < 20: self.bundle_manifest['manifestVersion'] = 1 self.type = 'application' self.bundle_files.append(watchapp_path) self.bundle_manifest['application'] = { 'timestamp': app_timestamp, 'sdk_version': sdk_version, 'name' : os.path.basename(watchapp_path), 'size': flen(watchapp_path), 'crc': stm32crc(watchapp_path), } self.has_watchapp = True return True def add_worker(self, worker_bin_path, worker_timestamp, sdk_version): if self.has_worker: raise Exception("Added multiple workers to a single bundle") if self.has_firmware: raise Exception("Cannot add worker and firmware to a single bundle") self.bundle_files.append(worker_bin_path) worker_name = os.path.basename(worker_bin_path) worker_size = flen(worker_bin_path) worker_crc = stm32crc(worker_bin_path) # NOTE: The type really should not be changed from 'application', but the 2.4 version of # the iOS app will only install background worker apps when the type is set to # 'worker'. All newer versions of the iOS app allow the correct name of 'application' # and version 2.5 accepts either. self.type = 'worker' self.bundle_manifest['worker'] = { 'timestamp': worker_timestamp, 'sdk_version': sdk_version, 'name': worker_name, 'size': worker_size, 'crc': worker_crc, } self.has_worker = True return True def add_jsapp(self, js_files): if self.has_jsapp: raise Exception("Added multiple js apps to single bundle") check_paths(*js_files) for f in js_files: self.bundle_files.append(f) self.has_jsapp = True return True def write(self, out_path = None, verbose = False): if not (self.has_firmware or self.has_watchapp): raise Exception("Bundle must contain either a firmware or watchapp") if not out_path: out_path = 'pebble-{}-{:d}.pbz'.format(self.type, self.generated_at) if verbose: pprint.pprint(self.bundle_manifest) print('writing bundle to {}'.format(out_path)) with zipfile.ZipFile(out_path, 'w') as z: for f in self.bundle_files: if isinstance(f, PebbleBundle): for bf in f.bundle_files: z.write(bf, os.path.join(f.subfolder, os.path.basename(bf))) f.bundle_manifest['type'] = f.type if f.rocky_info: f.bundle_manifest['rocky'] = f.rocky_info z.writestr(os.path.join(f.subfolder, 'manifest.json'), json.dumps(f.bundle_manifest)) else: z.write(f, os.path.basename(f)) if not self.has_children: self.bundle_manifest['type'] = self.type z.writestr('manifest.json', json.dumps(self.bundle_manifest)) if verbose: print('done!') def check_required_args(opts, *args): options = vars(opts) for required_arg in args: try: if not options[required_arg]: raise Exception("Missing argument {}".format(required_arg)) except KeyError: raise Exception("Missing argument {}".format(required_arg)) def make_firmware_bundle(firmware, firmware_timestamp, firmware_commit, firmware_type, board, firmware_version_tag, resources=None, resources_timestamp=None, outfile=None, verbose=False): bundle = PebbleBundle() firmware_path = os.path.expanduser(firmware) bundle.add_firmware(firmware_path, firmware_type, firmware_timestamp, firmware_commit, board, firmware_version_tag) if resources: resources_path = os.path.expanduser(args.resources) bundle.add_resources(resources_path, args.resources_timestamp) bundle.write(outfile, verbose) def make_watchapp_bundle(timestamp, appinfo, binaries, js, outfile=None, verbose=False): """ Makes a pbw for an watch app, which includes a firmware, a resource pack and optionally a list of javascript files. Keyword arguments timestamp -- bundle timestamp appinfo -- path to the appinfo.json for the watch app sdk_version -- version of the Pebble SDK used to build binaries binaries -- list of binaries built for Pebble platforms 'watchapp' -- path to the watchapp binary file 'resources' -- path to resource .pbpack 'worker_bin' -- (optional) path to the worker binary file 'sdk_version' -- version of SDK used to build binary 'subfolder' -- path to subfolder in PBW for platform binary js -- (optional) a list of paths to javascript files to be included outfile -- path to write the pbw to """ bundle = PebbleBundle() appinfo_path = os.path.expanduser(appinfo) bundle.add_appinfo(appinfo_path) rocky_files = {} for js_file in js: if js_file.endswith('rocky-app.js'): platform = os.path.dirname(os.path.relpath(js_file, 'build')).split('/', 1)[0] rocky_files[platform] = js_file js.remove(js_file) continue bundle.add_jsapp(js) if len(binaries) < 1: raise Exception("Cannot bundle watchapp without binaries") for binary in binaries: bundle.has_children = True platform_bundle = PebbleBundle(subfolder=binary['subfolder']) if rocky_files: rocky_file = rocky_files.get(platform_bundle.subfolder, rocky_files.get('resources', None)) platform_bundle.add_rockyjs(rocky_file, bundle) if binary['watchapp']: watchapp_path = os.path.expanduser(binary['watchapp']) platform_bundle.add_watchapp(watchapp_path, timestamp, binary['sdk_version']) bundle.has_watchapp = True if binary['worker_bin']: worker_bin_path = os.path.expanduser(binary['worker_bin']) platform_bundle.add_worker(worker_bin_path, timestamp, binary['sdk_version']) if binary['resources']: resources_path = os.path.expanduser(binary['resources']) platform_bundle.add_resources(resources_path, timestamp, binary['sdk_version']) bundle.bundle_files.append(platform_bundle) bundle.write(outfile, verbose) def cmd_firmware(args): make_firmware_bundle(**vars(args)) def cmd_watchapp(args): args.sdk_verison = dict(zip(['major', 'minor'], [int(x) for x in args.sdk_version.split('.')])) make_watchapp_bundle(**vars(args)) if __name__ == '__main__': parser = argparse.ArgumentParser(description='Create a Pebble bundle.') subparsers = parser.add_subparsers(help='commands') firmware_parser = subparsers.add_parser('firmware', help='create a Pebble firmware bundle') firmware_parser.add_argument('--firmware', help='path to the firmware .bin') firmware_parser.add_argument('--firmware-timestamp', help='the (git) timestamp of the firmware', type=int) firmware_parser.add_argument('--firmware-type', help='the type of firmware included in the bundle', choices = ['normal', 'recovery']) firmware_parser.add_argument('--board', help='the board for which the firmware was built', choices = ['bigboard', 'ev1', 'ev2']) firmware_parser.add_argument('--firmware-version', help='the firmware version tag') firmware_parser.set_defaults(func=cmd_firmware) watchapp_parser = subparsers.add_parser('watchapp', help='create Pebble watchapp bundle') watchapp_parser.add_argument('--appinfo', help='path to appinfo.json') watchapp_parser.add_argument('--watchapp', help='path to the watchapp .bin') watchapp_parser.add_argument('--watchapp-timestamp', help='the (git) timestamp of the app', type=int) watchapp_parser.add_argument('--javascript', help='path to the directory with the javascript app files to include') watchapp_parser.add_argument('--sdk-version', help='the SDK platform version required to run the app', type=str) watchapp_parser.add_argument('--resources', help='path to the generated resource pack') watchapp_parser.add_argument('--resources-timestamp', help='the (git) timestamp of the resource pack', type=int) watchapp_parser.add_argument("-v", "--verbose", help="print additional output", action="store_true") watchapp_parser.add_argument("-o", "--outfile", help="path to the output file") watchapp_parser.set_defaults(func=cmd_watchapp) if len(sys.argv) <= 1: parser.print_help() sys.exit(1) args = parser.parse_args() parser_func = args.func del args.func parser_func(args)