How can I make clangd automatically recognize header paths from libraries in a Nix shell?

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.