Development environment
The environment is the part of a project you set up once and then forget, until it quietly lies to you. This module sets it up so that a fresh clone builds and tests with a handful of commands, the editor sees exactly what the compiler sees, and other people's code cannot drown out your warnings.
5 code rules · 8/8 checks pass · 18 sources · fact-checked and reviewed
Key ideas
Write the build down, by name
A CMake presets file records each way of building the project (generator, build type, options) under a name, so
cmake --preset debugmeans the same thing on your laptop, on a second machine and in CI (F2.01). Build and test presets refer back to a configure preset, and workflow presets, added in presets version 6 (CMake 3.25), chain configure, build and test into one command.[1]Variants inherit, so they cannot drift
A preset that inherits another takes all of its settings except name, hidden, inherits, description and displayName, and overrides only what it sets itself; cache variables are merged. In the arm repository,
ciisdebugplus warnings-as-errors, so what CI builds is what you build every day. The check project behind this page confirms it: thecicompile line carries both the sanitizer flags it inherited and the-Werrorit added.[1][2]The editor must see what the compiler sees
clangd, the language server behind completion, go-to-definition and inline errors, compiles each file the way the build does, and learns how from
compile_commands.json(F2.02). CMake writes that file for Ninja and Makefile builds when asked. clangd looks for it in parent directories and in a folder calledbuild/, so with one build folder per preset it needs a symlink or a one-line.clangdfile.[3][4][5]Pin what you depend on, then test that it agrees
MuJoCo is built from source at the release tag 3.17.0, and the Python bindings are pinned to the same version, so the library under the C++ and the one under your plots cannot differ (F2.03). A commit hash is stricter still, since a tag can be moved; CMake's documentation advises one for servers you do not control. MuJoCo's own documentation recommends checking at run time that the header and the library agree; the arm repository makes that a test.[6][7]
Your warnings, not theirs
Strict warnings and the sanitizers live in one INTERFACE target that only your own targets link, PRIVATE, so consumers do not inherit the flags (F2.04). Third-party code comes in as SYSTEM, so its headers stop warning in your build (F2.03). The check project compiles the same untidy header both ways: warnings on an ordinary include path, none as SYSTEM.[2][8][9][10]
Python at the edges, locked
Python is for plots and analysis, not for the robot. uv keeps it in a project environment built from a lockfile, and
uv runbrings that environment up to date before every run (F2.05). The firstuv syncwritesuv.lock: commit it, and leave the environment folder out of version control.[11][12][13]Formatting is a file, not a habit
clang-format reads the
.clang-formatfile in the closest parent folder of each source file, so the editor and the command line format the same way without anyone remembering the rules, provided they run the same clang-format version. To enforce it, add a CI step that runsclang-format --dry-run --Werrorover the sources.[14][15]
Rules
The same entries as the formula sheet. Open an entry's details for its symbols and the checks behind it.
Configure, build and test with presets
Presets file version 6 (which adds workflow presets) needs CMake 3.25 or later. A preset that inherits another takes every field except name, hidden, inherits, description and displayName, and overrides only what it sets itself; cache variables are merged.
cmake --preset debug # configure: Ninja, Debug, sanitizers cmake --build --preset debug # build ctest --preset debug # run the tests cmake --workflow --preset ci # all three, exactly as CI runs themSymbols and checks
- ✓ cmake --workflow --preset ci: configure, build, test (exit code)expected 0 · got 0 · absolute error 0Debug, AddressSanitizer + UBSan, warnings as errors
- ✓ ci inherits debug: sanitizers from debug and -Werror of its ownobserved as stated
Give clangd the compile commands
Makefile and Ninja generators only. Alternatively, point clangd at the folder with a .clangd file (CompileFlags, CompilationDatabase: build/debug).
# in the base configure preset "cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": "ON" } # clangd looks in parent directories and in build/, not build/debug/: ln -s build/debug/compile_commands.json .Symbols and checks
- ✓ compile_commands.json lists every source fileexpected 9 · got 9 · absolute error 0
Pin a dependency, and keep its warnings out of yours
SYSTEM needs CMake 3.25 or later. Code that includes the dependency's headers no longer gets warnings from them; your own code still does. The CMake docs advise a commit hash over a tag for servers you do not control, since a tag can be moved; a hash rules out GIT_SHALLOW.
FetchContent_Declare(mujoco GIT_REPOSITORY https://github.com/google-deepmind/mujoco.git GIT_TAG 3.17.0 # a release tag; never a branch GIT_SHALLOW TRUE # needs a tag or branch, not a hash SYSTEM) # its headers become system headers FetchContent_MakeAvailable(mujoco)Symbols and checks
- ✓ warnings from the dependency's header, included as SYSTEMexpected 0 · got 0 · absolute error 0
- ✓ the same header on an ordinary include path warnsobserved as stated
Warnings and sanitizers as a target, linked PRIVATE
PRIVATE dependencies are used to build the target but are not part of its interface, so consumers do not inherit its compile flags. For a static library, CMake still records PRIVATE dependencies for the final link, so their link options, such as the sanitizer runtime, reach the program that uses the library.
add_library(arm_project_options INTERFACE) target_compile_options(arm_project_options INTERFACE -Wall -Wextra ...) target_link_libraries(arm_sim PUBLIC mujoco::mujoco arm::core # consumers need these too PRIVATE arm::project_options) # only arm_sim's own sourcesSymbols and checks
- ✓ library linking the options PRIVATE compiles with -Wshadowexpected 1 · got 1 · absolute error 0
- ✓ its consumer compiles without itobserved as stated
- ✓ the static library's PRIVATE link options reach the consumer's linkobserved as statedthe sanitizer runtime has to be linked into the final program
One locked Python environment per project
uv run checks that the lockfile matches pyproject.toml and the environment matches the lockfile before every run. Commit uv.lock.
uv sync # create .venv from pyproject.toml and uv.lock uv run python scripts/plot.py # locks and syncs first, then runs
Worked example: from a fresh clone to green tests
The arm repository on macOS with Homebrew. The check project behind this page uses the same preset layout, and its cmake --workflow --preset ci run, on Linux with Clang, is one of the checks listed under F2.01.
Install the toolchain
Apple's compiler and the tools the presets name. The versions matter less than having them all; the presets file states the minimum CMake.
xcode-select --install brew install cmake ninja eigen googletest clang-format uvConfigure, build, test
The first configure downloads MuJoCo at its pinned tag and builds it, so it is the slow one; later builds reuse it. Every test should pass before you change anything.
cmake --preset debug cmake --build --preset debug ctest --preset debugWire up the editor
The debug preset writes
compile_commands.jsonintobuild/debug/, which is one level deeper than clangd looks. Link it into the repository root, or add a.clangdfile that names the folder.ln -s build/debug/compile_commands.json . # or, in .clangd: # CompileFlags: # CompilationDatabase: build/debugSet up Python
One command creates the environment and, the first time, the lockfile; commit
uv.lock. After that, run scripts through uv so the environment is always current.uv sync uv run python scripts/<name>.pyRun what CI runs
The workflow preset configures, builds and tests with warnings as errors and the sanitizers on. If this passes locally, CI's job is to confirm it with a second compiler on a second operating system.
cmake --workflow --preset ci
If a fresh clone needs more than these commands, or a step that lives only in your memory, the environment is not finished. Write the missing step into a preset, a script or the README.
Best practice
One build folder per preset, under an ignored build/ folder.
"binaryDir": "${sourceDir}/build/${presetName}"keeps debug and release objects apart, and deleting a build folder touches nothing else.[1]Make CI run a preset you can run yourself.
cmake --workflow --preset cirepeats CI's steps locally in one command, so most CI failures reproduce at your desk instead of being guessed from a log. Failures specific to CI's own compiler still need CI.[1]Set flags on targets, never in CMAKE_CXX_FLAGS.
Target commands say which code gets which flags and whether consumers inherit them; a global variable applies everywhere, third-party code included.[8][10]
Start at zero warnings and keep it there.
The arm repository makes warnings errors in its CI preset, which works while the count is zero. Pfeifer argues against passing -Werror at all, because a new compiler brings new warnings and blocks the upgrade; his alternative is to fail only on new warnings and burn the old ones down. If an upgrade bites, switch to that.[10]
Pin dependencies by release tag or lockfile, and commit the lockfile.
A branch name or an unpinned package is a different dependency next month. Pinning covers what you fetch yourself; packages from Homebrew and apt still move, which is one more thing CI notices.[6][13]
Turn version agreement into a test.
A header compiled against one library version and linked against another can build cleanly and misbehave quietly. Compare
mjVERSION_HEADERwithmj_version()in a smoke test.[7]
Questions
- clangd says it cannot find a header that compiles fine. Why?
- It has not found
compile_commands.json, so it is guessing at the flags. It searches the file's parent folders and folders namedbuild/, notbuild/debug/. Symlink the file into the repository root, or point clangd at the folder in a.clangdfile.[4][5] - Why build MuJoCo from source instead of installing a package?
- The headers and the library then come from the same tag, built with your compiler, and the version is written in the CMake file rather than wherever a package manager happens to be. The cost is a slower first configure.[6][7]
- Isn't an INTERFACE target full of warning flags what "Effective CMake" warns against?
- It warns against making warning flags a usage requirement of a library, so that everyone who links it inherits them. Linking the options target PRIVATE avoids exactly that: only the target's own sources get the flags. The check project confirms that a consumer of such a library compiles without them.[2][8][10]
- Why does CI run on Linux when I work on a Mac?
- A second compiler, standard library and operating system catch code that only works on the first by accident. It is also where a missing dependency in the instructions shows up, because the CI machine starts empty every time.
- Do I need Python at all?
- For the robot, no; the control code is C++. For plots, quick analysis and the MuJoCo viewer from a script, it is the fastest tool, as long as it stays at the edges and its environment is locked.[11]
Watch
- listed
C++ Weekly, Episode 3: Intro to clang-tidy (opens in a new tab)[18]
Jason Turner · C++ Weekly (YouTube); announced on isocpp.org, 28 March 2016
A short, practical start with clang-tidy, the linter configured in the arm repository, including letting it fix code for you.
Read
- read
Effective CMake (slides) (opens in a new tab)[10]
Daniel Pfeifer · C++Now 2017, on Speaker Deck
The case for thinking in targets and usage requirements, from a CMake developer. Most modern CMake advice traces back to talks like this one.
- read
cmake-presets(7) (opens in a new tab)[1]
Kitware, CMake documentation
The presets reference, including what each schema version added.
- read
Getting started (project setup) (opens in a new tab)[4]
clangd documentation, LLVM
Setting up clangd, including where it looks for compile commands.
- read
Locking and syncing (opens in a new tab)[12]
Astral, uv documentation
How uv's lockfile and environment stay in step.
Sources
Numbered in order of first use on this page. read: checked against the claim. listed: exists and covers the topic, wording not checked. derived: worked out here and checked numerically.
- [1]read
cmake-presets(7) (opens in a new tab)
Kitware, CMake documentation
Schema versions (version 6, CMake 3.25: workflow presets); configurePresets.inherits
Presets file versions and the CMake release that added each; how inherits works. Read from the documentation source in the CMake repository (master, October 2026).
- [2]derived
Compiled checks for modules 02 to 04 (tools/robotics/cpp)
A small CMake project that mirrors the arm repository, built with presets and run by verify.py; every C++, CMake and GoogleTest claim marked checked is observed there.
- [3]read
CMAKE_EXPORT_COMPILE_COMMANDS (opens in a new tab)
Kitware, CMake documentation
Writes compile_commands.json with the exact compiler call for every source file; Makefile and Ninja generators only. Read from the documentation source in the CMake repository (master, October 2026).
- [4]read
Getting started (project setup) (opens in a new tab)
clangd documentation, LLVM
Project setup: compile_commands.json
clangd looks for compile_commands.json in the parent directories of the file being edited and in subdirectories named build/. Read from the llvm/clangd-www repository (September 2026).
- [5]read
Configuration (opens in a new tab)
clangd documentation, LLVM
CompileFlags: CompilationDatabase
CompilationDatabase names the directory to search for compile_commands.json. Read from the llvm/clangd-www repository.
- [6]read
FetchContent (opens in a new tab)
Kitware, CMake documentation
FetchContent_Declare: SYSTEM (added in 3.25); advice to use a commit hash for GIT_TAG
SYSTEM makes FetchContent_MakeAvailable add the dependency with add_subdirectory(... SYSTEM); for content from servers you do not control, a commit hash is advised over a branch or tag name. Read from the documentation source in the CMake repository (master, October 2026).
- [7]read
MuJoCo Programming chapter, Introduction (3.17.0) (opens in a new tab)
Google DeepMind
Version check: mjVERSION_HEADER and mj_version()
Recommends comparing the header's mjVERSION_HEADER with the library's mj_version() at run time. Read from the 3.17.0 source of the docs.
- [8]read
target_link_libraries (opens in a new tab)
Kitware, CMake documentation
Libraries for a Target and/or its Dependents
PUBLIC: linked and part of the link interface. PRIVATE: linked, not part of the interface. INTERFACE: interface only. Read from the documentation source in the CMake repository (master, October 2026).
- [9]read
SYSTEM (target property) (opens in a new tab)
Kitware, CMake documentation
A system target's interface include directories are treated as system include directories when compiling its consumers. Read from the documentation source in the CMake repository (master, October 2026).
- [10]read
Effective CMake (slides) (opens in a new tab)
Daniel Pfeifer · C++Now 2017, on Speaker Deck
Think in targets and their usage requirements; prefer target_* commands to directory-wide ones; -Wall is not a usage requirement (slide 20); never pass -Werror to the compiler, and treat new warnings as errors instead, burning existing ones down (slides 44–46).
- [11]read
Running commands in projects (opens in a new tab)
Astral, uv documentation
uv run runs a command in the project environment and makes sure it is up to date first. Read from the uv repository (October 2026).
- [12]read
Locking and syncing (opens in a new tab)
Astral, uv documentation
Locking and syncing are automatic: uv run locks and syncs before running; --locked makes an out-of-date lockfile an error. Read from the uv repository.
- [13]read
Project structure and files (opens in a new tab)
Astral, uv documentation
The lockfile
uv.lock holds the exact resolved versions and should be checked into version control; .venv should not. Read from the uv repository.
- [14]read
Clang-Format Style Options (opens in a new tab)
Clang documentation, LLVM 18
With -style=file, clang-format uses the .clang-format file in the closest parent directory of each input file. Read from llvm-project at llvmorg-18.1.8.
- [15]read
ClangFormat (opens in a new tab)
Clang documentation, LLVM 18
Command-line options: --dry-run, --Werror
--dry-run reports formatting changes without making them; --Werror turns those reports into errors. Read from llvm-project at llvmorg-18.1.8.
- [16]read
ExternalProject (Git options, shared by FetchContent) (opens in a new tab)
Kitware, CMake documentation
GIT_TAG; GIT_SHALLOW
GIT_SHALLOW clones only the commit named by GIT_TAG, and then works only with branch and tag names, not a commit hash. Read from the CMake repository.
- [17]read
cmake-buildsystem(7) (opens in a new tab)
Kitware, CMake documentation
Static Libraries; Transitive Link Properties (INTERFACE_LINK_OPTIONS)
Static libraries are archives, so CMake records their link dependencies for transitive use when linking the binaries that consume them; link usage requirements such as INTERFACE_LINK_OPTIONS travel as transitive link properties. Read from the CMake repository.
- [18]listed
C++ Weekly, Episode 3: Intro to clang-tidy (opens in a new tab)
Jason Turner · C++ Weekly (YouTube); announced on isocpp.org, 28 March 2016
Basic use of clang-tidy, including applying its automatic fixes.