How to export CMake targets with dependant include directories? - cmake

I am trying to export CMake targets such that they also include the public include directories of dependant targets.
Let there be three separate but depending CMake projects:
foobar_result defines a data type and methods for this datatype in result.hpp
foobar_interface defines a header-only abstract class in foobar_interface.hpp which uses the datatype defined in foobar_result (#include <result.hpp>)
foobar_implA is an implementation (foobar_implA.cpp, foobar_implA.hpp) of the abstract class in foobar_interface (#include <foobar_interface.hpp>), and hence indirectly depends on the datatype definition (it #include <result.hpp> via #include <foobar_interface.hpp>)
What I want to achieve is that when foobar_implA is find_package(foobar_interface), it should automatically populate the include directories and libraries with the dependencies of foobar_interface so that foobar_implA does not have to discover foobar_result's include directories manually.
So far, I tried to specify the three CMake projects as follows.
1. foobar_result
foobar_result defines a standard library with headers:
cmake_minimum_required(VERSION 3.5)
project(foobar_result)
include(GNUInstallDirs)
add_library(result SHARED src/result.cpp)
target_include_directories(result PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/${CMAKE_INSTALL_INCLUDEDIR}>
)
install(EXPORT ${PROJECT_NAME}
DESTINATION share/${PROJECT_NAME}/cmake
FILE ${PROJECT_NAME}Config.cmake
)
install(TARGETS result EXPORT ${PROJECT_NAME})
install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.hpp"
)
The exported foobar_resultConfig.cmake:
add_library(result SHARED IMPORTED)
set_target_properties(result PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
)
and foobar_resultConfig-noconfig.cmake:
set_target_properties(result PROPERTIES
IMPORTED_LOCATION_NOCONFIG "${_IMPORT_PREFIX}/lib/libresult.so"
IMPORTED_SONAME_NOCONFIG "libresult.so"
)
correctly define the includes and libraries.
2. foobar_interface
The interface uses the result library:
cmake_minimum_required(VERSION 3.5)
project(foobar_interface)
include(GNUInstallDirs)
find_package(foobar_result REQUIRED)
add_library(foobar_interface INTERFACE)
target_link_libraries(foobar_interface INTERFACE result)
target_include_directories(${PROJECT_NAME} INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/${CMAKE_INSTALL_INCLUDEDIR}>
)
install(EXPORT ${PROJECT_NAME}
DESTINATION share/${PROJECT_NAME}/cmake
FILE ${PROJECT_NAME}Config.cmake
)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.hpp"
)
install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME})
Now, although the interface foobar_interface links the result library, the exported configuration:
add_library(foobar_interface INTERFACE IMPORTED)
set_target_properties(foobar_interface PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
INTERFACE_LINK_LIBRARIES "result"
)
only depends on the result library, but not its include directories. foobar_interface only exports its own INTERFACE_INCLUDE_DIRECTORIES, but not those of its dependency foobar_result.
3. foobar_implA
The implementation of the interface depends only on foobar_implA:
cmake_minimum_required(VERSION 3.5)
project(foobar_implA)
include(GNUInstallDirs)
find_package(foobar_interface REQUIRED)
add_library(foobar_implA SHARED src/foobar_implA.cpp)
target_link_libraries(foobar_implA PUBLIC foobar_interface)
target_include_directories(foobar_implA PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:$<INSTALL_PREFIX>/${CMAKE_INSTALL_INCLUDEDIR}>
)
install(EXPORT ${PROJECT_NAME}
DESTINATION share/${PROJECT_NAME}/cmake
FILE ${PROJECT_NAME}Config.cmake
)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
FILES_MATCHING PATTERN "*.hpp"
)
install(TARGETS ${PROJECT_NAME} EXPORT ${PROJECT_NAME})
fails with:
In file included from [...]/src/foobar_implA/include/foobar_implA.hpp:3,
from [...]/src/foobar_implA/src/foobar_implA.cpp:1:
[...]/install/foobar_interface/include/foobar_interface.hpp:3:10: fatal error: result.hpp: No such file or directory
3 | #include <result.hpp>
| ^~~~~~~~~~~~
because find_package(foobar_interface) will not populate the include directories with the one defined in foobar_result.
I can only work around this issue by manually including foobar_result again:
find_package(foobar_result REQUIRED)
target_link_libraries(foobar_implA PUBLIC result)
but this is obviously not what I want, as this would not scale well when the dependencies in foobar_interface increase.
I am using colcon to separate the build and install spaces for all three projects. This way, all projects have to properly export and import their include and link directories. Installing all projects in the same prefix would also circumvent this problem, as foobar_interface include directories would also contain headers from foobar_result.

