Add support for selecting and combining build actions (#333)
What changed, and why it matters
This commit is a build-system and documentation refactor. It splits a single long firmware build script into smaller selectable actions (like 'build main firmware', 'build bootloader', 'assemble', 'sign', 'hash', etc.), fixes a Dockerfile environment-variable syntax, updates developer instructions, and adds a way to fix file ownership when running builds inside Docker. There is no indication it fixes a security vulnerability or introduces a security-relevant bug.
No security action required; treat as normal build tooling improvement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change refactors build_firmware.sh into a dispatcher that runs named actions (all/release/main/bootloader/assemble/nobootloader/sign/hash/ownership). It adds dependency checks before assemble and sign, quotes variables for shell safety, adds a fix_ownership helper using HOST_UID/HOST_GID, corrects Dockerfile ENV syntax, and updates build/development/reproducible-build docs. No cryptographic, firmware verification, or runtime security logic is changed.
Changed components
build_firmware.shDockerfiledocs/build.mddocs/development.mddocs/reproducible-build.mdInspect captured patch +194 / −60
diff --git a/Dockerfile b/Dockerfile
index 5e0059c..16ec106 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
FROM python:3.9.15@sha256:b5f024fa682187ef9305a2b5d2c4bb583bef83356259669fc80273bb2222f5ed
-ENV LANG C.UTF-8
+ENV LANG=C.UTF-8
ARG DEBIAN_FRONTEND=noninteractive
diff --git a/build_firmware.sh b/build_firmware.sh
index ad5ad03..2b79eaa 100755
--- a/build_firmware.sh
+++ b/build_firmware.sh
@@ -1,79 +1,209 @@
-#!/bin/bash
+#!/usr/bin/env bash
set -e
INFO="\e[1;36m"
ENDCOLOR="\e[0m"
-echo -e "${INFO}
+usage() {
+ echo "Usage: $0 [all|release|main|bootloader|assemble|nobootloader|sign|hash|ownership] ..."
+ exit 1
+}
+
+# If no args, default to "all"
+if [ $# -eq 0 ]; then
+ ACTIONS=("all")
+else
+ ACTIONS=("$@")
+fi
+
+run_main() {
+ echo -e "${INFO}
══════════════════════ Building main firmware ═════════════════════════════
${ENDCOLOR}"
-make clean
-make disco USE_DBOOT=1
+ make clean
+ make disco USE_DBOOT=1
+}
-echo -e "${INFO}
+run_bootloader() {
+ echo -e "${INFO}
═════════════════════ Building secure bootloader ══════════════════════════
${ENDCOLOR}"
-cd bootloader
-make clean
-make stm32f469disco READ_PROTECTION=1 WRITE_PROTECTION=1
-cd -
+ cd bootloader
+ make clean
+ make stm32f469disco READ_PROTECTION=1 WRITE_PROTECTION=1
+ cd -
+}
-echo -e "${INFO}
+run_assemble() {
+ echo -e "${INFO}
══════════════════════ Assembling final binaries ══════════════════════════
${ENDCOLOR}"
-mkdir -p release
-python3 ./bootloader/tools/make-initial-firmware.py -s ./bootloader/build/stm32f469disco/startup/release/startup.hex -b ./bootloader/build/stm32f469disco/bootloader/release/bootloader.hex -f ./bin/specter-diy.hex -bin ./release/initial_firmware.bin
-echo -e "Initial firmware saved to release/initial_firmware.bin"
+ # --- Dependency checks ---
+ REQUIRED_FILES=(
+ "./bin/specter-diy.hex"
+ "./bootloader/build/stm32f469disco/startup/release/startup.hex"
+ "./bootloader/build/stm32f469disco/bootloader/release/bootloader.hex"
+ )
+
+ MISSING=0
+ for f in "${REQUIRED_FILES[@]}"; do
+ if [ ! -f "$f" ]; then
+ echo -e "\e[1;31mERROR:\e[0m Required file missing: $f"
+ MISSING=1
+ fi
+ done
+
+ if [ "$MISSING" -eq 1 ]; then
+ echo -e "\nOne or more required components were not built."
+ echo -e "Please run: \e[1m./build_firmware.sh main bootloader\e[0m\n"
+ exit 1
+ fi
+ # ---------------------------
+
-python3 ./bootloader/tools/upgrade-generator.py gen -f ./bin/specter-diy.hex -b ./bootloader/build/stm32f469disco/bootloader/release/bootloader.hex -p stm32f469disco ./release/specter_upgrade.bin
-cp ./release/specter_upgrade.bin ./release/specter_upgrade_unsigned.bin
-echo "Unsigned upgrate file saved to release/specter_upgrade_unsigned.bin"
+ mkdir -p release
-HASH=$(python3 ./bootloader/tools/upgrade-generator.py message ./release/specter_upgrade.bin)
+ python3 ./bootloader/tools/make-initial-firmware.py \
+ -s ./bootloader/build/stm32f469disco/startup/release/startup.hex \
+ -b ./bootloader/build/stm32f469disco/bootloader/release/bootloader.hex \
+ -f ./bin/specter-diy.hex \
+ -bin ./release/initial_firmware.bin
+ echo -e "Initial firmware saved to release/initial_firmware.bin"
-echo "
-╔═════════════════════════════════════════════════════════════════════════╗
-║ Message to sign with vendor keys: ║
-║ ║
+ python3 ./bootloader/tools/upgrade-generator.py gen \
+ -f ./bin/specter-diy.hex \
+ -b ./bootloader/build/stm32f469disco/bootloader/release/bootloader.hex \
+ -p stm32f469disco \
+ ./release/specter_upgrade.bin
+
+ cp ./release/specter_upgrade.bin ./release/specter_upgrade_unsigned.bin
+ echo "Unsigned upgrade file saved to release/specter_upgrade_unsigned.bin"
+
+ HASH=$(python3 ./bootloader/tools/upgrade-generator.py message ./release/specter_upgrade.bin)
+ echo "
+╔════════════════════════════════════════════════════════════════════════════════╗
+║ Message to sign with vendor keys: ║
+║ ║
║ ${HASH} ║
-║ ║
-╚═════════════════════════════════════════════════════════════════════════╝
+║ ║
+╚════════════════════════════════════════════════════════════════════════════════╝
"
+}
-
-echo -e "${INFO}
+run_nobootloader() {
+ echo -e "${INFO}
═════════════════════ Building firmware without bootloader ════════════════
${ENDCOLOR}"
-make clean
-make disco
-cp ./bin/specter-diy.bin ./release/disco-nobootloader.bin
-cp ./bin/specter-diy.hex ./release/disco-nobootloader.hex
-echo -e "Standard firmware without bootloader saved to release/disco-nobootloader.{bin,hex}"
-echo -e "The BIN image can be flashed directly to a development board without the secure bootloader."
-
-echo -e "${INFO}
+
+ mkdir -p release
+ make clean
+ make disco
+ cp ./bin/specter-diy.bin ./release/disco-nobootloader.bin
+ cp ./bin/specter-diy.hex ./release/disco-nobootloader.hex
+ echo -e "Standard firmware without bootloader saved to release/disco-nobootloader.{bin,hex}"
+ echo -e "The BIN image can be flashed directly to a development board without the secure bootloader."
+}
+
+run_sign() {
+ echo -e "${INFO}
═════════════════════ Adding signature to the binary ══════════════════════
${ENDCOLOR}"
-while true; do
- echo "Provide a signature to add to the upgrade file, or just hit enter to stop."
- read SIGNATURE
- if [ -z $SIGNATURE ]; then
- break
+ # --- Dependency checks ---
+ REQUIRED_FILES=(
+ "./release/specter_upgrade.bin"
+ )
+
+ MISSING=0
+ for f in "${REQUIRED_FILES[@]}"; do
+ if [ ! -f "$f" ]; then
+ echo -e "\e[1;31mERROR:\e[0m Required file missing: $f"
+ MISSING=1
+ fi
+ done
+
+ if [ "$MISSING" -eq 1 ]; then
+ echo -e "\nOne or more required components were not built."
+ echo -e "Please run: \e[1m./build_firmware.sh assemble\e[0m\n"
+ exit 1
fi
- python3 ./bootloader/tools/upgrade-generator.py import-sig -s $SIGNATURE ./release/specter_upgrade.bin
- echo "Signature is added: ${SIGNATURE}"
-done
+ # ---------------------------
+
+ while true; do
+ echo "Provide a signature to add to the upgrade file, or just hit enter to stop."
+ read -r SIGNATURE
+ if [ -z "$SIGNATURE" ]; then
+ break
+ fi
+ python3 ./bootloader/tools/upgrade-generator.py import-sig -s "$SIGNATURE" ./release/specter_upgrade.bin
+ echo "Signature added: ${SIGNATURE}"
+ done
+}
-echo -e "${INFO}
+run_hash() {
+ echo -e "${INFO}
═════════════════════════ Hashes of the binaries: ═════════════════════════
${ENDCOLOR}"
-cd release
-sha256sum *.bin > sha256.txt
-cat sha256.txt
-
-echo "
+ mkdir -p release
+ cd release
+ sha256sum *.bin > sha256.txt
+ cat sha256.txt
+ echo "
Hashes saved to release/sha256.txt file.
"
+ cd -
+}
+
+fix_ownership() {
+ echo -e "${INFO}
+═════════════════════════ Fixing file ownership ═══════════════════════════
+${ENDCOLOR}"
+
+ if [ -n "$HOST_UID" ] && [ -n "$HOST_GID" ]; then
+ chown -R "$HOST_UID:$HOST_GID" bin 2>/dev/null || true
+ chown -R "$HOST_UID:$HOST_GID" release 2>/dev/null || true
+ chown -R "$HOST_UID:$HOST_GID" f469-disco/micropython/mpy-cross 2>/dev/null || true
+ chown -R "$HOST_UID:$HOST_GID" bootloader 2>/dev/null || true
+ echo "File ownership changed to local user/group"
+ else
+ echo "Skipping fix_ownership: HOST_UID and HOST_GID not set."
+ fi
+}
+
+# Map action_name to function
+dispatch() {
+ case "$1" in
+ all)
+ run_main
+ run_bootloader
+ run_assemble
+ run_sign
+ run_nobootloader
+ run_hash
+ fix_ownership
+ ;;
+ release)
+ run_main
+ run_bootloader
+ run_assemble
+ run_sign
+ run_hash
+ fix_ownership
+ ;;
+ main) run_main ;;
+ bootloader) run_bootloader ;;
+ assemble) run_assemble ;;
+ nobootloader) run_nobootloader ;;
+ sign) run_sign ;;
+ hash) run_hash ;;
+ ownership) fix_ownership ;;
+ *) echo "Unknown action: $1"; usage ;;
+ esac
+}
+
+# Execute requested actions in order
+for action in "${ACTIONS[@]}"; do
+ dispatch "$action"
+done
diff --git a/docs/build.md b/docs/build.md
index f510ea2..7e71241 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -4,6 +4,8 @@ Clone the repository recursively `git clone https://github.com/cryptoadvance/spe
`bootloader` folder contains a [secure bootloader](https://github.com/cryptoadvance/specter-bootloader) that you can customize with your own firmware signing keys.
+Check [reproducible-build](./reproducible-build.md) if you want to use Docker.
+
## Prerequisites for the Nix build
There are multiple ways to get all necessary tools. The recommended way is to use the Nix flake with direnv.
diff --git a/docs/development.md b/docs/development.md
index 3cd2044..2f871e4 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -6,13 +6,7 @@ See the [build documentation](build.md) for instructions on how to compile the c
## Enabling developer mode
-By default developer mode and USB communication are turned off. This means that when you connect the board to the computer it will NOT mount the `PYBFLASH` anymore and there will be no way to connect to debug shell.
-
-
-~~To turn on the developer mode get to the main screen (enter PIN code, generate recovery phrase, enter password), and then go to **Settings - Security - turn on Developer mode - Save**.~~
-(Currently deactivated for securtiy reasons)
-
-~~Now the board will restart and get mounted to the computer as before. You can also connect to the board over miniUSB and get to interactive console (baudrate 115200). You can use `screen` or `putty` or `minicom` for that, i.e. `screen /dev/tty.usbmodem14403 115200`.~~
+By default USB communication is off. This means that when you connect the board to the computer it will NOT mount the `PYBFLASH` anymore and there will be no way to connect to debug shell.
In order to connect to the board, modify those lines in [boot.py](https://github.com/cryptoadvance/specter-diy/blob/2f51e152bcdb184cf719792e6c5f972214e3dd36/boot/main/boot.py#L33-L40) like this:
```python
@@ -22,16 +16,19 @@ pyb.usb_mode("VCP+MSC") # debug mode with USB and mounted storages from start
#pyb.usb_mode("VCP") # debug mode with USB from start
# disable at start
# pyb.usb_mode(None)
+# os.dupterm(None,0)
+# os.dupterm(None,1)
```
-and after flashing, you can connect with something like:
+and after flashing, restart the device, go to Device settings > Communication > USB communication, turn it on and Confirm. Then you can connect using the micro-USB port and with something like:
```
# Linux
screen /dev/ttyACM0 115200 # or maybe ttyACM1 or ttyACM2
# Mac
-screen /dev/tty.usbmodem14403 115200`.
+screen /dev/tty.usbmodem14403 115200
```
+All `print()` statements will appear in the terminal output. You can keep both the mini-USB and micro-USB connected at the same time, with the power jumper in either position. This makes it easier to flash the firmware and then connect to the device.
## Writing a simple app
diff --git a/docs/reproducible-build.md b/docs/reproducible-build.md
index f6f1abb..facc29e 100644
--- a/docs/reproducible-build.md
+++ b/docs/reproducible-build.md
@@ -19,7 +19,7 @@ docker build -t diy .
3. Run the container in interactive mode:
```sh
-docker run -ti -v `pwd`:/app diy
+docker run -ti -v `pwd`:/app -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) diy
```
The container runs `./build_firmware.sh`, which now also drops `release/disco-nobootloader.{bin,hex}` alongside the signed
@@ -32,11 +32,16 @@ Get signatures from the description of the github release and enter one by one i
After adding signatures binaries in the `release` folder should be exactly the same as in github release. Hashes of the binaries will be saved to `release/sha256.txt`.
+**Note**: You can also run specifics tasks directly. For example:
+```sh
+docker run -ti -v `pwd`:/app -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) diy ./build_firmware.sh nobootloader ownership
+```
+
# Apple M1 users
-For Apple M1 add a plafrom flag to the docker commands:
+For Apple M1 add a platform flag to the docker commands:
```sh
docker build -t diy . --platform linux/x86_64
-docker run --platform linux/amd64 -ti -v `pwd`:/app diy
+docker run --platform linux/amd64 -ti -v `pwd`:/app -e HOST_UID=$(id -u) -e HOST_GID=$(id -g) diy
```
Why this scored 15/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.