Transitive dependencies for include directories break

dump_target_includes is a macro for testing

macro(dump_target_includes TARGET_NAME)
    set(OUT_TMP "====dump_target_includes begin (${TARGET_NAME})====")

    set(INCLUDE_DIRS $<TARGET_PROPERTY:${TARGET_NAME},INCLUDE_DIRECTORIES>)
    set(INTERFACE_INCLUDE_DIRS $<TARGET_PROPERTY:${TARGET_NAME},INTERFACE_INCLUDE_DIRECTORIES>)

    string(CONCAT OUT_TMP ${OUT_TMP} "\n\n----INCLUDE_DIRECTORIES----")
    foreach(PATH ${INCLUDE_DIRS})
        string(CONCAT OUT_TMP ${OUT_TMP} "\n${PATH}")
    endforeach()

    string(CONCAT OUT_TMP ${OUT_TMP} "\n\n----INTERFACE_INCLUDE_DIRECTORIES----")
    foreach(PATH ${INTERFACE_INCLUDE_DIRS})
        string(CONCAT OUT_TMP ${OUT_TMP} "\n${PATH}")
    endforeach()

    string(CONCAT OUT_TMP ${OUT_TMP} "\n\n====dump_target_includes end (${TARGET_NAME})====")

    file(GENERATE OUTPUT test_debug_genex.log
                  CONTENT ${OUT_TMP})
endmacro() 

lib a

installed as a standalone lib

lib b

lib b depends on lib a, also installed as a standalone lib

target_link_libraries(b PUBLIC a)
dump_target_includes(b)

The dump shows that INTERFACE_INCLUDE_DIRECTORIES of a is in INCLUDE_DIRECTORIES and INTERFACE_INCLUDE_DIRECTORIES of b, no problem

app c

app c depends on lib b

find_package(b REQUIRED)
dump_target_includes(b)

The dump shows that INTERFACE_INCLUDE_DIRECTORIES of a is NOT in INCLUDE_DIRECTORIES and INTERFACE_INCLUDE_DIRECTORIES of b, and app c failed to build for it can not find header files in a.

Why do transitive dependencies from a to c break?

BTW I have used genex $<BUILD_INTERFACE:...> and $<INSTALL_INTERFACE:...> around.

They don’t break; you just can’t ask for them during the configure stage. What do you want this information for?

Edit: See also this question:

No, I have used file(GENERATE ...), it can get genex values in generation stage.

OK, that wasn’t clear. The thing is that b has a link dependency on a. When CMake generates the full tree for use in a compilation line, that is when the full dependency tree is traversed and expanded. CMake cannot, in general, compute this full tree on-demand because there are ways of constructing them that depend on the target being linked. For example, this will make the include directories depend on whether an executable or library is consuming it:

target_include_directories(foo
  INTERFACE
    "$<$<STREQUAL:$<TARGET_PROPERTY,TYPE>,EXECUTABLE>:executable/only/include>")

There’s an issue about getting the transitive dependency closure of a target, but I can’t find it right now.

The CMakeLists.txt below for app c works as expected,
i.e. INTERFACE_INCLUDE_DIRECTORIES of a is in INCLUDE_DIRECTORIES and INTERFACE_INCLUDE_DIRECTORIES of b

find_package(a REQUIRED)
find_package(b REQUIRED)
dump_target_includes(b)

for comparison, the CMakeLists.txt below for app c doesn’t work as expected,
i.e. INTERFACE_INCLUDE_DIRECTORIES of a is not in INCLUDE_DIRECTORIES or INTERFACE_INCLUDE_DIRECTORIES of b

find_package(b REQUIRED)
dump_target_includes(b)

BTW, the revised version of dump_target_includes

macro(dump_target_includes TARGET_NAME)
    set(OUT_TMP "====dump_target_includes begin (${TARGET_NAME})====\n\n")

    set(INCLUDE_DIRS $<TARGET_PROPERTY:${TARGET_NAME},INCLUDE_DIRECTORIES>)
    set(INTERFACE_INCLUDE_DIRS $<TARGET_PROPERTY:${TARGET_NAME},INTERFACE_INCLUDE_DIRECTORIES>)

    string(CONCAT OUT_TMP ${OUT_TMP} "----INCLUDE_DIRECTORIES----\n")

    string(CONCAT OUT_TMP ${OUT_TMP} $<JOIN:$<TARGET_PROPERTY:${TARGET_NAME},INCLUDE_DIRECTORIES>,\n>\n\n)

    string(CONCAT OUT_TMP ${OUT_TMP} "----INTERFACE_INCLUDE_DIRECTORIES----\n")

    string(CONCAT OUT_TMP ${OUT_TMP} $<JOIN:$<TARGET_PROPERTY:${TARGET_NAME},INTERFACE_INCLUDE_DIRECTORIES>,\n>\n\n)

    string(CONCAT OUT_TMP ${OUT_TMP} "====dump_target_includes end (${TARGET_NAME})====")

    file(GENERATE OUTPUT test_debug_genex.log
                  CONTENT ${OUT_TMP})
endmacro()

No, that’s because the include directories are added after analyzing the interface link libraries of b. It sees a in that list and then traverses it. You just cannot get the full transitive closure using genexes today (and it’s unlikely to be implemented).