Intro
I was refactoring the build environment for an internal C++ project at work. To keep things simple, we used the following JSON library:
https://github.com/nlohmann/json
The build system was based on CMake. I planned to replace our wrapper scripts for CMake with a preset.json. If you haven’t heard of this feature yet: when you want to set specific configurations for customers via CMake defines, you can predefine these in a dedicated preset file. It works surprisingly well. For my experimental render engine, I put all the Windows packages installation via vcpkg into a preset file like this:
{
"version": 3,
"configurePresets": [
{
"name": "default",
"binaryDir": "${sourceDir}",
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": "contrib/vcpkg/scripts/buildsystems/vcpkg.cmake"
}
}
]
}
You now invoke the build with
cmake .\CMakeLists.txt --preset=default
under Windows, and the user no longer needs to worry about installing vcpkg, since it’s shipped under contrib.
I applied the same logic for compiler- and target-specific settings in the company project. And suddenly, the build failed. The error message read:
error parsing version, missing ;
The file mentioned in the errorversion simply contained the current version string and wasn't even used in the C++ build process at all — until now. So what happened?
What happened?
After some fruitless searching through the source code, trying to find out whether the version file had accidentally been included somewhere, I confirmed that this wasn’t the case. Otherwise, this build error would have appeared much earlier.
Next, I searched the CMake build environment to see if there was a pre-processing step that might be turning the version file into a mis-configured header file. Another dead end.
So what now? I identified the .cpp file that triggered the error. Unfortunately, the MSVC build output didn’t show which include caused the build to fail. So I used the following compiler switch:
/showIncludes
With this switch enabled, all included files for the translation unit are shown. While digging through the generated output — which listed hundreds of includes — I noticed one header that seemed to be triggering the build error:
json/include/nlohmann/detail/macro_scope.hpp
So I started studying this header more closely. And bingo:
#ifdef __has_include
#if __has_include(<version>)
#include <version>
#endif
#endif
If the preprocessor supports the __has_include feature, it checks whether a file named version exists. If it does, it gets included implicitly.
Why didn’t this happen in the old build setup I asked myself?
The answer is simple: in the new preset-based build, the build directory was set to the root directory of the project.
In the old build, we used a directory structure like:
build/<platform>/<config>
So I changed my CMake output directory to build. But that wasn't enough. It had to be at least build/ — in other words, at least two directory levels deep to avoid the problem. That workaround fixed the issue.
What have I learned
If build errors don’t make sense, enable all compiler options to extract more info from the build process. Without the option: /showIncludes I would never have found the root cause.
If even then the result seems illogical: accept that it’s still happening. If reality doesn’t match your expectations, change your perspective.
If I didn’t include the file, someone else must have. If it didn’t happen explicitly, it happened implicitly — in this case, via nlohmann.
Just because you don’t know a preprocessor directive to include headers doesn’t mean it doesn’t exist.
Visual Studio knows the __has_include feature — I didn’t.
Never make your build depend on the target directory structure.
I hate these kinds of bugs.
Thanks a lot for reading!
Top comments (0)