chore: Extract generation of headers from CMake
What changed, and why it matters
This is a build-system refactoring commit. It moves the generation of version.h and rust.h out of CMake and into a Python script and Rust build.rs, so the Rust code can be built standalone without CMake. There is no change to runtime firmware behavior, no bug fix, and no security patch visible in the diff.
No security action required. Treat as ordinary build-maintenance change; review for build reproducibility and developer workflow impact during normal QA.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit extracts version header generation from CMakeLists.txt into scripts/generate_version_headers.py and a new versions.json manifest. It also adds scripts/generate_rust_header.sh and updates bitbox02-sys/build.rs to generate version.h and rust.h when cargo runs standalone. Template files replace CMake configure_file inputs (.in -> .tmpl). The Rust crate bitbox02-rust now reads versions.json at build time to set FIRMWARE_VERSION_SHORT. No cryptographic, memory-safety, or device-control logic is modified.
Changed components
CMake build systemRust build scripts (build.rs)Version header generationcbindgen/rust.h generationInspect captured patch +882 / −475
diff --git a/AGENTS.md b/AGENTS.md
index e93b2de..da291e0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -46,9 +46,9 @@ features such as `cd && ...`, pass an explicit shell as the command, e.g.
pinned toolchain in `rust-toolchain.toml`; keep module paths aligned with `src/rust` and regenerate
bindings (`cbindgen`, protobuf) when interfaces change.
-For C code changes, run `./scripts/dev_exec.sh ./scripts/format` to format the code. For Python
-changes, run `./scripts/dev_exec.sh black` to format the code. For Rust code changes, run
-`./scripts/dev_exec.sh bash -lc 'cd src/rust && cargo fmt'`.
+* For C code changes, run `./scripts/dev_exec.sh ./scripts/format` to format the code.
+* For Python changes, run `./scripts/dev_exec.sh ./scripts/format-python` to format the code.
+* For Rust code changes, run `./scripts/dev_exec.sh -lc 'cd src/rust && cargo fmt'` to format the code.
## Testing Guidelines
Place new C specs in `test/unit-test` and add doubles to `test/hardware-fakes` when hardware
diff --git a/CMakeLists.txt b/CMakeLists.txt
index deacbfa..542a5ae 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -90,96 +90,42 @@ endif()
#-----------------------------------------------------------------------------
# Create version header file
#
-# Versions MUST contain three parts and start with lowercase 'v'.
-# Example 'v1.0.0'. They MUST not contain a pre-release label such as '-beta'.
-set(FIRMWARE_VERSION "v9.26.0")
-set(BOOTLOADER_VERSION "v1.1.2")
-
find_package(PythonInterp 3.6 REQUIRED)
-
-find_package(Git)
-if(GIT_FOUND AND PYTHONINTERP_FOUND)
- # TODO: --verify cannot be used in container. Add our pubkeys to image?
- execute_process(
- COMMAND ${PYTHON_EXECUTABLE} ./scripts/get_version firmware --check-semver --check-gpg
- RESULT_VARIABLE exit_code
- OUTPUT_VARIABLE GIT_FIRMWARE_VERSION_STRING
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
- OUTPUT_STRIP_TRAILING_WHITESPACE
- )
- if(NOT exit_code EQUAL "0")
- message(FATAL_ERROR "get_version firmware failed")
- endif()
- execute_process(
- COMMAND ${PYTHON_EXECUTABLE} ./scripts/get_version bootloader --check-semver --check-gpg
- RESULT_VARIABLE exit_code
- OUTPUT_VARIABLE GIT_BOOTLOADER_VERSION_STRING
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
- OUTPUT_STRIP_TRAILING_WHITESPACE
- )
- if(NOT exit_code EQUAL "0")
- message(FATAL_ERROR "get_version bootloader failed")
- endif()
-
- execute_process(
- COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
- RESULT_VARIABLE exit_code
- OUTPUT_VARIABLE GIT_COMMIT_HASH
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
- OUTPUT_STRIP_TRAILING_WHITESPACE
- )
- if(NOT exit_code EQUAL "0")
- set(GIT_COMMIT_HASH "0000000000000000000000000000000000000000")
- endif()
- execute_process(
- COMMAND ${GIT_EXECUTABLE} rev-parse --short=10 HEAD
- RESULT_VARIABLE exit_code
- OUTPUT_VARIABLE GIT_COMMIT_HASH_SHORT
- WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
- OUTPUT_STRIP_TRAILING_WHITESPACE
- )
- if(NOT exit_code EQUAL "0")
- set(GIT_COMMIT_HASH_SHORT "000000")
- endif()
-else()
- set(GIT_FIRMWARE_VERSION_STRING "v0.0.0")
- set(GIT_BOOTLOADER_VERSION_STRING "v0.0.0")
- set(GIT_COMMIT_HASH "0000000000000000000000000000000000000000")
- set(GIT_COMMIT_HASH_SHORT "0000000000")
-endif()
-
-# If the current HEAD is not on the matching tag append '-pre' for "pre-release"
-set(FIRMWARE_VERSION_FULL ${FIRMWARE_VERSION})
-if(NOT FIRMWARE_VERSION STREQUAL GIT_FIRMWARE_VERSION_STRING)
- string(APPEND FIRMWARE_VERSION_FULL "-pre+${GIT_COMMIT_HASH_SHORT}")
+set(VERSION_MANIFEST ${CMAKE_SOURCE_DIR}/versions.json)
+set(VERSION_GENERATOR ${CMAKE_SOURCE_DIR}/scripts/generate_version_headers.py)
+set(VERSION_TEMPLATE ${CMAKE_SOURCE_DIR}/src/version.h.tmpl)
+set(BOOTLOADER_VERSION_TEMPLATE ${CMAKE_SOURCE_DIR}/src/bootloader/bootloader_version.h.tmpl)
+set(GENERATED_VERSION_HEADERS_DIR ${CMAKE_BINARY_DIR}/src)
+set(GENERATED_VERSION_CMAKE ${CMAKE_BINARY_DIR}/generated_versions.cmake)
+set_property(
+ DIRECTORY
+ APPEND
+ PROPERTY CMAKE_CONFIGURE_DEPENDS
+ ${VERSION_MANIFEST}
+ ${VERSION_GENERATOR}
+ ${VERSION_TEMPLATE}
+ ${BOOTLOADER_VERSION_TEMPLATE}
+)
+execute_process(
+ COMMAND
+ ${PYTHON_EXECUTABLE}
+ ${VERSION_GENERATOR}
+ generate
+ --repo-root ${CMAKE_SOURCE_DIR}
+ --output-dir ${GENERATED_VERSION_HEADERS_DIR}
+ --cmake-vars-out ${GENERATED_VERSION_CMAKE}
+ WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+ RESULT_VARIABLE exit_code
+ ERROR_VARIABLE version_generator_error
+)
+if(NOT exit_code EQUAL "0")
+ message(FATAL_ERROR "generate_version_headers.py failed:\n${version_generator_error}")
endif()
-
-string(REPLACE "." ";" VERSION_LIST ${FIRMWARE_VERSION})
-list(GET VERSION_LIST 0 vMAJOR)
-list(GET VERSION_LIST 1 MINOR)
-list(GET VERSION_LIST 2 PATCH)
-string(REPLACE "v" "" MAJOR ${vMAJOR})
-
-string(LENGTH ${FIRMWARE_VERSION_FULL} FIRMWARE_VERSION_FULL_LEN)
-string(REGEX REPLACE "(.)" "'\\1', 0, " FIRMWARE_VERSION_FULL_W16 ${FIRMWARE_VERSION_FULL})
-set(FIRMWARE_VERSION_MAJOR ${MAJOR})
-set(FIRMWARE_VERSION_MINOR ${MINOR})
-set(FIRMWARE_VERSION_PATCH ${PATCH})
-
-# BOOTLOADER
-
-set(BOOTLOADER_VERSION_FULL ${BOOTLOADER_VERSION})
-if(NOT BOOTLOADER_VERSION STREQUAL GIT_BOOTLOADER_VERSION_STRING)
- string(APPEND BOOTLOADER_VERSION_FULL "-pre+${GIT_COMMIT_HASH_SHORT}")
+include(${GENERATED_VERSION_CMAKE})
+if(BOOTLOADER_VERSION_HAS_METADATA)
add_definitions("-DBOOTLOADER_VERSION_HAS_METADATA")
endif()
-string(LENGTH ${BOOTLOADER_VERSION_FULL} BOOTLOADER_VERSION_FULL_LEN)
-string(REGEX REPLACE "(.)" "'\\1', 0, " BOOTLOADER_VERSION_FULL_W16 ${BOOTLOADER_VERSION_FULL})
-
-configure_file(src/version.h.in src/version.h)
-configure_file(src/bootloader/bootloader_version.h.in src/bootloader/bootloader_version.h)
-
#-----------------------------------------------------------------------------
# Set the default compiler options (Only set global options that truly are necessary for all files)
diff --git a/releases/build.sh b/releases/build.sh
index 88193cd..78f2606 100755
--- a/releases/build.sh
+++ b/releases/build.sh
@@ -12,11 +12,9 @@ git clone --depth 1 --branch $1 --recurse-submodules https://github.com/BitBoxSw
cd temp;
-# The shallow clone above doesn't fetch tags. Even if only building the firmware, the CMakeLists.txt
-# fetches the bootloader version using `./scripts/get_version bootloader`, which requires a
-# bootloader tag. The build scripts can be changed to only use the firmware tag that is needed,
-# ignoring the others, but we fetch the tags here so that builds of previous releases continue to
-# work.
+# The shallow clone above doesn't fetch tags. The build uses release tags to decide whether the
+# firmware and bootloader versions should include pre-release metadata, so we fetch tags here to
+# keep current and previous releases buildable.
git fetch --tags;
# For v9.15.0, the reproducible build using this script failed with this error:
diff --git a/scripts/generate_rust_header.sh b/scripts/generate_rust_header.sh
new file mode 100644
index 0000000..3536637
--- /dev/null
+++ b/scripts/generate_rust_header.sh
@@ -0,0 +1,33 @@
+#!/bin/bash
+# SPDX-License-Identifier: Apache-2.0
+
+set -euo pipefail
+
+DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
+REPO_ROOT="$(realpath "$DIR/..")"
+RUST_WORKSPACE_DIR="${REPO_ROOT}/src/rust"
+LIBBITBOX02_RUST_SOURCE_DIR="${RUST_WORKSPACE_DIR}/bitbox02-rust-c"
+
+OUTPUT_DIR="${1:?usage: generate_rust_header.sh <output_dir>}"
+CARGO_BIN="${CARGO_BIN:-cargo}"
+CBINDGEN_BIN="${CBINDGEN_BIN:-cbindgen}"
+
+mkdir -p "${OUTPUT_DIR}"
+OUTPUT_DIR="$(realpath "${OUTPUT_DIR}")"
+METADATA_PATH="${OUTPUT_DIR}/rust-metadata.json"
+
+(
+ cd "${RUST_WORKSPACE_DIR}"
+ "${CARGO_BIN}" metadata \
+ --offline \
+ --format-version 1 \
+ --manifest-path "${LIBBITBOX02_RUST_SOURCE_DIR}/Cargo.toml" \
+ > "${METADATA_PATH}"
+ "${CBINDGEN_BIN}" \
+ --quiet \
+ --config "${RUST_WORKSPACE_DIR}/bitbox02-cbindgen.toml" \
+ --output "${OUTPUT_DIR}/rust.h" \
+ --profile release \
+ --metadata "${METADATA_PATH}" \
+ "${LIBBITBOX02_RUST_SOURCE_DIR}"
+)
diff --git a/scripts/generate_version_headers.py b/scripts/generate_version_headers.py
new file mode 100755
index 0000000..1702f55
--- /dev/null
+++ b/scripts/generate_version_headers.py
@@ -0,0 +1,433 @@
+#!/usr/bin/env python3
+"""
+Shared version metadata and header generator.
+
+This script serves two roles:
+1. Generate version headers (and optional CMake vars) from a manifest.
+2. Provide the legacy `scripts/get_version` behavior via an importable entrypoint.
+"""
+
+import argparse
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+import textwrap
+from string import Template
+
+
+SEMVER_RE = re.compile(
+ r"""
+ ^v
+ (?:0|[1-9][0-9]*)
+ \.
+ (?:0|[1-9][0-9]*)
+ \.
+ (?:0|[1-9][0-9]*)
+ (\-
+ (?:0|[1-9A-Za-z-][0-9A-Za-z-]*)
+ (\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))*
+ )?
+ (\+
+ [0-9A-Za-z-]+
+ (\.[0-9A-Za-z-]+)*
+ )?
+ $
+ """,
+ re.VERBOSE,
+)
+RELEASE_VERSION_RE = re.compile(r"^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$")
+
+ZERO_GIT_COMMIT_HASH = "0000000000000000000000000000000000000000"
+ZERO_GIT_COMMIT_HASH_SHORT = "0000000000"
+
+
+def eprintln(*args, **kwargs):
+ print(*args, **kwargs, file=sys.stderr)
+
+
+def system(*args, **kwargs):
+ res = subprocess.run(
+ *args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8", **kwargs
+ )
+ if res.returncode != 0:
+ eprintln("Failed to run `{}`".format(args[0]))
+ eprintln("stderr: {}".format(res.stderr))
+ return res
+
+
+def parse_tags(rows, only_signed, prefix):
+ """Parses `git tag` rows and returns matching refnames."""
+ rows = [row[4:] for row in rows if row.startswith("tag")]
+ rows = [row[2:] for row in rows if row[0] == "Y" or not only_signed]
+ if prefix is None:
+ rows = [row for row in rows if "/" not in row]
+ return rows
+
+
+def git_list_cmd(prefix, extra_args=None):
+ if extra_args is None:
+ extra_args = []
+ cmd = ["git", "tag", "--list", "--sort=taggerdate"]
+ cmd += extra_args
+ if prefix is None:
+ cmd += [
+ "--format=%(objecttype) %(if)%(contents:signature)%(then)Y%(else)N%(end) %(refname:strip=2)"
+ ]
+ else:
+ cmd += [
+ "--format=%(objecttype) %(if)%(contents:signature)%(then)Y%(else)N%(end) %(refname:strip=3)",
+ "{}/{}".format(prefix, "*"),
+ ]
+ return cmd
+
+
+def compute_tag_version(repo_root, prefix, check_gpg=False, verify=False, check_semver=False):
+ git = shutil.which("git")
+ if git is None:
+ raise RuntimeError("Command `git` not found.")
+
+ res = system(git_list_cmd(prefix, ["--points-at", "HEAD"]), cwd=repo_root)
+ if res.returncode != 0:
+ raise RuntimeError("Failed to list tags on HEAD")
+
+ rows = res.stdout.strip().splitlines()
+ tags = parse_tags(rows, check_gpg, prefix)
+
+ if not tags:
+ res = system(git_list_cmd(prefix), cwd=repo_root)
+ if res.returncode != 0:
+ raise RuntimeError("Failed to list repository tags")
+
+ rows = res.stdout.strip().splitlines()
+ tags = parse_tags(rows, check_gpg, prefix)
+
+ if not tags:
+ annotated = "signed" if check_gpg else "annotated"
+ if prefix is not None:
+ raise RuntimeError("No {} tags found with prefix {}".format(annotated, prefix))
+ raise RuntimeError("No {} tags found without prefix".format(annotated))
+
+ version = tags[-1]
+ selected_tag = version if prefix is None else "{}/{}".format(prefix, version)
+
+ if check_semver and not SEMVER_RE.match(version):
+ raise RuntimeError("Invalid format of tag '{}'".format(selected_tag))
+
+ if verify:
+ res = system(["git", "tag", "-v", selected_tag], cwd=repo_root)
+ if res.returncode != 0:
+ raise RuntimeError("Failed to verify tag '{}'".format(selected_tag))
+
+ res = system(
+ ["git", "rev-list", "--count", "{}..HEAD".format(selected_tag)],
+ cwd=repo_root,
+ )
+ if res.returncode != 0:
+ raise RuntimeError("Failed to count commits since '{}'".format(selected_tag))
+
+ try:
+ count = int(res.stdout.strip())
+ except ValueError:
+ raise RuntimeError(
+ "Failed to parse `git rev-list --count` as integer: {}".format(res.stdout.strip())
+ )
+
+ version += "+{}".format(count)
+ else:
+ version = tags[0]
+ selected_tag = version if prefix is None else "{}/{}".format(prefix, version)
+
+ if check_semver and not SEMVER_RE.match(version):
+ raise RuntimeError("Invalid format of tag '{}'".format(selected_tag))
+
+ if verify:
+ res = system(["git", "tag", "-v", selected_tag], cwd=repo_root)
+ if res.returncode != 0:
+ raise RuntimeError("Failed to verify tag '{}'".format(selected_tag))
+
+ res = system(["git", "status", "--porcelain"], cwd=repo_root)
+ if res.returncode != 0:
+ raise RuntimeError("Failed to read git status")
+
+ if res.stdout.strip():
+ if "+" in version:
+ version += ".dirty"
+ else:
+ version += "+dirty"
+
+ return version
+
+
+def git_output(repo_root, args, default=None):
+ git = shutil.which("git")
+ if git is None:
+ return default
+ try:
+ output = subprocess.run(
+ [git] + list(args),
+ cwd=repo_root,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ encoding="utf-8",
+ check=False,
+ )
+ except OSError:
+ return default
+ if output.returncode != 0:
+ return default
+ value = output.stdout.strip()
+ return value if value else default
+
+
+def strict_tag_version(repo_root, prefix):
+ return compute_tag_version(repo_root, prefix, check_gpg=True, verify=False, check_semver=True)
+
+
+def load_manifest(manifest_path):
+ with open(manifest_path, "r", encoding="utf-8") as infile:
+ manifest = json.load(infile)
+ for key in ("firmware", "bootloader"):
+ value = manifest.get(key)
+ if not isinstance(value, str) or not RELEASE_VERSION_RE.match(value):
+ raise ValueError("Manifest entry '{}' must be a semver string like v1.2.3".format(key))
+ return manifest
+
+
+def build_version_info(base_version, tag_version, git_commit_hash_short):
+ parts = base_version.split(".")
+ if len(parts) != 3 or not parts[0].startswith("v"):
+ raise ValueError("invalid version format: {}".format(base_version))
+ full_version = base_version
+ has_metadata = tag_version != base_version
+ if has_metadata:
+ full_version = "{}-pre+{}".format(base_version, git_commit_hash_short)
+ return {
+ "base": base_version,
+ "full": full_version,
+ "full_len": len(full_version),
+ "full_w16": "".join("'{}', 0, ".format(ch) for ch in full_version),
+ "major": parts[0][1:],
+ "minor": parts[1],
+ "patch": parts[2],
+ "has_metadata": has_metadata,
+ }
+
+
+def render_template(template_path, substitutions):
+ with open(template_path, "r", encoding="utf-8") as infile:
+ template = Template(infile.read())
+ return template.substitute(substitutions)
+
+
+def write_file(path, contents):
+ parent = os.path.dirname(path)
+ if parent:
+ os.makedirs(parent, exist_ok=True)
+ with open(path, "w", encoding="utf-8") as outfile:
+ outfile.write(contents)
+
+
+def cmake_quote(value):
+ return value.replace("\\", "\\\\").replace('"', '\\"')
+
+
+def write_cmake_vars(
+ path,
+ firmware_info,
+ bootloader_info,
+ git_firmware_version_string,
+ git_bootloader_version_string,
+):
+ contents = textwrap.dedent(
+ """\
+ set(GIT_FIRMWARE_VERSION_STRING "{git_firmware_version_string}")
+ set(GIT_BOOTLOADER_VERSION_STRING "{git_bootloader_version_string}")
+ set(FIRMWARE_VERSION_FULL "{firmware_version_full}")
+ set(BOOTLOADER_VERSION_FULL "{bootloader_version_full}")
+ set(BOOTLOADER_VERSION_HAS_METADATA {bootloader_has_metadata})
+ """
+ ).format(
+ git_firmware_version_string=cmake_quote(git_firmware_version_string),
+ git_bootloader_version_string=cmake_quote(git_bootloader_version_string),
+ bootloader_version_full=cmake_quote(bootloader_info["full"]),
+ firmware_version_full=cmake_quote(firmware_info["full"]),
+ bootloader_has_metadata="TRUE" if bootloader_info["has_metadata"] else "FALSE",
+ )
+ write_file(path, contents)
+
+
+def generate_headers(repo_root, output_dir, cmake_vars_out=None, manifest_path=None):
+ if manifest_path is None:
+ manifest_path = os.path.join(repo_root, "versions.json")
+ manifest = load_manifest(manifest_path)
+
+ git_commit_hash = git_output(repo_root, ["rev-parse", "HEAD"], ZERO_GIT_COMMIT_HASH)
+ git_commit_hash_short = git_output(
+ repo_root, ["rev-parse", "--short=10", "HEAD"], ZERO_GIT_COMMIT_HASH_SHORT
+ )
+
+ git_firmware_version_string = strict_tag_version(repo_root, "firmware")
+ git_bootloader_version_string = strict_tag_version(repo_root, "bootloader")
+ firmware_info = build_version_info(
+ manifest["firmware"],
+ git_firmware_version_string,
+ git_commit_hash_short,
+ )
+ bootloader_info = build_version_info(
+ manifest["bootloader"],
+ git_bootloader_version_string,
+ git_commit_hash_short,
+ )
+
+ substitutions = {
+ "FIRMWARE_VERSION_FULL": firmware_info["full"],
+ "FIRMWARE_VERSION": firmware_info["base"],
+ "FIRMWARE_VERSION_FULL_LEN": str(firmware_info["full_len"]),
+ "FIRMWARE_VERSION_FULL_W16": firmware_info["full_w16"],
+ "FIRMWARE_VERSION_MAJOR": firmware_info["major"],
+ "FIRMWARE_VERSION_MINOR": firmware_info["minor"],
+ "FIRMWARE_VERSION_PATCH": firmware_info["patch"],
+ "GIT_COMMIT_HASH": git_commit_hash,
+ "GIT_COMMIT_HASH_SHORT": git_commit_hash_short,
+ "BOOTLOADER_VERSION_FULL": bootloader_info["full"],
+ "BOOTLOADER_VERSION_FULL_W16": bootloader_info["full_w16"],
+ "BOOTLOADER_VERSION_FULL_LEN": str(bootloader_info["full_len"]),
+ }
+
+ write_file(
+ os.path.join(output_dir, "version.h"),
+ render_template(
+ os.path.join(repo_root, "src", "version.h.tmpl"),
+ substitutions,
+ ),
+ )
+ write_file(
+ os.path.join(output_dir, "bootloader", "bootloader_version.h"),
+ render_template(
+ os.path.join(repo_root, "src", "bootloader", "bootloader_version.h.tmpl"),
+ substitutions,
+ ),
+ )
+ if cmake_vars_out is not None:
+ write_cmake_vars(
+ cmake_vars_out,
+ firmware_info,
+ bootloader_info,
+ git_firmware_version_string,
+ git_bootloader_version_string,
+ )
+
+
+def main_generate(argv=None):
+ parser = argparse.ArgumentParser(description="Generate version headers from versions.json")
+ parser.add_argument("--repo-root", required=True)
+ parser.add_argument("--output-dir", required=True)
+ parser.add_argument("--cmake-vars-out")
+ parser.add_argument("--manifest")
+ args = parser.parse_args(argv)
+
+ try:
+ generate_headers(
+ repo_root=os.path.abspath(args.repo_root),
+ output_dir=os.path.abspath(args.output_dir),
+ cmake_vars_out=(
+ os.path.abspath(args.cmake_vars_out) if args.cmake_vars_out is not None else None
+ ),
+ manifest_path=(os.path.abspath(args.manifest) if args.manifest is not None else None),
+ )
+ except (RuntimeError, ValueError) as err:
+ eprintln(err)
+ return 1
+ return 0
+
+
+def main_get_version(argv=None):
+ parser = argparse.ArgumentParser(
+ description=textwrap.dedent(
+ """
+ %(prog)s is a tool for creating a version string out of annotated tags. If there isn't any
+ tag on the current HEAD it will print the newest tag concatenated with the count of commits
+ since that commit (i.e. vX.Y.Z-COUNT, similar to git-describe).
+
+ If there are modified or untracked files in the repository it will append `+dirty` to the
+ version.
+
+ Optionally it is also possible to enforce that the tag is signed and return an error
+ otherwise.
+
+ Using `prefix` it supports "monorepo" style repositories, where many "components" live in
+ the same repository with individual release schedules. Releases must then be tagged with
+ <prefix>/vX.Y.Z. If `prefix` is used and there isn't any tag on HEAD it will count the
+ commits since the newest tag with the correct prefix.
+
+ `--list` can be used for debugging, it will print all tags in the repository with
+ information about if the tags. The first column indicates if it is a lightweight (commit)
+ tag or if it is an annotated tag (tag). The second column shows if it contains a signature
+ (Y) or not (N). The third column is the ref/tag name.
+ """
+ ),
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument("prefix", nargs="?", default=None)
+ parser.add_argument(
+ "--check-gpg", help="Require a gpg signature of chosen tag", action="store_true"
+ )
+ parser.add_argument("--verify", help="Verify gpg signature of chosen tag", action="store_true")
+ parser.add_argument(
+ "--check-semver",
+ help="Require tag to follow `vX.Y.Z` naming scheme",
+ action="store_true",
+ )
+ parser.add_argument("--list", help="List all tags", action="store_true")
+ args = parser.parse_args(argv)
+
+ git = shutil.which("git")
+ if git is None:
+ eprintln("Command `git` not found.")
+ return 1
+
+ if args.list:
+ res = system(git_list_cmd(None))
+ if res.returncode != 0:
+ return res.returncode
+ sys.stdout.write(res.stdout)
+ return 0
+
+ try:
+ print(
+ compute_tag_version(
+ repo_root=os.getcwd(),
+ prefix=args.prefix,
+ check_gpg=args.check_gpg,
+ verify=args.verify,
+ check_semver=args.check_semver,
+ )
+ )
+ except RuntimeError as err:
+ eprintln(err)
+ return 1
+ return 0
+
+
+def main(argv=None):
+ parser = argparse.ArgumentParser(description="Version metadata helpers")
+ subparsers = parser.add_subparsers(dest="command")
+ subparsers.add_parser("generate", help="Generate version headers and optional CMake vars")
+ subparsers.add_parser("get-version", help="Print the version derived from git tags")
+ args, remaining = parser.parse_known_args(argv)
+
+ if args.command is None:
+ parser.error("a command is required")
+
+ if args.command == "generate":
+ return main_generate(remaining)
+ if args.command == "get-version":
+ return main_get_version(remaining)
+ parser.error("unknown command")
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/get_version b/scripts/get_version
index 84efbdf..5959245 100755
--- a/scripts/get_version
+++ b/scripts/get_version
@@ -1,240 +1,10 @@
#!/usr/bin/env python3
-"""
-Parse the tags and strip the prefix
-First argument should be prefix to match against
-We only support annotated tags, you can check the signature with --verify.
+"""Compatibility wrapper for the shared version helper."""
-NOTE: annotated tags have "taggerdate" and "objecttype==tag"
- unannotated tags will have "objecttype==commit" since they are metadataless
-
-NOTE: If this is called from github action, ensure github action hasn't
- replaced the tag during checkout:
- https://github.com/actions/checkout/issues/1638
-
-Requires python3.6
-"""
-
-import subprocess
-import shutil
-import argparse
-import os
import sys
-import textwrap
-import re
-
-SEMVER_RE = re.compile(
- r"""
- ^v
- (?:0|[1-9][0-9]*)
- \.
- (?:0|[1-9][0-9]*)
- \.
- (?:0|[1-9][0-9]*)
- (\-
- (?:0|[1-9A-Za-z-][0-9A-Za-z-]*)
- (\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))*
- )?
- (\+
- [0-9A-Za-z-]+
- (\.[0-9A-Za-z-]+)*
- )?
- $
- """,
- re.VERBOSE,
-)
-
-
-def parse_tags(rows, only_signed, prefix):
- """Parses the rows given by `git tag`. Removes the objectypes and signatures and returns the
- refnames"""
- # Removes any line that doesn't start with 'tag' and strip 'tag ' from those lines
- rows = [x[4:] for x in rows if x.startswith("tag")]
- # Strips "Y " or "N " and removes non-signed if requested
- rows = [x[2:] for x in rows if x[0] == "Y" or not only_signed]
- if prefix is None:
- rows = [x for x in rows if "/" not in x]
- return rows
-
-
-def system(*args):
- """Wrapper around subprocess.run"""
- res = subprocess.run(
- *args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8"
- )
- if res.returncode != 0:
- eprintln(f"Failed to run `{args[0]}`")
- eprintln(f"stderr: {res.stderr}")
- return res
-
-
-def eprintln(*args, **kwargs):
- """Helper for printing to stderr"""
- print(*args, **kwargs, file=os.sys.stderr)
-
-
-def git_list_cmd(prefix, extra_args=None):
- """The arguments to launch git tag. The output will be something like `tag Y v1.0.0` or
- `commit N v0.0.1`"""
- if extra_args is None:
- extra_args = []
- cmd = ["git", "tag", "--list", "--sort=taggerdate"]
- cmd += extra_args
- if prefix is None:
- cmd += [
- "--format=%(objecttype) %(if)%(contents:signature)%(then)Y%(else)N%(end) %(refname:strip=2)"
- ]
- else:
- cmd += [
- "--format=%(objecttype) %(if)%(contents:signature)%(then)Y%(else)N%(end) %(refname:strip=3)",
- f"{prefix}/*",
- ]
- return cmd
-
-
-def main():
- """Main function"""
- # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements
- parser = argparse.ArgumentParser(
- description=textwrap.dedent(
- """
- %(prog)s is a tool for creating a version string out of annotated tags. If there isn't any
- tag on the current HEAD it will print the newest tag concatenated with the count of commits
- since that commit (i.e. vX.Y.Z-COUNT, similar to git-describe).
-
- If there are modified or untracked files in the repository it will append `+dirty` to the
- version.
-
- Optionally it is also possible to enforce that the tag is signed and return an error
- otherwise.
-
- Using `prefix` it supports "monorepo" style repositories, where many "components" live in
- the same repository with individual release schedules. Releases must then be tagged with
- <prefix>/vX.Y.Z. If `prefix` is used and there isn't any tag on HEAD it will count the
- commits since the newest tag with the correct prefix.
-
- `--list` can be used for debugging, it will print all tags in the repository with
- information about if the tags. The first column indicates if it is a lightweight (commit)
- tag or if it is an annotated tag (tag). The second column shows if it contains a signature
- (Y) or not (N). The third column is the ref/tag name.
- """
- ),
- formatter_class=argparse.RawDescriptionHelpFormatter,
- )
- parser.add_argument("prefix", nargs="?", default=None)
- # TODO: Check if there is signature with `contents:signature`
- parser.add_argument(
- "--check-gpg", help="Require a gpg signature of chosen tag", action="store_true"
- )
- parser.add_argument(
- "--verify", help="Verify gpg signature of chosen tag", action="store_true"
- )
- parser.add_argument(
- "--check-semver",
- help="Require tag to follow `vX.Y.Z` naming scheme",
- action="store_true",
- )
- parser.add_argument("--list", help="List all tags", action="store_true")
- args = parser.parse_args()
-
- git = shutil.which("git")
- if git is None:
- eprintln("Command `git` not found.")
- return 1
-
- if args.list:
- res = system(git_list_cmd(None))
- if res.returncode != 0:
- return res.returncode
- sys.stdout.write(res.stdout)
- return 0
-
- res = system(git_list_cmd(args.prefix, ["--points-at", "HEAD"]))
- if res.returncode != 0:
- return res.returncode
-
- rows = res.stdout.strip().splitlines()
- tags = parse_tags(rows, args.check_gpg, args.prefix)
-
- # If there were no tags on current HEAD, get the last created tag and count commits since then
- if not tags:
- res = system(git_list_cmd(args.prefix))
- if res.returncode != 0:
- return res.returncode
-
- rows = res.stdout.strip().splitlines()
- tags = parse_tags(rows, args.check_gpg, args.prefix)
-
- if not tags:
- annotated = "annotated"
- if args.check_gpg:
- annotated = "signed"
- if args.prefix is not None:
- eprintln(f"No {annotated} tags found with prefix {args.prefix}")
- else:
- eprintln(f"No {annotated} tags found without prefix")
- return 1
-
- # The last one should be the newest
- version = tags[-1]
- selected_tag = version
-
- if args.prefix is not None:
- selected_tag = f"{args.prefix}/{version}"
-
- if args.check_semver:
- if not SEMVER_RE.match(version):
- eprintln(f"Invalid format of tag '{selected_tag}'")
- return 1
-
- if args.verify:
- res = system(["git", "tag", "-v", f"{selected_tag}"])
- if res.returncode != 0:
- return res.returncode
-
- res = system(["git", "rev-list", "--count", f"{selected_tag}..HEAD"])
- if res.returncode != 0:
- return res.returncode
-
- count = 0
- try:
- count = int(res.stdout.strip())
- except ValueError:
- eprintln(
- f"Failed to parse `git rev-list --count` as integer: {res.stdout.strip()}"
- )
-
- version += f"+{count}"
- else:
- version = tags[0]
- selected_tag = version
-
- if args.prefix is not None:
- selected_tag = f"{args.prefix}/{version}"
-
- if args.check_semver:
- if not SEMVER_RE.match(version):
- eprintln(f"Invalid format of tag '{selected_tag}'")
- return 1
-
- if args.verify:
- res = system(["git", "tag", "-v", f"{selected_tag}"])
- if res.returncode != 0:
- return res.returncode
-
- res = system(["git", "status", "--porcelain"])
- if res.returncode != 0:
- return res.returncode
-
- if res.stdout.strip():
- # semver metadata fields are separated by dots
- if '+' in version:
- version += ".dirty"
- else:
- version += "+dirty"
- print(version)
- return 0
+from generate_version_headers import main_get_version
if __name__ == "__main__":
- sys.exit(main())
+ sys.exit(main_get_version())
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index c50128d..270afe7 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -231,25 +231,14 @@ set(LIBBITBOX02_RUST_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/rust/bitbox02-rust-c
set(LIBBITBOX02_RUST ${LIBBITBOX02_RUST} PARENT_SCOPE)
# Generate c-headers for the rust library
-# Working dir must be set to the rust workspace so that cargo finds the
-# configuration for vendoring sources
add_custom_target(rust-cbindgen
- # cbindgen can automatically create the metadata, but it does so without the `--offline` flag.
- # The workaround is to manually create and pass the metadata.
COMMAND
- ${CARGO}
- metadata
- --offline
- --manifest-path ${LIBBITBOX02_RUST_SOURCE_DIR}/Cargo.toml
- > ${CMAKE_CURRENT_BINARY_DIR}/rust-metadata
- COMMAND
- ${CBINDGEN}
- --quiet
- --config ${CMAKE_CURRENT_SOURCE_DIR}/rust/bitbox02-cbindgen.toml
- --output ${CMAKE_CURRENT_BINARY_DIR}/rust/rust.h
- --profile release
- --metadata ${CMAKE_CURRENT_BINARY_DIR}/rust-metadata
- ${LIBBITBOX02_RUST_SOURCE_DIR}
+ ${CMAKE_COMMAND} -E env
+ CARGO_BIN=${CARGO}
+ CBINDGEN_BIN=${CBINDGEN}
+ bash
+ ${CMAKE_CURRENT_SOURCE_DIR}/../scripts/generate_rust_header.sh
+ ${CMAKE_CURRENT_BINARY_DIR}/rust
WORKING_DIRECTORY
${CMAKE_CURRENT_SOURCE_DIR}/rust
BYPRODUCTS
@@ -269,7 +258,6 @@ if(NOT CMAKE_CROSSCOMPILING)
${CMAKE_COMMAND} -E env
CMAKE_SYSROOT=${CMAKE_SYSROOT}
CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}
- FIRMWARE_VERSION_SHORT=${FIRMWARE_VERSION}
# only one test thread because of unsafe concurrent access to `SafeData`, `mock_sd()` and `mock_memory()`. Using mutexes instead leads to mutex poisoning and very messy output in case of a unit test failure.
${CARGO}
test
@@ -292,7 +280,6 @@ if(NOT CMAKE_CROSSCOMPILING)
${CMAKE_COMMAND} -E env
CMAKE_SYSROOT=${CMAKE_SYSROOT}
CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}
- FIRMWARE_VERSION_SHORT=${FIRMWARE_VERSION}
${CARGO} clippy
$<$<BOOL:${CMAKE_VERBOSE_MAKEFILE}>:-v>
--all-features
@@ -387,7 +374,6 @@ foreach(type ${RUST_LIBS})
CMAKE_CURRENT_BINARY_DIR=${CMAKE_CURRENT_BINARY_DIR}
RUSTFLAGS=${RUSTFLAGS}
CFLAGS=${CARGO_C_FLAGS}
- FIRMWARE_VERSION_SHORT=${FIRMWARE_VERSION}
$<$<BOOL:${SCCACHE_PROGRAM}>:RUSTC_WRAPPER=${SCCACHE_PROGRAM}>
RUSTC_BOOTSTRAP=1
MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}
@@ -443,7 +429,6 @@ if(CMAKE_CROSSCOMPILING)
COMMAND
CMAKE_SYSROOT=${CMAKE_SYSROOT}
${CMAKE_COMMAND} -E env
- FIRMWARE_VERSION_SHORT=${FIRMWARE_VERSION}
${CARGO} doc --document-private-items --target-dir ${CMAKE_BINARY_DIR}/docs-rust --target thumbv7em-none-eabi
COMMAND
${CMAKE_COMMAND} -E echo "See docs at file://${CMAKE_BINARY_DIR}/docs-rust/thumbv7em-none-eabi/doc/bitbox02_rust/index.html"
diff --git a/src/bootloader/bootloader_version.h.in b/src/bootloader/bootloader_version.h.in
deleted file mode 100644
index 480dab2..0000000
--- a/src/bootloader/bootloader_version.h.in
+++ /dev/null
@@ -1,11 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-
-#ifndef _BOOTLOADER_VERSION_H
-#define _BOOTLOADER_VERSION_H
-
-// 16 bit wide char version to be used in usb descriptors.
-#define BOOTLOADER_VERSION "@BOOTLOADER_VERSION_FULL@"
-#define BOOTLOADER_VERSION_W16 @BOOTLOADER_VERSION_FULL_W16@
-#define BOOTLOADER_VERSION_LEN @BOOTLOADER_VERSION_FULL_LEN@
-
-#endif
diff --git a/src/bootloader/bootloader_version.h.tmpl b/src/bootloader/bootloader_version.h.tmpl
new file mode 100644
index 0000000..a43536f
--- /dev/null
+++ b/src/bootloader/bootloader_version.h.tmpl
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef _BOOTLOADER_VERSION_H
+#define _BOOTLOADER_VERSION_H
+
+// 16 bit wide char version to be used in usb descriptors.
+#define BOOTLOADER_VERSION "${BOOTLOADER_VERSION_FULL}"
+#define BOOTLOADER_VERSION_W16 ${BOOTLOADER_VERSION_FULL_W16}
+#define BOOTLOADER_VERSION_LEN ${BOOTLOADER_VERSION_FULL_LEN}
+
+#endif
diff --git a/src/rust/bitbox02-rust/Cargo.toml b/src/rust/bitbox02-rust/Cargo.toml
index c98c4ab..e26bf40 100644
--- a/src/rust/bitbox02-rust/Cargo.toml
+++ b/src/rust/bitbox02-rust/Cargo.toml
@@ -108,3 +108,6 @@ firmware = []
[dev-dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
+
+[build-dependencies]
+serde_json = { workspace = true }
diff --git a/src/rust/bitbox02-rust/build.rs b/src/rust/bitbox02-rust/build.rs
index 83e1978..7d34b65 100644
--- a/src/rust/bitbox02-rust/build.rs
+++ b/src/rust/bitbox02-rust/build.rs
@@ -1,14 +1,80 @@
// SPDX-License-Identifier: Apache-2.0
-// Emit a warning if FIRMWARE_VERSION_SHORT isn't set. We don't want this to be a hard error during
-// development so that rust tools are happy.
+use std::env;
+use std::fs;
+use std::path::{Path, PathBuf};
+
+const MAX_BACKUP_GENERATOR_LEN: usize = 19;
+
fn main() {
- let version = option_env!("FIRMWARE_VERSION_SHORT");
- if let Some(version) = version {
- if version.is_empty() {
- println!("cargo::warning=FIRMWARE_VERSION_SHORT is empty");
+ let version = firmware_version_short();
+ println!("cargo::rustc-env=FIRMWARE_VERSION_SHORT={version}");
+}
+
+fn firmware_version_short() -> String {
+ let manifest_path = versions_manifest_path();
+ println!("cargo::rerun-if-changed={}", manifest_path.display());
+
+ let manifest = fs::read_to_string(&manifest_path).unwrap_or_else(|err| {
+ panic!(
+ "failed to read versions manifest at {}: {err}",
+ manifest_path.display()
+ )
+ });
+ let manifest: serde_json::Value = serde_json::from_str(&manifest).unwrap_or_else(|err| {
+ panic!(
+ "failed to parse versions manifest at {}: {err}",
+ manifest_path.display()
+ )
+ });
+
+ let version = manifest
+ .get("firmware")
+ .and_then(serde_json::Value::as_str)
+ .unwrap_or_else(|| {
+ panic!(
+ "versions manifest at {} does not contain a string 'firmware' entry",
+ manifest_path.display()
+ )
+ });
+
+ if !is_release_version(version) {
+ panic!(
+ "versions manifest entry 'firmware' must be a release semver like v1.2.3, got: {}",
+ version
+ );
+ }
+ if version.len() > MAX_BACKUP_GENERATOR_LEN {
+ panic!(
+ "firmware version '{}' exceeds backup generator limit of {} bytes",
+ version, MAX_BACKUP_GENERATOR_LEN
+ );
+ }
+
+ version.to_owned()
+}
+
+fn versions_manifest_path() -> PathBuf {
+ Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()).join("../../../versions.json")
+}
+
+fn is_release_version(version: &str) -> bool {
+ let Some(version) = version.strip_prefix('v') else {
+ return false;
+ };
+ let mut parts = version.split('.');
+ match (parts.next(), parts.next(), parts.next(), parts.next()) {
+ (Some(major), Some(minor), Some(patch), None) => {
+ is_numeric_identifier(major)
+ && is_numeric_identifier(minor)
+ && is_numeric_identifier(patch)
}
- } else {
- println!("cargo::warning=FIRMWARE_VERSION_SHORT is not set");
+ _ => false,
}
}
+
+fn is_numeric_identifier(part: &str) -> bool {
+ !part.is_empty()
+ && part.bytes().all(|byte| byte.is_ascii_digit())
+ && (part == "0" || !part.starts_with('0'))
+}
diff --git a/src/rust/bitbox02-rust/src/version.rs b/src/rust/bitbox02-rust/src/version.rs
index 18ecba2..cd929fd 100644
--- a/src/rust/bitbox02-rust/src/version.rs
+++ b/src/rust/bitbox02-rust/src/version.rs
@@ -1,12 +1,4 @@
// SPDX-License-Identifier: Apache-2.0
/// Firmware version, short format, e.g. "v9.12.0".
-// We don't want this to be a hard error during development so that rust tools are happy.
-pub static FIRMWARE_VERSION_SHORT: &str = {
- let version = option_env!("FIRMWARE_VERSION_SHORT");
- if let Some(version) = version {
- version
- } else {
- ""
- }
-};
+pub static FIRMWARE_VERSION_SHORT: &str = env!("FIRMWARE_VERSION_SHORT");
diff --git a/src/rust/bitbox02-sys/build.rs b/src/rust/bitbox02-sys/build.rs
index 2e0253f..db89baf 100644
--- a/src/rust/bitbox02-sys/build.rs
+++ b/src/rust/bitbox02-sys/build.rs
@@ -2,8 +2,8 @@
use std::env;
use std::io::ErrorKind;
-use std::path::PathBuf;
-use std::process::Command;
+use std::path::{Path, PathBuf};
+use std::process::{Command, Output};
const ALLOWLIST_VARS: &[&str] = &[
"BASE58_CHECKSUM_LEN",
@@ -295,17 +295,41 @@ const FAKEHARDWARE_SOURCES: &[&str] = &[
"test/hardware-fakes/src/fake_spi_mem.c",
];
-pub fn main() -> Result<(), &'static str> {
- // We could theoretically list every header file that we end up depending on, but that is hard
- // to maintain. So instead we just listen to changes on "wrapper.h" which is good enough.
- println!("cargo::rerun-if-changed=wrapper.h");
+type BuildResult<T> = Result<T, String>;
- // Check if we have `bindgen` executable
- if let Err(e) = Command::new("bindgen").spawn() {
- if e.kind() == ErrorKind::NotFound {
- return Err("`bindgen` was not found! Check your PATH!");
- }
- }
+pub fn main() -> BuildResult<()> {
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
+ let repo_root = manifest_dir
+ .join("../../..")
+ .canonicalize()
+ .map_err(|err| format!("failed to find repo root: {err}"))?;
+
+ emit_rerun_if_changed("wrapper.h");
+ emit_rerun_if_changed("../bitbox02-cbindgen.toml");
+ emit_rerun_if_changed("../bitbox02-rust-c/Cargo.toml");
+ emit_rerun_if_changed("../bitbox02-rust-c/src");
+ emit_rerun_if_changed("../bitbox02-rust/Cargo.toml");
+ emit_rerun_if_changed("../bitbox02-rust/src");
+ emit_rerun_if_changed("../bitbox02/Cargo.toml");
+ emit_rerun_if_changed("../bitbox02/src");
+ emit_rerun_if_changed("../util/Cargo.toml");
+ emit_rerun_if_changed("../util/src");
+ emit_rerun_if_changed("../bitbox-aes/Cargo.toml");
+ emit_rerun_if_changed("../bitbox-aes/src");
+ emit_rerun_if_changed("../bitbox-framed-serial-link/Cargo.toml");
+ emit_rerun_if_changed("../bitbox-framed-serial-link/src");
+ emit_rerun_if_changed("../bitbox-bytequeue/Cargo.toml");
+ emit_rerun_if_changed("../bitbox-bytequeue/src");
+ emit_rerun_if_changed("../../../versions.json");
+ emit_rerun_if_changed("../../../src/version.h.tmpl");
+ emit_rerun_if_changed("../../../src/bootloader/bootloader_version.h.tmpl");
+ emit_rerun_if_changed("../../../scripts/generate_version_headers.py");
+ emit_rerun_if_changed("../../../scripts/generate_rust_header.sh");
+
+ // Generating version.h/bootloader_version.h depends on the current state of the git repo
+ emit_git_rerun_if_changed(&repo_root);
+
+ ensure_command_exists("bindgen")?;
let target = env::var("TARGET").expect("TARGET not set");
let cross_compiling = target == "thumbv7em-none-eabi";
@@ -336,63 +360,70 @@ pub fn main() -> Result<(), &'static str> {
}
}
+ let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
+
let mut includes = vec![
// $INCLUDES
- "../..",
- "../../ui/ugui",
- "../../platform",
- "../../qtouch",
- "../../usb/class",
- "../../usb/class/hid",
- "../../usb/class/hid/hww",
- "../../usb/class/hid/u2f",
+ "../..".to_owned(),
+ "../../ui/ugui".to_owned(),
+ "../../platform".to_owned(),
+ "../../qtouch".to_owned(),
+ "../../usb/class".to_owned(),
+ "../../usb/class/hid".to_owned(),
+ "../../usb/class/hid/hww".to_owned(),
+ "../../usb/class/hid/u2f".to_owned(),
// ASF4 headers allowed in unit tests
- "../../../external/asf4-drivers/hal/utils/include",
+ "../../../external/asf4-drivers/hal/utils/include".to_owned(),
// fatfs
- "../../rust/fatfs-sys/depend/fatfs/source",
+ "../../rust/fatfs-sys/depend/fatfs/source".to_owned(),
];
- // rust.h is created by cbindgen in the cmake build directory
- let out_dir = env::var("OUT_DIR").unwrap();
- let rust_h_dir = PathBuf::from([&out_dir, "../../../../../.."].join("/"));
- println!("rust_h_dir: {:?}", rust_h_dir.canonicalize());
- includes.push(rust_h_dir.as_os_str().to_str().unwrap());
+ let generated_headers_dir = if cross_compiling {
+ env::var("CMAKE_CURRENT_BINARY_DIR")
+ .map(PathBuf::from)
+ .unwrap_or_else(|_| out_dir.join("../../../../../.."))
+ } else {
+ ensure_command_exists("cbindgen")?;
+ generate_native_headers(&repo_root, &out_dir)?;
+ out_dir.clone()
+ };
+ includes.push(generated_headers_dir.display().to_string());
if cross_compiling {
includes.extend([
// SAMD51A
- "../../../external/samd51a-ds/include",
+ "../../../external/samd51a-ds/include".to_owned(),
// ASF4-min
- "../../../external/asf4-drivers",
- "../../../external/asf4-drivers/Config",
- "../../../external/asf4-drivers/hal/include",
- "../../../external/asf4-drivers/hal/include",
- "../../../external/asf4-drivers/hpl/core",
- "../../../external/asf4-drivers/hpl/gclk",
- "../../../external/asf4-drivers/hpl/pm",
- "../../../external/asf4-drivers/hpl/port",
- "../../../external/asf4-drivers/hpl/pukcc",
- "../../../external/asf4-drivers/hpl/rtc",
- "../../../external/asf4-drivers/hpl/spi",
- "../../../external/asf4-drivers/hri",
- "../../../external/asf4-drivers/qtouch",
- "../../../external/asf4-drivers/qtouch/include",
- "../../../external/asf4-drivers/sd_mmc",
- "../../../external/asf4-drivers/usb",
- "../../../external/asf4-drivers/usb/class",
- "../../../external/asf4-drivers/usb/class/hid",
- "../../../external/asf4-drivers/usb/device",
+ "../../../external/asf4-drivers".to_owned(),
+ "../../../external/asf4-drivers/Config".to_owned(),
+ "../../../external/asf4-drivers/hal/include".to_owned(),
+ "../../../external/asf4-drivers/hal/include".to_owned(),
+ "../../../external/asf4-drivers/hpl/core".to_owned(),
+ "../../../external/asf4-drivers/hpl/gclk".to_owned(),
+ "../../../external/asf4-drivers/hpl/pm".to_owned(),
+ "../../../external/asf4-drivers/hpl/port".to_owned(),
+ "../../../external/asf4-drivers/hpl/pukcc".to_owned(),
+ "../../../external/asf4-drivers/hpl/rtc".to_owned(),
+ "../../../external/asf4-drivers/hpl/spi".to_owned(),
+ "../../../external/asf4-drivers/hri".to_owned(),
+ "../../../external/asf4-drivers/qtouch".to_owned(),
+ "../../../external/asf4-drivers/qtouch/include".to_owned(),
+ "../../../external/asf4-drivers/sd_mmc".to_owned(),
+ "../../../external/asf4-drivers/usb".to_owned(),
+ "../../../external/asf4-drivers/usb/class".to_owned(),
+ "../../../external/asf4-drivers/usb/class/hid".to_owned(),
+ "../../../external/asf4-drivers/usb/device".to_owned(),
// ASF4
- "../../../external/asf4-drivers/diskio",
+ "../../../external/asf4-drivers/diskio".to_owned(),
// CMSIS
- "../../../external/CMSIS/Include",
+ "../../../external/CMSIS/Include".to_owned(),
]);
} else {
// unit test framework includes
- includes.push("../../../test/hardware-fakes/include");
+ includes.push("../../../test/hardware-fakes/include".to_owned());
}
- let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs");
+ let out_path = out_dir.join("bindings.rs");
let out_path = out_path.into_os_string().into_string().unwrap();
// Needs to match the definitions in `CMakeList.txt' files (unit tests, hardware fakes and
@@ -407,33 +438,26 @@ pub fn main() -> Result<(), &'static str> {
];
definitions.extend(&extra_flags);
- let res = Command::new("bindgen")
- .args(["--output", &out_path])
- .arg("--use-core")
- .arg("--with-derive-default")
- .args(
- ALLOWLIST_FNS
- .iter()
- .flat_map(|s| ["--allowlist-function", s]),
- )
- .args(ALLOWLIST_TYPES.iter().flat_map(|s| ["--allowlist-type", s]))
- .args(ALLOWLIST_VARS.iter().flat_map(|s| ["--allowlist-var", s]))
- .args(RUSTIFIED_ENUMS.iter().flat_map(|s| ["--rustified-enum", s]))
- .args(OPAQUE_TYPES.iter().flat_map(|s| ["--opaque-type", s]))
- .arg("wrapper.h")
- .arg("--")
- .args(&definitions)
- .args(includes.iter().map(|s| format!("-I{s}")))
- .output()
- .expect("Failed to run bindgen");
- if !res.status.success() {
- println!(
- "bindgen-out:\n{}\n\nbindgen-err:\n{}",
- std::str::from_utf8(&res.stdout).unwrap(),
- std::str::from_utf8(&res.stderr).unwrap()
- );
- return Err("Bindgen failed");
- }
+ run_command(
+ Command::new("bindgen")
+ .args(["--output", &out_path])
+ .arg("--use-core")
+ .arg("--with-derive-default")
+ .args(
+ ALLOWLIST_FNS
+ .iter()
+ .flat_map(|s| ["--allowlist-function", s]),
+ )
+ .args(ALLOWLIST_TYPES.iter().flat_map(|s| ["--allowlist-type", s]))
+ .args(ALLOWLIST_VARS.iter().flat_map(|s| ["--allowlist-var", s]))
+ .args(RUSTIFIED_ENUMS.iter().flat_map(|s| ["--rustified-enum", s]))
+ .args(OPAQUE_TYPES.iter().flat_map(|s| ["--opaque-type", s]))
+ .arg("wrapper.h")
+ .arg("--")
+ .args(&definitions)
+ .args(includes.iter().map(|s| format!("-I{s}"))),
+ "run bindgen",
+ )?;
let excludes = if let Ok(libtype) = env::var("LIB_TYPE") {
match libtype.as_str() {
@@ -474,3 +498,104 @@ pub fn main() -> Result<(), &'static str> {
Ok(())
}
+
+fn emit_rerun_if_changed(path: &str) {
+ println!("cargo::rerun-if-changed={path}");
+}
+
+fn emit_git_rerun_if_changed(repo_root: &Path) {
+ let Some(git_dir) = git_output(repo_root, &["rev-parse", "--absolute-git-dir"]) else {
+ return;
+ };
+ let git_dir = PathBuf::from(git_dir);
+
+ for path in [
+ git_dir.join("HEAD"),
+ git_dir.join("index"),
+ git_dir.join("packed-refs"),
+ git_dir.join("refs/tags"),
+ ] {
+ if path.exists() {
+ println!("cargo::rerun-if-changed={}", path.display());
+ }
+ }
+
+ if let Some(head_ref) = git_output(repo_root, &["symbolic-ref", "-q", "HEAD"]) {
+ let ref_path = git_dir.join(head_ref);
+ if ref_path.exists() {
+ println!("cargo::rerun-if-changed={}", ref_path.display());
+ }
+ }
+}
+
+fn ensure_command_exists(command: &str) -> BuildResult<()> {
+ match Command::new(command).arg("--version").output() {
+ Ok(_) => Ok(()),
+ Err(err) if err.kind() == ErrorKind::NotFound => {
+ Err(format!("`{command}` was not found! Check your PATH!"))
+ }
+ Err(err) => Err(format!("failed to run `{command} --version`: {err}")),
+ }
+}
+
+fn run_command(command: &mut Command, context: &str) -> BuildResult<Output> {
+ let output = command
+ .output()
+ .map_err(|err| format!("failed to {context}: {err}"))?;
+ if !output.status.success() {
+ return Err(format!(
+ "{context} failed\nstdout:\n{}\n\nstderr:\n{}",
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+ Ok(output)
+}
+
+fn generate_native_headers(repo_root: &Path, out_dir: &Path) -> BuildResult<()> {
+ let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
+ let script_path = repo_root.join("scripts/generate_rust_header.sh");
+ let rust_header_dir = out_dir.join("rust");
+
+ let mut command = Command::new("bash");
+ command.arg(&script_path).arg(&rust_header_dir);
+ if let Ok(cargo_bin) = env::var("CARGO") {
+ command.env("CARGO_BIN", cargo_bin);
+ }
+ if let Ok(cbindgen_bin) = env::var("CBINDGEN_BIN") {
+ command.env("CBINDGEN_BIN", cbindgen_bin);
+ }
+ command.current_dir(manifest_dir.join(".."));
+ run_command(&mut command, "generate rust.h")?;
+
+ generate_version_headers(repo_root, out_dir)
+}
+
+fn generate_version_headers(repo_root: &Path, out_dir: &Path) -> BuildResult<()> {
+ let script_path = repo_root.join("scripts/generate_version_headers.py");
+ run_command(
+ Command::new("python3")
+ .arg(&script_path)
+ .arg("generate")
+ .arg("--repo-root")
+ .arg(repo_root)
+ .arg("--output-dir")
+ .arg(out_dir),
+ "generate version headers",
+ )?;
+ Ok(())
+}
+
+fn git_output(repo_root: &Path, args: &[&str]) -> Option<String> {
+ let output = Command::new("git")
+ .arg("-C")
+ .arg(repo_root)
+ .args(args)
+ .output()
+ .ok()?;
+ if !output.status.success() {
+ return None;
+ }
+ let value = String::from_utf8_lossy(&output.stdout).trim().to_owned();
+ if value.is_empty() { None } else { Some(value) }
+}
diff --git a/src/version.h.in b/src/version.h.in
deleted file mode 100644
index 0631c53..0000000
--- a/src/version.h.in
+++ /dev/null
@@ -1,22 +0,0 @@
-// SPDX-License-Identifier: Apache-2.0
-
-#ifndef VERSION_H
-#define VERSION_H
-
-#include <platform/platform_config.h>
-/* Standard firmware */
-#define DIGITAL_BITBOX_VERSION "@FIRMWARE_VERSION_FULL@"
-#define DIGITAL_BITBOX_VERSION_SHORT "@FIRMWARE_VERSION@"
-
-#define DIGITAL_BITBOX_VERSION_LEN @FIRMWARE_VERSION_FULL_LEN@
-#define DIGITAL_BITBOX_VERSION_W16 @FIRMWARE_VERSION_FULL_W16@
-
-#define DIGITAL_BITBOX_VERSION_MAJOR @FIRMWARE_VERSION_MAJOR@
-#define DIGITAL_BITBOX_VERSION_MINOR @FIRMWARE_VERSION_MINOR@
-#define DIGITAL_BITBOX_VERSION_PATCH @FIRMWARE_VERSION_PATCH@
-
-// TODO: Add hash somewhere were it can be read out
-#define DIGITAL_BITBOX_GITHASH "@GIT_COMMIT_HASH@"
-#define DIGITAL_BITBOX_GITHASH_SHORT "@GIT_COMMIT_HASH_SHORT@"
-
-#endif
diff --git a/src/version.h.tmpl b/src/version.h.tmpl
new file mode 100644
index 0000000..4622707
--- /dev/null
+++ b/src/version.h.tmpl
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: Apache-2.0
+
+#ifndef VERSION_H
+#define VERSION_H
+
+#include <platform/platform_config.h>
+/* Standard firmware */
+#define DIGITAL_BITBOX_VERSION "${FIRMWARE_VERSION_FULL}"
+#define DIGITAL_BITBOX_VERSION_SHORT "${FIRMWARE_VERSION}"
+
+#define DIGITAL_BITBOX_VERSION_LEN ${FIRMWARE_VERSION_FULL_LEN}
+#define DIGITAL_BITBOX_VERSION_W16 ${FIRMWARE_VERSION_FULL_W16}
+
+#define DIGITAL_BITBOX_VERSION_MAJOR ${FIRMWARE_VERSION_MAJOR}
+#define DIGITAL_BITBOX_VERSION_MINOR ${FIRMWARE_VERSION_MINOR}
+#define DIGITAL_BITBOX_VERSION_PATCH ${FIRMWARE_VERSION_PATCH}
+
+// TODO: Add hash somewhere were it can be read out
+#define DIGITAL_BITBOX_GITHASH "${GIT_COMMIT_HASH}"
+#define DIGITAL_BITBOX_GITHASH_SHORT "${GIT_COMMIT_HASH_SHORT}"
+
+#endif
diff --git a/test/simulator-graphical-bb03/Cargo.lock b/test/simulator-graphical-bb03/Cargo.lock
index bcfaf07..f1dba5e 100644
--- a/test/simulator-graphical-bb03/Cargo.lock
+++ b/test/simulator-graphical-bb03/Cargo.lock
@@ -448,6 +448,7 @@ dependencies = [
"num-bigint",
"num-traits",
"prost",
+ "serde_json",
"sha2",
"sha3",
"streaming-silent-payments",
@@ -1614,6 +1615,12 @@ dependencies = [
"either",
]
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
[[package]]
name = "jni"
version = "0.21.1"
@@ -2858,6 +2865,19 @@ dependencies = [
"syn",
]
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
[[package]]
name = "sha2"
version = "0.10.9"
@@ -3939,6 +3959,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
[[package]]
name = "zune-core"
version = "0.4.12"
diff --git a/test/simulator-graphical/Cargo.lock b/test/simulator-graphical/Cargo.lock
index 51ff865..ab78306 100644
--- a/test/simulator-graphical/Cargo.lock
+++ b/test/simulator-graphical/Cargo.lock
@@ -392,6 +392,7 @@ dependencies = [
"num-bigint",
"num-traits",
"prost",
+ "serde_json",
"sha2",
"sha3",
"streaming-silent-payments",
@@ -1529,6 +1530,12 @@ dependencies = [
"either",
]
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
[[package]]
name = "jni"
version = "0.21.1"
@@ -2750,6 +2757,19 @@ dependencies = [
"syn",
]
+[[package]]
+name = "serde_json"
+version = "1.0.149"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
[[package]]
name = "serde_spanned"
version = "0.6.9"
@@ -3967,6 +3987,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
[[package]]
name = "zune-core"
version = "0.4.12"
diff --git a/versions.json b/versions.json
new file mode 100644
index 0000000..3a07f4d
--- /dev/null
+++ b/versions.json
@@ -0,0 +1,4 @@
+{
+ "firmware": "v9.26.0",
+ "bootloader": "v1.1.2"
+}
Why this scored 14/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.