Mohamed Elashri

Developing ROOT CERN in VSCode

Working on ROOT as a user is one thing, working on ROOT as a developer is something else entirely. ROOT is a large C++ project with decades of history, thousands of contributors, and every corner of C++ put to use. To do meaningful development inside it, you need more than just a make command, you need a setup that lets you move quickly, inspect the codebase with precision, and debug effectively.

I’ve settled on a workflow based on VSCode, the CMake Tools extension, and compile_commands.json. It gives me fast builds, accurate navigation through ROOT’s labyrinth of headers, and a debugger that can step cleanly through shared libraries and test binaries. To make it reproducible and portable across machines, I also rely on CMakePresets.json, which plays well with VSCode out of the box.

The first step is still an out-of-source build. ROOT’s tree should stay clean, and the build artifacts go into a dedicated directory:

git clone https://github.com/root-project/root.git
cd root
mkdir .build && cd .build

CMake configuration is where development time is usually won or lost. For a development build, I prefer a minimal but functional feature set, with testing enabled and compile commands exported.

A direct invocation looks like:

cmake .. \
  -DCMAKE_BUILD_TYPE=RelWithDebInfo \
  -Dminimal=ON \
  -Ddataframe=ON \
  -Dtree=ON \
  -Dvecops=ON \
  -DROOT_ENABLE_IMT=ON \
  -DROOT_USE_IMT=ON \
  -Dhist=ON \
  -Dpyroot=OFF \
  -Dtmva=OFF \
  -Droofit=OFF \
  -Dx11=OFF \
  -Dtesting=ON \
  -Droottest=ON \
  -Dbuiltin_llvm=ON \
  -Dbuiltin_clang=ON \
  -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

I generally use RelWithDebInfo to keep optimization realistic but retain symbols (-O2 -g3 -fno-omit-frame-pointer) for profiling and backtraces.

make -j$(nproc)

At this stage, the editor can be wired in. I open the repository root in VSCode, point CMake Tools to .build, and symlink compile_commands.json into the workspace:

ln -s .build/compile_commands.json compile_commands.json

In .vscode/settings.json:

{
  "C_Cpp.default.compileCommands": "${workspaceFolder}/compile_commands.json"
}

Or, if I prefer clangd over cpptools:

{
  "clangd.arguments": [
    "--compile-commands-dir=${workspaceFolder}",
    "--background-index"
  ],
  "C_Cpp.intelliSenseEngine": "Disabled"
}

With this in place, navigation, autocompletion, and diagnostics all behave correctly across ROOT’s headers.

One improvement I find essential for portability is CMakePresets.json. This file lets me capture the entire configuration in version-controlled form, so I don’t need to remember long command lines. A simple preset looks like this:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "dev-relwithdebinfo",
      "displayName": "Development (RelWithDebInfo)",
      "generator": "Unix Makefiles",
      "binaryDir": "${sourceDir}/.build",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "RelWithDebInfo",
        "minimal": "ON",
        "dataframe": "ON",
        "tree": "ON",
        "vecops": "ON",
        "ROOT_ENABLE_IMT": "ON",
        "ROOT_USE_IMT": "ON",
        "hist": "ON",
        "pyroot": "OFF",
        "tmva": "OFF",
        "roofit": "OFF",
        "x11": "OFF",
        "testing": "ON",
        "roottest": "ON",
        "builtin_llvm": "ON",
        "builtin_clang": "ON",
        "CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "build-all",
      "configurePreset": "dev-relwithdebinfo",
      "jobs": 10
    }
  ],
  "testPresets": [
    {
      "name": "all-tests",
      "configurePreset": "dev-relwithdebinfo",
      "output": { "outputOnFailure": true }
    }
  ]
}

Once this file is in the repository root, VSCode automatically detects it. From the Command Palette I just pick CMake: Select Configure Presetdev-relwithdebinfo, and builds/tests can be run directly through the extension. This makes the workflow reproducible across machines and simplifies onboarding when collaborating with others.

Sometimes I want more than one configuration handy. For example, a minimal developer build for fast iteration, and another with extra components like roofit or pyroot enabled. Presets make this trivial. Here’s a snippet showing two side by side:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "minimal-dev",
      "displayName": "Minimal Development",
      "binaryDir": "${sourceDir}/.build-minimal",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "RelWithDebInfo",
        "minimal": "ON",
        "roofit": "OFF",
        "pyroot": "OFF"
      }
    },
    {
      "name": "extended-dev",
      "displayName": "Extended Development (with RooFit & PyROOT)",
      "binaryDir": "${sourceDir}/.build-extended",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "RelWithDebInfo",
        "minimal": "ON",
        "roofit": "ON",
        "pyroot": "ON"
      }
    }
  ]
}

Switching between them in VSCode is as simple as changing the active configure preset. This way, I can keep one build tree lean for day-to-day hacking and another with heavier options enabled for feature-specific work.

Debugging is the final piece. The main ROOT binary lives in .build/bin/root.exe, and that’s usually where I attach. For gdb, .vscode/launch.json includes:

{
  "name": "Debug ROOT (gdb)",
  "type": "cppdbg",
  "request": "launch",
  "program": "${workspaceFolder}/.build/bin/root.exe",
  "args": ["-l"],
  "cwd": "${workspaceFolder}",
  "environment": [
    { "name": "LD_LIBRARY_PATH", "value": "${workspaceFolder}/.build/lib:$LD_LIBRARY_PATH" }
  ],
  "MIMode": "gdb",
  "miDebuggerPath": "gdb",
  "externalConsole": true,
  "setupCommands": [
    { "text": "-enable-pretty-printing" },
    { "text": "set breakpoint pending on" },
    { "text": "set follow-fork-mode child" }
  ]
}

For Clang-based builds, switching to lldb works just as well, with a nearly identical config.

Tests are equally straightforward. Running ctest -N in .build lists available executables, any of which can be added to launch.json. With CMake Tools, it’s also possible to right-click and debug a specific test, reusing the same environment.

This combination, minimal build, compile_commands.json, multiple presets for different workflows, and debugger integration gives a development environment that is both reproducible and flexible. Presets remove boilerplate, the editor stays in sync with the build system, and debugging works smoothly across ROOT’s plugin-heavy architecture. That’s the setup I rely on when hacking on ROOT, and it keeps the focus where it belongs, on the code itself.