Build Tools
This document covers the build tooling and other utilities developed to aid the
development process. It is a usage reference for the wrapper scripts in scripts/.
For how the build itself works (the CMake layout, toolchain files, presets and the generated libraries) see the Build System reference.
Scripts
Section titled “Scripts”All of the scripts found in scripts/ serve as wrappers of either python tools
or vendor utilities (e.g. MJBots, STMicroelectronics), and should be used as
simple interfaces to these tools.
bootstrap.sh
Section titled “bootstrap.sh”The bootstrap script sets up a fresh Ubuntu/Debian machine for ESW development: system
packages, the ARM GNU toolchain, uv, STM32CubeMX/CubeProgrammer/CubeCLT (and optionally
CubeIDE), git submodules, PATH setup, desktop launcher entries, VS Code and its extensions, and
the Python venv, all via an ansible playbook
(ansible/bootstrap.yml). It installs ansible itself if it isn’t already present. Run it once on
a new machine as follows.
./scripts/bootstrap.shSTM32CubeMX, STM32CubeProgrammer and STM32CubeCLT require a login-gated manual download from ST’s
website, so the script will pause partway through and ask you to download all three installers
into the gitignored install/ directory before continuing; see
STM32Cube* for details.
CubeCLT ships STM32_Programmer_CLI, which is what the build scripts and CI use, but not the
programmer GUI. The full STM32CubeProgrammer is installed separately for interactive flashing,
option-byte editing and reading memory back off a board. It is deliberately not added to
PATH, since its bin/ contains a second STM32_Programmer_CLI that would shadow CubeCLT’s. It
is launched from its desktop entry instead.
Because CubeMX and CubeProgrammer ship graphical installers with no silent-install flag, two
installer windows open during the run for you to click through. Each installer is recorded against
a stamp file in install/, keyed to the installer’s own filename, so re-running bootstrap.sh
does not pop those windows again, while dropping a newer installer into install/ does
re-run it. That is how you upgrade: download the new archives, re-run ./scripts/bootstrap.sh.
The IzPack installers write their own .desktop files, but they run under sudo, so those land
in root’s home and never reach your launcher. ansible/tasks/desktop-entries.yml writes managed
entries for STM32CubeMX, STM32CubeProgrammer and STM32CubeIDE into ~/.local/share/applications
instead.
STM32CubeIDE is the one optional piece: if its archive isn’t in install/, the playbook says so
and moves on. It is used purely as a graphical debugger against an ELF build.sh produced, never
to build (see
Debugging with STM32CubeIDE).
The ARM cross-compiler comes from STM32CubeCLT, which bundles the same 14.3.rel1 release ESW
builds with. The standalone ARM GNU toolchain is also installed, but only as a fallback: the
PATH written to /etc/profile.d/mrover-esw.sh puts CubeCLT’s copy first and appends the
standalone one, so CubeCLT wins whenever it is present.
Toolchain versions are pinned in three places that must move together when CubeCLT is upgraded:
arm_gnu_link/arm_gnu_name in ansible/bootstrap.yml, ARM_GNU_LINK in Dockerfile.arm-gnu,
and EXPECTED_GCC_VERSION in scripts/doctor.sh. Dependabot cannot see any of them, as it does not
read apt package versions, ST’s login-gated downloads, or a toolchain URL pinned in an ENV.
doctor.sh is the backstop: it warns when the compiler actually in use drifts from what CI
builds with.
uv is the exception, and is pinned in exactly one place:
FROM ghcr.io/astral-sh/uv:0.12.8 AS uvEverything reads that one line. CI and the release workflow run inside the image and get the
binary it copies out; .github/workflows/site.yml greps the tag and hands it to setup-uv;
ansible/tasks/uv.yml greps it too and uses uv self update <version>, which converges from
either direction, so a developer’s uv matches CI’s exactly. It has to be a named FROM stage
rather than an inline COPY --from=ghcr.io/astral-sh/uv:... because Dependabot’s Docker ecosystem
parses FROM lines only, and that is what makes this pin update itself.
Keeping it identical everywhere matters because uv writes tools/uv.lock, and the lockfile
revision moves with the tool. required-version in tools/pyproject.toml is a matching floor, so
an older uv fails loudly instead of silently rewriting the lock at a revision CI cannot read.
Once bootstrap finishes, open a new terminal and run doctor.sh to confirm the
install is good.
Editor
Section titled “Editor”VS Code is installed alongside the toolchain, with the extensions this repo’s workflow needs.
The list lives in the playbook, as the vscode_extensions variable in ansible/bootstrap.yml:
| Extension | Why |
|---|---|
stmicroelectronics.stm32-vscode-extension | ST’s own tooling |
llvm-vs-code-extensions.vscode-clangd | build.sh generates a .clangd per project |
marus25.cortex-debug | drives CubeCLT’s ST-LINK_gdbserver, the non-CubeIDE debug path |
It is kept there rather than in .vscode/extensions.json because .vscode/ is gitignored, so
that file is never committed and cannot be a shared source of truth. scripts/doctor.sh parses
the same variable when it checks your install, so the two cannot drift apart. Add an extension by
appending one entry per line to that list.
Only missing extensions are installed, since VS Code keeps them up to date itself. VS Code proper is
updated by apt/brew like any other package, which is why the playbook writes an enabled
/etc/apt/sources.list.d/vscode.sources: the code package ships that file disabled in some
installs, which silently orphans the editor at whatever version was first installed.
Skip the editor entirely with --skip-tags vscode if you use CLion or something else; nothing in
the build depends on it.
doctor.sh
Section titled “doctor.sh”Checks that a development environment is set up correctly, and is the fastest way to find out why
something isn’t working. It resolves every tool ESW needs (cmake, ninja, git, uv, the
arm-none-eabi-* cross-compilers, STM32_Programmer_CLI, ST-LINK_gdbserver, STM32CubeMX,
STM32CubeProgrammer, clang-format, shellcheck) and reports the version and location of
each, rather than stopping at the first thing it can’t find.
./scripts/doctor.sh [--build] [--verbose]Beyond tool presence it checks that:
arm-none-eabi-gccresolves inside STM32CubeCLT rather than the fallback toolchain, and that its version matches the one CI builds with. A fallback that has quietly taken over means CubeCLT’sPATHentry has gone stale, usually after a CubeCLT upgrade.- Every directory named in
/etc/profile.d/mrover-esw.shstill exists, and is actually on thePATHof the shell you randoctor.shfrom. The second half catches the common case of a shell that predates the profile. See the note in STM32Cube* for why zsh makes that easy to hit. - No leftover hand-written
/etc/profile.dsnippet from the old manual setup is competing with it forPATHprecedence. (ST’s owncubeclt-bin-path_*.sh, installed by the CubeCLT package, is expected and ignored.) - VS Code is installed and every extension in the playbook’s
vscode_extensionslist is present. - The launcher entries exist and still point at binaries that are there. An upgrade that relocates an install shows up here. STM32CubeIDE is reported when installed and reported as skipped when not, without counting as a warning.
- The
lib/stm32g4/STM32CubeG4submodule is initialized. tools/.venvis in sync withtools/uv.lock.
Missing tools are errors and exit non-zero; a fallback compiler, a stale PATH profile, a missing
programmer GUI and broken launcher entries are warnings, since none of them stops a build on its
own. The desktop-entry and profile.d checks are Linux-only and are skipped elsewhere.
--build
Additionally runs an end-to-end build of src/tests/logger and confirms an .elf links. This is
the real proof that CubeCLT is installed correctly, as it exercises the cross-compiler, the CMake
toolchain file, and the DBC/Python codegen in one shot. On failure the tail of the build log is
printed.
--verbose
Prints the full build log instead of the last 20 lines.
venv.sh
Section titled “venv.sh”Syncs the tools/.venv Python virtual environment from tools/uv.lock by running
uv sync --project tools. This is purely a convenience: every script that needs Python runs
through uv run, which creates and syncs the environment on demand, so nothing breaks if you
forget it. Run it to pre-warm the environment, or standalone instead of the full bootstrap.sh
if the rest of the toolchain is already installed.
./scripts/venv.shnew.sh
Section titled “new.sh”The scripts/new.sh script is designed to aid the creation of new STM32 projects
that leverage the HAL libraries provided by STM. The script can be run as follows.
./scripts/new.sh --mcu <mcu> --src <path-to-new-project> [--lib <library>]# OR./scripts/new.sh --board <board> --src <path-to-new-project> [--lib <library>]# OR./scripts/new.sh --help # to display the options menuIn the invocation above, the script accepts either an MCU (e.g. STMG431CBTx)
or a development board (e.g. NUCLEO-G431RB).
Under the hood, this script will run CubeMX and create an STM32 project for the specified MCU. It does this by running a script of STM32CubeMX commands that create the project in the correct CMake/GCC configuration, and then manually patches the IOC to ensure some settings that aren’t linked to STM32CubeMX commands are correctly set.
The directory provided by the --src flag will be used for the creation of the
new project, and once the script finishes execution the project should be able
to be built.
Libraries can be provided with the --lib flag; if, for example, the new project
should link with ESW’s library for commonly used stm32 header files (defined by
lib/stm32/CMakeLists.txt) and the CAN messages defined by the auto-generated
DBC headers (defined by lib/dbc/CMakeLists.txt), the script should be run with
... --lib stm32 --lib dbc options. This will result in the created
<src>/CMakeLists.txt linking against these libraries. This is not necessary to
get all the libraries correct at this point, as the file can be modified later.
The created <src>/CMakeLists.txt will then contain the following section.
# Add linked librariestarget_link_libraries(${CMAKE_PROJECT_NAME} stm32cubemx
# Add user defined libraries stm32 dbc)build.sh
Section titled “build.sh”The build script is a wrapper around the CMake and the GCC distribution bundled with the STM32CubeCLT, as well as the STMCubeProgrammer CLI. The build script can be run in the following configurations.
1. Clean Project
The script’s --clean flag will remove all build artifacts from the project source.
This may be necessary to run after some updates to build files, as CMake is only
configured once and the cached build artifacts are used after.
./scripts/build.sh --src <src> --clean2. Build Project
The default behavior of the script is to build the specified project using CMake.
The script consumes the --src <path-to-project> flag to denote the directory
containing the project. If the project target is different from the directory name
(which is not the case for projects generated with the ./scripts/new.sh script)
the --target <target> is provided. Additionally, the build preset can be altered
with the --preset <preset> flag. The default preset provided with the CMake
configuration is Debug, but Release is also valid. If no preset is specified,
the script will default to Debug. To build a project, the script can be run as
follows.
./scripts/build.sh --src <path-to-project> [--preset <preset>] [--target <target>]3. Flash Project
To flash the executable file for a project to an MCU, use the --flash flag. When
set, the script will attempt a build as above, and if successful invoke the
STM32CubeProgrammer CLI to connect to an ST-LINK and flash the executable via SWD.
./scripts/build.sh --src <path-to-project> [--preset <preset>] --flashFinally, the build script will ensure the existence of a <src>/.clangd file for
development environment compatibility. This needs to be generated per-project as
it contains some project-specific and system-specific parameters.
style.sh
Section titled “style.sh”The style script enforces code style across all ESW code. It runs in CI, and must be passing for PRs to be accepted. The script can be run with the following arguments.
./scripts/style.sh [--format] [--lint] [--fix] [--verbose]If no options are provided, the --format run configuration will be used by default.
The functionality of the options is enumerated below.
--format
This configuration will run clang-format and ruff to format C/C++ and Python
code, respectively.
--lint
This configuration will run linters ruff and shellcheck for Python and shell
scripts, respectively. This configuration will also run ty for static type analysis
of Python scripts.
--fix
This configuration will fix all possible formatting and linting issues found with the associated flags the script is run with. If the script finishes unsuccessfully with this flag, there are issues with the codebase that the tools cannot automatically resolve.
--verbose
This flag runs the script with verbose output.
fdcanusb.sh
Section titled “fdcanusb.sh”This script is a wrapper over MJBot’s fdcanusb_daemon. It is designed to run with
the MJBots FDCANUSB to allow connectivity to CAN networks over USB. The script is run
as follows.
sudo ./scripts/fdcanusb.sh --net <vcan-network>The --net flag specifies the virtual CAN network the fdcanusb should use. To work
with the ROS2 stack, this should be something like can[0-3]. The script runs the
underlying fdcanusb_daemon in verbose mode, so the full CAN frame of all messages
on the bus will be directed to standard output.
monitor.sh
Section titled “monitor.sh”Wraps tools/scripts/monitor.py, which displays serial log data sent from the MCU over the
ST-LINKv3’s VCP-TX/VCP-RX pins. Accepts --baud and --log-level, passed straight through.
./scripts/monitor.sh [--baud <rate>] [--log-level <level>]Python Tools
Section titled “Python Tools”The ESW python tools in tools/ handle code generation for the firmware build, CAN
communication, project scaffolding and serial monitoring. They are managed by uv, so there is no
setup step: every script runs through uv run --project tools.
For the full reference (every module, every script and its flags) see Python Tools.
| Module | Purpose |
|---|---|
esw.can | CAN bus access, DBC parsing, CAN header generation |
esw.config | board register definitions and value packing |
esw.cubemx | CubeMX project generation and CMake rendering |
esw.stlink | ST-LINKv3 serial log monitoring |
esw.visualization | live plotting for bench testing |