pebble_sdk.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  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. from waflib.Configure import conf
  16. from waflib.Errors import ConfigurationError
  17. from waflib import Logs
  18. import sdk_paths
  19. from generate_appinfo import generate_appinfo_c
  20. from process_sdk_resources import generate_resources
  21. import report_memory_usage
  22. from sdk_helpers import (configure_libraries, configure_platform, find_sdk_component,
  23. get_target_platforms, truncate_to_32_bytes, validate_message_keys_object)
  24. def _extract_project_info(conf, info_json, json_filename):
  25. """
  26. Extract project info from "pebble" object, or copy configuration directly if read from
  27. appinfo.json
  28. :param conf: the ConfigurationContext
  29. :param info_json: the JSON blob contained in appinfo.json or package.json
  30. :return: JSON blob containing project information for build
  31. """
  32. if 'pebble' in info_json:
  33. project_info = info_json['pebble']
  34. validate_message_keys_object(conf, project_info, 'package.json')
  35. project_info['name'] = info_json['name']
  36. project_info['shortName'] = project_info['longName'] = project_info['displayName']
  37. # Validate version specified in package.json to avoid issues later
  38. if not info_json['version']:
  39. conf.fatal("Project is missing a version")
  40. version = _validate_version(conf, info_json['version'])
  41. project_info['versionLabel'] = version
  42. if isinstance(info_json['author'], basestring):
  43. project_info['companyName'] = (
  44. info_json['author'].split('(', 1)[0].split('<', 1)[0].strip())
  45. elif isinstance(info_json['author'], dict) and 'name' in info_json['author']:
  46. project_info['companyName'] = info_json['author']['name']
  47. else:
  48. conf.fatal("Missing author name in project info")
  49. elif 'package.json' == json_filename:
  50. try:
  51. with open(conf.path.get_src().find_node('appinfo.json').abspath(), 'r') as f:
  52. info_json = json.load(f)
  53. except AttributeError:
  54. conf.fatal("Could not find Pebble project info in package.json and no appinfo.json file"
  55. " exists")
  56. project_info = info_json
  57. validate_message_keys_object(conf, project_info, 'appinfo.json')
  58. else:
  59. project_info = info_json
  60. validate_message_keys_object(conf, project_info, 'appinfo.json')
  61. return project_info
  62. def _generate_appinfo_c_file(task):
  63. """
  64. This Task generates the appinfo.auto.c file that is included in binary metadata
  65. :param task: the instance of this task
  66. :return: N/A
  67. """
  68. info_json = dict(getattr(task.generator.env, task.vars[0]))
  69. info_json['shortName'] = truncate_to_32_bytes(info_json['shortName'])
  70. info_json['companyName'] = truncate_to_32_bytes(info_json['companyName'])
  71. current_platform = task.generator.env.PLATFORM_NAME
  72. generate_appinfo_c(info_json, task.outputs[0].abspath(), current_platform)
  73. def _write_appinfo_json_file(task):
  74. """
  75. This task writes the content of the PROJECT_INFO environment variable to appinfo.json in the
  76. build directory. PROJECT_INFO is generated from reading in either a package.json file or an
  77. old-style appinfo.json file.
  78. :param task: the task instance
  79. :return: None
  80. """
  81. appinfo = dict(getattr(task.generator.env, task.vars[0]))
  82. capabilities = appinfo.get('capabilities', [])
  83. for lib in dict(task.generator.env).get('LIB_JSON', []):
  84. if 'pebble' in lib:
  85. capabilities.extend(lib['pebble'].get('capabilities', []))
  86. appinfo['capabilities'] = list(set(capabilities))
  87. for key in task.env.BLOCK_MESSAGE_KEYS:
  88. del appinfo['appKeys'][key]
  89. if appinfo:
  90. with open(task.outputs[0].abspath(), 'w') as f:
  91. json.dump(appinfo, f, indent=4)
  92. else:
  93. task.generator.bld.fatal("Unable to find project info to populate appinfo.json file with")
  94. def _validate_version(ctx, original_version):
  95. """
  96. Validates the format of the version field in an app's project info, and strips off a
  97. zero-valued patch version number, if it exists, to be compatible with the Pebble FW
  98. :param ctx: the ConfigureContext object
  99. :param version: the version provided in project info (package.json/appinfo.json)
  100. :return: a MAJOR.MINOR version that is acceptable for Pebble FW
  101. """
  102. version = original_version.split('.')
  103. if len(version) > 3:
  104. ctx.fatal("App versions must be of the format MAJOR or MAJOR.MINOR or MAJOR.MINOR.0. An "
  105. "invalid version of {} was specified for the app. Try {}.{}.0 instead".
  106. format(original_version, version[0], version[1]))
  107. elif not (0 <= int(version[0]) <= 255):
  108. ctx.fatal("An invalid or out of range value of {} was specified for the major version of "
  109. "the app. The valid range is 0-255.".format(version[0]))
  110. elif not (0 <= int(version[1]) <= 255):
  111. ctx.fatal("An invalid or out of range value of {} was specified for the minor version of "
  112. "the app. The valid range is 0-255.".format(version[1]))
  113. elif len(version) > 2 and not (int(version[2]) == 0):
  114. ctx.fatal("The patch version of an app must be 0, but {} was specified ({}). Try {}.{}.0 "
  115. "instead.".
  116. format(version[2], original_version, version[0], version[1]))
  117. return version[0] + '.' + version[1]
  118. def options(opt):
  119. """
  120. Specify the options available when invoking waf; uses OptParse
  121. :param opt: the OptionContext object
  122. :return: N/A
  123. """
  124. opt.load('pebble_sdk_common')
  125. opt.add_option('-t', '--timestamp', dest='timestamp',
  126. help="Use a specific timestamp to label this package (ie, your repository's "
  127. "last commit time), defaults to time of build")
  128. def configure(conf):
  129. """
  130. Configure the build using information obtained from a JSON file
  131. :param conf: the ConfigureContext object
  132. :return: N/A
  133. """
  134. conf.load('pebble_sdk_common')
  135. # This overrides the default config in pebble_sdk_common.py
  136. if conf.options.timestamp:
  137. conf.env.TIMESTAMP = conf.options.timestamp
  138. conf.env.BUNDLE_NAME = "app_{}.pbw".format(conf.env.TIMESTAMP)
  139. else:
  140. conf.env.BUNDLE_NAME = "{}.pbw".format(conf.path.name)
  141. # Read in package.json for environment configuration, or fallback to appinfo.json for older
  142. # projects
  143. info_json_node = (conf.path.get_src().find_node('package.json') or
  144. conf.path.get_src().find_node('appinfo.json'))
  145. if info_json_node is None:
  146. conf.fatal('Could not find package.json')
  147. with open(info_json_node.abspath(), 'r') as f:
  148. info_json = json.load(f)
  149. project_info = _extract_project_info(conf, info_json, info_json_node.name)
  150. conf.env.PROJECT_INFO = project_info
  151. conf.env.BUILD_TYPE = 'rocky' if project_info.get('projectType', None) == 'rocky' else 'app'
  152. if getattr(conf.env.PROJECT_INFO, 'enableMultiJS', False):
  153. if not conf.env.WEBPACK:
  154. conf.fatal("'enableMultiJS' is set to true, but unable to locate webpack module at {} "
  155. "Please set enableMultiJS to false, or reinstall the SDK.".
  156. format(conf.env.NODE_PATH))
  157. if conf.env.BUILD_TYPE == 'rocky':
  158. conf.find_program('node nodejs', var='NODE',
  159. errmsg="Unable to locate the Node command. "
  160. "Please check your Node installation and try again.")
  161. c_files = [c_file.path_from(conf.path.find_node('src'))
  162. for c_file in conf.path.ant_glob('src/**/*.c')]
  163. if c_files:
  164. Logs.pprint('YELLOW', "WARNING: C source files are not supported for Rocky.js "
  165. "projects. The following C files are being skipped: {}".
  166. format(c_files))
  167. if 'resources' in project_info and 'media' in project_info['resources']:
  168. conf.env.RESOURCES_JSON = project_info['resources']['media']
  169. if 'publishedMedia' in project_info['resources']:
  170. conf.env.PUBLISHED_MEDIA_JSON = project_info['resources']['publishedMedia']
  171. conf.env.REQUESTED_PLATFORMS = project_info.get('targetPlatforms', [])
  172. conf.env.LIB_DIR = "node_modules"
  173. get_target_platforms(conf)
  174. # With new-style projects, check for libraries specified in package.json
  175. if 'dependencies' in info_json:
  176. configure_libraries(conf, info_json['dependencies'])
  177. conf.load('process_message_keys')
  178. # base_env is set to a shallow copy of the current ConfigSet for this ConfigureContext
  179. base_env = conf.env
  180. for platform in conf.env.TARGET_PLATFORMS:
  181. # Create a deep copy of the `base_env` ConfigSet and set conf.env to a shallow copy of
  182. # the resultant ConfigSet
  183. conf.setenv(platform, base_env)
  184. configure_platform(conf, platform)
  185. # conf.env is set back to a shallow copy of the default ConfigSet stored in conf.all_envs['']
  186. conf.setenv('')
  187. def build(bld):
  188. """
  189. This method is invoked from a project's wscript with the `ctx.load('pebble_sdk')` call and
  190. sets up all of the task generators for the SDK. After all of the build methods have run,
  191. the configured task generators will run, generating build tasks and managing dependencies.
  192. See https://waf.io/book/#_task_generators for more details on task generator setup.
  193. :param bld: the BuildContext object
  194. :return: N/A
  195. """
  196. bld.load('pebble_sdk_common')
  197. # cached_env is set to a shallow copy of the current ConfigSet for this BuildContext
  198. cached_env = bld.env
  199. for platform in bld.env.TARGET_PLATFORMS:
  200. # bld.env is set to a shallow copy of the ConfigSet labeled <platform>
  201. bld.env = bld.all_envs[platform]
  202. # Set the build group (set of TaskGens) to the group labeled <platform>
  203. if bld.env.USE_GROUPS:
  204. bld.set_group(bld.env.PLATFORM_NAME)
  205. # Generate an appinfo file specific to the current platform
  206. build_node = bld.path.get_bld().make_node(bld.env.BUILD_DIR)
  207. bld(rule=_generate_appinfo_c_file,
  208. target=build_node.make_node('appinfo.auto.c'),
  209. vars=['PROJECT_INFO'])
  210. # Generate an appinfo.json file for the current platform to bundle in a PBW
  211. bld(rule=_write_appinfo_json_file,
  212. target=bld.path.get_bld().make_node('appinfo.json'),
  213. vars=['PROJECT_INFO'])
  214. # Generate resources specific to the current platform
  215. resource_node = None
  216. if bld.env.RESOURCES_JSON:
  217. try:
  218. resource_node = bld.path.find_node('resources')
  219. except AttributeError:
  220. bld.fatal("Unable to locate resources at resources/")
  221. # Adding the Rocky.js source file needs to happen before the setup of the Resource
  222. # Generators
  223. if bld.env.BUILD_TYPE == 'rocky':
  224. rocky_js_file = bld.path.find_or_declare('resources/rocky-app.js')
  225. rocky_js_file.parent.mkdir()
  226. bld.pbl_js_build(source=bld.path.ant_glob(['src/rocky/**/*.js',
  227. 'src/common/**/*.js']),
  228. target=rocky_js_file)
  229. resource_node = bld.path.get_bld().make_node('resources')
  230. bld.env.RESOURCES_JSON = [{'type': 'js',
  231. 'name': 'JS_SNAPSHOT',
  232. 'file': rocky_js_file.path_from(resource_node)}]
  233. resource_path = resource_node.path_from(bld.path) if resource_node else None
  234. generate_resources(bld, resource_path)
  235. # Running `pbl_build` needs to happen after the setup of the Resource Generators so
  236. # `report_memory_usage` is aware of the existence of the JS bytecode file
  237. if bld.env.BUILD_TYPE == 'rocky':
  238. rocky_c_file = build_node.make_node('src/rocky.c')
  239. bld(rule='cp "${SRC}" "${TGT}"',
  240. source=find_sdk_component(bld, bld.env, 'include/rocky.c'),
  241. target=rocky_c_file)
  242. # Check for rocky script (This is done in `build` to preserve the script as a node
  243. # instead of as an absolute path as would be required in `configure`. This is to keep
  244. # the signatures the same for both FW builds and SDK builds.
  245. if not bld.env.JS_TOOLING_SCRIPT:
  246. bld.fatal("Unable to locate tooling for this Rocky.js app build. Please "
  247. "try re-installing this version of the SDK.")
  248. bld.pbl_build(source=[rocky_c_file],
  249. target=build_node.make_node("pebble-app.elf"),
  250. bin_type='rocky')
  251. # bld.env is set back to a shallow copy of the original ConfigSet that was set when this `build`
  252. # method was invoked
  253. bld.env = cached_env
  254. @conf
  255. def pbl_program(self, *k, **kw):
  256. """
  257. This method is bound to the build context and is called by specifying `bld.pbl_program()`. We
  258. set the custom features `c`, `cprogram` and `pebble_cprogram` to run when this method is
  259. invoked.
  260. :param self: the BuildContext object
  261. :param k: none expected
  262. :param kw:
  263. source - the source C files to be built and linked
  264. target - the destination binary file for the compiled source
  265. :return: a task generator instance with keyword arguments specified
  266. """
  267. kw['bin_type'] = 'app'
  268. kw['features'] = 'c cprogram pebble_cprogram memory_usage'
  269. kw['app'] = kw['target']
  270. kw['resources'] = (
  271. self.path.find_or_declare(self.env.BUILD_DIR).make_node('app_resources.pbpack'))
  272. return self(*k, **kw)
  273. @conf
  274. def pbl_worker(self, *k, **kw):
  275. """
  276. This method is bound to the build context and is called by specifying `bld.pbl_worker()`. We set
  277. the custom features `c`, `cprogram` and `pebble_cprogram` to run when this method is invoked.
  278. :param self: the BuildContext object
  279. :param k: none expected
  280. :param kw:
  281. source - the source C files to be built and linked
  282. target - the destination binary file for the compiled source
  283. :return: a task generator instance with keyword arguments specified
  284. """
  285. kw['bin_type'] = 'worker'
  286. kw['features'] = 'c cprogram pebble_cprogram memory_usage'
  287. kw['worker'] = kw['target']
  288. return self(*k, **kw)