12

I'm modeling dependencies with target_link_libraries, as is done in this blog post.

target_link_libraries(Foo
    LibraryA
    LibraryB
)

This is working great, but for various reasons I need to use add_custom_target to preprocess to a file through a custom command. The problem is, this custom target depends on the includes of LibraryA and LibraryB. I was really hoping to do the following like how target_link_libraries works (see the LibraryA and LibraryB bit):

add_custom_target(Bar ALL
    COMMAND ${CMAKE_C_COMPILER} thing.cpp LibraryA LibraryB /P
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/Path/Here
    COMMENT "Preprocessing to a file"
    VERBATIM
)

However, this doesn't work. LibraryA and LibraryB are put in as they appear. Even if it did work, I imagine I would get more than the includes, since I think the targets include the library as well. Maybe this is not a good approach.

So, what can I do here? How can I extract the include directories from each target, for use in the custom command? I found if I find_package(Foo REQUIRED) I get access to Foo_DIR, but that points to the build directory and not the source directory where the includes are.

Kevin
  • 16,549
  • 8
  • 60
  • 74
Stradigos
  • 814
  • 1
  • 13
  • 29
  • To be clear, is your goal to extract the include directories from the targets LibraryA and LibraryB, and use these directories in the custom command? – Kevin Nov 08 '19 at 04:00
  • @squareskittles Correct. I just need the headers from LibraryA and LibraryB. – Stradigos Nov 08 '19 at 04:34
  • How are you using them in the custom command? If `CMAKE_C_COMPILER` is `gcc` do you intend to pass these as include directories to the compiler? Using the `-I` flag? – Kevin Nov 08 '19 at 04:37
  • 1
    That's part of what I'm trying to find out. If I had a variable that could provide the path I could use the Include flag to get them into the custom command. If I can't get that because I'm modeling dependencies with targets, maybe there's something I can do. I just don't know what. – Stradigos Nov 08 '19 at 04:46

2 Answers2

17

You can extract the include directories from each target using get_target_property(). A target's INCLUDE_DIRECTORIES property contains the include directories for that target. Since you have two targets, LibraryA and LibraryB, we have to call it twice. Then, we can concatenate the list of include directories together using foreach(). If you are using these as include directories in a compiler command (such as MSVC), you can append the /I compiler option to each directory in the loop also:

# Get the include directories for the target.
get_target_property(LIBA_INCLUDES LibraryA INCLUDE_DIRECTORIES)
get_target_property(LIBB_INCLUDES LibraryB INCLUDE_DIRECTORIES)

# Construct the compiler string for the include directories.
foreach(dir ${LIBA_INCLUDES} ${LIBB_INCLUDES})
    string(APPEND INCLUDE_COMPILER_STRING "/I${dir} ")
endforeach()

Then, you can call the custom target command using the constructed INCLUDE_COMPILER_STRING variable:

add_custom_target(Bar ALL
    COMMAND ${CMAKE_C_COMPILER} thing.cpp ${INCLUDE_COMPILER_STRING} /P
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/Path/Here
    COMMENT "Preprocessing to a file"
    VERBATIM
)

If you wanted something more concise, you could use the generator expression example here, which gets the targets' include directories and expands them inline, within your custom target command. Something like this could work also:

add_custom_target(Bar ALL
    COMMAND ${CMAKE_C_COMPILER} thing.cpp 
        "/I$<JOIN:$<TARGET_PROPERTY:LibraryA,INCLUDE_DIRECTORIES>,;/I>"
        "/I$<JOIN:$<TARGET_PROPERTY:LibraryB,INCLUDE_DIRECTORIES>,;/I>"
        /P
    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/Path/Here
    COMMENT "Preprocessing to a file"
    VERBATIM
    COMMAND_EXPAND_LISTS
)
Kevin
  • 16,549
  • 8
  • 60
  • 74
  • I'm getting `LIBA_INCLUDES-NOTFOUND` in the above example, and I was only able to get that by using `find_package(LibraryA REQUIRED)`. If I don't use `find_package()` then it can't find the target which results in a CMake error. I'm using a LibraryAConfig.cmake.in file where I call `find_dependency(LibraryA REQUIRED)`. I tried setting the variable in there and printing it from the CMakeLists but the variable either wasn't set or didn't propagate because the test message I had was blank. – Stradigos Nov 08 '19 at 15:20
  • I had a bit more luck with the concise version, but the problem is using the target brings in multiple include directories and they are put inside a single set of quotations. Naturally, the compiler is treating that as one large include path instead of three individual paths. I've experimented with just trying to put the quotes around the JOIN command, but the JOIN result becomes garbled. – Stradigos Nov 08 '19 at 15:20
  • Fixed it! Your line should be `"/I$,;/I>"` The semi-colon is very important. I also added `COMMAND_EXPAND_LISTS`. Please update your answer! Thank you for the help, this is great! – Stradigos Nov 08 '19 at 15:28
  • 1
    @Stradigos I was not aware you were using `find_package()` for those libraries, so yes, `get_target_property()` may not work as expected in that case. And apologies! I forgot to add the `COMMAND_EXPAND_LISTS` list portion to that last example. I updated the answer based on your feedback. Glad it's working! – Kevin Nov 08 '19 at 16:43
  • 2
    This does not handle transitive dependencies. – Lars Bilke Dec 09 '21 at 13:53
2

As the comment, the current accepted answer does not handle transitive dependencies. And this question has been confusing me all day, so I'll sort it out now.

I'm in build the LibraryLinkUtilities here. This is my CMakeLists.txt used in project:

cmake_minimum_required(VERSION 3.15.0)

project ("CMakeProject1")

set(LLU_ROOT "D:/test/LibraryLinkUtilities/install")
set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE)

find_package(LLU NO_MODULE PATH_SUFFIXES LLU)

add_library(${PROJECT_NAME} SHARED ${PROJECT_NAME}.cpp)

target_link_libraries(${PROJECT_NAME} PRIVATE LLU::LLU)

When I open the .sln with Visual Studio, It work well, I mean I can build it in any build type. But I find the include directories is empty in Configuation. This make me crazy, because I want to know the project have include which directory exactly. Then I use the function print_target_properties fixed here to print all properties about imported target:

function(print_target_properties target)
    if(NOT TARGET ${target})
      message(STATUS "There is no target named '${target}'")
      return()
    endif()

    foreach(property ${CMAKE_PROPERTY_LIST})
        string(REPLACE "<CONFIG>" "DEBUG" property ${property})

        get_property(was_set TARGET ${target} PROPERTY ${property} SET)
        if(was_set)
            get_target_property(value ${target} ${property})
            message("${target} ${property} = ${value}")
        endif()
    endforeach()
endfunction()

print_target_properties(LLU::LLU)

enter image description here

Note the red line place, the LLU::LLU dependent with WSTP::WSTP and WolframLibrary::WolframLibrary. So I use this code to print all include directories:

include(CMakePrintHelpers)
get_target_property(LLUDEPENDS LLU::LLU INTERFACE_LINK_LIBRARIES)
cmake_print_properties(TARGETS LLU::LLU ${LLUDEPENDS} PROPERTIES INTERFACE_INCLUDE_DIRECTORIES)

enter image description here

yode
  • 483
  • 7
  • 16