Makefiles: incorrect/missing dependencies when using symbolic links
Symptom:
After the initial cmake generation and build of my code set, subsequent make invocations would continue to always rebuild what coincidentally happened to be the most time-consuming target, so I started looking into the issue.
Discussion/Recommendation: (Note: despite the wall of text that it took to find it, the actual issue appears pretty simple.)
Why do you even touch the .d
output of the compiler instead of directly transplanting the paths into what I assume is supposed to be some sort of cache or such? This leads to precisely these kinds of semantic mismatch issues, instead of doing whatever the compiler does, which instead of replicating the logic, should be left to the compiler anyway - since it's the one that generated the files in the first place, and knows better?
If it's possible that this is a recurring problem with the many uses of this path handling code in the codebase, perhaps another issue should be opened for tracking fixing this in other occurrences as well?
Diagnosis:
After difficulties finding any useful information in the make -d output, about what was forcing a rebuild of the target in question, I switched to using remake (https://github.com/rocky/remake) which is a make fork that has much better inspection capabilities.
The log output of remake -d --trace
contained:
CMakeFiles/main_chapter0_prog1.dir/build.make:89: update target 'CMakeFiles/main_chapter0_prog1.dir/<shortened>/4_prog1/ppp/prog1/chapter0/GUI/main.cpp.o' due to: /<shortened>/4_prog1/include/std_lib_facilities.h /<shortened>/4_prog1/include/_std_lib_facilities.h
This is a nonexistent path. The correct path is /<shortened>/4_prog1/ppp/include/std_lib_facilities.h
; note the ppp
which can be observed in other paths. The correct path is in this case accessed by the following code, which I was experimenting with:
// include hack https://gcc.gnu.org/onlinedocs/cpp/_005f_005fhas_005finclude.html
//TODO test both branches
#if defined __has_include
# if __has_include ("../../../include/std_lib_facilities.h")
// include "../../../include/std_lib_facilities.h"
# else
# include "../../../../include/std_lib_facilities.h"
# endif
#else
#error "__has_include not supported when including std_lib_facilities wrapper in >TODO<"
#endif
Note that the nature of this code is not actually relevant, this is just to show a bit better what I had set up here. Note that one of the preprocessor branches is commented out, so there is only one actual branch in use.
This code is supposed to import the header via the path: /<shortened>/4_prog1/ppp/prog1/chapter0/GUI/../../../../include/std_lib_facilities.h
.
Further investigation by grep shows that the incorrect header location only shows up in these files, and only after the original generation and build, when a second build is attempted:
$ grep -irl /<shortened>/4_prog1/include/std_lib_facilities.h
CMakeFiles/main_chapter0_prog1.dir/compiler_depend.make
CMakeFiles/main_chapter0_prog1.dir/compiler_depend.internal
After numerous false starts involving strace-ing to attempt to find what causes the incorrect lines to be written and what writes the compiler_depend files, and looking at gcc -MD/-MF/whatever invocations that generate obviously correct dynamic .d dependency files (which contain the correct include path) that are included by make, a friend unstuck me in the debug process and I figured out the rest of the issue:
It was a this point that I realized/it was pointed out to me that the faulty path had exactly the number of path components removed as there were ..
components, instead of interpreting symbolic links. At this point it can be assumed that the behaviour is caused by simply interpeting paths as strings instead of trying to resolve anything.
From previous strace output observation (PID write()s to relevant file or something) I found a variant of the command below, and one assumes it has something to do with the compiler_depend files. I don't remember how I found the execve line for it exactly, but in the knowledge that make must somehow invoke cmake, I found this command line via grep:
$ grep -irl cmake_depends
CMakeFiles/main_chapter0_prog1.dir/build.make
CMakeFiles/main_chapter0_prog1.dir/DependInfo.cmake
, where build.make
contains among other things, the following:
CMakeFiles/main_chapter0_prog1.dir/depend:
cd /<shortened>/4_prog1/ppp/infra/build && $(CMAKE_COMMAND) -E cmake_depends "Unix Makefiles" /<shortened>/4_prog1/ppp/infra /<shortened>/4_prog1/ppp/infra /<shortened>/4_prog1/ppp/infra/build /<shortened>/4_prog1/ppp/infra/build /<shortened>/4_prog1/ppp/infra/build/CMakeFiles/main_chapter0_prog1.dir/DependInfo.cmake --color=$(COLOR)
The cmake_depend 'command mode' appears to be undocumented beyond some StackOverflow questions.
On this command line we can also see a mention of the DependInfo.cmake
file, which also sounds relevant.
Looking at the DependInfo.cmake
file we find the following:
<omitted>
# The set of dependency files which are needed:
set(CMAKE_DEPENDS_DEPENDENCY_FILES
"/<shortened>/4_prog1/ppp/prog1/chapter0/GUI/GUI.cpp" "CMakeFiles/main_chapter0_prog1.dir/<shortened>/4_prog1/ppp/prog1/chapter0/GUI/GUI.cpp.o" "gcc" "CMakeFiles/main_chapter0_prog1.dir/<shortened>/4_prog1/ppp/prog1/chapter0/GUI/GUI.cpp.o.d"
<omitted> )
<omitted>
Searching Google for CMAKE_DEPENDS_DEPENDENCY_FILES
leads to https://github.com/Kitware/CMake/blob/master/Source/cmLocalUnixMakefileGenerator3.cxx , which looks relevant, so I started digging in here at this point.
Given the name, contents, and invocation, this is probably the code that either writes, or causes something else to write the faulty include path.
This is where I finally find what I'm looking for:
https://gitlab.kitware.com/cmake/cmake/-/blob/56dafdf19906bc19c5f90c0ea8ede02c3b44958b/Source/cmLocalUnixMakefileGenerator3.cxx#L1429
A few lines below mentions can be found of compiler_depend.make
and compiler_depend.internal
:
https://gitlab.kitware.com/cmake/cmake/-/blob/56dafdf19906bc19c5f90c0ea8ede02c3b44958b/Source/cmLocalUnixMakefileGenerator3.cxx#L1434
After inspecting this area of the function, and the called functions, it can be seen that the CheckDependencies
function is what actually creates the dependency entries to be written out later. The definition is located in https://gitlab.kitware.com/cmake/cmake/-/blob/56dafdf19906bc19c5f90c0ea8ede02c3b44958b/Source/cmDependsCompiler.cxx#L29
My compiler is gcc so either this, or the 'custom'
branch handle it; https://gitlab.kitware.com/cmake/cmake/-/blob/56dafdf19906bc19c5f90c0ea8ede02c3b44958b/Source/cmDependsCompiler.cxx#L139
Through the gcc .d
file handling code, this eventually invokes the cmSystemTools::CollapseFullPath
function; https://gitlab.kitware.com/cmake/cmake/-/blob/56dafdf19906bc19c5f90c0ea8ede02c3b44958b/Source/cmGccDepfileReader.cxx#L41
This is implemented in CollapseFullPathImpl
: https://gitlab.kitware.com/cmake/cmake/-/blob/56dafdf19906bc19c5f90c0ea8ede02c3b44958b/Source/kwsys/SystemTools.cxx#L3467 , which calls SystemToolsAppendComponents
; https://gitlab.kitware.com/cmake/cmake/-/blob/56dafdf19906bc19c5f90c0ea8ede02c3b44958b/Source/kwsys/SystemTools.cxx#L3442
This is the code that, as can be seen, indeed does the naive relative path compression, and is the root cause of this problem. (Well, I imagine. I haven't tried recompiling CMake with a dirty hack of a patch.)