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

  1. 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 debug means 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]

  2. 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, ci is debug plus warnings-as-errors, so what CI builds is what you build every day. The check project behind this page confirms it: the ci compile line carries both the sanitizer flags it inherited and the -Werror it added.[1][2]

  3. 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 called build/, so with one build folder per preset it needs a symlink or a one-line .clangd file.[3][4][5]

  4. 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]

  5. 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]

  6. 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 run brings that environment up to date before every run (F2.05). The first uv sync writes uv.lock: commit it, and leave the environment folder out of version control.[11][12][13]

  7. Formatting is a file, not a habit

    clang-format reads the .clang-format file 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 runs clang-format --dry-run --Werror over 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.

  1. 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 them
    F2.01✓ 2/2 checked[1]
    Symbols 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
  2. 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 .
    F2.02✓ 1/1 checked[3][4][5]
    Symbols and checks
    • ✓ compile_commands.json lists every source fileexpected 9 · got 9 · absolute error 0
  3. 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)
    F2.03✓ 2/2 checked[6][9][16]
    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
  4. 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 sources
    F2.04✓ 3/3 checked[8][10][17]
    Symbols 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
  5. 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.

  1. 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 uv
  2. Configure, 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 debug
  3. Wire up the editor

    The debug preset writes compile_commands.json into build/debug/, which is one level deeper than clangd looks. Link it into the repository root, or add a .clangd file that names the folder.

    ln -s build/debug/compile_commands.json .
    # or, in .clangd:
    # CompileFlags:
    #   CompilationDatabase: build/debug
  4. Set 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>.py
  5. Run 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

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 named build/, not build/debug/. Symlink the file into the repository root, or point clangd at the folder in a .clangd file.[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

Read

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. [1]

    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).

    read
  2. [2]

    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.

    derived
  3. [3]

    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).

    read
  4. [4]

    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).

    read
  5. [5]

    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.

    read
  6. [6]

    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).

    read
  7. [7]

    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.

    read
  8. [9]

    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).

    read
  9. [10]

    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).

    read
  10. [11]

    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).

    read
  11. [12]

    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.

    read
  12. [13]

    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.

    read
  13. [14]

    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.

    read
  14. [15]

    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.

    read
  15. [16]

    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.

    read
  16. [17]

    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.

    read
  17. [18]

    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.

    listed