sdk_helpers.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. # Copyright 2024 Google LLC
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. import json
  15. import os
  16. import struct
  17. import re
  18. from waflib import Logs
  19. from pebble_package import LibraryPackage
  20. from pebble_sdk_platform import pebble_platforms, maybe_import_internal
  21. from pebble_sdk_version import set_env_sdk_version
  22. from resources.types.resource_object import ResourceObject
  23. def _get_pbi_size(data):
  24. """
  25. This method takes resource data and determines the dimensions of the pbi
  26. :param data: the data contained in the pbi, starting at the header
  27. :return: tuple containing the width and height of the pbi
  28. """
  29. # Read the first byte at header offset 0x08 for width
  30. width = struct.unpack('<h', data[8:10])[0]
  31. # Read the next 2 bytes after the width to get the height
  32. height = struct.unpack('<h', data[10:12])[0]
  33. return width, height
  34. def _get_pdc_size(data):
  35. """
  36. This method takes resource data and determines the dimensions of the PDC
  37. :param data: the data contained in the PDC, starting at the header
  38. :return: tuple containing the width and height of the PDC
  39. """
  40. # Read the first 2 bytes at header offset 0x06 for width
  41. width = struct.unpack('>I', data[6:8])[0]
  42. # Read the next 2 bytes after the width to get the height
  43. height = struct.unpack('>I', data[8:10])[0]
  44. return width, height
  45. def _get_png_size(data):
  46. """
  47. This resource takes resource data and determines the dimensions of the PNG
  48. :param data: the data contained in the PNG, starting at the IHDR
  49. :return: tuple containing the width and height of the PNG
  50. """
  51. # Assert that this is the IHDR header
  52. assert data[:4] == 'IHDR'
  53. # Read the first 4 bytes after IHDR for width
  54. width = struct.unpack('>I', data[4:8])[0]
  55. # Read the next 4 bytes after the width to get the height
  56. height = struct.unpack('>I', data[8:12])[0]
  57. return width, height
  58. def _get_supported_platforms(ctx, has_rocky=False):
  59. """
  60. This method returns all of the supported SDK platforms, based off of SDK requirements found on
  61. the filesystem
  62. :param ctx: the Context object
  63. :return: a list of the platforms that are supported for the given SDK
  64. """
  65. sdk_check_nodes = ['lib/libpebble.a',
  66. 'pebble_app.ld.template',
  67. 'tools',
  68. 'include',
  69. 'include/pebble.h']
  70. supported_platforms = os.listdir(ctx.env.PEBBLE_SDK_ROOT)
  71. invalid_platforms = []
  72. for platform in supported_platforms:
  73. pebble_sdk_platform = ctx.root.find_node(ctx.env.PEBBLE_SDK_ROOT).find_node(platform)
  74. for node in sdk_check_nodes:
  75. if pebble_sdk_platform.find_node(node) is None:
  76. if ctx.root.find_node(ctx.env.PEBBLE_SDK_COMMON).find_node(node) is None:
  77. invalid_platforms.append(platform)
  78. break
  79. for platform in invalid_platforms:
  80. supported_platforms.remove(platform)
  81. if has_rocky and 'aplite' in supported_platforms:
  82. supported_platforms.remove('aplite')
  83. ctx.env.SUPPORTED_PLATFORMS = supported_platforms
  84. return supported_platforms
  85. def append_to_attr(self, attr, new_values):
  86. """
  87. This helper method appends `new_values` to `attr` on the object `self`
  88. :param self: the object
  89. :param attr: the attribute to modify
  90. :param new_values: the value(s) to set on the attribute
  91. :return: N/A
  92. """
  93. values = self.to_list(getattr(self, attr, []))
  94. if not isinstance(new_values, list):
  95. new_values = [new_values]
  96. values.extend(new_values)
  97. setattr(self, attr, values)
  98. def configure_libraries(ctx, libraries):
  99. dependencies = libraries.keys()
  100. lib_json = []
  101. lib_resources_json = {}
  102. index = 0
  103. while index < len(dependencies):
  104. info, resources, additional_deps = process_package(ctx, dependencies[index])
  105. lib_json.append(info)
  106. lib_resources_json[dependencies[index]] = resources
  107. dependencies.extend(additional_deps)
  108. index += 1
  109. # Store package.json info for each library and add resources to an environment variable for
  110. # dependency-checking
  111. ctx.env.LIB_JSON = lib_json
  112. if lib_resources_json:
  113. ctx.env.LIB_RESOURCES_JSON = lib_resources_json
  114. def configure_platform(ctx, platform):
  115. """
  116. Configure a build for the <platform> specified
  117. :param ctx: the ConfigureContext
  118. :param platform: the hardware platform this build is being targeted for
  119. :return: N/A
  120. """
  121. pebble_sdk_root = get_node_from_abspath(ctx, ctx.env.PEBBLE_SDK_ROOT)
  122. ctx.env.PLATFORM = pebble_platforms[platform]
  123. ctx.env.PEBBLE_SDK_PLATFORM = pebble_sdk_root.find_node(str(platform)).abspath()
  124. ctx.env.PLATFORM_NAME = ctx.env.PLATFORM['NAME']
  125. for attribute in ['DEFINES']: # Attributes with list values
  126. ctx.env.append_unique(attribute, ctx.env.PLATFORM[attribute])
  127. for attribute in ['BUILD_DIR', 'BUNDLE_BIN_DIR']: # Attributes with a single value
  128. ctx.env[attribute] = ctx.env.PLATFORM[attribute]
  129. ctx.env.append_value('INCLUDES', ctx.env.BUILD_DIR)
  130. ctx.msg("Found Pebble SDK for {} in:".format(platform), ctx.env.PEBBLE_SDK_PLATFORM)
  131. process_info = (
  132. pebble_sdk_root.find_node(str(platform)).find_node('include/pebble_process_info.h'))
  133. set_env_sdk_version(ctx, process_info)
  134. if is_sdk_2x(ctx.env.SDK_VERSION_MAJOR, ctx.env.SDK_VERSION_MINOR):
  135. ctx.env.append_value('DEFINES', "PBL_SDK_2")
  136. else:
  137. ctx.env.append_value('DEFINES', "PBL_SDK_3")
  138. ctx.load('pebble_sdk_gcc')
  139. def find_sdk_component(ctx, env, component):
  140. """
  141. This method finds an SDK component, either in the platform SDK folder, or the 'common' folder
  142. :param ctx: the Context object
  143. :param env: the environment which contains platform SDK folder path for the current platform
  144. :param component: the SDK component being sought
  145. :return: the path to the SDK component being sought
  146. """
  147. return (ctx.root.find_node(env.PEBBLE_SDK_PLATFORM).find_node(component) or
  148. ctx.root.find_node(env.PEBBLE_SDK_COMMON).find_node(component))
  149. def get_node_from_abspath(ctx, path):
  150. return ctx.root.make_node(path)
  151. def get_target_platforms(ctx):
  152. """
  153. This method returns a list of target platforms for a build, by comparing the list of requested
  154. platforms to the list of supported platforms, returning all of the supported platforms if no
  155. specific platforms are requested
  156. :param ctx: the Context object
  157. :return: list of target platforms for the build
  158. """
  159. supported_platforms = _get_supported_platforms(ctx, ctx.env.BUILD_TYPE == 'rocky')
  160. if not ctx.env.REQUESTED_PLATFORMS:
  161. target_platforms = supported_platforms
  162. else:
  163. target_platforms = list(set(supported_platforms) & set(ctx.env.REQUESTED_PLATFORMS))
  164. if not target_platforms:
  165. ctx.fatal("No valid targetPlatforms specified in appinfo.json. Valid options are {}"
  166. .format(supported_platforms))
  167. ctx.env.TARGET_PLATFORMS = sorted([p.encode('utf-8') for p in target_platforms], reverse=True)
  168. return target_platforms
  169. def is_sdk_2x(major, minor):
  170. """
  171. This method checks if a <major>.<minor> API version are associated with a 2.x version of the SDK
  172. :param major: the major API version to check
  173. :param minor: the minor API version to check
  174. :return: boolean representing whether a 2.x SDK is being used or not
  175. """
  176. LAST_2X_MAJOR_VERSION = 5
  177. LAST_2X_MINOR_VERSION = 19
  178. return (major, minor) <= (LAST_2X_MAJOR_VERSION, LAST_2X_MINOR_VERSION)
  179. def process_package(ctx, package, root_lib_node=None):
  180. """
  181. This method parses the package.json for a given package and returns relevant information
  182. :param ctx: the Context object
  183. :param root_lib_node: node containing the package to be processed, if not the standard LIB_DIR
  184. :param package: the package to parse information for
  185. :return:
  186. - a dictionary containing the contents of package.json
  187. - a dictionary containing the resources object for the package
  188. - a list of dependencies for this package
  189. """
  190. resources_json = {}
  191. if not root_lib_node:
  192. root_lib_node = ctx.path.find_node(ctx.env.LIB_DIR)
  193. if root_lib_node is None:
  194. ctx.fatal("Missing {} directory".format(ctx.env.LIB_DIR))
  195. lib_node = root_lib_node.find_node(str(package))
  196. if lib_node is None:
  197. ctx.fatal("Missing library for {} in {}".format(str(package), ctx.env.LIB_DIR))
  198. else:
  199. libinfo_node = lib_node.find_node('package.json')
  200. if libinfo_node is None:
  201. ctx.fatal("Missing package.json for {} library".format(str(package)))
  202. else:
  203. if lib_node.find_node(ctx.env.LIB_DIR):
  204. error_str = ("ERROR: Multiple versions of the same package are not supported by "
  205. "the Pebble SDK due to namespace issues during linking. Package '{}' "
  206. "contains the following duplicate and incompatible dependencies, "
  207. "which may lead to additional build errors and/or unpredictable "
  208. "runtime behavior:\n".format(package))
  209. packages_str = ""
  210. for package in lib_node.find_node(ctx.env.LIB_DIR).ant_glob('**/package.json'):
  211. with open(package.abspath()) as f:
  212. info = json.load(f)
  213. if not dict(ctx.env.PROJECT_INFO).get('enableMultiJS', False):
  214. if not 'pebble' in info:
  215. continue
  216. packages_str += " '{}': '{}'\n".format(info['name'], info['version'])
  217. if packages_str:
  218. Logs.pprint("RED", error_str + packages_str)
  219. with open(libinfo_node.abspath()) as f:
  220. libinfo = json.load(f)
  221. if 'pebble' in libinfo:
  222. if ctx.env.BUILD_TYPE == 'rocky':
  223. ctx.fatal("Packages containing C binaries are not compatible with Rocky.js "
  224. "projects. Please remove '{}' from the `dependencies` object in "
  225. "package.json".format(libinfo['name']))
  226. libinfo['path'] = lib_node.make_node('dist').path_from(ctx.path)
  227. if 'resources' in libinfo['pebble']:
  228. if 'media' in libinfo['pebble']['resources']:
  229. resources_json = libinfo['pebble']['resources']['media']
  230. # Extract package into "dist" folder
  231. dist_node = lib_node.find_node('dist.zip')
  232. if not dist_node:
  233. ctx.fatal("Missing dist.zip file for {}. Are you sure this is a Pebble "
  234. "library?".format(package))
  235. lib_package = LibraryPackage(dist_node.abspath())
  236. lib_package.unpack(libinfo['path'])
  237. lib_js_node = lib_node.find_node('dist/js')
  238. if lib_js_node:
  239. libinfo['js_paths'] = [lib_js.path_from(ctx.path) for lib_js in
  240. lib_js_node.ant_glob(['**/*.js', '**/*.json'])]
  241. else:
  242. libinfo['js_paths'] = [lib_js.path_from(ctx.path) for lib_js in
  243. lib_node.ant_glob(['**/*.js', '**/*.json'],
  244. excl="**/*.min.js")]
  245. dependencies = libinfo['dependencies'].keys() if 'dependencies' in libinfo else []
  246. return libinfo, resources_json, dependencies
  247. def truncate_to_32_bytes(name):
  248. """
  249. This method takes an input string and returns a 32-byte truncated string if the input string is
  250. longer than 32 bytes
  251. :param name: the string to truncate
  252. :return: the truncated string, if the input string was > 32 bytes, or else the original input
  253. string
  254. """
  255. return name[:30] + '..' if len(name) > 32 else name
  256. def validate_message_keys_object(ctx, project_info, info_json_type):
  257. """
  258. Verify that the appropriately-named message key object is present in the project info file
  259. :param ctx: the ConfigureContext object
  260. :param project_info: JSON object containing project info
  261. :param info_json_type: string containing the name of the file used to extract project info
  262. :return: N/A
  263. """
  264. if 'appKeys' in project_info and info_json_type == 'package.json':
  265. ctx.fatal("Project contains an invalid object `appKeys` in package.json. Please use "
  266. "`messageKeys` instead.")
  267. if 'messageKeys' in project_info and info_json_type == 'appinfo.json':
  268. ctx.fatal("Project contains an invalid object `messageKeys` in appinfo.json. Please use "
  269. "`appKeys` instead.")
  270. def validate_resource_not_larger_than(ctx, resource_file, dimensions=None, width=None, height=None):
  271. """
  272. This method takes a resource file and determines whether the file's dimensions exceed the
  273. maximum allowed values provided.
  274. :param resource_file: the path to the resource file
  275. :param dimensions: tuple specifying max width and height
  276. :param width: number specifying max width
  277. :param height: number specifying max height
  278. :return: boolean for whether the resource is larger than the maximum allowed size
  279. """
  280. if not dimensions and not width and not height:
  281. raise TypeError("Missing values for maximum width and/or height to validate against")
  282. if dimensions:
  283. width, height = dimensions
  284. with open(resource_file, 'rb') as f:
  285. if resource_file.endswith('.reso'):
  286. reso = ResourceObject.load(resource_file)
  287. if reso.definition.type == 'bitmap':
  288. storage_format = reso.definition.storage_format
  289. else:
  290. storage_format = reso.definition.type
  291. if storage_format == 'pbi':
  292. resource_size = _get_pbi_size(reso.data)
  293. elif storage_format == 'png':
  294. resource_size = _get_png_size(reso.data[12:])
  295. elif storage_format == 'raw':
  296. try:
  297. assert reso.data[4:] == 'PDCI'
  298. except AssertionError:
  299. ctx.fatal("Unsupported published resource type for {}".format(resource_file))
  300. else:
  301. resource_size = _get_pdc_size(reso.data[4:])
  302. else:
  303. data = f.read(24)
  304. if data[1:4] == 'PNG':
  305. resource_size = _get_png_size(data[12:])
  306. elif data[:4] == 'PDCI':
  307. resource_size = _get_pdc_size(data[4:])
  308. else:
  309. ctx.fatal("Unsupported published resource type for {}".format(resource_file))
  310. if width and height:
  311. return resource_size <= (width, height)
  312. elif width:
  313. return resource_size[0] <= width
  314. elif height:
  315. return resource_size[1] <= height
  316. def wrap_task_name_with_platform(self):
  317. """
  318. This method replaces the existing waf Task class's __str__ method with the original content
  319. of the __str__ method, as well as an additional "<platform> | " before the task information,
  320. if a platform is set.
  321. :param self: the task instance
  322. :return: the user-friendly string to print
  323. """
  324. src_str = ' '.join([a.nice_path() for a in self.inputs])
  325. tgt_str = ' '.join([a.nice_path() for a in self.outputs])
  326. sep = ' -> ' if self.outputs else ''
  327. name = self.__class__.__name__.replace('_task', '')
  328. # Modification to the original __str__ method
  329. if self.env.PLATFORM_NAME:
  330. name = self.env.PLATFORM_NAME + " | " + name
  331. return '%s: %s%s%s\n' % (name, src_str, sep, tgt_str)