I’m developing a C++ library named mygui
, which depends on libraries like glfw
, spdlog
, etc. The project is built using CMake. Since CMake cannot find libraries under /nix/store
on its own, I use shell.nix + direnv
(or alternatively, flake.nix + nix develop
) to provide a proper development environment.
Here’s an example shell.nix
file I’m using:
let
pkgs = import <nixpkgs> { config = { }; overlays = [ ]; };
in
pkgs.mkShellNoCC {
buildInputs = with pkgs; [
hello
gcc14
glfw3
libGL
cmake
xorg.libX11
xorg.libXrandr
xorg.libXinerama
xorg.libXi
xorg.libXxf86vm
xorg.libXcursor
xorg.xorgproto
spdlog
];
nativeBuildInputs = with pkgs; [ pkg-config ];
shellHook = ''
export TMPDIR=/tmp
hello
'';
}
When I enter the shell using direnv, everything works fine: CMake can find the dependencies, and I can compile mygui
successfully.
However, clangd
does not work properly. For example:
#include "spdlog/spdlog.h"
#include "GLFW/glfw3.h"
Even though GCC can find these headers, clangd
cannot. It doesn’t understand the environment provided by shell.nix
. To work around this, I have to manually export CPATH
in shellHook
:
shellHook = ''
export TMPDIR=/tmp
export CPATH="${pkgs.glfw3}/include:${pkgs.spdlog.dev}/include:${pkgs.fmt.dev}/include"
hello
'';
This is manageable in a small demo project. But what if I have a large project with many dependencies? Manually adding each header path quickly becomes unmaintainable. Even worse, some dependencies (like spdlog
) depend on others (fmt
), so I have to include their headers too.
If my project depends on libraries A, B, and C, which in turn depend on AA, AB, AC, BA, BB, BC, and so on, the number of header paths I need to manually include grows exponentially.
Question:
Is there any way to make clangd
automatically recognize the header paths provided by shell.nix
or nix develop
environments?
Additional context:
As far as I understand, the way shell.nix
works is by providing a gcc-wrapper
, which is used to compile the project. During the build process, it is able to detect and use the required libraries (like glfw
, spdlog
, etc.) through internal mechanisms. Once the build is done, the environment is restored to its original state.
This means that shell.nix
doesn’t rely on environment variables like CPATH
or C_INCLUDE_PATH
. At the same time, dependencies like glfw
and spdlog
are not explicitly recorded in compile_commands.json
during the build process, so clangd
cannot infer these include paths.
For example, this is what an entry in my compile_commands.json
looks like without manually exporting CPATH
:
{
"directory": "/home/someone/.../mygui/build",
"command": "/nix/store/8zlrggpa17k8akk049y19140z004am0l-gcc-wrapper-14.2.0/bin/c++ -DFMT_SHARED -DSPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG -DSPDLOG_COMPILED_LIB -DSPDLOG_FMT_EXTERNAL -DSPDLOG_SHARED_LIB -I/home/someone/.../mygui/include -fopenmp -D_LINUX -std=c++23 -g -std=gnu++23 -o CMakeFiles/ch03.dir/src/ch03/ch03.cpp.o -c /home/someone/.../mygui/ch03.cpp",
"file": "/home/someone/.../mygui/ch03.cpp",
"output": "CMakeFiles/ch03.dir/src/ch03/ch03.cpp.o"
}
There’s no trace of glfw
, spdlog
, or other library include paths.
However, if I manually export the CPATH
variable in shell.nix
, those include paths do show up in compile_commands.json
:
{
"directory": "/home/someone/.../mygui/build",
"command": "/nix/store/8zlrggpa17k8akk049y19140z004am0l-gcc-wrapper-14.2.0/bin/c++ -DFMT_SHARED -DSPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG -DSPDLOG_COMPILED_LIB -DSPDLOG_FMT_EXTERNAL -DSPDLOG_SHARED_LIB -I/home/someone/.../mygui/include -isystem /nix/store/d7xajskw8gyi6fyi22s656mqcjfg9y7w-spdlog-1.14.1-dev/include -isystem /nix/store/97w91z5z25b9ckabpykmibg4651jx7l3-fmt-10.2.1-dev/include -fopenmp -D_LINUX -std=c++23 -g -std=gnu++23 -o CMakeFiles/ch03.dir/src/ch03/ch03.cpp.o -c /home/someone/.../mygui/ch03.cpp",
"file": "/home/someone/.../mygui/ch03.cpp",
"output": "CMakeFiles/ch03.dir/src/ch03/ch03.cpp.o"
}
So it seems that exporting CPATH
helps CMake
include those paths in the compile_commands.json
, making them visible to clangd
.