Related

cmake and deb dependencies [duplicate]

Now I have a lib I made my self that I want to use in another CMake c++ project. It exists in my computer like this.
${MY_LIB_PATH}\include
${MY_LIB_PATH}\lib\x86\debug\lib-files
${MY_LIB_PATH}\lib\x86\release\lib-files
${MY_LIB_PATH}\lib\x64\debug\lib-files
${MY_LIB_PATH}\lib\x64\release\lib-files
What would a basic config file be like which makes CMake find_package know those? I expected it would be very simple because it just doesn't have much information to provide. But this page just make my head hurt.
Sorry, I decided to copy the source code around so I don't really know which answer should be accepted.
Don't write a config yourself; use CMake's export command. It's too broad to cover here in its entirety, but here's a modified example from one of my projects:
install(TARGETS
your_target
EXPORT YourPackageConfig
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
export(TARGETS
your_target
NAMESPACE YourPackage::
FILE "${CMAKE_CURRENT_BINARY_DIR}/YourPackageConfig.cmake"
)
install(EXPORT
YourPackageConfig
DESTINATION "${CMAKE_INSTALL_DATADIR}/YourPackage/cmake"
NAMESPACE YourPackage::
)
This will create the config file for you, so other projects can use it via find_package.
find_package(YourPackage REQUIRED)
target_link_libraries(foo YouprPackage::your_target)
This handles the IMPORTED targets automatically, and also lets you embed compiler flags, include paths, library dependencies, and even which files are part of your interface (basically, anything that falls under the INTERFACE properties).
Put a "${libname}-config.cmake" in library's root.
Then add an IMPORTED target in that file.
There is a example for libprotobuf.
add_library(libprotobuf STATIC IMPORTED GLOBAL)
set_target_properties(libprotobuf PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/prebuilt/android/${ANDROID_ABI}/libprotobuf.a"
IMPORTED_LINK_INTERFACE_LIBRARIES "${ZLIB_LIBRARIES};${CMAKE_THREAD_LIBS_INIT}"
INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/src")
Set Env or CMake variable "${libname}_DIR" to "${MY_LIB_PATH}"
Use it.
find_package(${libname})
#.......
target_link_libraries(main ${libname})
Maybe this older doc could be a tad more lightweight. And there is also this tutorial or this other one. The last one is perhaps the simplest.
Hope to not have added more pain :-)
Following the docs should give something roughly like the following (supposing your library is mylib):
MyLib/MyLibConfig.cmake.in
# - Config file for the MyLib package
# It defines the following variables
# MYLIB_INCLUDE_DIRS - include directories for MyLib
# MYLIB_LIBRARIES - libraries to link against
# MYLIB_EXECUTABLE - the bar executable
# Compute paths
get_filename_component(MYLIB_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
set(MYLIB_INCLUDE_DIRS "#CONF_INCLUDE_DIRS#")
# Our library dependencies (contains definitions for IMPORTED targets)
if(NOT TARGET mylib AND NOT MyLib_BINARY_DIR)
include("${MYLIB_CMAKE_DIR}/MyLibTargets.cmake")
endif()
# These are IMPORTED targets created by MyLibTargets.cmake
set(MYLIB_LIBRARIES mylib)
MyLib/MyLibConfigVersion.cmake.in
set(PACKAGE_VERSION "#MYLIB_VERSION#")
# Check whether the requested PACKAGE_FIND_VERSION is compatible
if("${PACKAGE_VERSION}" VERSION_LESS "${PACKAGE_FIND_VERSION}")
set(PACKAGE_VERSION_COMPATIBLE FALSE)
else()
set(PACKAGE_VERSION_COMPATIBLE TRUE)
if ("${PACKAGE_VERSION}" VERSION_EQUAL "${PACKAGE_FIND_VERSION}")
set(PACKAGE_VERSION_EXACT TRUE)
endif()
endif()
main MyLib/CMakeLists.txt
...
set(MYLIB_MAJOR_VERSION 0)
set(MYLIB_MINOR_VERSION 1)
set(MYLIB_PATCH_VERSION 0)
set(MYLIB_VERSION
${MYLIB_MAJOR_VERSION}.${MYLIB_MINOR_VERSION}.${MYLIB_PATCH_VERSION})
...
add_library(mylib SHARED mylib.c ...)
...
install(TARGETS mylib
# IMPORTANT: Add the mylib library to the "export-set"
EXPORT MyLibTargets
RUNTIME DESTINATION "${INSTALL_BIN_DIR}" COMPONENT bin
LIBRARY DESTINATION "${INSTALL_LIB_DIR}" COMPONENT shlib
PUBLIC_HEADER DESTINATION "${INSTALL_INCLUDE_DIR}/mylib"
COMPONENT dev)
...
# The interesting stuff goes here
# ===============================
# Add all targets to the build-tree export set
export(TARGETS mylib
FILE "${PROJECT_BINARY_DIR}/MyLibTargets.cmake")
# Export the package for use from the build-tree
# (this registers the build-tree with a global CMake-registry)
export(PACKAGE MyLib)
# Create the MyLibConfig.cmake and MyLibConfigVersion files
file(RELATIVE_PATH REL_INCLUDE_DIR "${INSTALL_CMAKE_DIR}"
"${INSTALL_INCLUDE_DIR}")
# ... for the build tree
set(CONF_INCLUDE_DIRS "${PROJECT_SOURCE_DIR}" "${PROJECT_BINARY_DIR}")
configure_file(MyLibConfig.cmake.in
"${PROJECT_BINARY_DIR}/MyLibConfig.cmake" #ONLY)
# ... for the install tree
set(CONF_INCLUDE_DIRS "\${MYLIB_CMAKE_DIR}/${REL_INCLUDE_DIR}")
configure_file(MyLibConfig.cmake.in
"${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/MyLibConfig.cmake" #ONLY)
# ... for both
configure_file(MyLibConfigVersion.cmake.in
"${PROJECT_BINARY_DIR}/MyLibConfigVersion.cmake" #ONLY)
# Install the MyLibConfig.cmake and MyLibConfigVersion.cmake
install(FILES
"${PROJECT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/MyLibConfig.cmake"
"${PROJECT_BINARY_DIR}/MyLibConfigVersion.cmake"
DESTINATION "${INSTALL_CMAKE_DIR}" COMPONENT dev)
# Install the export set for use with the install-tree
install(EXPORT MyLibTargets DESTINATION
"${INSTALL_CMAKE_DIR}" COMPONENT dev)

CMake: target not in export set

I have a library foo which depends on bar. bar provides a nice barConfig.cmake upon installation.
cmake_minimum_required(VERSION 3.7)
project(foo VERSION 1.0)
find_package(bar)
add_library(foo SHARED foo.c)
target_link_libraries(foo PUBLIC bar)
install(TARGETS foo EXPORT fooTargets LIBRARY DESTINATION lib)
export(EXPORT fooTargets FILE fooTargets.cmake)
install(EXPORT fooTargets FILE fooTargets.cmake DESTINATION lib/cmake)
This works great. We simply apt install bar-dev then compile foo.
But since these packages are both developed by us and are related, my team would like to develop them in the same IDE session and compile them at the same time. I want to allow that, but I don't want to change the fact that these are already deployed as seperate packages and can be built independently.
if (DIRECTORY_EXISTS ../bar)
set(BAR_NO_INSTALL ON)
set(BAR_SKIP_TESTS ON)
add_subdirectory(../bar ${CMAKE_CURRENT_BINARY_DIR}/bar)
else()
find_package(bar)
endif()
If ../bar/ exists, then this results in:
CMake Error: install(EXPORT "fooTargets" ...) includes target "foo" which
requires target "bar" that is not in the export set.
How can I prevent the need to export bar in the foo package?
I'm trying to figure out why find_package(bar) works, but add_subdirectory(bar) doesn't. barConfig.cmake via barTargets.cmake defines bar like so:
add_library(bar SHARED IMPORTED)
set_property(TARGET bar APPEND PROPERTY IMPORTED_CONFIGURATIONS NOCONFIG)
set_target_properties(bar PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "../bar/include"
IMPORTED_LOCATION_NOCONFIG "build/libbar.so.1.0.13574"
IMPORTED_SONAME_NOCONFIG "libbar.so.1"
)
My guess was the IMPORTED property that is used when bar is created in the autogenerated barConfig.cmake. I tried adding setting property (below), but saw no differences:
set_target_properties(bar PROPERTIES IMPORTED TRUE)
fooConfig.cmake does include the following which creates target bar (not bar::bar).
find_depdendency(bar)
I tried the following aliasing, hoping that an alias' inability to be exported would help me. But it still expects me to export bar.
add_library(barImported ALIAS bar)
target_link_libraries(foo PUBLIC barImported)
I tried linking only the build interface, hoping that bar would not need to be exported when installed. But that also had no effect.
target_link_libraries(foo PUBLIC $<BUILD_INTERFACE:bar>)
How can I prevent the need to export bar in the foo package?
You can't, at least not without hacks (see below).
I'm trying to figure out why find_package(bar) works, but add_subdirectory(bar) doesn't. barConfig.cmake via barTargets.cmake defines bar like so:
Because in one case, the target is imported (which can only be set on creation) and in the other case, the target is normal. Normal targets must be installed if normal targets that depend on them are installed.
The semantic issue with add_subdirectory is that CMake believes that any code you add this way is first-party. I have found that add_subdirectory and its relative FetchContent are more trouble than they're worth for installed dependencies. They're useful for build-or-test-only dependencies that do not need to be shipped.
One way to work around this is to use the COMPONENT / NAMELINK_COMPONENT features of the install() command. Then you would set the CPACK_COMPONENTS_ALL variable to include only the components from foo and not those from bar. When specifying components, you should avoid generic names like development or runtime, and instead prefix those names with the project name: foo_development, bar_runtime, etc.
Unfortunately, this does not affect the cmake --build . --target install command. You'll have to go through cmake --install . --component <comp> or CPack.
A hack is to add the bar dependency to foo as $<BUILD_INTERFACE:...> and then re-attach it to foo in fooConfig.cmake after include-ing your generated target export file and finding bar via find_dependency.
Here's the solution I came up with. This is for an arbitrary library foo (dynamic and static versions plus tests) which could also be a dependency in the same manner that I import bar:
cmake_minimum_required(VERSION 3.7)
include(GNUInstallDirs)
project(foo VERSION 1.0)
# Options set to
# ON explicitly for build machines,
# OFF by default for developers who build dependencies inline and only build for local use
option(FOO_EXPORT_TARGETS "Generate fooConfig.cmake for packaging." OFF)
option(FOO_BUILD_TESTS "Build and run tests. Default off for aircraft. Turn on in your IDE if developing foo" OFF)
# This is best implemented as a macro/function from an included cmake
set(BAR_ROOT "/noexist" CACHE PATH "Location of BAR for in-source building")
if (DIRECTORY_EXISTS "${BAR_ROOT}")
add_subdirectory(${BAR_ROOT})
else()
find_package(bar)
endif()
set(fooSources foo.c)
set(fooPublicHeaders include/foo.h)
add_library(fooObjects OBJECT ${fooSources})
target_include_directories(fooObjects
PUBLIC
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
"$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
PRIVATE
$<TARGET_PROPERTY:bar,INTERFACE_INCLUDE_DIRECTORIES>
)
add_library(foo SHARED $<TARGET_OBJECTS:fooObjects>)
add_library(fooStatic STATIC $<TARGET_OBJECTS:fooObjects>)
target_link_libraries(foo PUBLIC fooObjects bar)
target_link_libraries(fooStatic PUBLIC fooObjects barStatic)
set_target_properties(fooObjects foo fooStatic
PROPERTIES
POSITION_INDEPENDENT_CODE ON
)
if (NOT FOO_EXPORT_TARGETS)
install(TARGETS foo LIBRARY DESTINATION lib)
else()
set_target_properties(foo fooStatic
PROPERTIES
VERSION ${foo_VERSION}
SOVERSION ${foo_VERSION_MAJOR}
PUBLIC_HEADER "${fooPublicHeaders}"
)
install(
TARGETS foo fooStatic fooObjects
EXPORT fooTargets
RUNTIME DESTINATION ${CMAKE_INSTALL_FULL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_FULL_LIBDIR}
PUBLIC_HEADER DESTINATION ${CMAKE_INSTALL_FULL_INCLUDEDIR}/foo
)
include(CMakePackageConfigHelpers)
set(configDir "${CMAKE_INSTALL_FULL_LIBDIR}/cmake/foo-${foo_VERSION_MAJOR}")
configure_package_config_file(
"${CMAKE_CURRENT_LIST_DIR}/fooConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/fooConfig.cmake"
INSTALL_DESTINATION ${configDir}
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/fooConfigVersion.cmake"
VERSION ${foo_VERSION}
COMPATIBILITY SameMajorVersion
)
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/fooConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/fooConfigVersion.cmake"
DESTINATION ${configDir}
)
export (EXPORT fooTargets FILE fooTargets.cmake)
install(EXPORT fooTargets FILE fooTargets.cmake DESTINATION ${configDir})
endif()
if (FOO_BUILD_TESTS)
add_executable(test_foo test.c)
target_link_libraries(test_foo foo)
enable_testing()
add_test(NAME test_foo COMMAND test_foo WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/test)
endif()
With the following fooConfig.cmake.in:
include(CMakeFindDependencyMacro)
#PACKAGE_INIT#
find_dependency(bar)
if(NOT TARGET foo)
include("${CMAKE_CURRENT_LIST_DIR}/fooTargets.cmake")
endif()
The build machines create a debian package out of this. This is what the debian/rules files look like to ensure it is compiled without any in-source building, with packaging, and with tests:
#!/usr/bin/make -f
#export DH_VERBOSE = 1
%:
dh $#
override_dh_auto_configure:
dh_auto_configure -- \
-DCMAKE_LIBRARY_PATH=$(DEB_HOST_MULTIARCH) \
-DFOO_BUILD_TESTS=ON \
-DFOO_EXPORT_TARGETS=ON \
-DBAR_ROOT=/noexist

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)

CMake: link order of imported targets incorrect

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)

target_link_libraries erroneously pulls in MODULE library at link time

I have two C++ projects using CMake 3.12.2.
The first one is MODULE library (a dynamically loaded plugin). It installs a DLL, a header file and the configuration file for CMake.
cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
project(MyPlugin LANGUAGES CXX)
set(CMAKE_DEBUG_POSTFIX "d")
# MODULE libraries are dynamically loaded at runtime and never linked against
add_library(MyPlugin MODULE
include/a.h
src/a.cpp
src/b.h
src/b.cpp
)
target_include_directories(MyPlugin
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/MyPlugin>
PRIVATE
src
)
install(TARGETS MyPlugin EXPORT MyPluginConfig
# MODULE libraries are installed as LIBRARY
LIBRARY DESTINATION plugins COMPONENT Runtime
RUNTIME DESTINATION bin COMPONENT Runtime
PUBLIC_HEADER DESTINATION include/MyPlugin COMPONENT Development
)
install(FILES $<TARGET_PDB_FILE:MyPlugin> DESTINATION plugins OPTIONAL COMPONENT Runtime)
install(
DIRECTORY include/
DESTINATION include/MyPlugin
FILES_MATCHING PATTERN "*.h"
)
install(EXPORT MyPluginConfig
NAMESPACE MyPlugin::
DESTINATION lib/cmake/MyPlugin
)
The second one is a simple executable which pulls in the header file of the plugin via target_link_libraries (the modern CMake way).
cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
project(MyExe LANGUAGES CXX)
set(CMAKE_DEBUG_POSTFIX "d")
find_package(MyPlugin REQUIRED)
add_executable(MyExe src/main.cpp)
target_link_libraries(MyExe MyPlugin::MyPlugin)
Using the vs2015 generated solution the link fails because the plugin DLL is fed during the link of the executable...
Has anyone a solution for this or should I file a bug?
Regards.
A solution to this issue is to split the library in two: an interface library which only provides the headers and a module library which does not export any header.
The module library CMakeLists.txt becomes:
cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
project(MyPlugin LANGUAGES CXX)
set(CMAKE_DEBUG_POSTFIX "d")
# Move public headers to a dedicated INTERFACE library
add_library(MyPluginInterface INTERFACE)
add_custom_target(Includes SOURCES include/a.h)
target_include_directories(MyPluginInterfacecmake_minimum_required(VERSION 3.12 FATAL_ERROR)
project(MyPlugin LANGUAGES CXX)
set(CMAKE_DEBUG_POSTFIX "d")
# Move public headers to a dedicated INTERFACE library
add_library(MyPluginInterface INTERFACE)
add_custom_target(Includes SOURCES include/a.h)
target_include_directories(MyPluginInterface
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/MyPlugin>
)
install(TARGETS MyPluginInterface EXPORT MyPluginConfig
PUBLIC_HEADER DESTINATION include/MyPlugin COMPONENT Development
)
# MODULE libraries are dynamically loaded at runtime and never linked against
add_library(MyPlugin MODULE
src/a.cpp
src/b.h
src/b.cpp
)
target_link_libraries(MyPlugin MyPluginInterface)
install(TARGETS MyPlugin
# MODULE libraries are installed as LIBRARY
LIBRARY DESTINATION plugins COMPONENT Runtime
RUNTIME DESTINATION bin COMPONENT Runtime
)
install(FILES $<TARGET_PDB_FILE:MyPlugin> DESTINATION plugins OPTIONAL COMPONENT Runtime)
install(
DIRECTORY include/
DESTINATION include/MyPlugin
FILES_MATCHING PATTERN "*.h"
)
install(EXPORT MyPluginConfig
NAMESPACE MyPlugin::
DESTINATION lib/cmake/MyPlugin
)
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include/MyPlugin>
)
install(TARGETS MyPluginInterface EXPORT MyPluginConfig
PUBLIC_HEADER DESTINATION include/MyPlugin COMPONENT Development
)
# MODULE libraries are dynamically loaded at runtime and never linked against
add_library(MyPlugin MODULE
src/a.cpp
src/b.h
src/b.cpp
)
target_link_libraries(MyPlugin MyPluginInterface)
install(TARGETS MyPlugin
# MODULE libraries are installed as LIBRARY
LIBRARY DESTINATION plugins COMPONENT Runtime
RUNTIME DESTINATION bin COMPONENT Runtime
)
install(FILES $<TARGET_PDB_FILE:MyPlugin> DESTINATION plugins OPTIONAL COMPONENT Runtime)
install(
DIRECTORY include/
DESTINATION include/MyPlugin
FILES_MATCHING PATTERN "*.h"
)
install(EXPORT MyPluginConfig
NAMESPACE MyPlugin::
DESTINATION lib/cmake/MyPlugin
)
The executable CMakeLists.txt becomes:
cmake_minimum_required(VERSION 3.12 FATAL_ERROR)
project(MyExe LANGUAGES CXX)
set(CMAKE_DEBUG_POSTFIX "d")
find_package(MyPlugin REQUIRED)
add_executable(MyExe src/main.cpp)
target_link_libraries(MyExe MyPlugin::MyPluginInterface)