# 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. import json import os import struct import re from waflib import Logs from pebble_package import LibraryPackage from pebble_sdk_platform import pebble_platforms, maybe_import_internal from pebble_sdk_version import set_env_sdk_version from resources.types.resource_object import ResourceObject def _get_pbi_size(data): """ This method takes resource data and determines the dimensions of the pbi :param data: the data contained in the pbi, starting at the header :return: tuple containing the width and height of the pbi """ # Read the first byte at header offset 0x08 for width width = struct.unpack('I', data[6:8])[0] # Read the next 2 bytes after the width to get the height height = struct.unpack('>I', data[8:10])[0] return width, height def _get_png_size(data): """ This resource takes resource data and determines the dimensions of the PNG :param data: the data contained in the PNG, starting at the IHDR :return: tuple containing the width and height of the PNG """ # Assert that this is the IHDR header assert data[:4] == 'IHDR' # Read the first 4 bytes after IHDR for width width = struct.unpack('>I', data[4:8])[0] # Read the next 4 bytes after the width to get the height height = struct.unpack('>I', data[8:12])[0] return width, height def _get_supported_platforms(ctx, has_rocky=False): """ This method returns all of the supported SDK platforms, based off of SDK requirements found on the filesystem :param ctx: the Context object :return: a list of the platforms that are supported for the given SDK """ sdk_check_nodes = ['lib/libpebble.a', 'pebble_app.ld.template', 'tools', 'include', 'include/pebble.h'] supported_platforms = os.listdir(ctx.env.PEBBLE_SDK_ROOT) invalid_platforms = [] for platform in supported_platforms: pebble_sdk_platform = ctx.root.find_node(ctx.env.PEBBLE_SDK_ROOT).find_node(platform) for node in sdk_check_nodes: if pebble_sdk_platform.find_node(node) is None: if ctx.root.find_node(ctx.env.PEBBLE_SDK_COMMON).find_node(node) is None: invalid_platforms.append(platform) break for platform in invalid_platforms: supported_platforms.remove(platform) if has_rocky and 'aplite' in supported_platforms: supported_platforms.remove('aplite') ctx.env.SUPPORTED_PLATFORMS = supported_platforms return supported_platforms def append_to_attr(self, attr, new_values): """ This helper method appends `new_values` to `attr` on the object `self` :param self: the object :param attr: the attribute to modify :param new_values: the value(s) to set on the attribute :return: N/A """ values = self.to_list(getattr(self, attr, [])) if not isinstance(new_values, list): new_values = [new_values] values.extend(new_values) setattr(self, attr, values) def configure_libraries(ctx, libraries): dependencies = libraries.keys() lib_json = [] lib_resources_json = {} index = 0 while index < len(dependencies): info, resources, additional_deps = process_package(ctx, dependencies[index]) lib_json.append(info) lib_resources_json[dependencies[index]] = resources dependencies.extend(additional_deps) index += 1 # Store package.json info for each library and add resources to an environment variable for # dependency-checking ctx.env.LIB_JSON = lib_json if lib_resources_json: ctx.env.LIB_RESOURCES_JSON = lib_resources_json def configure_platform(ctx, platform): """ Configure a build for the specified :param ctx: the ConfigureContext :param platform: the hardware platform this build is being targeted for :return: N/A """ pebble_sdk_root = get_node_from_abspath(ctx, ctx.env.PEBBLE_SDK_ROOT) ctx.env.PLATFORM = pebble_platforms[platform] ctx.env.PEBBLE_SDK_PLATFORM = pebble_sdk_root.find_node(str(platform)).abspath() ctx.env.PLATFORM_NAME = ctx.env.PLATFORM['NAME'] for attribute in ['DEFINES']: # Attributes with list values ctx.env.append_unique(attribute, ctx.env.PLATFORM[attribute]) for attribute in ['BUILD_DIR', 'BUNDLE_BIN_DIR']: # Attributes with a single value ctx.env[attribute] = ctx.env.PLATFORM[attribute] ctx.env.append_value('INCLUDES', ctx.env.BUILD_DIR) ctx.msg("Found Pebble SDK for {} in:".format(platform), ctx.env.PEBBLE_SDK_PLATFORM) process_info = ( pebble_sdk_root.find_node(str(platform)).find_node('include/pebble_process_info.h')) set_env_sdk_version(ctx, process_info) if is_sdk_2x(ctx.env.SDK_VERSION_MAJOR, ctx.env.SDK_VERSION_MINOR): ctx.env.append_value('DEFINES', "PBL_SDK_2") else: ctx.env.append_value('DEFINES', "PBL_SDK_3") ctx.load('pebble_sdk_gcc') def find_sdk_component(ctx, env, component): """ This method finds an SDK component, either in the platform SDK folder, or the 'common' folder :param ctx: the Context object :param env: the environment which contains platform SDK folder path for the current platform :param component: the SDK component being sought :return: the path to the SDK component being sought """ return (ctx.root.find_node(env.PEBBLE_SDK_PLATFORM).find_node(component) or ctx.root.find_node(env.PEBBLE_SDK_COMMON).find_node(component)) def get_node_from_abspath(ctx, path): return ctx.root.make_node(path) def get_target_platforms(ctx): """ This method returns a list of target platforms for a build, by comparing the list of requested platforms to the list of supported platforms, returning all of the supported platforms if no specific platforms are requested :param ctx: the Context object :return: list of target platforms for the build """ supported_platforms = _get_supported_platforms(ctx, ctx.env.BUILD_TYPE == 'rocky') if not ctx.env.REQUESTED_PLATFORMS: target_platforms = supported_platforms else: target_platforms = list(set(supported_platforms) & set(ctx.env.REQUESTED_PLATFORMS)) if not target_platforms: ctx.fatal("No valid targetPlatforms specified in appinfo.json. Valid options are {}" .format(supported_platforms)) ctx.env.TARGET_PLATFORMS = sorted([p.encode('utf-8') for p in target_platforms], reverse=True) return target_platforms def is_sdk_2x(major, minor): """ This method checks if a . API version are associated with a 2.x version of the SDK :param major: the major API version to check :param minor: the minor API version to check :return: boolean representing whether a 2.x SDK is being used or not """ LAST_2X_MAJOR_VERSION = 5 LAST_2X_MINOR_VERSION = 19 return (major, minor) <= (LAST_2X_MAJOR_VERSION, LAST_2X_MINOR_VERSION) def process_package(ctx, package, root_lib_node=None): """ This method parses the package.json for a given package and returns relevant information :param ctx: the Context object :param root_lib_node: node containing the package to be processed, if not the standard LIB_DIR :param package: the package to parse information for :return: - a dictionary containing the contents of package.json - a dictionary containing the resources object for the package - a list of dependencies for this package """ resources_json = {} if not root_lib_node: root_lib_node = ctx.path.find_node(ctx.env.LIB_DIR) if root_lib_node is None: ctx.fatal("Missing {} directory".format(ctx.env.LIB_DIR)) lib_node = root_lib_node.find_node(str(package)) if lib_node is None: ctx.fatal("Missing library for {} in {}".format(str(package), ctx.env.LIB_DIR)) else: libinfo_node = lib_node.find_node('package.json') if libinfo_node is None: ctx.fatal("Missing package.json for {} library".format(str(package))) else: if lib_node.find_node(ctx.env.LIB_DIR): error_str = ("ERROR: Multiple versions of the same package are not supported by " "the Pebble SDK due to namespace issues during linking. Package '{}' " "contains the following duplicate and incompatible dependencies, " "which may lead to additional build errors and/or unpredictable " "runtime behavior:\n".format(package)) packages_str = "" for package in lib_node.find_node(ctx.env.LIB_DIR).ant_glob('**/package.json'): with open(package.abspath()) as f: info = json.load(f) if not dict(ctx.env.PROJECT_INFO).get('enableMultiJS', False): if not 'pebble' in info: continue packages_str += " '{}': '{}'\n".format(info['name'], info['version']) if packages_str: Logs.pprint("RED", error_str + packages_str) with open(libinfo_node.abspath()) as f: libinfo = json.load(f) if 'pebble' in libinfo: if ctx.env.BUILD_TYPE == 'rocky': ctx.fatal("Packages containing C binaries are not compatible with Rocky.js " "projects. Please remove '{}' from the `dependencies` object in " "package.json".format(libinfo['name'])) libinfo['path'] = lib_node.make_node('dist').path_from(ctx.path) if 'resources' in libinfo['pebble']: if 'media' in libinfo['pebble']['resources']: resources_json = libinfo['pebble']['resources']['media'] # Extract package into "dist" folder dist_node = lib_node.find_node('dist.zip') if not dist_node: ctx.fatal("Missing dist.zip file for {}. Are you sure this is a Pebble " "library?".format(package)) lib_package = LibraryPackage(dist_node.abspath()) lib_package.unpack(libinfo['path']) lib_js_node = lib_node.find_node('dist/js') if lib_js_node: libinfo['js_paths'] = [lib_js.path_from(ctx.path) for lib_js in lib_js_node.ant_glob(['**/*.js', '**/*.json'])] else: libinfo['js_paths'] = [lib_js.path_from(ctx.path) for lib_js in lib_node.ant_glob(['**/*.js', '**/*.json'], excl="**/*.min.js")] dependencies = libinfo['dependencies'].keys() if 'dependencies' in libinfo else [] return libinfo, resources_json, dependencies def truncate_to_32_bytes(name): """ This method takes an input string and returns a 32-byte truncated string if the input string is longer than 32 bytes :param name: the string to truncate :return: the truncated string, if the input string was > 32 bytes, or else the original input string """ return name[:30] + '..' if len(name) > 32 else name def validate_message_keys_object(ctx, project_info, info_json_type): """ Verify that the appropriately-named message key object is present in the project info file :param ctx: the ConfigureContext object :param project_info: JSON object containing project info :param info_json_type: string containing the name of the file used to extract project info :return: N/A """ if 'appKeys' in project_info and info_json_type == 'package.json': ctx.fatal("Project contains an invalid object `appKeys` in package.json. Please use " "`messageKeys` instead.") if 'messageKeys' in project_info and info_json_type == 'appinfo.json': ctx.fatal("Project contains an invalid object `messageKeys` in appinfo.json. Please use " "`appKeys` instead.") def validate_resource_not_larger_than(ctx, resource_file, dimensions=None, width=None, height=None): """ This method takes a resource file and determines whether the file's dimensions exceed the maximum allowed values provided. :param resource_file: the path to the resource file :param dimensions: tuple specifying max width and height :param width: number specifying max width :param height: number specifying max height :return: boolean for whether the resource is larger than the maximum allowed size """ if not dimensions and not width and not height: raise TypeError("Missing values for maximum width and/or height to validate against") if dimensions: width, height = dimensions with open(resource_file, 'rb') as f: if resource_file.endswith('.reso'): reso = ResourceObject.load(resource_file) if reso.definition.type == 'bitmap': storage_format = reso.definition.storage_format else: storage_format = reso.definition.type if storage_format == 'pbi': resource_size = _get_pbi_size(reso.data) elif storage_format == 'png': resource_size = _get_png_size(reso.data[12:]) elif storage_format == 'raw': try: assert reso.data[4:] == 'PDCI' except AssertionError: ctx.fatal("Unsupported published resource type for {}".format(resource_file)) else: resource_size = _get_pdc_size(reso.data[4:]) else: data = f.read(24) if data[1:4] == 'PNG': resource_size = _get_png_size(data[12:]) elif data[:4] == 'PDCI': resource_size = _get_pdc_size(data[4:]) else: ctx.fatal("Unsupported published resource type for {}".format(resource_file)) if width and height: return resource_size <= (width, height) elif width: return resource_size[0] <= width elif height: return resource_size[1] <= height def wrap_task_name_with_platform(self): """ This method replaces the existing waf Task class's __str__ method with the original content of the __str__ method, as well as an additional " | " before the task information, if a platform is set. :param self: the task instance :return: the user-friendly string to print """ src_str = ' '.join([a.nice_path() for a in self.inputs]) tgt_str = ' '.join([a.nice_path() for a in self.outputs]) sep = ' -> ' if self.outputs else '' name = self.__class__.__name__.replace('_task', '') # Modification to the original __str__ method if self.env.PLATFORM_NAME: name = self.env.PLATFORM_NAME + " | " + name return '%s: %s%s%s\n' % (name, src_str, sep, tgt_str)