From a777737bc9e334ab333986aa7b0ca7732447f253 Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 22:58:13 +0200 Subject: [PATCH 01/11] build: drop dead waf tools Remove three custom waf tools that nothing uses: - binary_header.py: loaded at configure time but no task generator in the tree uses the binary_header feature. It also references the re and os modules without importing them, so parts of it would raise NameError if ever exercised. - c_inject_include_files.py: never loaded by any wscript, and it references waflib.Node.Nod3 (typo), so loading it would fail. - compress.py: no importers anywhere in the tree. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- docs/reference/formats/sle.md | 5 +- tools/waf/binary_header.py | 158 ---------------------------- tools/waf/c_inject_include_files.py | 38 ------- tools/waf/compress.py | 16 --- wscript | 2 - 5 files changed, 2 insertions(+), 217 deletions(-) delete mode 100644 tools/waf/binary_header.py delete mode 100644 tools/waf/c_inject_include_files.py delete mode 100644 tools/waf/compress.py diff --git a/docs/reference/formats/sle.md b/docs/reference/formats/sle.md index fb8fd480a8..e22cc12592 100644 --- a/docs/reference/formats/sle.md +++ b/docs/reference/formats/sle.md @@ -2,9 +2,8 @@ SLE is a small run-length encoding tuned for binary blobs that mix long zero runs with otherwise incompressible data. The encoder is -`tools/waf/sparse_length_encoding.py` (used by the `binary_header` waf -feature in `tools/waf/binary_header.py` to embed compressed blobs as C -arrays); the firmware-side decoder is `src/fw/util/sle.c`. +`tools/waf/sparse_length_encoding.py`; the firmware-side decoder is +`src/fw/util/sle.c`. The encoded stream is: diff --git a/tools/waf/binary_header.py b/tools/waf/binary_header.py deleted file mode 100644 index fc62a92939..0000000000 --- a/tools/waf/binary_header.py +++ /dev/null @@ -1,158 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Google LLC -# SPDX-License-Identifier: Apache-2.0 - -import binascii - -import sparse_length_encoding - -from waflib import Task, TaskGen, Utils, Node, Errors - - -class binary_header(Task.Task): - """ - Create a header file containing an array with contents from a binary file. - """ - - def run(self): - if getattr(self.generator, "hex", False): - # Input file is hexadecimal ASCII characters with whitespace - text = self.inputs[0].read( - encoding=getattr(self.generator, "encoding", "ISO8859-1") - ) - # Strip all whitespace so that binascii is happy - text = "".join(text.split()) - code = binascii.unhexlify(text) - else: - code = self.inputs[0].read("rb") - - array_name = getattr(self.generator, "array_name", None) - if not array_name: - array_name = re.sub(r"[^A-Za-z0-9]", "_", self.inputs[0].name) - - if getattr(self.generator, "compressed", False): - encoded_code = b"".join(sparse_length_encoding.encode(code)) - # verify that it was encoded correctly - if b"".join(sparse_length_encoding.decode(encoded_code)) != code: - raise Errors.WafError("encoding error") - code = encoded_code - - output = ["#pragma once", "#include "] - output += ["static const uint8_t %s[] = {" % array_name] - line = [] - for n, b in enumerate(code): - line += ["0x%.2x," % b] - if n % 16 == 15: - output += ["".join(line)] - line = [] - if line: - output += ["".join(line)] - output += ["};", ""] - - self.outputs[0].write( - "\n".join(output), encoding=getattr(self.generator, "encoding", "ISO8859-1") - ) - self.generator.bld.raw_deps[self.uid()] = self.dep_vars = "array_name" - - if getattr(self.generator, "chmod", None): - os.chmod(self.outputs[0].abspath(), self.generator.chmod) - - def sig_vars(self): - dependent_generator_vars = [ - "hex", - "encoding", - "array_name", - "compressed", - "chmod", - ] - vars = [] - for k in dependent_generator_vars: - try: - vars.append((k, getattr(self.generator, k))) - except AttributeError: - pass - self.m.update(Utils.h_list(vars)) - return self.m.digest() - - -@TaskGen.feature("binary_header") -@TaskGen.before_method("process_source", "process_rule") -def process_binary_header(self): - """ - Define a transformation that substitutes the contents of *source* files to - *target* files:: - - def build(bld): - bld( - features='binary_header', - source='foo.bin', - target='foo.auto.h', - array_name='s_some_array', - compressed=True - ) - bld( - features='binary_header', - source='bar.hex', - target='bar.auto.h', - hex=True - ) - - If the *hex* parameter is True, the *source* files are read in an ASCII - hexadecimal format, where each byte is represented by a pair of hexadecimal - digits with optional whitespace. If *hex* is False or not specified, the - file is treated as a raw binary file. - - If the *compressed* parameter is True, the *source* files are compressed with - sparse length encoding (see tools/waf/sparse_length_encoding.py). - - The name of the array variable defaults to the source file name with all - characters that are invaid C identifiers replaced with underscores. The name - can be explicitly specified by setting the *array_name* parameter. - - This method overrides the processing by - :py:meth:`waflib.TaskGen.process_source`. - """ - - src = Utils.to_list(getattr(self, "source", [])) - if isinstance(src, Node.Node): - src = [src] - tgt = Utils.to_list(getattr(self, "target", [])) - if isinstance(tgt, Node.Node): - tgt = [tgt] - if len(src) != len(tgt): - raise Errors.WafError("invalid number of source/target for %r" % self) - - for x, y in zip(src, tgt): - if not x or not y: - raise Errors.WafError("null source or target for %r" % self) - a, b = None, None - - if isinstance(x, str) and isinstance(y, str) and x == y: - a = self.path.find_node(x) - b = self.path.get_bld().make_node(y) - if not os.path.isfile(b.abspath()): - b.sig = None - b.parent.mkdir() - else: - if isinstance(x, str): - a = self.path.find_resource(x) - elif isinstance(x, Node.Node): - a = x - if isinstance(y, str): - b = self.path.find_or_declare(y) - elif isinstance(y, Node.Node): - b = y - - if not a: - raise Errors.WafError("could not find %r for %r" % (x, self)) - - has_constraints = False - tsk = self.create_task("binary_header", a, b) - for k in ("after", "before", "ext_in", "ext_out"): - val = getattr(self, k, None) - if val: - has_constraints = True - setattr(tsk, k, val) - - tsk.before = [k for k in ("c", "cxx") if k in Task.classes] - - self.source = [] diff --git a/tools/waf/c_inject_include_files.py b/tools/waf/c_inject_include_files.py deleted file mode 100644 index f36f3adece..0000000000 --- a/tools/waf/c_inject_include_files.py +++ /dev/null @@ -1,38 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Google LLC -# SPDX-License-Identifier: Apache-2.0 - -""" -Adds `-include` flags for list of files to CFLAGS and ASFLAGS, by adding an -optional attribute `inject_include_files`. -""" - -from waflib.Node import Nod3 -from waflib.TaskGen import feature, after_method -from waflib.Utils import def_attrs, to_list - - -@feature("c", "asm") -@after_method("create_compiled_task") -def process_include_files(self): - def_attrs(self, inject_include_files=None) - if not self.inject_include_files: - return - - include_flags = [] - for include_file in to_list(self.inject_include_files): - if isinstance(include_file, Nod3): - node = include_file - elif isinstance(include_file, str): - node = self.path.find_node(include_file) - if not node: - self.bld.fatal("%s does not exist." % include_file) - else: - self.bld.fatal("Expecting str or Nod3 in `inject_include_files` list") - include_file_path = node.abspath() - include_flags.append("-include%s" % include_file_path) - - self.env.append_unique("CFLAGS", include_flags) - self.env.append_unique("ASFLAGS", include_flags) - - for s in self.source: - self.bld.add_manual_dependency(s, node) diff --git a/tools/waf/compress.py b/tools/waf/compress.py deleted file mode 100644 index 218e44651a..0000000000 --- a/tools/waf/compress.py +++ /dev/null @@ -1,16 +0,0 @@ -# SPDX-FileCopyrightText: 2024 Google LLC -# SPDX-License-Identifier: Apache-2.0 - - -def compress(task): - cmd = ["cp", task.inputs[0].abspath(), task.inputs[0].get_bld().abspath()] - task.exec_command(cmd) - - cmd = [ - "xz", - "--keep", - "--check=crc32", - "--lzma2=dict=4KiB", - task.inputs[0].get_bld().abspath(), - ] - task.exec_command(cmd) diff --git a/wscript b/wscript index 89f43570e3..118a8fb87d 100644 --- a/wscript +++ b/wscript @@ -226,8 +226,6 @@ def configure(conf): conf.load('protoc') - conf.load('binary_header') - platform = pebble_platforms[conf.env.PLATFORM_NAME] define = 'MAX_FONT_GLYPH_SIZE={}'.format(platform['MAX_FONT_GLYPH_SIZE']) conf.env.append_value('DEFINES', [define]) From c6f21a9c5d99e9842310f90cae52a9cc05997784 Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 23:00:17 +0200 Subject: [PATCH 02/11] tools/waf/pebble_test: fix shared-list mutation and per-source naming Two latent defects in the clar test harness: - clar() declared mutable default arguments (test_libs=[], override_includes=[]) and add_clar_test() appended pthread/m to test_libs and libutil/libbtutil/duma to the caller's use list in place. The shared defaults accumulated duplicates across all call sites, and multi-platform tests grew their use list once per platform. Copy the lists before modifying them. - clar() derived test_name in a loop over test_sources but built the test binaries in a separate loop afterwards, using the loop-leaked last test_source with the first source's name. A glob matching more than one file would silently drop all but the last. Nest the platform loop so every matched source gets its own test. No behavior change for existing call sites: every current test glob matches exactly one file, and the duplicate libs were harmless. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- tools/waf/pebble_test.py | 50 ++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/tools/waf/pebble_test.py b/tools/waf/pebble_test.py index d64de582c8..dc6d68db5a 100644 --- a/tools/waf/pebble_test.py +++ b/tools/waf/pebble_test.py @@ -361,8 +361,10 @@ def _generate_clar_harness(task): idl_includes = [root_build_dir + "src/idl"] includes += idl_includes - if use is None: - use = [] + # Copy the caller's lists before appending: they are shared across + # platforms and call sites. + use = list(use) if use is not None else [] + test_libs = list(test_libs) # Add DUMA for memory corruption checking # conditionally disable duma based on DUMA_DISABLED being defined # DUMA is found in tests/vendor/duma @@ -490,8 +492,8 @@ def clar( sources_ant_glob=None, test_sources_ant_glob=None, test_sources=None, - test_libs=[], - override_includes=[], + test_libs=None, + override_includes=None, add_includes=None, defines=None, test_name=None, @@ -525,6 +527,8 @@ def clar( # Make a copy so if we modify it we don't accidentally modify the callers list defines = list(defines or []) + test_libs = list(test_libs or []) + override_includes = list(override_includes or []) defines.append("UNITTEST") defines.append("MEMFAULT=0") @@ -550,21 +554,23 @@ def clar( for test_source in test_sources: if test_name is None: - test_name = test_source.name - test_name = test_name[: test_name.rfind(".")] # Scrape the extension - - for platform in platforms: - add_clar_test( - bld, - test_name, - test_source, - sources_ant_glob, - sources, - test_libs, - override_includes, - add_includes, - defines, - runtime_deps, - platform, - use, - ) + source_test_name = test_source.name + source_test_name = source_test_name[: source_test_name.rfind(".")] + else: + source_test_name = test_name + + for platform in platforms: + add_clar_test( + bld, + source_test_name, + test_source, + sources_ant_glob, + sources, + test_libs, + override_includes, + add_includes, + defines, + runtime_deps, + platform, + use, + ) From 9a21b4b3b879d716020118a6851e85bd2fec214f Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 23:04:32 +0200 Subject: [PATCH 03/11] tools: deduplicate FirmwareDescription helpers PebbleFirmwareBinaryInfo and the FirmwareDescription struct insertion existed in two diverged copies: tools/fw_binary_info.py + tools/insert_firmware_descr.py (manual CLIs) and a vendored copy in pebble-commander's imaging command, which is the live path for boards without pblboot (asterix gets the struct prepended at PULSE flash time). Make pebble.commander.util.fw_binary_info the canonical implementation, next to the vendored stm32_crc it depends on, so the package stays self-contained. The tools/ scripts become thin shims: fw_binary_info.py subclasses to keep its .elf input support (which needs the repo-local binutils helper), and insert_firmware_descr.py keeps its CLI. Output is byte-identical to the previous implementations. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- tools/fw_binary_info.py | 104 ++------------ tools/insert_firmware_descr.py | 32 +---- .../pebble/commander/_commands/imaging.py | 115 +--------------- .../pebble/commander/util/fw_binary_info.py | 130 ++++++++++++++++++ 4 files changed, 144 insertions(+), 237 deletions(-) create mode 100644 tools/libs/pebble-commander/pebble/commander/util/fw_binary_info.py diff --git a/tools/fw_binary_info.py b/tools/fw_binary_info.py index ba12c8f95e..a7e7f868ae 100755 --- a/tools/fw_binary_info.py +++ b/tools/fw_binary_info.py @@ -3,63 +3,16 @@ # SPDX-License-Identifier: Apache-2.0 -from binascii import crc32 import os -import struct -from functools import reduce -import stm32_crc +from pebble.commander.util.fw_binary_info import ( + PebbleFirmwareBinaryInfo as _PebbleFirmwareBinaryInfo, +) -class PebbleFirmwareBinaryInfo(object): - V1_STRUCT_VERSION = 1 - V1_STRUCT_DEFINTION = [ - ("20s", "build_id"), - ("L", "version_timestamp"), - ("32s", "version_tag"), - ("8s", "version_short"), - ("?", "is_recovery_firmware"), - ("B", "hw_platform"), - ("B", "metadata_version"), - ] - # The platforms which use a legacy defective crc32 - LEGACY_CRC_PLATFORMS = [ - 0, # unknown (assume legacy) - 1, # OneEV1 - 2, # OneEV2 - 3, # OneEV2_3 - 4, # OneEV2_4 - 5, # OnePointFive - 6, # TwoPointFive - 7, # SnowyEVT2 - 8, # SnowyDVT - 9, # SpaldingEVT - 10, # BobbyDVT - 11, # Spalding - 0xFF, # OneBigboard - 0xFE, # OneBigboard2 - 0xFD, # SnowyBigboard - 0xFC, # SnowyBigboard2 - 0xFB, # SpaldingBigboard - ] - - def get_crc(self): - _, ext = os.path.splitext(self.path) - assert ext == ".bin", "Can only calculate crc for .bin files" - with open(self.path, "rb") as f: - image = f.read() - if self.hw_platform in self.LEGACY_CRC_PLATFORMS: - # use the legacy defective crc - return stm32_crc.crc32(image) - else: - # use a regular crc - return crc32(image) & 0xFFFFFFFF - - def _get_footer_struct(self): - fmt = "<" + reduce( - lambda s, t: s + t[0], PebbleFirmwareBinaryInfo.V1_STRUCT_DEFINTION, "" - ) - return struct.Struct(fmt) +class PebbleFirmwareBinaryInfo(_PebbleFirmwareBinaryInfo): + """Extends the pebble-commander implementation with .elf input support, + which needs the repo-local binutils helper.""" def _get_footer_data_from_elf(self, path): import binutils @@ -69,46 +22,13 @@ def _get_footer_data_from_elf(self, path): build_id_data = binutils.section_bytes(path, ".note.gnu.build-id")[16:] return build_id_data + fw_version_data - def _get_footer_data_from_bin(self, path): - with open(path, "rb") as f: - struct_size = self.struct.size - f.seek(-struct_size, 2) - footer_data = f.read() - return footer_data - - def _parse_footer_data(self, footer_data): - z = zip( - PebbleFirmwareBinaryInfo.V1_STRUCT_DEFINTION, - self.struct.unpack(footer_data), - ) - return {entry[1]: data for entry, data in z} - - def __init__(self, elf_or_bin_path): - self.path = elf_or_bin_path - self.struct = self._get_footer_struct() - _, ext = os.path.splitext(elf_or_bin_path) + def _get_footer_data(self, path): + _, ext = os.path.splitext(path) if ext == ".elf": - footer_data = self._get_footer_data_from_elf(elf_or_bin_path) - elif ext == ".bin": - footer_data = self._get_footer_data_from_bin(elf_or_bin_path) - else: - raise ValueError('Unexpected extension. Must be ".bin" or ".elf"') - self.info = self._parse_footer_data(footer_data) - - # Trim leading NULLS on the strings: - for k in ["version_tag", "version_short"]: - self.info[k] = self.info[k].rstrip(b"\x00") - - def __str__(self): - return str(self.info) - - def __repr__(self): - return self.info.__repr__() - - def __getattr__(self, name): - if name in self.info: - return self.info[name] - raise AttributeError + return self._get_footer_data_from_elf(path) + if ext == ".bin": + return self._get_footer_data_from_bin(path) + raise ValueError('Unexpected extension. Must be ".bin" or ".elf"') if __name__ == "__main__": diff --git a/tools/insert_firmware_descr.py b/tools/insert_firmware_descr.py index b4c8a981fa..b4356251d8 100644 --- a/tools/insert_firmware_descr.py +++ b/tools/insert_firmware_descr.py @@ -3,39 +3,9 @@ # SPDX-License-Identifier: Apache-2.0 -from __future__ import with_statement, print_function - import sys -import struct - -from fw_binary_info import PebbleFirmwareBinaryInfo - - -# typedef struct ATTR_PACKED FirmwareDescription { -# uint32_t description_length; -# uint32_t firmware_length; -# uint32_t checksum; -# } FirmwareDescription; -FW_DESCR_FORMAT = " Date: Tue, 18 Aug 2026 23:14:01 +0200 Subject: [PATCH 04/11] tools: extract gitinfo into a standalone module Move the git revision logic from tools/waf/gitinfo.py into tools/gitinfo.py with a plain subprocess implementation and a small CLI (--json), so post-link and bundle tooling can use it without waf. The waf module becomes a shim keeping the ctx-based signature for the remaining wscript callers. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- tools/gitinfo.py | 84 ++++++++++++++++++++++++++++++++++++++++++++ tools/waf/gitinfo.py | 55 ++--------------------------- 2 files changed, 87 insertions(+), 52 deletions(-) create mode 100755 tools/gitinfo.py diff --git a/tools/gitinfo.py b/tools/gitinfo.py new file mode 100755 index 0000000000..5f0ff1ba28 --- /dev/null +++ b/tools/gitinfo.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Core Devices LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Git revision info for the firmware build. + +Standalone (no waf): used by the build system, the post-link image tooling +and the bundle tooling. The version dict keys are part of the interface; +`git_version.auto.h` substitution and the bundle manifest depend on them. +""" + +import argparse +import json +import re +import subprocess +import sys + + +def _git(args, cwd=None): + return ( + subprocess.check_output(["git"] + args, cwd=cwd, stderr=subprocess.DEVNULL) + .decode() + .strip() + ) + + +def get_git_revision(cwd=None): + commit = _git(["rev-parse", "--short", "HEAD"], cwd) + timestamp = _git(["log", "-1", "--format=%ct", "HEAD"], cwd) + + try: + tag = _git(["describe", "--dirty"], cwd) + except subprocess.CalledProcessError: + tag = "v9.9.9-dev" + print(f"Git tag not found, using {tag}", file=sys.stderr) + + # Validate that git tag follows the required form: + # See https://github.com/pebble/tintin/wiki/Firmware,-PRF-&-Bootloader-Versions + # An optional fourth numeric component (e.g. v4.9.142.1) is accepted for + # point releases; it is only exposed through TAG and PATCH_VERBOSE_STRING. + # Note: version_regex.groups() returns sequence ('0', '0', '0', '0', 'suffix'): + version_regex = re.search( + r"^v(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?(?:(?:-)(.+))?$", tag + ) + if not version_regex: + raise ValueError(f"Invalid tag: {tag}") + + # Get version numbers from version_regex.groups() sequence and replace None values with 0 + # e.g. v2-beta11 => ('2', None, None, None, 'beta11') => ('2', '0', '0') + version = [x if x else "0" for x in version_regex.groups()] + + # Used for pebble_pipeline payload, generate a string that contains everything after minor. + # Force include patch as 0 if it doesn't exist. + patch_verbose = str(version[2]) + if version_regex.group(4): + patch_verbose += "." + version[3] + str_after_patch = version[4] + if str_after_patch: + patch_verbose += "-" + str_after_patch + + return { + "TAG": tag, + "COMMIT": commit, + "TIMESTAMP": timestamp, + "MAJOR_VERSION": version[0], + "MINOR_VERSION": version[1], + "PATCH_VERSION": version[2], + "MAJOR_MINOR_PATCH_STRING": ".".join(version[0:3]), + "PATCH_VERBOSE_STRING": patch_verbose, + } + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Print firmware git revision info") + parser.add_argument("-C", "--repo", default=None, help="Repository directory") + parser.add_argument("--json", action="store_true", help="Output as JSON") + args = parser.parse_args() + + revision = get_git_revision(cwd=args.repo) + if args.json: + print(json.dumps(revision, indent=2)) + else: + for key, value in revision.items(): + print(f"{key}={value}") diff --git a/tools/waf/gitinfo.py b/tools/waf/gitinfo.py index eb9465cf4f..cdfc03404c 100644 --- a/tools/waf/gitinfo.py +++ b/tools/waf/gitinfo.py @@ -1,59 +1,10 @@ # SPDX-FileCopyrightText: 2024 Google LLC # SPDX-License-Identifier: Apache-2.0 -import re +"""waf shim over tools/gitinfo.py, keeping the ctx-based signature.""" -import waflib.Context -import waflib.Logs +from tools.gitinfo import get_git_revision as _get_git_revision def get_git_revision(ctx): - commit = ctx.cmd_and_log( - ["git", "rev-parse", "--short", "HEAD"], quiet=waflib.Context.BOTH - ).strip() - timestamp = ctx.cmd_and_log( - ["git", "log", "-1", "--format=%ct", "HEAD"], quiet=waflib.Context.BOTH - ).strip() - - try: - tag = ctx.cmd_and_log( - ["git", "describe", "--dirty"], quiet=waflib.Context.BOTH - ).strip() - except Exception: - tag = "v9.9.9-dev" - waflib.Logs.warn(f"Git tag not found, using {tag}") - - # Validate that git tag follows the required form: - # See https://github.com/pebble/tintin/wiki/Firmware,-PRF-&-Bootloader-Versions - # An optional fourth numeric component (e.g. v4.9.142.1) is accepted for - # point releases; it is only exposed through TAG and PATCH_VERBOSE_STRING. - # Note: version_regex.groups() returns sequence ('0', '0', '0', '0', 'suffix'): - version_regex = re.search( - r"^v(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?(?:(?:-)(.+))?$", tag - ) - if not version_regex: - raise ValueError(f"Invalid tag: {tag}") - - # Get version numbers from version_regex.groups() sequence and replace None values with 0 - # e.g. v2-beta11 => ('2', None, None, None, 'beta11') => ('2', '0', '0') - version = [x if x else "0" for x in version_regex.groups()] - - # Used for pebble_pipeline payload, generate a string that contains everything after minor. - # Force include patch as 0 if it doesn't exist. - patch_verbose = str(version[2]) - if version_regex.group(4): - patch_verbose += "." + version[3] - str_after_patch = version[4] - if str_after_patch: - patch_verbose += "-" + str_after_patch - - return { - "TAG": tag, - "COMMIT": commit, - "TIMESTAMP": timestamp, - "MAJOR_VERSION": version[0], - "MINOR_VERSION": version[1], - "PATCH_VERSION": version[2], - "MAJOR_MINOR_PATCH_STRING": ".".join(version[0:3]), - "PATCH_VERBOSE_STRING": patch_verbose, - } + return _get_git_revision(cwd=ctx.srcnode.abspath()) From 9cf2695c3feb32539e22c071c122bf1393de285c Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 23:15:33 +0200 Subject: [PATCH 05/11] build: move firmware and resource size caps to Kconfig Replace the hardcoded per-SoC size tables in wscript and pbl with two derived Kconfig symbols: - FW_MAX_SIZE: defaults to FW_FLASH_SIZE, which already encodes the per-board/per-variant bank sizes, with an nRF52 exception keeping 32 KiB reserved for the bootloader in the non-recovery bank. - SYSTEM_RESOURCES_MAX_SIZE: per-SoC pbpack cap. Verified against the old tables for every board across normal/prf/prf+mfg variants; all values are identical. One edge case improves: PRF built with RECOVERY_FW_AS_FW now gets the normal-area cap on SF32LB52 instead of the 576 KiB recovery-bank cap, since it is linked into the normal firmware area. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- Kconfig | 20 ++++++++++++++++++++ pbl | 18 +++--------------- wscript | 32 ++++++-------------------------- 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/Kconfig b/Kconfig index 8bdcf816f0..5dcea2dd13 100644 --- a/Kconfig +++ b/Kconfig @@ -34,6 +34,26 @@ config PBLBOOT layout with direct XIP. Disable for boards whose SoC bootloader consumes a raw image and uses a single firmware slot. +config FW_MAX_SIZE + hex + default 0xf8000 if SOC_NRF52 && (!RECOVERY_FW || MFG) + default FW_FLASH_SIZE + help + Maximum size of the firmware image binary, enforced when + bundling and flashing. Defaults to the firmware flash bank + size; the nRF52 normal-firmware bank keeps 32 KiB reserved for + the bootloader. + +config SYSTEM_RESOURCES_MAX_SIZE + hex + default 0x100000 if SOC_NRF52 + default 0x200000 if SOC_SF32LB52 + default 0x200000 if SOC_QEMU + default 0x40000 + help + Maximum size of the system resources pbpack, enforced after the + resource build. + menu "Compiler options" config LTO diff --git a/pbl b/pbl index 019acad716..e3b6dca33f 100755 --- a/pbl +++ b/pbl @@ -199,22 +199,10 @@ class FirmwareTooLargeException(Exception): def _check_firmware_image_size(env, path): - BYTES_PER_K = 1024 firmware_size = os.path.getsize(path) - if env.CONFIG_SOC_NRF52: - if env.VARIANT == "prf" and not env.CONFIG_MFG: - max_firmware_size = 512 * BYTES_PER_K - else: - max_firmware_size = (1024 - 32) * BYTES_PER_K - elif env.CONFIG_SOC_SF32LB52: - if env.VARIANT == "prf" and not env.CONFIG_MFG: - max_firmware_size = 576 * BYTES_PER_K - else: - max_firmware_size = 3072 * BYTES_PER_K - elif env.CONFIG_QEMU: - max_firmware_size = 4096 * BYTES_PER_K - else: - _fatal("Cannot check firmware size against unknown micro family") + max_firmware_size = env.CONFIG_FW_MAX_SIZE + if not max_firmware_size: + _fatal("CONFIG_FW_MAX_SIZE not set, cannot check firmware size") if firmware_size > max_firmware_size: raise FirmwareTooLargeException( diff --git a/wscript b/wscript index 118a8fb87d..5ad6ee15e4 100644 --- a/wscript +++ b/wscript @@ -647,14 +647,9 @@ def size_resources(ctx): if pbpack_path is None: ctx.fatal('No resource pbpack found') - if ctx.env.CONFIG_SOC_NRF52: - max_size = 1024 * 1024 - elif ctx.env.CONFIG_SOC_SF32LB52: - max_size = 2048 * 1024 - elif ctx.env.CONFIG_QEMU: - max_size = 2048 * 1024 - else: - max_size = 256 * 1024 + max_size = ctx.env.CONFIG_SYSTEM_RESOURCES_MAX_SIZE + if not max_size: + ctx.fatal('CONFIG_SYSTEM_RESOURCES_MAX_SIZE not set, cannot check resources size') pbpack_actual_size = os.path.getsize(pbpack_path.path_from(ctx.path)) @@ -840,25 +835,10 @@ class FirmwareTooLargeException(Exception): def _check_firmware_image_size(ctx, path): - BYTES_PER_K = 1024 firmware_size = os.path.getsize(path) - # Determine flash and bootloader size so we can calculate the max firmware size - if ctx.env.CONFIG_SOC_NRF52: - if ctx.env.VARIANT == 'prf' and not ctx.env.CONFIG_MFG: - max_firmware_size = 512 * BYTES_PER_K - else: - # 1024k of flash and 32k bootloader - max_firmware_size = (1024 - 32) * BYTES_PER_K - elif ctx.env.CONFIG_SOC_SF32LB52: - if ctx.env.VARIANT == 'prf' and not ctx.env.CONFIG_MFG: - max_firmware_size = 576 * BYTES_PER_K - else: - # 3072k of flash - max_firmware_size = 3072 * BYTES_PER_K - elif ctx.env.CONFIG_QEMU: - max_firmware_size = 4096 * BYTES_PER_K - else: - ctx.fatal('Cannot check firmware size against unknown micro family') + max_firmware_size = ctx.env.CONFIG_FW_MAX_SIZE + if not max_firmware_size: + ctx.fatal('CONFIG_FW_MAX_SIZE not set, cannot check firmware size') if firmware_size > max_firmware_size: raise FirmwareTooLargeException('Firmware is too large! Size is 0x%x should be less than 0x%x' \ From 617e21f1fc31c56e2e6bb00102353a5d4e1bd090 Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 23:16:36 +0200 Subject: [PATCH 06/11] build: emit build-info.json at configure time Write a neutral JSON description of the configured build (board, platform, variant, slot, runners, artifact paths, and every CONFIG_* symbol) to build/build-info.json. This is the interface the standalone build tooling reads instead of parsing waf's c4che, and it is producer-agnostic: any build system that writes the same file can drive the same tools. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- tools/build_info.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ wscript | 40 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 tools/build_info.py diff --git a/tools/build_info.py b/tools/build_info.py new file mode 100644 index 0000000000..09007d55da --- /dev/null +++ b/tools/build_info.py @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2026 Core Devices LLC +# SPDX-License-Identifier: Apache-2.0 + +"""build-info.json: neutral description of a configured build. + +Written by the configure step, consumed by the standalone build tooling +(fw_image, bundling, QEMU images, the pbl CLI) so none of it needs to +parse waf's internal state. The producer does not have to be waf: any +build system that writes the same file can drive the same tools. +""" + +import json +import os + +FILENAME = "build-info.json" +FORMAT_VERSION = 1 + + +def build_info_path(build_dir): + return os.path.join(build_dir, FILENAME) + + +def write_build_info(build_dir, info): + info = dict(info) + info["format_version"] = FORMAT_VERSION + with open(build_info_path(build_dir), "w") as f: + json.dump(info, f, indent=2, sort_keys=True) + f.write("\n") + + +def load_build_info(build_dir): + path = build_info_path(build_dir) + if not os.path.isfile(path): + raise FileNotFoundError( + f"{path} not found -- configure the build first " + "(./pbl configure --board BOARD)" + ) + with open(path) as f: + info = json.load(f) + version = info.get("format_version") + if version != FORMAT_VERSION: + raise ValueError( + f"{path} has format_version {version}, expected {FORMAT_VERSION} " + "-- re-run configure" + ) + return info diff --git a/wscript b/wscript index 5ad6ee15e4..27eead40fd 100644 --- a/wscript +++ b/wscript @@ -324,6 +324,46 @@ def configure(conf): import tool_check tool_check.tool_check() + _write_build_info(conf) + + +def _write_build_info(conf): + """Describe the configured build in build/build-info.json, the neutral + interface the standalone tooling (fw_image, bundling, pbl) consumes.""" + import tools.build_info + + env = conf.all_envs[''] + config = {k: v for k, v in env.get_merged_dict().items() + if k.startswith('CONFIG_')} + is_prf = env.VARIANT == 'prf' + log_hashed = bool(env.CONFIG_LOG_HASHED) + + tools.build_info.write_build_info(conf.bldnode.abspath(), { + 'board': env.BOARD, + 'board_name': env.BOARD_NAME, + 'board_revision': env.BOARD_REVISION or None, + 'board_normalized': env.BOARD_NORMALIZED, + 'platform': env.PLATFORM_NAME, + 'min_sdk_version': env.MIN_SDK_VERSION, + 'variant': env.VARIANT, + 'js_engine': env.JS_ENGINE, + 'slot': None if env.SLOT == -1 else env.SLOT, + 'runners': env.SUPPORTED_RUNNERS or [], + 'runner': env.RUNNER or None, + # Paths are relative to the build directory. + 'artifacts': { + 'elf': 'pebbleos.elf', + 'bin': 'pebbleos.bin', + 'hex': 'pebbleos.hex', + 'map': 'pebbleos.map', + 'pbpack': None if is_prf else 'system_resources.pbpack', + 'fw_loghash_dict': 'pebbleos_loghash_dict.json' if log_hashed else None, + 'loghash_dict': LOGHASH_OUT_PATH if log_hashed else None, + 'pot': None if is_prf else 'pebbleos.pot', + }, + 'config': config, + }) + def stop_build_timer(ctx): t = datetime.datetime.utcnow() - ctx.pbl_build_start_time From ff1f6085adbfe21b927d74ae1bb2ae3a00ee5fd5 Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 23:20:27 +0200 Subject: [PATCH 07/11] build: extract the post-link image pipeline into tools/fw_image.py Replace the five per-step waf rules in _link_firmware (objcopy hex, objcopy bin, two pblboot header inserts, loghash extraction) plus the merge_loghash_dicts post-build function with a single standalone CLI driven by build-info.json. waf now invokes it as one rule with the ELF as input and the hex/bin/loghash dictionaries as outputs. Along the way: - tools/waf/pblboot.py moves to tools/pblboot.py, dropping its two waf task-rule wrappers; the argparse CLI and the header/priority functions are unchanged. - tools/waf/generate_log_strings_json.py is removed; the extraction and format-specifier check now live in fw_image.py, writing both pebbleos_loghash_dict.json and src/fw/loghash_dict.json (the merge step had exactly one input dict, so both files were always identical). - bld.LOGHASH_DICTS and env.PBLBOOT_PRIORITY/FIRMWARE_OFFSET plumbing goes away. Verified: objcopy outputs and the loghash dictionary are byte- identical to what the replaced waf rules produce for the same ELF, and the pblboot header CRC/offset/content check out. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- tools/fw_image.py | 106 +++++++++++++++++++++++++ tools/{waf => }/pblboot.py | 34 +------- tools/waf/generate_log_strings_json.py | 35 -------- wscript | 56 +++++-------- 4 files changed, 128 insertions(+), 103 deletions(-) create mode 100755 tools/fw_image.py rename tools/{waf => }/pblboot.py (82%) delete mode 100644 tools/waf/generate_log_strings_json.py diff --git a/tools/fw_image.py b/tools/fw_image.py new file mode 100755 index 0000000000..3276100e66 --- /dev/null +++ b/tools/fw_image.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Core Devices LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Post-link firmware image pipeline. + +Turns the linked ELF into the flashable images and log-hash dictionaries: + + pebbleos.elf -> pebbleos.hex / pebbleos.bin (pblboot header when + CONFIG_PBLBOOT) + -> pebbleos_loghash_dict.json (when CONFIG_LOG_HASHED) + -> src/fw/loghash_dict.json + +Driven entirely by build/build-info.json; standalone from the build system +that produced the ELF. +""" + +import argparse +import json +import os +import subprocess +import sys + +import build_info +import gitinfo +import pblboot + +# Debug/no-load sections stripped from the flashable images. +OBJCOPY_STRIP_ARGS = [ + "-S", + "-R", + ".stack", + "-R", + ".priv_bss", + "-R", + ".bss", + "-R", + ".retained", +] + + +def _objcopy(elf, out, fmt): + subprocess.check_call( + ["arm-none-eabi-objcopy"] + OBJCOPY_STRIP_ARGS + ["-O", fmt, elf, out] + ) + + +def _make_images(build_dir, elf, artifacts, config): + hex_out = os.path.join(build_dir, artifacts["hex"]) + bin_out = os.path.join(build_dir, artifacts["bin"]) + + if not config.get("CONFIG_PBLBOOT"): + _objcopy(elf, hex_out, "ihex") + _objcopy(elf, bin_out, "binary") + return + + revision = gitinfo.get_git_revision(cwd=os.path.dirname(os.path.abspath(__file__))) + priority = pblboot.boot_priority(revision["TAG"], int(revision["TIMESTAMP"])) + offset = config["CONFIG_FIRMWARE_OFFSET"] + + nohdr_hex = os.path.splitext(hex_out)[0] + ".nohdr.hex" + nohdr_bin = os.path.splitext(bin_out)[0] + ".nohdr.bin" + _objcopy(elf, nohdr_hex, "ihex") + _objcopy(elf, nohdr_bin, "binary") + pblboot.insert_header_hex(nohdr_hex, hex_out, offset, priority) + pblboot.insert_header_bin(nohdr_bin, bin_out, offset, priority) + + +def _make_loghash_dicts(build_dir, elf, artifacts): + from log_hashing.check_elf_log_strings import check_dict_log_strings + from log_hashing.newlogging import get_log_dict_from_file + + log_dict = get_log_dict_from_file(elf) + if not log_dict: + sys.exit(f"Unable to get log strings from {elf}") + + # Confirm that the log strings satisfy the rules + output = check_dict_log_strings(log_dict) + if output: + sys.exit(output) + + for key in ("fw_loghash_dict", "loghash_dict"): + path = os.path.join(build_dir, artifacts[key]) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + json.dump(log_dict, f, indent=2, sort_keys=True) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--build-dir", required=True, help="Configured build directory") + parser.add_argument("--elf", help="Firmware ELF (defaults to the configured one)") + args = parser.parse_args() + + info = build_info.load_build_info(args.build_dir) + artifacts = info["artifacts"] + config = info["config"] + elf = args.elf or os.path.join(args.build_dir, artifacts["elf"]) + + _make_images(args.build_dir, elf, artifacts, config) + if config.get("CONFIG_LOG_HASHED"): + _make_loghash_dicts(args.build_dir, elf, artifacts) + + +if __name__ == "__main__": + main() diff --git a/tools/waf/pblboot.py b/tools/pblboot.py similarity index 82% rename from tools/waf/pblboot.py rename to tools/pblboot.py index e5e57541a9..e7025ac63c 100644 --- a/tools/waf/pblboot.py +++ b/tools/pblboot.py @@ -51,7 +51,7 @@ def boot_priority(tag=None, commit_timestamp=None): return (PRIORITY_BAND_DEV << 56) | (int(now.timestamp()) & 0xFFFFFFFF) -def _insert_header_hex(fin, fout, offset, priority): +def insert_header_hex(fin, fout, offset, priority): # Load the hex file ih = IntelHex(fin) @@ -85,7 +85,7 @@ def _insert_header_hex(fin, fout, offset, priority): out_ih.write_hex_file(fout) -def _insert_header_bin(fin, fout, offset, priority): +def insert_header_bin(fin, fout, offset, priority): # Read the input binary file with open(fin, "rb") as f: content = f.read() @@ -102,32 +102,6 @@ def _insert_header_bin(fin, fout, offset, priority): f.write(content) -def _env_priority(bld): - if bld.env.PBLBOOT_PRIORITY: - return int(bld.env.PBLBOOT_PRIORITY) - return boot_priority() - - -def insert_header_hex(task): - bld = task.generator.bld - _insert_header_hex( - task.inputs[0].abspath(), - task.outputs[0].abspath(), - bld.env.FIRMWARE_OFFSET, - _env_priority(bld), - ) - - -def insert_header_bin(task): - bld = task.generator.bld - _insert_header_bin( - task.inputs[0].abspath(), - task.outputs[0].abspath(), - bld.env.FIRMWARE_OFFSET, - _env_priority(bld), - ) - - if __name__ == "__main__": parser = argparse.ArgumentParser( description="Generate a firmware with pblboot header" @@ -148,6 +122,6 @@ def insert_header_bin(task): priority = boot_priority(args.tag, args.commit_timestamp) if args.input.endswith(".bin"): - _insert_header_bin(args.input, args.output, args.offset, priority) + insert_header_bin(args.input, args.output, args.offset, priority) else: - _insert_header_hex(args.input, args.output, args.offset, priority) + insert_header_hex(args.input, args.output, args.offset, priority) diff --git a/tools/waf/generate_log_strings_json.py b/tools/waf/generate_log_strings_json.py deleted file mode 100644 index 8b2f492d02..0000000000 --- a/tools/waf/generate_log_strings_json.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python -# SPDX-FileCopyrightText: 2024 Google LLC -# SPDX-License-Identifier: Apache-2.0 - - -import json -from waflib import Logs - -from tools.log_hashing.check_elf_log_strings import check_dict_log_strings -from tools.log_hashing.newlogging import get_log_dict_from_file - - -def wafrule(task): - elf_filename = task.inputs[0].abspath() - log_strings_json_filename = task.outputs[0].abspath() - - return generate_log_strings_json(elf_filename, log_strings_json_filename) - - -def generate_log_strings_json(elf_filename, log_strings_json_filename): - log_dict = get_log_dict_from_file(elf_filename) - if not log_dict: - error = "Unable to get log strings from {}".format(elf_filename) - Logs.pprint("RED", error) - return error - - # Confirm that the log strings satisfy the rules - output = check_dict_log_strings(log_dict) - if output: - Logs.pprint("RED", output) - return "NewLogging string formatting error" - - # Create log_strings.json - with open(log_strings_json_filename, "w") as json_file: - json.dump(log_dict, json_file, indent=2, sort_keys=True) diff --git a/wscript b/wscript index 27eead40fd..89dc617515 100644 --- a/wscript +++ b/wscript @@ -31,13 +31,10 @@ sys.path.append(os.path.join(waf_dir, 'tools/log_hashing')) sys.path.append(os.path.join(waf_dir, 'sdk/tools/')) sys.path.append(os.path.join(waf_dir, 'tools/waf')) -import tools.waf.generate_log_strings_json import tools.waf.generate_timezone_data import tools.waf.gitinfo import tools.waf.boards import tools.waf.ldscript -import tools.waf.objcopy -import tools.waf.pblboot import tools.waf.pebble_sdk_gcc as pebble_sdk_gcc import tools.runners as pebble_runners from tools.waf.pebble_sdk_locator import activate_sdk @@ -436,8 +433,7 @@ def _link_firmware(bld, sources): fw_linkflags.append('-Wl,--require-defined=g_memfault_build_id') uses.append('memfault') - # Used by pblboot image tools; the C define mirrors the historical name. - bld.env.FIRMWARE_OFFSET = bld.env.CONFIG_FIRMWARE_OFFSET + # The C define mirrors the historical name. bld.env.append_value('DEFINES', [f'FIRMWARE_OFFSET={bld.env.CONFIG_FIRMWARE_OFFSET}']) # Build and link the firmware ELF @@ -453,30 +449,24 @@ def _link_firmware(bld, sources): x.env.append_value('LINKFLAGS', fw_linkflags) - if bld.env.CONFIG_PBLBOOT: - git_revision = tools.waf.gitinfo.get_git_revision(bld) - bld.env.PBLBOOT_PRIORITY = str(tools.waf.pblboot.boot_priority( - git_revision['TAG'], int(git_revision['TIMESTAMP']))) - nohdr_hex_node = elf_node.change_ext('.nohdr.hex') - bld(rule=tools.waf.objcopy.objcopy_hex, source=elf_node, target=nohdr_hex_node) - hex_node = elf_node.change_ext('.hex') - bld(rule=tools.waf.pblboot.insert_header_hex, source=nohdr_hex_node, target=hex_node) - nohdr_bin_node = elf_node.change_ext('.nohdr.bin') - bld(rule=tools.waf.objcopy.objcopy_bin, source=elf_node, target=nohdr_bin_node) - bin_node = elf_node.change_ext('.bin') - bld(rule=tools.waf.pblboot.insert_header_bin, source=nohdr_bin_node, target=bin_node) - else: - hex_node = elf_node.change_ext('.hex') - bld(rule=tools.waf.objcopy.objcopy_hex, source=elf_node, target=hex_node) - bin_node = elf_node.change_ext('.bin') - bld(rule=tools.waf.objcopy.objcopy_bin, source=elf_node, target=bin_node) - - # Create the log_strings .elf and check the format specifier rules + # Post-link image pipeline: hex/bin (+ pblboot header) and the loghash + # dictionaries, all handled by the standalone tools/fw_image.py driven + # from build-info.json. + targets = [elf_node.change_ext('.hex'), elf_node.change_ext('.bin')] if bld.env.CONFIG_LOG_HASHED: - fw_loghash_node = bld.path.get_bld().make_node('pebbleos_loghash_dict.json') - bld(rule=tools.waf.generate_log_strings_json.wafrule, - source=elf_node, target=fw_loghash_node, path=bld.path) - bld.LOGHASH_DICTS.append(fw_loghash_node) + targets.append(bld.path.get_bld().make_node('pebbleos_loghash_dict.json')) + targets.append(bld.path.get_bld().make_node(LOGHASH_OUT_PATH)) + bld(rule=_fw_image_rule, source=elf_node, target=targets) + + +def _fw_image_rule(task): + bld = task.generator.bld + return task.exec_command([ + sys.executable, + os.path.join(bld.srcnode.abspath(), 'tools', 'fw_image.py'), + '--build-dir', bld.bldnode.abspath(), + '--elf', task.inputs[0].abspath(), + ]) def _build_recovery(bld): @@ -596,7 +586,6 @@ def _build_fw(bld): def build(bld): bld.DYNAMIC_RESOURCES = [] - bld.LOGHASH_DICTS = [] # Start this timer here to include the time to generate tasks. bld.pbl_build_start_time = datetime.datetime.utcnow() @@ -661,15 +650,6 @@ def build(bld): bld.recurse('resources') bld.add_post_fun(size_resources) - if bld.env.CONFIG_LOG_HASHED: - bld.add_post_fun(merge_loghash_dicts) - - -def merge_loghash_dicts(bld): - loghash_dict = bld.path.get_bld().make_node(LOGHASH_OUT_PATH) - - import log_hashing.newlogging - log_hashing.newlogging.merge_loghash_dict_json_files(loghash_dict, bld.LOGHASH_DICTS) class SizeResources(BuildContext): From d5c158356150dfd1ccc289cfe7fffab76ac8d9c3 Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 23:23:05 +0200 Subject: [PATCH 08/11] build: extract firmware bundling into tools/make_fw_bundle.py Move the .pbz bundling glue (_make_bundle/_get_version_info) from the root wscript into a standalone CLI driven by build-info.json: version info via tools/gitinfo, the CONFIG_FW_MAX_SIZE gate, resource pack / loghash / layouts attachment and the output naming convention. The waf bundle command is gone: ./pbl bundle invokes the tool without spawning a waf process, and CI uses it directly. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- .github/workflows/build-firmware.yml | 2 +- .github/workflows/build-prf.yml | 2 +- .github/workflows/release.yml | 4 +- pbl | 13 +++- tools/make_fw_bundle.py | 104 +++++++++++++++++++++++++++ wscript | 81 --------------------- 6 files changed, 120 insertions(+), 86 deletions(-) create mode 100755 tools/make_fw_bundle.py diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml index aa6bba8daf..33992d9895 100644 --- a/.github/workflows/build-firmware.yml +++ b/.github/workflows/build-firmware.yml @@ -90,7 +90,7 @@ jobs: run: ./waf build - name: Bundle - run: ./waf bundle + run: ./pbl bundle - name: Store uses: actions/upload-artifact@v6 diff --git a/.github/workflows/build-prf.yml b/.github/workflows/build-prf.yml index cf47ad0854..5d4566431c 100644 --- a/.github/workflows/build-prf.yml +++ b/.github/workflows/build-prf.yml @@ -96,7 +96,7 @@ jobs: run: ./waf build - name: Bundle - run: ./waf bundle + run: ./pbl bundle - name: Store uses: actions/upload-artifact@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7e2668e54..a4d0a06f9b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,7 +54,7 @@ jobs: run: ./waf build - name: Bundle PRF - run: ./waf bundle + run: ./pbl bundle - name: Copy PRF artifacts run: | @@ -172,7 +172,7 @@ jobs: run: ./waf build - name: Bundle firmware - run: ./waf bundle + run: ./pbl bundle - name: Copy firmware artifacts run: | diff --git a/pbl b/pbl index e3b6dca33f..5c5f67ca84 100755 --- a/pbl +++ b/pbl @@ -28,7 +28,7 @@ BUILD_DIR = os.path.join(REPO_ROOT, "build") LANG_DIR_REL = "resources/normal/base/lang" # Commands forwarded verbatim to ./waf (no build env / waflib needed). -WAF_PASSTHROUGH = ("configure", "menuconfig", "build", "bundle", "clean", "test", "waf") +WAF_PASSTHROUGH = ("configure", "menuconfig", "build", "clean", "test", "waf") # QEMU SDL decorations per board. The first entry is used as the default. QEMU_DECORATIONS = { @@ -501,6 +501,14 @@ def cmd_bork(env, options): _run_runner(env, options, "erase") +def cmd_bundle(env, options): + """Bundle the built firmware into a .pbz (no waf involved).""" + ret = subprocess.call([sys.executable, "tools/make_fw_bundle.py", + "--build-dir", BUILD_DIR]) + if ret != 0: + _fatal("bundle failed") + + def cmd_image_resources(env, options): tty = options.tty if tty is None: @@ -745,6 +753,7 @@ def cmd_pack_all_langs(env, options): OPERATIONAL = { + "bundle": cmd_bundle, "flash": cmd_flash, "console": cmd_console, "debug": cmd_debug, @@ -831,6 +840,8 @@ def build_parser(): sub.add_parser("bork", parents=[dry, runner, runner_args], help="Reset and wipe a connected device") + sub.add_parser("bundle", help="Bundle the built firmware into a .pbz") + p = sub.add_parser("image_resources", parents=[dry], help="UART-image system resources") p.add_argument("--tty", help="tty for serial imaging") diff --git a/tools/make_fw_bundle.py b/tools/make_fw_bundle.py new file mode 100755 index 0000000000..e74565a27c --- /dev/null +++ b/tools/make_fw_bundle.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Core Devices LLC +# SPDX-License-Identifier: Apache-2.0 + +"""Bundle a built firmware into a .pbz. + +Wraps tools/mkbundle.py with the build-specific glue that used to live in +the root wscript: version info from git, the firmware size gate, resource +pack / loghash / layouts attachment and the output naming convention. +Driven by build/build-info.json; needs a completed build. +""" + +import argparse +import os +import sys + +import build_info +import gitinfo +import mkbundle + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def _get_version_info(): + revision = gitinfo.get_git_revision(cwd=REPO_ROOT) + if revision["TAG"] != "?": + return revision["TAG"], int(revision["TIMESTAMP"]), revision["COMMIT"] + return "dev", 0, "" + + +def _check_firmware_image_size(path, max_firmware_size): + firmware_size = os.path.getsize(path) + if firmware_size > max_firmware_size: + sys.exit( + f"Firmware is too large! Size is {firmware_size:#x} " + f"should be less than {max_firmware_size:#x}" + ) + + +def make_bundle(build_dir, out_path=None): + info = build_info.load_build_info(build_dir) + artifacts = info["artifacts"] + config = info["config"] + + fw_type = "recovery" if info["variant"] == "prf" else "normal" + fw_bin = os.path.join(build_dir, artifacts["bin"]) + board = info["board_normalized"] + + version_string, version_ts, version_commit = _get_version_info() + slot = info["slot"] if fw_type == "normal" else None + + if out_path is None: + slot_suffix = "" if slot is None else f"_slot{slot}" + out_path = os.path.join( + build_dir, f"{fw_type}_{board}_{version_string}{slot_suffix}.pbz" + ) + + max_size = config.get("CONFIG_FW_MAX_SIZE") + if not max_size: + sys.exit("CONFIG_FW_MAX_SIZE not set, cannot check firmware size") + _check_firmware_image_size(fw_bin, max_size) + + b = mkbundle.PebbleBundle() + try: + b.add_firmware( + fw_bin, fw_type, version_ts, version_commit, board, version_string, slot + ) + except mkbundle.MissingFileException as e: + sys.exit(f"Error: Missing file {e.filename}, have you run ./pbl build yet?") + + if fw_type == "normal": + b.add_resources(os.path.join(build_dir, artifacts["pbpack"]), version_ts) + + if not config.get("CONFIG_RELEASE") and config.get("CONFIG_LOG_HASHED"): + b.add_loghash(os.path.join(build_dir, artifacts["loghash_dict"])) + + b.add_license(os.path.join(REPO_ROOT, "LICENSE")) + + if fw_type == "normal": + layouts = os.path.join(build_dir, "resources", "layouts.json.auto") + if os.path.isfile(layouts): + b.add_layouts(layouts) + + b.write(out_path) + print(f"Writing bundle to: {out_path}") + return out_path + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--build-dir", required=True, help="Configured build directory") + parser.add_argument( + "-o", + "--out", + help="Output .pbz path (defaults to the " + "conventional name in the build directory)", + ) + args = parser.parse_args() + + make_bundle(args.build_dir, args.out) + + +if __name__ == "__main__": + main() diff --git a/wscript b/wscript index 89dc617515..d41ae0998e 100644 --- a/wscript +++ b/wscript @@ -49,13 +49,6 @@ activate_sdk(waflib.Context.run_dir or os.getcwd()) LOGHASH_OUT_PATH = 'src/fw/loghash_dict.json' -@conf -def get_pbz_node(ctx, fw_type, board_type, version_string, slot=None): - return ctx.path.get_bld().make_node('{}_{}_{}{}.pbz'.format( - fw_type, board_type, version_string, "" if slot is None else f"_slot{slot}" - )) - - @conf def get_pbpack_node(ctx): return ctx.path.get_bld().make_node('system_resources.pbpack') @@ -718,80 +711,6 @@ def docs_all(ctx): """builds the documentation with all dependency graphs out to build/doxygen""" ctx.exec_command('doxygen Doxyfile-all-graphs', stdout=None, stderr=None) -# Bundle commands -################################################# - - -def _get_version_info(ctx): - # FIXME: it's probably a better idea to lift board + version info from the .bin file... this can get out of sync! - git_revision = tools.waf.gitinfo.get_git_revision(ctx) - if git_revision['TAG'] != '?': - version_string = git_revision['TAG'] - version_ts = int(git_revision['TIMESTAMP']) - version_commit = git_revision['COMMIT'] - else: - version_string = 'dev' - version_ts = 0 - version_commit = '' - return version_string, version_ts, version_commit - - -def _make_bundle(ctx, fw_bin_path, fw_type='normal', board=None, resource_path=None, write=True): - import mkbundle - - if board is None: - board = ctx.env.BOARD_NORMALIZED - - b = mkbundle.PebbleBundle() - - version_string, version_ts, version_commit = _get_version_info(ctx) - slot = ctx.env.SLOT if fw_type == 'normal' and ctx.env.SLOT != -1 else None - out_file = ctx.get_pbz_node(fw_type, ctx.env.BOARD_NORMALIZED, version_string, slot).path_from(ctx.path) - - try: - _check_firmware_image_size(ctx, fw_bin_path) - b.add_firmware(fw_bin_path, fw_type, version_ts, version_commit, board, version_string, slot) - except FirmwareTooLargeException as e: - ctx.fatal(str(e)) - except mkbundle.MissingFileException as e: - ctx.fatal('Error: Missing file ' + e.filename + ', have you run ./waf build yet?') - - if resource_path is not None: - b.add_resources(resource_path, version_ts) - if not ctx.env.CONFIG_RELEASE and ctx.env.CONFIG_LOG_HASHED: - loghash_dict = ctx.path.get_bld().make_node(LOGHASH_OUT_PATH).abspath() - b.add_loghash(loghash_dict) - - # Add a LICENSE.txt file - b.add_license('LICENSE') - - if fw_type == 'normal': - layouts_node = ctx.path.get_bld().find_node('resources/layouts.json.auto') - if layouts_node is not None: - b.add_layouts(layouts_node.path_from(ctx.path)) - - if write: - b.write(out_file) - waflib.Logs.pprint('CYAN', 'Writing bundle to: %s' % out_file) - - return b - - -class BundleCommand(BuildContext): - cmd = 'bundle' - fun = 'bundle' - - -def bundle(ctx): - """bundles a firmware""" - - if ctx.env.VARIANT == 'prf': - _make_bundle(ctx, ctx.get_pebbleos_node().path_from(ctx.path), fw_type='recovery') - else: - _make_bundle(ctx, ctx.get_pebbleos_node().path_from(ctx.path), - resource_path=ctx.get_pbpack_node().path_from(ctx.path)) - - # QEMU flash image commands ################################################# From 17a84efb9660cc2b05e41019ae1afcb9a6b792c0 Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 23:25:17 +0200 Subject: [PATCH 09/11] build: extract QEMU flash images into tools/make_qemu_images.py Move the inline qemu_image_micro/qemu_image_spi logic from the root wscript into a standalone CLI driven by build-info.json. The waf qemu_image_* commands are gone: ./pbl qemu builds the images directly, a new ./pbl qemu_images command covers the CI usage, and pbl's last waf-forwarding helper (_run_waf) goes away. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- .github/workflows/build-qemu-sdkshell.yml | 4 +- .github/workflows/build-qemu.yml | 4 +- .github/workflows/release.yml | 8 ++- pbl | 36 ++++++----- tools/make_qemu_images.py | 75 +++++++++++++++++++++++ wscript | 55 ----------------- 6 files changed, 109 insertions(+), 73 deletions(-) create mode 100755 tools/make_qemu_images.py diff --git a/.github/workflows/build-qemu-sdkshell.yml b/.github/workflows/build-qemu-sdkshell.yml index c573d6ffcd..a475317508 100644 --- a/.github/workflows/build-qemu-sdkshell.yml +++ b/.github/workflows/build-qemu-sdkshell.yml @@ -74,7 +74,9 @@ jobs: run: ./waf configure --board ${{ matrix.board }} -DCONFIG_SHELL_SDK=y - name: Build - run: ./waf build qemu_image_micro qemu_image_spi + run: | + ./pbl build + ./pbl qemu_images - name: Store uses: actions/upload-artifact@v6 diff --git a/.github/workflows/build-qemu.yml b/.github/workflows/build-qemu.yml index e58fddb313..e04ba46a82 100644 --- a/.github/workflows/build-qemu.yml +++ b/.github/workflows/build-qemu.yml @@ -74,7 +74,9 @@ jobs: run: ./waf configure --board ${{ matrix.board }} - name: Build - run: ./waf build qemu_image_micro qemu_image_spi + run: | + ./pbl build + ./pbl qemu_images - name: Store uses: actions/upload-artifact@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4d0a06f9b..cc97e9fbb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -242,7 +242,9 @@ jobs: run: ./waf configure --board '${{ matrix.board }}' - name: Build QEMU images - run: ./waf build qemu_image_micro qemu_image_spi + run: | + ./pbl build + ./pbl qemu_images - name: Copy QEMU artifacts run: | @@ -254,7 +256,9 @@ jobs: run: ./waf configure --board '${{ matrix.board }}' -DCONFIG_SHELL_SDK=y - name: Build SDK shell QEMU images - run: ./waf build qemu_image_micro qemu_image_spi + run: | + ./pbl build + ./pbl qemu_images - name: Copy SDK shell QEMU artifacts run: | diff --git a/pbl b/pbl index 5c5f67ca84..66d87faf3c 100755 --- a/pbl +++ b/pbl @@ -62,15 +62,6 @@ def _run_shell(cmd): return subprocess.call(cmd, shell=True) -def _run_waf(*args): - """Run a ./waf subcommand for build steps that pbl orchestrates but does - not perform itself (e.g. the QEMU flash images).""" - if _DRY_RUN: - _pprint("YELLOW", "[dry-run] ./waf " + " ".join(args)) - return 0 - return subprocess.call([sys.executable, "waf"] + list(args), cwd=REPO_ROOT) - - # --- Configured build env ------------------------------------------------- class Env: @@ -333,15 +324,28 @@ def _qemu_launch(env, options): _run_shell(cmd_line) +def _make_qemu_images(*images): + if _DRY_RUN: + _pprint("YELLOW", "[dry-run] make_qemu_images " + " ".join(images)) + return 0 + return subprocess.call([sys.executable, "tools/make_qemu_images.py", + "--build-dir", BUILD_DIR] + list(images)) + + +def cmd_qemu_images(env, options): + """Build the QEMU micro + SPI flash images from the built firmware.""" + if _make_qemu_images() != 0: + _fatal("Failed to build QEMU flash images") + + def cmd_qemu(env, options): - # The flash images are build artifacts, so let waf produce them. Always - # rebuild the micro-flash image; by default rebuild the SPI flash too - # (--keep-flash-image keeps the stored apps/data from a previous run). - if _run_waf("qemu_image_micro") != 0: + # Always rebuild the micro-flash image; by default rebuild the SPI flash + # too (--keep-flash-image keeps the stored apps/data from a previous run). + if _make_qemu_images("micro") != 0: _fatal("Failed to build QEMU micro flash image") spi_flash = os.path.join(BUILD_DIR, "qemu_spi_flash.bin") if not options.keep_flash_image or not os.path.isfile(spi_flash): - if _run_waf("qemu_image_spi") != 0: + if _make_qemu_images("spi") != 0: _fatal("Failed to build QEMU SPI flash image") _qemu_launch(env, options) @@ -758,6 +762,7 @@ OPERATIONAL = { "console": cmd_console, "debug": cmd_debug, "qemu": cmd_qemu, + "qemu_images": cmd_qemu_images, "screenshot": cmd_screenshot, "touch": cmd_touch, "swipe": cmd_swipe, @@ -814,6 +819,9 @@ def build_parser(): p.add_argument("--qemu-decoration", default=None, choices=QEMU_DECORATION_CHOICES, help="SDL decoration for QEMU (defaults to the per-board default)") + sub.add_parser("qemu_images", + help="Build the QEMU micro + SPI flash images") + p = sub.add_parser("touch", help="Inject a touch tap into running QEMU (pixels)") p.add_argument("x", type=int, help="x coordinate in screen pixels") p.add_argument("y", type=int, help="y coordinate in screen pixels") diff --git a/tools/make_qemu_images.py b/tools/make_qemu_images.py new file mode 100755 index 0000000000..56ae0d64a1 --- /dev/null +++ b/tools/make_qemu_images.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Core Devices LLC +# SPDX-License-Identifier: Apache-2.0 + +"""QEMU flash images from built firmware artifacts. + +micro: firmware hex -> qemu_micro_flash.bin (padded to a 512-byte boundary) +spi: system resources pbpack placed at the board's resource offset in a + 0xff-padded SPI flash image (qemu_spi_flash.bin) + +Driven by build/build-info.json; needs a completed build. +""" + +import argparse +import os + +import build_info +from intelhex import IntelHex + + +def make_micro_image(build_dir, artifacts): + fw_hex = os.path.join(build_dir, artifacts["hex"]) + out_path = os.path.join(build_dir, "qemu_micro_flash.bin") + print(f"Writing micro flash image to {out_path}") + + img = IntelHex(fw_hex) + img.padding = 0xFF + flash_end = ((img.maxaddr() + 511) // 512) * 512 + img.tobinfile(out_path, start=0x00000000, end=flash_end - 1) + + +def make_spi_image(build_dir, artifacts, config): + if config.get("CONFIG_QEMU"): + # QEMU generic boards: resources at offset 0x620000 in 32MB flash + resources_begin = 0x620000 + image_size = 0x2000000 + else: + resources_begin = 0x280000 + image_size = 0x400000 + + out_path = os.path.join(build_dir, "qemu_spi_flash.bin") + print(f"Writing SPI flash image to {out_path}") + with open(os.path.join(build_dir, artifacts["pbpack"]), "rb") as f: + res_img = f.read() + + with open(out_path, "wb") as f: + # Pad the first section before system resources with FF's + f.write(bytes([0xFF]) * resources_begin) + f.write(res_img) + # Pad with 0xFF up to image size + f.write(bytes([0xFF]) * (image_size - resources_begin - len(res_img))) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--build-dir", required=True, help="Configured build directory") + parser.add_argument( + "images", + nargs="*", + choices=["micro", "spi"], + help="Which images to build (default: both)", + ) + args = parser.parse_args() + if not args.images: + args.images = ["micro", "spi"] + + info = build_info.load_build_info(args.build_dir) + if "micro" in args.images: + make_micro_image(args.build_dir, info["artifacts"]) + if "spi" in args.images: + make_spi_image(args.build_dir, info["artifacts"], info["config"]) + + +if __name__ == "__main__": + main() diff --git a/wscript b/wscript index d41ae0998e..56507aec3e 100644 --- a/wscript +++ b/wscript @@ -711,61 +711,6 @@ def docs_all(ctx): """builds the documentation with all dependency graphs out to build/doxygen""" ctx.exec_command('doxygen Doxyfile-all-graphs', stdout=None, stderr=None) -# QEMU flash image commands -################################################# - -class QemuImageMicroCommand(BuildContext): - cmd = 'qemu_image_micro' - fun = 'qemu_image_micro' - - -class QemuImageSpiCommand(BuildContext): - cmd = 'qemu_image_spi' - fun = 'qemu_image_spi' - - -def qemu_image_micro(ctx): - """creates the micro-flash image for qemu""" - from intelhex import IntelHex - - fw_hex = ctx.get_pebbleos_node().change_ext('.hex') - micro_flash_node = ctx.path.get_bld().make_node('qemu_micro_flash.bin') - micro_flash_path = micro_flash_node.path_from(ctx.path) - Logs.pprint('CYAN', 'Writing micro flash image to {}'.format(micro_flash_path)) - - img = IntelHex(fw_hex.path_from(ctx.path)) - img.padding = 0xff - flash_end = ((img.maxaddr() + 511) // 512) * 512 - img.tobinfile(micro_flash_path, start=0x00000000, end=flash_end - 1) - - -def qemu_image_spi(ctx): - """creates a SPI flash image for qemu""" - if ctx.env.CONFIG_QEMU: - # QEMU generic boards: resources at offset 0x620000 in 32MB flash - resources_begin = 0x620000 - image_size = 0x2000000 - else: - resources_begin = 0x280000 - image_size = 0x400000 - - spi_flash_node = ctx.path.get_bld().make_node('qemu_spi_flash.bin') - spi_flash_path = spi_flash_node.path_from(ctx.path) - Logs.pprint('CYAN', 'Writing SPI flash image to {}'.format(spi_flash_path)) - with open(spi_flash_path, 'wb') as qemu_spi_img_file: - # Pad the first section before system resources with FF's - qemu_spi_img_file.write(bytes([0xff]) * resources_begin) - - # Write system resources: - pbpack = ctx.get_pbpack_node() - res_img = open(pbpack.path_from(ctx.path), 'rb').read() - qemu_spi_img_file.write(res_img) - - # Pad with 0xFF up to image size - tail_padding_size = image_size - resources_begin - len(res_img) - qemu_spi_img_file.write(bytes([0xff]) * tail_padding_size) - - # Flash commands ################################################# From 6bf98eaa988aa7422f494e5f087be4f54b135c85 Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 23:26:13 +0200 Subject: [PATCH 10/11] pbl: read build-info.json instead of waf's c4che Load build state from the neutral build/build-info.json written at configure time rather than importing waf's c4che/_cache.py. The Env shim keeps waf's ConfigSet semantics (missing keys read as []) so the runner helpers are unchanged. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- pbl | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/pbl b/pbl index 66d87faf3c..7b7c8c54de 100755 --- a/pbl +++ b/pbl @@ -9,13 +9,12 @@ openocd, reset, bork, image_*, make_lang, pack_lang, pack_all_langs) that used to live in the root ``wscript``but are not build steps, and provides thin pass-through wrappers for the everyday ``./waf`` commands so there is a single entry point. -Build state is read directly from the configured build dir -(``build/c4che/_cache.py``); no waf process is spawned for operational -commands. Run ``./waf configure --board BOARD`` first. +Build state is read from ``build/build-info.json``, written at configure +time; no waf process is spawned for operational commands. Run +``./pbl configure --board BOARD`` first. """ import argparse -import importlib.util import os import platform import shlex @@ -82,13 +81,27 @@ class Env: def load_env(): - cache = os.path.join(BUILD_DIR, "c4che", "_cache.py") - if not os.path.isfile(cache): - _fatal("Build not configured -- run ./waf configure --board BOARD first.") - spec = importlib.util.spec_from_file_location("_waf_cache", cache) - mod = importlib.util.module_from_spec(spec) - spec.loader.exec_module(mod) - data = {k: v for k, v in vars(mod).items() if not k.startswith("__")} + from tools.build_info import load_build_info + + try: + info = load_build_info(BUILD_DIR) + except (FileNotFoundError, ValueError) as e: + _fatal(str(e)) + + data = dict(info["config"]) + data.update({ + "BOARD": info["board"], + "BOARD_NAME": info["board_name"], + "BOARD_REVISION": info["board_revision"] or "", + "BOARD_NORMALIZED": info["board_normalized"], + "PLATFORM_NAME": info["platform"], + "MIN_SDK_VERSION": info["min_sdk_version"], + "VARIANT": info["variant"], + "JS_ENGINE": info["js_engine"], + "SLOT": -1 if info["slot"] is None else info["slot"], + "SUPPORTED_RUNNERS": info["runners"], + "RUNNER": info["runner"], + }) return Env(data) From ed7635150d524827500839e886e902ff29fcf798 Mon Sep 17 00:00:00 2001 From: Joshua Jun Date: Tue, 18 Aug 2026 23:41:09 +0200 Subject: [PATCH 11/11] ci, docs: use ./pbl instead of ./waf everywhere All remaining ./waf invocations in the workflows become ./pbl (configure/build/test forward through pbl's passthrough; bundle and qemu_images are pbl commands now). The workflow path filters gain 'pbl' so CLI changes retrigger the builds, and the docs drop stale ./waf and binary_header references. Co-Authored-By: Claude Fable 5 Signed-off-by: Joshua Jun --- .github/workflows/build-firmware.yml | 5 +++-- .github/workflows/build-prf.yml | 5 +++-- .github/workflows/build-qemu-sdkshell.yml | 3 ++- .github/workflows/build-qemu.yml | 3 ++- .github/workflows/build-translation-source.yml | 4 ++-- .github/workflows/nix.yml | 4 ++-- .github/workflows/release.yml | 16 ++++++++-------- .github/workflows/test.yml | 5 +++-- docs/development/testing.md | 2 +- 9 files changed, 26 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build-firmware.yml b/.github/workflows/build-firmware.yml index 33992d9895..5b1686232d 100644 --- a/.github/workflows/build-firmware.yml +++ b/.github/workflows/build-firmware.yml @@ -32,6 +32,7 @@ jobs: - 'third_party/**' - 'tools/waf/**' - 'waf' + - 'pbl' - 'wscript' build-firmware: @@ -84,10 +85,10 @@ jobs: BOARD: ${{ matrix.board }} - name: Configure - run: ./waf configure --board '${{ matrix.board }}' + run: ./pbl configure --board '${{ matrix.board }}' - name: Build - run: ./waf build + run: ./pbl build - name: Bundle run: ./pbl bundle diff --git a/.github/workflows/build-prf.yml b/.github/workflows/build-prf.yml index 5d4566431c..2b52f999aa 100644 --- a/.github/workflows/build-prf.yml +++ b/.github/workflows/build-prf.yml @@ -30,6 +30,7 @@ jobs: - 'third_party/**' - 'tools/waf/**' - 'waf' + - 'pbl' - 'wscript' build-prf: @@ -90,10 +91,10 @@ jobs: OPTS="-DCONFIG_MFG=y -DCONFIG_LOG_HASHED=n" fi - ./waf configure --board '${{ matrix.board }}' --variant=prf $OPTS + ./pbl configure --board '${{ matrix.board }}' --variant=prf $OPTS - name: Build - run: ./waf build + run: ./pbl build - name: Bundle run: ./pbl bundle diff --git a/.github/workflows/build-qemu-sdkshell.yml b/.github/workflows/build-qemu-sdkshell.yml index a475317508..a757264ee3 100644 --- a/.github/workflows/build-qemu-sdkshell.yml +++ b/.github/workflows/build-qemu-sdkshell.yml @@ -31,6 +31,7 @@ jobs: - 'third_party/**' - 'tools/waf/**' - 'waf' + - 'pbl' - 'wscript' build-qemu-sdkshell: @@ -71,7 +72,7 @@ jobs: run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} - name: Configure - run: ./waf configure --board ${{ matrix.board }} -DCONFIG_SHELL_SDK=y + run: ./pbl configure --board ${{ matrix.board }} -DCONFIG_SHELL_SDK=y - name: Build run: | diff --git a/.github/workflows/build-qemu.yml b/.github/workflows/build-qemu.yml index e04ba46a82..5b0fa3468d 100644 --- a/.github/workflows/build-qemu.yml +++ b/.github/workflows/build-qemu.yml @@ -31,6 +31,7 @@ jobs: - 'third_party/**' - 'tools/waf/**' - 'waf' + - 'pbl' - 'wscript' build-qemu: @@ -71,7 +72,7 @@ jobs: run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} - name: Configure - run: ./waf configure --board ${{ matrix.board }} + run: ./pbl configure --board ${{ matrix.board }} - name: Build run: | diff --git a/.github/workflows/build-translation-source.yml b/.github/workflows/build-translation-source.yml index 582c3e33da..868f92a6b0 100644 --- a/.github/workflows/build-translation-source.yml +++ b/.github/workflows/build-translation-source.yml @@ -34,10 +34,10 @@ jobs: pip install -r requirements.txt - name: Configure - run: ./waf configure --board ${{ env.BOARD_NAME }} + run: ./pbl configure --board ${{ env.BOARD_NAME }} - name: Build - run: ./waf build + run: ./pbl build - name: Copy POT template run: cp build/pebbleos.pot resources/normal/base/lang/tintin.pot diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 442c634c1f..e7bfd5a0a8 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -68,10 +68,10 @@ jobs: ' - name: Configure - run: nix develop --command ./waf configure --board asterix + run: nix develop --command ./pbl configure --board asterix - name: Build - run: nix develop --command ./waf build + run: nix develop --command ./pbl build nix-status: needs: [changes-nix, nix-shell] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc97e9fbb5..0885407fa6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,10 +48,10 @@ jobs: BOARD: ${{ matrix.board }} - name: Configure - run: ./waf configure --board '${{ matrix.board }}' --variant=prf -DCONFIG_RELEASE=y + run: ./pbl configure --board '${{ matrix.board }}' --variant=prf -DCONFIG_RELEASE=y - name: Build PRF - run: ./waf build + run: ./pbl build - name: Bundle PRF run: ./pbl bundle @@ -83,10 +83,10 @@ jobs: --endpoint-url "${{ vars.LOG_HASH_BUCKET_ENDPOINT }}" - name: Configure PRF MFG - run: ./waf configure --board '${{ matrix.board }}' --variant=prf -DCONFIG_MFG=y -DCONFIG_LOG_HASHED=n -DCONFIG_RELEASE=y + run: ./pbl configure --board '${{ matrix.board }}' --variant=prf -DCONFIG_MFG=y -DCONFIG_LOG_HASHED=n -DCONFIG_RELEASE=y - name: Build MFG PRF - run: ./waf build + run: ./pbl build - name: Copy MFG PRF artifacts run: | @@ -166,10 +166,10 @@ jobs: fi - name: Configure - run: ./waf configure --board '${{ matrix.board }}' -DCONFIG_FIRMWARE_SLOT=${{ matrix.slot }} -DCONFIG_RELEASE=y + run: ./pbl configure --board '${{ matrix.board }}' -DCONFIG_FIRMWARE_SLOT=${{ matrix.slot }} -DCONFIG_RELEASE=y - name: Build firmware - run: ./waf build + run: ./pbl build - name: Bundle firmware run: ./pbl bundle @@ -239,7 +239,7 @@ jobs: pip install -r requirements.txt - name: Configure - run: ./waf configure --board '${{ matrix.board }}' + run: ./pbl configure --board '${{ matrix.board }}' - name: Build QEMU images run: | @@ -253,7 +253,7 @@ jobs: cp build/qemu_spi_flash.bin artifacts/${{ matrix.board }}_${{ github.ref_name }}_spi_flash.bin - name: Configure SDK shell - run: ./waf configure --board '${{ matrix.board }}' -DCONFIG_SHELL_SDK=y + run: ./pbl configure --board '${{ matrix.board }}' -DCONFIG_SHELL_SDK=y - name: Build SDK shell QEMU images run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1dd63cd206..1e674cf1ce 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,6 +35,7 @@ jobs: - 'third_party/**' - 'tools/waf/**' - 'waf' + - 'pbl' - 'wscript' build-test: @@ -64,12 +65,12 @@ jobs: pip install -r requirements.txt - name: Configure - run: ./waf configure --board ${{env.TEST_BOARD}} + run: ./pbl configure --board ${{env.TEST_BOARD}} - name: Run tests # -k keeps going after a failing test so the run reports every failure, # not just the first. The job still fails if anything failed. - run: ./waf test -k + run: ./pbl test -k - name: Publish Test Report uses: mikepenz/action-junit-report@v6 diff --git a/docs/development/testing.md b/docs/development/testing.md index 3d912a2c3a..c1f1fb3507 100644 --- a/docs/development/testing.md +++ b/docs/development/testing.md @@ -14,7 +14,7 @@ Tests are built and run with waf. Configure first (any board works; CI uses ./pbl test ``` -Useful options (see `./waf --help` for the full list): +Useful options (see `./pbl test --help` for the full list): - `-M REGEX` / `--match REGEX`: only build/run test files matching the regex, e.g. `./pbl test -M test_animation`