From 520b348ad3efae0b1c16082c2ad4aa2e723b2708 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Mon, 3 Aug 2026 12:23:40 -0400 Subject: [PATCH 1/4] doc: Update comment on CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES workaround The workaround was added in 977d721 so clang-tidy could find headers that nix compiler wrappers inject internally. Its comment has since become inaccurate and misleading: - It names clang-tidy as the tool that needs the workaround, but clang-tidy no longer does: the nixpkgs clang-tools wrapper was fixed upstream (https://github.com/NixOS/nixpkgs/pull/462747). IWYU is the tool that needs it now. - It says the tool "ignores $NIX_CFLAGS_COMPILE." In fact the nixpkgs analysis-tool wrappers do read $NIX_CFLAGS_COMPILE and re-add its paths. - It says the missing headers are capnp (dependency) headers. That is backwards: dependency headers are passed via -isystem in $NIX_CFLAGS_COMPILE and are found. The headers that go missing are the C++ standard library headers (e.g. ), because the include-what-you-use wrapper drops the -cxx-isystem flags those use. - It pins the whole mechanism on $NIX_CFLAGS_COMPILE, omitting that standard library paths are injected through the compiler wrapper's own flag files, and it neither explains why the tools normally cope nor that the workaround only compensates for a temporary wrapper bug. Rewrite the comment to describe the nix header-injection mechanism from first principles, identify IWYU's dropped -cxx-isystem flags as the specific reason the workaround is still needed, and note that it is removable once that wrapper bug is fixed upstream. Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index bf50018a..82436a0c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,10 +104,29 @@ endif() if(MP_ENABLE_CLANG_TIDY OR MP_ENABLE_IWYU) # Workaround for nix from https://gitlab.kitware.com/cmake/cmake/-/issues/20912#note_793338 - # Nix injects header paths via $NIX_CFLAGS_COMPILE; CMake tags these as - # CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES and omits them from the compile - # database, so clang-tidy, which ignores $NIX_CFLAGS_COMPILE, can't find capnp - # headers. Setting them as standard passes them to clang-tidy. + # + # On non-nix systems, compilers find C++ standard library headers in standard + # locations like /usr/include, and find dependency headers through -I flags on + # the compiler command line. On nix neither is true: compiler binaries are + # wrapped by scripts that inject the standard library header locations + # themselves, and dependency header locations are passed to the wrapper through + # $NIX_CFLAGS_COMPILE instead of on the command line. So CMake records all these + # paths as CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES and leaves them off the + # command lines it writes to the compile database. + # + # This normally does not matter, because analysis tools have their own wrapper + # scripts that re-add these paths the same way. But the nixpkgs + # include-what-you-use wrapper only re-adds -isystem flags and drops the + # -cxx-isystem flags used for the C++ standard library, so IWYU cannot find + # standard headers like . Setting the implicit include directories as + # standard directories puts them back on every command line, which is what lets + # the buggy IWYU wrapper find them. + # + # This workaround is temporary: + # - clang-tidy no longer needs it: the nixpkgs clang-tools wrapper was fixed in + # https://github.com/NixOS/nixpkgs/pull/462747. + # - IWYU will stop needing it once its wrapper is fixed the same way (see + # https://github.com/bitcoin-core/libmultiprocess/pull/238#discussion_r3342093865). set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES ${CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES}) endif() From 36373a58551c6c3bc47e563f9baaa9873e13879d Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Mon, 3 Aug 2026 12:24:05 -0400 Subject: [PATCH 2/4] ci: Make IWYU use the same standard library as the build Since cbb1e43 the llvm CI job is intended to test libc++ instead of libstdc++, in the build and in IWYU. That works, but only accidentally: the nixpkgs include-what-you-use wrapper bakes in the include paths of the toolchain IWYU was built against (libstdc++ on Linux), and IWYU only sees libc++ headers because the explicit -isystem flags generated by the CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES workaround in CMakeLists.txt take precedence over the wrapper's environment variables. Relying on that is fragile: IWYU invoked outside the CMake build (or after that workaround is removed) silently analyzes libstdc++, with a mapping file that only matches libc++. Make shell.nix responsible for this instead: rebind the clang recorded in the IWYU wrapper to the shell's compiler, so IWYU resolves the same standard library the build uses in any context. Also expand the IWYU_MAPPING_FILE comment to explain how the mapping file is consumed and kept consistent. The override changes the IWYU derivation, so shells rebuild it from source once per channel instead of fetching it from the binary cache. Co-Authored-By: Claude Fable 5 --- shell.nix | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/shell.nix b/shell.nix index 2d115fea..e62de358 100644 --- a/shell.nix +++ b/shell.nix @@ -53,6 +53,23 @@ let })).override (lib.optionalAttrs enableLibcxx { clangStdenv = llvm.libcxxStdenv; }); clang = if enableLibcxx then llvm.libcxxClang else llvm.clang; clang-tools = llvm.clang-tools.override { inherit enableLibcxx; }; + # IWYU parses source files with its own embedded clang frontend, locating + # standard library headers through CPATH/CPLUS_INCLUDE_PATH variables set by + # its nixpkgs wrapper script: + # https://github.com/NixOS/nixpkgs/blob/master/pkgs/development/tools/analysis/include-what-you-use/wrapper + # The wrapper derives those variables from the clang recorded in the + # derivation's `clang` attribute, which defaults to the toolchain IWYU was + # built against (libstdc++-flavored on Linux), not the toolchain this shell + # uses. Rebind it to this shell's compiler so IWYU resolves the same + # standard library the build uses. + # + # Mixing pkgs and crossPkgs here is intentional: IWYU comes from pkgs + # because it runs on the build machine, while `clang` comes from crossPkgs + # (via llvm) so that in a cross shell IWYU is baked with the cross + # toolchain's target headers — what it needs to analyze a cross build. + include-what-you-use = pkgs.include-what-you-use.overrideAttrs (old: { + inherit clang; + }); cmakeHashes = { "3.12.4" = "sha256-UlVYS/0EPrcXViz/iULUcvHA5GecSUHYS6raqbKOMZQ="; }; @@ -81,6 +98,13 @@ in crossPkgs.mkShell { CC = if gcc == null then null else "${gcc}/bin/gcc"; CXX = if gcc == null then null else "${gcc}/bin/g++"; - # Tell IWYU where its libc++ mapping lives + # Tell IWYU where its libc++ mapping file lives. libcxx.imp maps + # libc++-internal detail headers (e.g. <__vector/vector.h>) to the public + # headers IWYU should suggest instead. IWYU only applies mapping tables + # compiled into its binary unless a mapping file is passed explicitly — it + # does not discover the libcxx.imp shipped alongside the headers it parses — + # so CMakeLists.txt forwards this variable via -Xiwyu --mapping_file. Taking + # the file from llvm.libcxx keeps it consistent with the headers IWYU parses + # via the include-what-you-use override above. IWYU_MAPPING_FILE = if enableLibcxx then "${llvm.libcxx.dev}/include/c++/v1/libcxx.imp" else null; } From 30363632d72d92ad4b0e440e00adaa7358ad2e94 Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Mon, 3 Aug 2026 12:24:14 -0400 Subject: [PATCH 3/4] ci: Provide IWYU only in non-minimal shells Move include-what-you-use into the non-minimal tool group alongside clang and clang-tools. Minimal shells, used by the cross-compiling gnu32 job, do not run analysis tools, and after the previous commit shipping IWYU there would pull the cross clang closure into the shell and rebuild IWYU per cross target for no benefit. Cross shells that do provide IWYU get the cross toolchain's target headers baked in, which is what analyzing a cross build requires (the embedded clang frontend parses in target mode given a matching --target flag). Co-Authored-By: Claude Fable 5 --- shell.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell.nix b/shell.nix index e62de358..f24c5a76 100644 --- a/shell.nix +++ b/shell.nix @@ -88,11 +88,11 @@ in crossPkgs.mkShell { ]; nativeBuildInputs = with pkgs; [ cmakeBuild - include-what-you-use ninja ] ++ lib.optional (gcc != null) gcc ++ lib.optionals (!minimal) [ clang clang-tools + include-what-you-use ]; CC = if gcc == null then null else "${gcc}/bin/gcc"; From 03329f69d5a1095117485903d604bacd596f9b2c Mon Sep 17 00:00:00 2001 From: Ryan Ofsky Date: Wed, 21 Jan 2026 21:29:44 -0500 Subject: [PATCH 4/4] nix: list clang-tools before clang so its tools find standard headers The clang-tools package provides clangd, clang-tidy, clang-check and about 20 other clang-tools-extra programs, wrapped so they can find the standard library headers on Nix. The clang package provides the same programs too, but as raw binaries that cannot. Both end up on the PATH, and nativeBuildInputs order sets PATH priority (the earlier entry wins), so list clang-tools first to make the working copies win. This lets clangd (in an editor) and clang-tidy or clang-check (run by hand) resolve and the rest of the standard library. Without it, the raw clang-check fails immediately over any source file: include/mp/util.h:8:10: fatal error: 'array' file not found The wrapping is a Nix quirk. On a normal system the standard library lives in a default location like /usr/include that clang searches automatically, so these tools work out of the box. Nix has no such default: glibc and libstdc++ live in isolated store paths, and only the clang compiler wrapper knows where. It injects the right -isystem paths when it compiles, but standalone tools like clang-check and clangd run clang's parser directly, never going through the compiler wrapper, so they need another way to learn the paths. That is what the clang-tools wrappers do. Before running the real tool, each reads the libc-cflags and libcxx-cxxflags files from the clang compiler wrapper, which hold the -idirafter and -cxx-isystem flags for the glibc and libstdc++ header directories. The wrapper copies those directories into the C_INCLUDE_PATH and CPLUS_INCLUDE_PATH environment variables, which clang reads and adds to its header search path. The raw binaries do none of this. Why does the clang package ship these tools at all? It exposes them only as a side effect: its cc-wrapper setup-hook adds the whole unwrapped-clang bin/ to the PATH so the compiler driver and adjacent programs are reachable, and upstream LLVM installs the clang-tools-extra programs into that same bin/. Why not fix this in CMake instead? CMakeLists.txt already does, as an alternative workaround: it adds the compiler's implicit include directories to the compile database via CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES, so tools that read compile_commands.json find the standard headers regardless of package order. But it is only enabled alongside IWYU or clang-tidy, so it is not always present, and it is more fragile and nonstandard: it bakes the detected store paths into the compile commands as explicit -isystem flags that would not normally be there. Ordering clang-tools first fixes the tools themselves, so they work independently of the CMake configuration. This does not affect include-what-you-use, which is a separate package with no PATH collision and its own wrapper script. https://web.archive.org/web/20260311024938/https://blog.kotatsu.dev/posts/2024-04-10-nixpkgs-clangd-missing-headers/ https://github.com/NixOS/nixpkgs/issues/76486 Co-Authored-By: Claude Opus 4.8 --- shell.nix | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/shell.nix b/shell.nix index f24c5a76..1f61698c 100644 --- a/shell.nix +++ b/shell.nix @@ -90,8 +90,16 @@ in crossPkgs.mkShell { cmakeBuild ninja ] ++ lib.optional (gcc != null) gcc ++ lib.optionals (!minimal) [ - clang + # List clang-tools before clang so its wrapped tools take PATH priority + # (the first package in the list wins). Both packages provide the same + # tools (clangd, clang-tidy, clang-check, ...), but the clang package's + # copies are unwrapped and cannot find standard library headers like + # , while the clang-tools copies are wrapper scripts that add + # the C and C++ standard library include paths. + # https://web.archive.org/web/20260311024938/https://blog.kotatsu.dev/posts/2024-04-10-nixpkgs-clangd-missing-headers/ + # https://github.com/NixOS/nixpkgs/issues/76486 clang-tools + clang include-what-you-use ];