CMake: link order of imported targets incorrect - cmake

I have the following scenario:
I import two prebuilt libraries into my project (libA, libB)
libB has a dependency on libA
The executable depends on both libA and libB
However, the relative linking order in my link.txt is incorrect
/usr/bin/c++ CMakeFiles/bin.dir/main.cpp.o -o bin ../libA.a ../libB.a
I would expect libA.a to be listed after libB.a.
The CMakeLists.txt looks something along the following lines
cmake_minimum_required(VERSION 3.13)
project(cmake_test)
set(lib_dir ${CMAKE_CURRENT_SOURCE_DIR})
add_library(MY::libA IMPORTED INTERFACE)
set_target_properties(MY::libA PROPERTIES INTERFACE_LINK_LIBRARIES "${lib_dir}/libA.a")
add_library(MY::libB IMPORTED INTERFACE)
set_target_properties(MY::libB PROPERTIES INTERFACE_LINK_LIBRARIES "MY::libA;${lib_dir}/libB.a")
add_executable(bin ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
target_link_libraries(bin PUBLIC MY::libB MY::libA)
Below a description of my attempts to solve the problem. Some without success and some with sucecss but using modifications that render the code usless for the production environment.
Successful attempts:
Remove the depedency of bin on libA (i.e. replace the last line by target_link_libraries(bin PUBLIC MY::libB). This works but I cannot remove the dependency in real code.
Replace the target type IMPORTED INTERFACE by IMPORTED STATIC. Use IMPORTED_LOCATION instead of INTERFACE_LINK_LIBRARIES and use target_link_libraries to express the dependency of libB on libA. In this case the link.txt yields: [...] -o bin ../libA.a ../libB.a ../libA.a. As soon as I revert the target type for libB the link order breaks down again. In the production environment, however, one of the targets is created by conan as IMPORTED INTERFACE.
Attempts without success (same behaviour as described):
Create a separate IMPORTED target (use IMPORTED_LOCATION) for every lib and group them inside an INTERFACE target
Sprinkle the code with ADD_DEPENDENCIES
Remove libA from the INTERFACE_LINK_LIBRARIES in line 9 and use target_link_libraries(MY::libB INTERFACE MY::libA) instead. Same result.
Example code that shows the same failure using INTERFACES as a building block
cmake_minimum_required(VERSION 3.13)
project(cmake_test)
set(lib_dir ${CMAKE_CURRENT_SOURCE_DIR})
# libA
add_library(MY::libA_file1 IMPORTED STATIC)
set_target_properties(MY::libA_file1 PROPERTIES IMPORTED_LOCATION "${lib_dir}/libA.a")
add_library(libA INTERFACE)
target_link_libraries(libA INTERFACE MY::libA_file1)
# libB
add_library(MY::libB_file1 IMPORTED STATIC)
set_target_properties(MY::libB_file1 PROPERTIES IMPORTED_LOCATION "${lib_dir}/libB.a")
add_library(libB INTERFACE)
target_link_libraries(libB INTERFACE MY::libB_file1 libA)
add_executable(bin ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
target_link_libraries(bin PUBLIC libA libB)

You incorrectly think about INTERFACE_LINK_LIBRARIES property as a "content" of the library's target, which is ordered by target_link_libraries call.
Using
target_link_libraries(MY::libB INTERFACE MY::libA)
you setup link dependency between library targets MY::libB and MY::libA. That is, "content" of MY::libB target should come before "content" of MY::libA target in the linking command line.
But INTERFACE_LINK_LIBRARIES property is NOT a "content" of the library target! It is just an additional link dependency.
As opposite, IMPORTED_LOCATION (for non-INTERFACE IMPORTED target) is a "content" of the library, and target_link_libraries affects on its ordering.
It seems that you cannot add link dependency for a library, using INTERFACE library target. You should use IMPORTED library target for that purpose:
# Collect libraries related to 'libA'
file(GLOB libs_A "${lib_dir}/libA*.a")
# For each library create IMPORTED target with IMPORTED_LOCATION property.
set(libs_A_targets)
foreach(lib_A ${libs_A})
# Form a unique name for the IMPORTED target: subtarget_A_*
string(REGEX REPLACE "^${lib_dir}/libA([^.]*).a$" "subtarget_A_\\1" lib_A_target ${lib_A})
# Create a target with this name
add_library(${lib_A_target} STATIC IMPORTED)
set_target_properties(${lib_A_target} PROPERTIES IMPORTED_LOCATION ${lib_A})
# And add the target into the list
list(APPEND libs_A_targets ${lib_A_target})
endforeach()
# In a similar way collect libraries for libB.
set(lib_B_targets ...)
# Now link each libB* library with each libA*.
foreach(lib_B_target ${libs_B_targets})
target_link_libraries(${lib_B_target} INTERFACE ${libs_A_targets})
endforeach()
# Now interface libraries, which combine libA* and libB*, can be created
add_library(libA INTERFACE)
target_link_libraries(libA INTERFACE ${libs_A_targets})
add_library(libB INTERFACE)
target_link_libraries(libB INTERFACE ${libs_B_targets})
# Now these INTERFACE libraries can be linked into an executable in any order
add_executable(bin ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp)
target_link_libraries(bin PUBLIC libA libB)

Related

INCLUDE_DIRECTORIES property of dependent target is not populated in CMake

I've read in few places, this is one of them, that when using the meaning of the PUBLIC, PRIVATE and INTERFACE keywords in the context of commands such as target_include_directories is as follows:
PRIVATE - directories after this keyword will be added to the INCLUDE_DIRECTORIES property of the target specified.
INTERFACE - directories after this keyword will be added to the INTERFACE_INCLUDE_DIRECTORIES property of the target specified.
PUBLIC - directories after this keyword will be added to both.
Directories that will be added to the INTERFACE_INCLUDE_DIRECTORIES will be added to the INCLUDE_DIRECTORIES of any target dependent on the current target.
OK I run the following experiment:
// libmymath/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
add_library(MyMath src/mymath.cpp)
target_include_directories(MyMath PUBLIC include)
get_target_property(MYMATH_INC_DIR MyMath INCLUDE_DIRECTORIES)
get_target_property(MYMATH_INC_DIR_INTERFACE MyMath INTERFACE_INCLUDE_DIRECTORIES)
message("Libmath INCLUDE_DIRECTORIES: ${MYMATH_INC_DIR}")
message("Libmath INCLUDE_DIRECTORIES INTERFACE: ${MYMATH_INC_DIR_INTERFACE}")
calculator/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
add_executable(calculator calculator.cpp)
target_link_libraries(calculator MyMath)
get_target_property(CALCULATOR_INC_DIR calculator INCLUDE_DIRECTORIES)
get_target_property(CALCULATOR_INC_DIR_INTERFACE calculator INTERFACE_INCLUDE_DIRECTORIES)
message("Calculator INCLUDE_DIRECTORIES: ${CALCULATOR_INC_DIR}")
message("Calculator INCLUDE_DIRECTORIES INTERFACE: ${CALCULATOR_INC_DIR_INTERFACE}")
// top-level CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(Math)
add_subdirectory(libmath)
add_subdirectory(calculator)
get_target_property(CALC_INC_DIR calculator INCLUDE_DIRECTORIES)
get_target_property(CALC_INC_DIR_INTERFACE calculator INTERFACE_INCLUDE_DIRECTORIES)
message("From top-level CMakeLists.txt: Calculator INCLUDE_DIRECTORIES: ${CALC_INC_DIR}")
message("From top-level CMakeLists.txt: Calculator INTERFACE_INCLUDE_DIRECTORIES: ${CALC_INC_DIR_INTERFACE}")
Running this gives:
Libmath INCLUDE_DIRECTORIES: /home/yoav/playground/cmake/math/libmath/include
Libmath INCLUDE_DIRECTORIES INTERFACE: /home/yoav/playground/cmake/math/libmath/include
Calculator INCLUDE_DIRECTORIES: CALCULATOR_INC_DIR-NOTFOUND
Calculator INCLUDE_DIRECTORIES INTERFACE: CALCULATOR_INC_DIR_INTERFACE-NOTFOUND
From top-level CMakeLists.txt: Calculator INCLUDE_DIRECTORIES: CALC_INC_DIR-NOTFOUND
From top-level CMakeLists.txt: Calculator INTERFACE_INCLUDE_DIRECTORIES: CALC_INC_DIR_INTERFACE-NOTFOUND
So we see that the INCLUDE_DIRECTORIES property of calculator is not populated, although I can see in the flags.make file of this target that the CXX_INCLUDE variable is set properly !
What gives?
Content of target property INTERFACE_INCLUDE_DIRECTORIES is actually transferred into property INCLUDE_DIRECTORIES of every target, which consumes (with target_link_libraries command) given target.
But CMake defers all transfers to a consuming target until the generation phase, which occurs after whole CMakeLists.txt has been processed. This is why one cannot obtain final value of the property using get_target_property command: this command can only obtain current value of the property, before the transferring.
However, target properties referred by the generator expressions are expanded to the final value. (But again, this expansion is observable only at the generation phase and after it).
One way to see the expanded value of a generator expression is file(GENERATE): this command will create a file, which contains expanded generator expressions.
Example:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project (hello)
add_library(foo SHARED foo.c)
target_include_directories(foo PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_library(bar SHARED bar.c)
target_include_directories(bar PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include1)
target_link_libraries(bar PUBLIC foo)
get_target_property(bar_include_directories bar INCLUDE_DIRECTORIES)
message(STATUS "bar include directories: ${bar_include_directories}")
file(GENERATE
OUTPUT bar_include_directories.txt
CONTENT "include directories: $<TARGET_PROPERTY:bar,INCLUDE_DIRECTORIES>\n"
)
When configure this CMake project, only current value of the property will be printed:
build$ cmake ..
-- bar include directories: /home/tester/tests/cmake/include1
-- Configuring done
-- Generating done
-- Build files have been written to: /home/tester/tests/cmake/build
But the file bar_include_directories.txt will contain final value of the property:
$ cat bar_include_directories.txt
include directories: /home/tester/tests/cmake/include1;/home/tester/tests/cmake
Directories that will be added to the INTERFACE_INCLUDE_DIRECTORIES will be added to the INCLUDE_DIRECTORIES of any target dependent on the current target.
I believe this is a simple way to explain it that way, but indeed this is not something that can be observed inside cmake scripts, but it's rather "generated" when cmake generates the build system files.
target_link_libraries populates LINK_LIBRARIES and INTERFACE_LINK_LIBRARIES properties of a target. Then like after cmake has run all scripts and is generating build files, it goes through LINK_LIBRARIES of a target and takes INTERFACE_INCLUDE_DIRECTORIES of dependent targets and generates build system files that include these directories for that target.

How to set COMPILE_DEFINITIONS through cmake command line

I am building a couple of 3rd party libraries for integration.
There are two different source codes for two libs, libA and libB.
The libA has following, in its CMakeLists.txt
project(libA)
...
add_library(${PROJECT} ${SRCS})
target_link_library(${PROJECT} DOESNOTMATTER)
target_compile_definitions(${PROJECT_NAME} PUBLIC LETS_USE_GORILLA)
libA compilation is successful separately as shared lib.
libB is dependent on libA.
While compiling libB, it is expected that public compile definition of libA are automatically taken care by cmake to be available in libB. But they are not.
Now, if I add below target_compile_definitions in CMakeLists.txt of libB, it compiles successfully.
project(libB)
...
add_library(${PROJECT} ${SRCS})
target_link_library(${PROJECT} libA)
target_compile_definitions(${PROJECT_NAME} PUBLIC LETS_USE_GORILLA)
But, as this is a 3rd party code, I am not allowed to change the CMakeLists.txt.
Question - How can I pass some arguments in cmake command line to change the COMPILE_DEFINITIONS

Cannot specify compile options for imported target "..."

I want to provide the users of my library with two targets: one that specifies the include path etc., and one that carries useful extra compile options. However, for the extra target some of my users are getting the error
Cannot specify compile options for imported target "myproject::extra"
so it seems on older CMake versions.
I tested with CMake 3.9.2. The test project, including CI is on GitHub, with failing build here.
(How) can my approach be rendered robust for all CMake versions?
The project's main CMakeLists.txt:
cmake_minimum_required(VERSION 3.0)
project(myproject)
add_library(myproject INTERFACE)
set(MYPROJECT_VERSION "1.0.0")
target_include_directories(myproject INTERFACE
$<INSTALL_INTERFACE:include>
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>)
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/" DESTINATION include)
install(TARGETS myproject EXPORT myproject-targets)
install(EXPORT myproject-targets FILE myprojectTargets.cmake DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/myproject")
write_basic_package_version_file("${CMAKE_CURRENT_BINARY_DIR}/myprojectConfigVersion.cmake" VERSION ${MYPROJECT_VERSION} COMPATIBILITY AnyNewerVersion)
install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/myprojectConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/myprojectConfigVersion.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/myproject")
The project's myprojectConfig.cmake:
include(CMakeFindDependencyMacro)
if(NOT TARGET myproject)
include("${CMAKE_CURRENT_LIST_DIR}/myprojectTargets.cmake")
endif()
if(NOT TARGET myproject::extra)
add_library(myproject::extra INTERFACE IMPORTED)
if(MSVC)
target_compile_options(myproject::extra INTERFACE /W4)
else()
target_compile_options(myproject::extra INTERFACE -Wall)
endif()
endif()
The user's project CMakeLists.txt could then look as follows:
cmake_minimum_required(VERSION 3.0)
project(myexec)
find_package(myproject REQUIRED)
add_executable(myexec main.cpp)
target_link_libraries(myexec PRIVATE myproject myproject::extra)
List of functions applicable for IMPORTED and INTERFACE targets changes as CMake evolves.
Most of such functions affects only on specific target properties. So, instead of calling a function, you may set the property directly. This will work in any CMake version:
# Works only in new CMake versions
target_compile_options(myproject::extra INTERFACE /W4)
# Equivalent which works in any CMake version
set_property(TARGET myproject::extra PROPERTY INTERFACE_COMPILE_OPTIONS /W4)

What to do with the output of ExternalProject_add? IMPORTED vs INTERFACE libraries

This is my topmost CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(test LANGUAGES Fortran)
add_executable(main main.f90)
add_subdirectory(external)
target_link_libraries(main extlib)
Subdirectory "external" defines one external project, which produces some libraries and header files. What is the best practice of collecting the output of ExternalProject_add so that I could link that to the main executable? Currently, I'm using an INTERFACE library, like this:
include(ExternalProject)
ExternalProject_add(my-external
SOURCE_DIR ext_source
CONFIGURE_COMMAND
${CMAKE_CURRENT_LIST_DIR}/configure
--prefix=${CMAKE_CURRENT_BINARY_DIR}
BUILD_COMMAND make)
add_library(extlib INTERFACE)
add_dependencies(extlib my-external)
target_include_directories(extlib INTERFACE ${CMAKE_CURRENT_BINARY_DIR}/include)
foreach(_lib lib1 lib2 lib3)
target_link_libraries(extlib INTERFACE
${CMAKE_CURRENT_BINARY_DIR}/lib/${_lib}.a)
endforeach()
Searching the web, it seems to me that people often use IMPORTED libraries instead for capturing the results of ExternalProject_add. However, you can only connect one IMPORTED library with one file produced by ExternalProject_add and any header directories would need to be separately propagated back to the parent directory so that main could use them. It seems to me than an INTERFACE library is better here because you can glob everything produced by the external project into a single target. In my actual project I have several external projects and ideally I would like to have one target per external project to keep things clean.
In general, should I use IMPORTED or INTERFACE libraries when dealing with external libraries and what would be the main benefits?
EDIT
Based on the dicussion below, I made an attempt using imported libraries only:
foreach(_lib lib1 lib2 lib3)
add_library(${_lib} STATIC IMPORTED GLOBAL)
add_dependencies(${_lib} my-external)
set_target_properties(${_lib} PROPERTIES
IMPORTED_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/lib/${_lib}.a)
endforeach()
set_target_properties(lib1 PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR}/include)
target_link_libraries(lib1 INTERFACE lib2 lib3)
It takes about the same effort as with an interface library and I guess is ultimately a matter of style.

CMake Include dependencies of imported library without linking

I am using CMake 3.5.2.
Consider the following situation. I have an imported library Foo::Foo:
add_library(Foo::Foo UNKNOWN IMPORTED)
This imported library has been populated with appropriate properties:
set_target_properties(Foo::Foo PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "/path/to/include/blah" "/another/path/include/other"
IMPORTED_LINK_INTERFACE_LIBRARIES blah other
IMPORTED_LOCATION "/path/to/libfoo.a-or-so")
I have a convenience library called bar. I need it to include Foo::Foo's include directories, but I do not want it to link against Foo::Foo.
add_library(bar STATIC "${BAR_SOURCES}")
How can I add just the include dependencies from Foo::Foo? Here is what I have tried that has failed:
# This did not include any includes from Foo::Foo
target_link_libraries(bar INTERFACE Foo::Foo)
# This included only the first include directory from Foo::Foo
target_include_directories(bar PUBLIC "$<TARGET_PROPERTY:Foo::Foo,INTERFACE_INCLUDE_DIRECTORIES>")
I have given you example a try. You should change the code in your example to:
set_target_properties(
Foo::Foo PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES
"/path/to/include/blah;/path/to/include/other"
IMPORTED_LINK_INTERFACE_LIBRARIES "blah.a"
IMPORTED_LOCATION "/path/to/libfoo.a-or-so"
)
The call to set_target_properties() only accepts "property" / "value" pairs (with spaces as delimiter). And your example just wasn't throwing any errors because you can always define your own properties (with any naming).
Please transfer your include directory list into a "CMake List" (string with semicolon separated elements).
Alternative
If you just want to "reset" the transitive libraries you can do this with e.g.:
target_link_libraries(bar Foo::Foo)
set_target_properties(bar PROPERTIES INTERFACE_LINK_LIBRARIES "")
I've used this approach when I was building a shared library in the same project as I was linking against the same (and I did not want the library dependencies of the shared library also being linked into the target using the shared library).
References
target_link_libraries()