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 Preset → dev-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.