# 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 os import time import types from waflib import Logs from waflib.Configure import conf from waflib.Task import Task from waflib.TaskGen import after_method, before_method, feature from waflib.Tools import c, c_preproc import ldscript, process_bundle, process_headers, process_js, report_memory_usage, xcode_pebble from pebble_sdk_platform import maybe_import_internal from sdk_helpers import (append_to_attr, find_sdk_component, get_node_from_abspath, wrap_task_name_with_platform) # Override the default waf task __str__ method to include display of the HW platform being targeted Task.__str__ = wrap_task_name_with_platform def options(opt): """ Specify the options available when invoking waf; uses OptParse. This method is called from app and lib waftools by `opt.load('pebble_sdk_common')` :param opt: the OptionContext object :return: N/A """ opt.load('gcc') opt.add_option('-d', '--debug', action='store_true', default=False, dest='debug', help='Build in debug mode') opt.add_option('--no-groups', action='store_true', default=False, dest='no_groups') opt.add_option('--sandboxed-build', action='store_true', default=False, dest='sandbox') def configure(conf): """ Configure the tools for the build by locating SDK prerequisites on the filesystem :param conf: the ConfigureContext :return: N/A """ if not conf.options.debug: conf.env.append_value('DEFINES', 'RELEASE') else: Logs.pprint("CYAN", "Debug enabled") if conf.options.no_groups: conf.env.USE_GROUPS = False else: conf.env.USE_GROUPS = True conf.env.SANDBOX = conf.options.sandbox conf.env.VERBOSE = conf.options.verbose conf.env.TIMESTAMP = int(time.time()) # If waf is in ~/pebble-dev/PebbleSDK-X.XX/waf # Then this file is in ~/pebble-dev/PebbleSDK-X.XX/.waflib-xxxx/waflib/extras/ # => we need to go up 3 directories to find the folder containing waf pebble_sdk = conf.root.find_dir(os.path.dirname(__file__)).parent.parent.parent if pebble_sdk is None: conf.fatal("Unable to find Pebble SDK!\n" "Please make sure you are running waf directly from your SDK.") conf.env.PEBBLE_SDK_ROOT = pebble_sdk.abspath() # Set location of Pebble SDK common folder pebble_sdk_common = pebble_sdk.find_node('common') conf.env.PEBBLE_SDK_COMMON = pebble_sdk_common.abspath() if 'NODE_PATH' in os.environ: conf.env.NODE_PATH = conf.root.find_node(os.environ['NODE_PATH']).abspath() webpack_path = conf.root.find_node(conf.env.NODE_PATH).find_node('.bin').abspath() try: conf.find_program('webpack', path_list=[webpack_path]) except conf.errors.ConfigurationError: pass # Error will be caught after checking for enableMultiJS setting else: Logs.pprint('YELLOW', "WARNING: Unable to find $NODE_PATH variable required for SDK " "build. Please verify this build was initiated with a recent " "pebble-tool.") maybe_import_internal(conf.env) def build(bld): """ This method is invoked from the app or lib waftool with the `bld.load('pebble_sdk_common')` call and sets up additional task generators for the SDK. :param bld: the BuildContext object :return: N/A """ # cached_env is set to a shallow copy of the current ConfigSet for this BuildContext bld.env = bld.all_envs[''] bld.load('file_name_c_define') # Process message keys bld(features='message_keys') cached_env = bld.env for platform in bld.env.TARGET_PLATFORMS: # bld.env is set to a shallow copy of the ConfigSet labeled bld.env = bld.all_envs[platform] # Create a build group (set of TaskGens) for if bld.env.USE_GROUPS: bld.add_group(bld.env.PLATFORM_NAME) # Generate a linker script specific to the current platform build_node = bld.path.get_bld().find_or_declare(bld.env.BUILD_DIR) bld(features='subst', source=find_sdk_component(bld, bld.env, 'pebble_app.ld.template'), target=build_node.make_node('pebble_app.ld.auto'), **bld.env.PLATFORM) # Locate Rocky JS tooling script js_tooling_script = find_sdk_component(bld, bld.env, 'tools/generate_snapshot.js') bld.env.JS_TOOLING_SCRIPT = js_tooling_script if js_tooling_script else None # bld.env is set back to a shallow copy of the original ConfigSet that was set when this # `build` method was invoked bld.env = cached_env # Create a build group for bundling (should run after the build groups for each platform) if bld.env.USE_GROUPS: bld.add_group('bundle') def _wrap_c_preproc_scan(task): """ This function is a scanner function that wraps c_preproc.scan to fix up pebble.h dependencies. pebble.h is outside out the bld/src trees so therefore it's not considered a valid dependency and isn't scanned for further dependencies. Normally this would be fine but pebble.h includes an auto-generated resource id header which is really a dependency. We detect this include and add the resource id header file to the nodes being scanned by c_preproc. :param task: the task instance :return: N/A """ (nodes, names) = c_preproc.scan(task) if 'pebble.h' in names: nodes.append(get_node_from_abspath(task.generator.bld, task.env.RESOURCE_ID_HEADER)) nodes.append(get_node_from_abspath(task.generator.bld, task.env.MESSAGE_KEYS_HEADER)) return nodes, names @feature('c') @before_method('process_source') def setup_pebble_c(task_gen): """ This method is called before all of the c aliases (objects, shlib, stlib, program, etc) and ensures that the SDK `include` path for the current platform, as well as the project root directory and the project src directory are included as header search paths (includes) for the build. :param task_gen: the task generator instance :return: N/A """ platform = task_gen.env.PLATFORM_NAME append_to_attr(task_gen, 'includes', [find_sdk_component(task_gen.bld, task_gen.env, 'include'), '.', 'include', 'src']) append_to_attr(task_gen, 'includes', platform) for lib in task_gen.bld.env.LIB_JSON: if 'pebble' in lib: lib_include_node = task_gen.bld.path.find_node(lib['path']).find_node('include') append_to_attr(task_gen, 'includes', [lib_include_node, lib_include_node.find_node(str(lib['name'])).find_node(platform)]) @feature('c') @after_method('process_source') def fix_pebble_h_dependencies(task_gen): """ This method is called before all of the c aliases (objects, shlib, stlib, program, etc) and ensures that the _wrap_c_preproc_scan method is run for all c tasks. :param task_gen: the task generator instance :return: N/A """ for task in task_gen.tasks: if type(task) == c.c: # Swap out the bound member function for our own task.scan = types.MethodType(_wrap_c_preproc_scan, task, c.c) @feature('pebble_cprogram') @before_method('process_source') def setup_pebble_cprogram(task_gen): """ This method is called before all of the c aliases (objects, shlib, stlib, program, etc) and adds the appinfo.auto.c file to the source file list, adds the SDK pebble library to the lib path for the build, sets the linkflags for the build, and specifies the linker script to use during the linking step. :param task_gen: the task generator instance :return: None """ build_node = task_gen.path.get_bld().make_node(task_gen.env.BUILD_DIR) platform = task_gen.env.PLATFORM_NAME if not hasattr(task_gen, 'bin_type') or getattr(task_gen, 'bin_type') != 'lib': append_to_attr(task_gen, 'source', build_node.make_node('appinfo.auto.c')) append_to_attr(task_gen, 'source', build_node.make_node('src/resource_ids.auto.c')) if task_gen.env.MESSAGE_KEYS: append_to_attr(task_gen, 'source', get_node_from_abspath(task_gen.bld, task_gen.env.MESSAGE_KEYS_DEFINITION)) append_to_attr(task_gen, 'stlibpath', find_sdk_component(task_gen.bld, task_gen.env, 'lib').abspath()) append_to_attr(task_gen, 'stlib', 'pebble') for lib in task_gen.bld.env.LIB_JSON: # Skip binary check for non-Pebble libs if not 'pebble' in lib: continue binaries_path = task_gen.bld.path.find_node(lib['path']).find_node('binaries') if binaries_path: # Check for existence of platform folders inside binaries folder platform_binary_path = binaries_path.find_node(platform) if not platform_binary_path: task_gen.bld.fatal("Library {} is missing the {} platform folder in {}". format(lib['name'], platform, binaries_path)) # Check for existence of binary for each platform if lib['name'].startswith('@'): scoped_name = lib['name'].rsplit('/', 1) lib_binary = (platform_binary_path.find_node(str(scoped_name[0])). find_node("lib{}.a".format(scoped_name[1]))) else: lib_binary = platform_binary_path.find_node("lib{}.a".format(lib['name'])) if not lib_binary: task_gen.bld.fatal("Library {} is missing a binary for the {} platform". format(lib['name'], platform)) # Link library binary (supports scoped names) if lib['name'].startswith('@'): append_to_attr(task_gen, 'stlibpath', platform_binary_path.find_node(str(scoped_name[0])).abspath()) append_to_attr(task_gen, 'stlib', scoped_name[1]) else: append_to_attr(task_gen, 'stlibpath', platform_binary_path.abspath()) append_to_attr(task_gen, 'stlib', lib['name']) append_to_attr(task_gen, 'linkflags', ['-Wl,--build-id=sha1', '-Wl,-Map,pebble-{}.map,--emit-relocs'.format(getattr(task_gen, 'bin_type', 'app'))]) if not hasattr(task_gen, 'ldscript'): task_gen.ldscript = ( build_node.find_or_declare('pebble_app.ld.auto').path_from(task_gen.path)) def _get_entry_point(ctx, js_type, waf_js_entry_point): """ Returns the appropriate JS entry point, extracted from a project's package.json file, wscript or common SDK default :param ctx: the BuildContext :param js_type: type of JS build, pkjs or rockyjs :param waf_js_entry_point: the JS entry point specified by waftools :return: the JS entry point for the bundled JS file """ fallback_entry_point = waf_js_entry_point if not fallback_entry_point: if js_type == 'pkjs': if ctx.path.find_node('src/pkjs/index.js'): fallback_entry_point = 'src/pkjs/index.js' else: fallback_entry_point = 'src/js/app.js' if js_type == 'rockyjs': fallback_entry_point = 'src/rocky/index.js' project_info = ctx.env.PROJECT_INFO if not project_info.get('main'): return fallback_entry_point if project_info['main'].get(js_type): return str(project_info['main'][js_type]) return fallback_entry_point @conf def pbl_bundle(self, *k, **kw): """ This method is bound to the build context and is called by specifying `bld.pbl_bundle`. We set the custome features `js` and `bundle` to run when this method is invoked. :param self: the BuildContext object :param k: none expected :param kw: binaries - a list containing dictionaries specifying the HW platform targeted by the binary built, the app binary, and an optional worker binary js - the source JS files to be bundled js_entry_file - an optional parameter to specify the entry JS file when enableMultiJS is set to 'true' :return: a task generator instance with keyword arguments specified """ if kw.get('bin_type', 'app') == 'lib': kw['features'] = 'headers js package' else: if self.env.BUILD_TYPE == 'rocky': kw['js_entry_file'] = _get_entry_point(self, 'pkjs', kw.get('js_entry_file')) kw['features'] = 'js bundle' return self(*k, **kw) @conf def pbl_build(self, *k, **kw): """ This method is bound to the build context and is called by specifying `bld.pbl_build()`. We set the custom features `c`, `cprogram` and `pebble_cprogram` to run when this method is invoked. This method is intended to someday replace `pbl_program` and `pbl_worker` so that all apps, workers, and libs will run through this method. :param self: the BuildContext object :param k: none expected :param kw: source - the source C files to be built and linked target - the destination binary file for the compiled source :return: a task generator instance with keyword arguments specified """ valid_bin_types = ('app', 'worker', 'lib', 'rocky') bin_type = kw.get('bin_type', None) if bin_type not in valid_bin_types: self.fatal("The pbl_build method requires that a valid bin_type attribute be specified. " "Valid options are {}".format(valid_bin_types)) if bin_type == 'rocky': kw['features'] = 'c cprogram pebble_cprogram memory_usage' elif bin_type in ('app', 'worker'): kw['features'] = 'c cprogram pebble_cprogram memory_usage' kw[bin_type] = kw['target'] elif bin_type == 'lib': kw['features'] = 'c cstlib memory_usage' path, name = kw['target'].rsplit('/', 1) kw['lib'] = self.path.find_or_declare(path).make_node("lib{}.a".format(name)) # Pass values needed for memory usage report if bin_type != 'worker': kw['resources'] = ( self.env.PROJECT_RESBALL if bin_type == 'lib' else self.path.find_or_declare(self.env.BUILD_DIR).make_node('app_resources.pbpack')) return self(*k, **kw) @conf def pbl_js_build(self, *k, **kw): """ This method is bound to the build context and is called by specifying `bld.pbl_cross_compile()`. When this method is invoked, we set the custom feature `rockyjs` to run, which handles processing of JS files in preparation for Rocky.js bytecode compilation (this actually happens during resource generation) :param self: the BuildContext object :param k: none expected :param kw: source - the source JS files that will eventually be compiled into bytecode target - the destination JS file that will be specified as the source file for the bytecode compilation process :return: a task generator instance with keyword arguments specified """ kw['js_entry_file'] = _get_entry_point(self, 'rockyjs', kw.get('js_entry_file')) kw['features'] = 'rockyjs' return self(*k, **kw)